migration wip
This commit is contained in:
496
lib/responsive.ts
Normal file
496
lib/responsive.ts
Normal file
@@ -0,0 +1,496 @@
|
||||
/**
|
||||
* Responsive Design Utilities for KLZ Cables
|
||||
* Mobile-first approach with comprehensive breakpoint detection and responsive helpers
|
||||
*/
|
||||
|
||||
// Breakpoint definitions matching Tailwind config
|
||||
export const BREAKPOINTS = {
|
||||
xs: 475,
|
||||
sm: 640,
|
||||
md: 768,
|
||||
lg: 1024,
|
||||
xl: 1280,
|
||||
'2xl': 1400,
|
||||
'3xl': 1600,
|
||||
} as const;
|
||||
|
||||
export type BreakpointKey = keyof typeof BREAKPOINTS;
|
||||
|
||||
// Viewport interface
|
||||
export interface Viewport {
|
||||
width: number;
|
||||
height: number;
|
||||
isMobile: boolean;
|
||||
isTablet: boolean;
|
||||
isDesktop: boolean;
|
||||
isLargeDesktop: boolean;
|
||||
breakpoint: BreakpointKey;
|
||||
}
|
||||
|
||||
// Responsive prop interface
|
||||
export interface ResponsiveProp<T> {
|
||||
mobile?: T;
|
||||
tablet?: T;
|
||||
desktop?: T;
|
||||
default?: T;
|
||||
}
|
||||
|
||||
// Visibility options interface
|
||||
export interface VisibilityOptions {
|
||||
mobile?: boolean;
|
||||
tablet?: boolean;
|
||||
desktop?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current viewport information (client-side only)
|
||||
*/
|
||||
export function getViewport(): Viewport {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
width: 0,
|
||||
height: 0,
|
||||
isMobile: false,
|
||||
isTablet: false,
|
||||
isDesktop: false,
|
||||
isLargeDesktop: false,
|
||||
breakpoint: 'xs',
|
||||
};
|
||||
}
|
||||
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
// Determine breakpoint
|
||||
let breakpoint: BreakpointKey = 'xs';
|
||||
if (width >= BREAKPOINTS['3xl']) breakpoint = '3xl';
|
||||
else if (width >= BREAKPOINTS['2xl']) breakpoint = '2xl';
|
||||
else if (width >= BREAKPOINTS.xl) breakpoint = 'xl';
|
||||
else if (width >= BREAKPOINTS.lg) breakpoint = 'lg';
|
||||
else if (width >= BREAKPOINTS.md) breakpoint = 'md';
|
||||
else if (width >= BREAKPOINTS.sm) breakpoint = 'sm';
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
isMobile: width < BREAKPOINTS.md,
|
||||
isTablet: width >= BREAKPOINTS.md && width < BREAKPOINTS.lg,
|
||||
isDesktop: width >= BREAKPOINTS.lg,
|
||||
isLargeDesktop: width >= BREAKPOINTS.xl,
|
||||
breakpoint,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if viewport matches specific breakpoint conditions
|
||||
*/
|
||||
export function checkBreakpoint(
|
||||
condition: 'mobile' | 'tablet' | 'desktop' | 'largeDesktop' | BreakpointKey,
|
||||
viewport: Viewport
|
||||
): boolean {
|
||||
const conditions = {
|
||||
mobile: viewport.isMobile,
|
||||
tablet: viewport.isTablet,
|
||||
desktop: viewport.isDesktop,
|
||||
largeDesktop: viewport.isLargeDesktop,
|
||||
};
|
||||
|
||||
if (condition in conditions) {
|
||||
return conditions[condition as keyof typeof conditions];
|
||||
}
|
||||
|
||||
// Check specific breakpoint
|
||||
const targetBreakpoint = BREAKPOINTS[condition as BreakpointKey];
|
||||
return viewport.width >= targetBreakpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsive prop resolver - returns appropriate value based on viewport
|
||||
*/
|
||||
export function resolveResponsiveProp<T>(
|
||||
value: T | ResponsiveProp<T>,
|
||||
viewport: Viewport
|
||||
): T {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return value as T;
|
||||
}
|
||||
|
||||
const prop = value as ResponsiveProp<T>;
|
||||
|
||||
if (viewport.isMobile && prop.mobile !== undefined) {
|
||||
return prop.mobile;
|
||||
}
|
||||
if (viewport.isTablet && prop.tablet !== undefined) {
|
||||
return prop.tablet;
|
||||
}
|
||||
if (viewport.isDesktop && prop.desktop !== undefined) {
|
||||
return prop.desktop;
|
||||
}
|
||||
|
||||
return (prop.default ?? Object.values(prop)[0]) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate responsive image sizes attribute
|
||||
*/
|
||||
export function generateImageSizes(): string {
|
||||
return '(max-width: 640px) 100vw, (max-width: 768px) 50vw, (max-width: 1024px) 33vw, 25vw';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimal image dimensions for different breakpoints
|
||||
*/
|
||||
export function getImageDimensionsForBreakpoint(
|
||||
breakpoint: BreakpointKey,
|
||||
aspectRatio: number = 16 / 9
|
||||
) {
|
||||
const baseWidths = {
|
||||
xs: 400,
|
||||
sm: 640,
|
||||
md: 768,
|
||||
lg: 1024,
|
||||
xl: 1280,
|
||||
'2xl': 1400,
|
||||
'3xl': 1600,
|
||||
};
|
||||
|
||||
const width = baseWidths[breakpoint];
|
||||
const height = Math.round(width / aspectRatio);
|
||||
|
||||
return { width, height };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate responsive srcset for images
|
||||
*/
|
||||
export function generateSrcset(
|
||||
baseUrl: string,
|
||||
formats: string[] = ['webp', 'jpg']
|
||||
): string {
|
||||
const sizes = [480, 640, 768, 1024, 1280, 1600];
|
||||
|
||||
return formats
|
||||
.map(format =>
|
||||
sizes
|
||||
.map(size => `${baseUrl}-${size}w.${format} ${size}w`)
|
||||
.join(', ')
|
||||
)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if element is in viewport (for lazy loading)
|
||||
*/
|
||||
export function isInViewport(element: HTMLElement, offset = 0): boolean {
|
||||
if (!element || typeof window === 'undefined') return false;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
return (
|
||||
rect.top >= -offset &&
|
||||
rect.left >= -offset &&
|
||||
rect.bottom <= (window.innerHeight + offset) &&
|
||||
rect.right <= (window.innerWidth + offset)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate responsive CSS clamp values for typography
|
||||
*/
|
||||
export function clamp(
|
||||
min: number,
|
||||
preferred: number,
|
||||
max: number,
|
||||
unit: 'rem' | 'px' = 'rem'
|
||||
): string {
|
||||
const minVal = unit === 'rem' ? `${min}rem` : `${min}px`;
|
||||
const maxVal = unit === 'rem' ? `${max}rem` : `${max}px`;
|
||||
const preferredVal = `${preferred}vw`;
|
||||
|
||||
return `clamp(${minVal}, ${preferredVal}, ${maxVal})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get touch target size based on device type
|
||||
*/
|
||||
export function getTouchTargetSize(isMobile: boolean, isLargeDesktop: boolean): string {
|
||||
if (isLargeDesktop) return '72px'; // lg
|
||||
if (isMobile) return '44px'; // sm (minimum)
|
||||
return '56px'; // md
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsive spacing utility
|
||||
*/
|
||||
export function getResponsiveSpacing(
|
||||
base: number,
|
||||
viewport: Viewport,
|
||||
multiplier: { mobile?: number; tablet?: number; desktop?: number } = {}
|
||||
): string {
|
||||
const { isMobile, isTablet, isDesktop } = viewport;
|
||||
|
||||
let factor = 1;
|
||||
if (isMobile) factor = multiplier.mobile ?? 1;
|
||||
else if (isTablet) factor = multiplier.tablet ?? 1.25;
|
||||
else if (isDesktop) factor = multiplier.desktop ?? 1.5;
|
||||
|
||||
return `${base * factor}rem`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate responsive grid template
|
||||
*/
|
||||
export function getResponsiveGrid(
|
||||
viewport: Viewport,
|
||||
options: {
|
||||
mobile?: number;
|
||||
tablet?: number;
|
||||
desktop?: number;
|
||||
gap?: string;
|
||||
} = {}
|
||||
): { columns: number; gap: string } {
|
||||
const { isMobile, isTablet, isDesktop } = viewport;
|
||||
|
||||
const columns = isMobile
|
||||
? (options.mobile ?? 1)
|
||||
: isTablet
|
||||
? (options.tablet ?? 2)
|
||||
: (options.desktop ?? 3);
|
||||
|
||||
const gap = options.gap ?? (isMobile ? '1rem' : isTablet ? '1.5rem' : '2rem');
|
||||
|
||||
return { columns, gap };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if touch device
|
||||
*/
|
||||
export function isTouchDevice(): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
|
||||
return (
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
(navigator as any).msMaxTouchPoints > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate responsive meta tag content
|
||||
*/
|
||||
export function generateViewportMeta(): string {
|
||||
return 'width=device-width, initial-scale=1, viewport-fit=cover, maximum-scale=5, minimum-scale=1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsive text truncation with ellipsis
|
||||
*/
|
||||
export function truncateText(
|
||||
text: string,
|
||||
viewport: Viewport,
|
||||
maxLength: { mobile?: number; tablet?: number; desktop?: number } = {}
|
||||
): string {
|
||||
const limit = viewport.isMobile
|
||||
? (maxLength.mobile ?? 100)
|
||||
: viewport.isTablet
|
||||
? (maxLength.tablet ?? 150)
|
||||
: (maxLength.desktop ?? 200);
|
||||
|
||||
return text.length > limit ? `${text.substring(0, limit)}...` : text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate optimal line height based on viewport and text size
|
||||
*/
|
||||
export function getOptimalLineHeight(
|
||||
fontSize: number,
|
||||
viewport: Viewport
|
||||
): string {
|
||||
const baseLineHeight = 1.6;
|
||||
|
||||
// Tighter line height for mobile to improve readability
|
||||
if (viewport.isMobile) {
|
||||
return fontSize < 16 ? '1.5' : '1.4';
|
||||
}
|
||||
|
||||
// More breathing room for larger screens
|
||||
if (viewport.isDesktop) {
|
||||
return fontSize > 24 ? '1.3' : '1.5';
|
||||
}
|
||||
|
||||
return baseLineHeight.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate responsive CSS custom properties
|
||||
*/
|
||||
export function generateResponsiveCSSVars(
|
||||
prefix: string,
|
||||
values: {
|
||||
mobile: Record<string, string>;
|
||||
tablet?: Record<string, string>;
|
||||
desktop?: Record<string, string>;
|
||||
}
|
||||
): string {
|
||||
const { mobile, tablet, desktop } = values;
|
||||
|
||||
let css = `:root {`;
|
||||
Object.entries(mobile).forEach(([key, value]) => {
|
||||
css += `--${prefix}-${key}: ${value};`;
|
||||
});
|
||||
css += `}`;
|
||||
|
||||
if (tablet) {
|
||||
css += `@media (min-width: ${BREAKPOINTS.md}px) { :root {`;
|
||||
Object.entries(tablet).forEach(([key, value]) => {
|
||||
css += `--${prefix}-${key}: ${value};`;
|
||||
});
|
||||
css += `} }`;
|
||||
}
|
||||
|
||||
if (desktop) {
|
||||
css += `@media (min-width: ${BREAKPOINTS.lg}px) { :root {`;
|
||||
Object.entries(desktop).forEach(([key, value]) => {
|
||||
css += `--${prefix}-${key}: ${value};`;
|
||||
});
|
||||
css += `} }`;
|
||||
}
|
||||
|
||||
return css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate responsive offset for sticky elements
|
||||
*/
|
||||
export function getStickyOffset(
|
||||
viewport: Viewport,
|
||||
elementHeight: number
|
||||
): number {
|
||||
if (viewport.isMobile) {
|
||||
return elementHeight * 0.5;
|
||||
}
|
||||
if (viewport.isTablet) {
|
||||
return elementHeight * 0.75;
|
||||
}
|
||||
return elementHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate responsive animation duration
|
||||
*/
|
||||
export function getResponsiveDuration(
|
||||
baseDuration: number,
|
||||
viewport: Viewport
|
||||
): number {
|
||||
if (viewport.isMobile) {
|
||||
return baseDuration * 0.75; // Faster on mobile
|
||||
}
|
||||
return baseDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if viewport is in safe area for content
|
||||
*/
|
||||
export function isContentSafeArea(viewport: Viewport): boolean {
|
||||
// Ensure minimum content width for readability
|
||||
const minWidth = 320;
|
||||
return viewport.width >= minWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsive form field width
|
||||
*/
|
||||
export function getFormFieldWidth(
|
||||
viewport: Viewport,
|
||||
options: { full?: boolean; half?: boolean; third?: boolean } = {}
|
||||
): string {
|
||||
if (options.full || viewport.isMobile) return '100%';
|
||||
if (options.half) return '48%';
|
||||
if (options.third) return '31%';
|
||||
|
||||
return viewport.isTablet ? '48%' : '31%';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate responsive accessibility attributes
|
||||
*/
|
||||
export function getResponsiveA11yProps(viewport: Viewport) {
|
||||
return {
|
||||
// Larger touch targets on mobile
|
||||
'aria-touch-target': viewport.isMobile ? 'large' : 'standard',
|
||||
// Mobile-optimized announcements
|
||||
'aria-mobile-optimized': viewport.isMobile ? 'true' : 'false',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if viewport width meets minimum requirement
|
||||
*/
|
||||
export function meetsMinimumWidth(viewport: Viewport, minWidth: number): boolean {
|
||||
return viewport.width >= minWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get responsive column count for grid layouts
|
||||
*/
|
||||
export function getResponsiveColumns(viewport: Viewport): number {
|
||||
if (viewport.isMobile) return 1;
|
||||
if (viewport.isTablet) return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate responsive padding based on viewport
|
||||
*/
|
||||
export function getResponsivePadding(viewport: Viewport): string {
|
||||
if (viewport.isMobile) return '1rem';
|
||||
if (viewport.isTablet) return '1.5rem';
|
||||
if (viewport.isDesktop) return '2rem';
|
||||
return '3rem';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if viewport is landscape orientation
|
||||
*/
|
||||
export function isLandscape(viewport: Viewport): boolean {
|
||||
return viewport.width > viewport.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimal image quality based on viewport
|
||||
*/
|
||||
export function getOptimalImageQuality(viewport: Viewport): number {
|
||||
if (viewport.isMobile) return 75;
|
||||
if (viewport.isTablet) return 85;
|
||||
return 90;
|
||||
}
|
||||
|
||||
export default {
|
||||
BREAKPOINTS,
|
||||
getViewport,
|
||||
checkBreakpoint,
|
||||
resolveResponsiveProp,
|
||||
generateImageSizes,
|
||||
getImageDimensionsForBreakpoint,
|
||||
generateSrcset,
|
||||
isInViewport,
|
||||
clamp,
|
||||
getTouchTargetSize,
|
||||
getResponsiveSpacing,
|
||||
getResponsiveGrid,
|
||||
isTouchDevice,
|
||||
generateViewportMeta,
|
||||
truncateText,
|
||||
getOptimalLineHeight,
|
||||
generateResponsiveCSSVars,
|
||||
getStickyOffset,
|
||||
getResponsiveDuration,
|
||||
isContentSafeArea,
|
||||
getFormFieldWidth,
|
||||
getResponsiveA11yProps,
|
||||
meetsMinimumWidth,
|
||||
getResponsiveColumns,
|
||||
getResponsivePadding,
|
||||
isLandscape,
|
||||
getOptimalImageQuality,
|
||||
};
|
||||
Reference in New Issue
Block a user