/** * 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 { 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( value: T | ResponsiveProp, viewport: Viewport ): T { if (typeof value !== 'object' || value === null) { return value as T; } const prop = value as ResponsiveProp; 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; tablet?: Record; desktop?: Record; } ): 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, };