Files
klz-cables.com/lib/responsive.ts
2025-12-29 18:18:48 +01:00

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,
};