496 lines
11 KiB
TypeScript
496 lines
11 KiB
TypeScript
/**
|
|
* 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,
|
|
}; |