343 lines
9.1 KiB
TypeScript
343 lines
9.1 KiB
TypeScript
/**
|
|
* Responsive breakpoint tracking using Svelte 5 runes
|
|
*
|
|
* Provides reactive viewport dimensions and breakpoint detection that
|
|
* automatically updates on window resize. Includes touch device detection
|
|
* and orientation tracking.
|
|
*
|
|
* Default breakpoints match Tailwind CSS:
|
|
* - xs: < 640px (mobile)
|
|
* - sm: 640px (mobile)
|
|
* - md: 768px (tablet portrait)
|
|
* - lg: 1024px (tablet)
|
|
* - xl: 1280px (desktop)
|
|
* - 2xl: 1536px (desktop large)
|
|
*
|
|
* @example
|
|
* ```svelte
|
|
* <script lang="ts">
|
|
* import { responsiveManager } from '$shared/lib/helpers';
|
|
*
|
|
* // Singleton is auto-initialized
|
|
* </script>
|
|
*
|
|
* {#if responsiveManager.isMobile}
|
|
* <MobileNav />
|
|
* {:else}
|
|
* <DesktopNav />
|
|
* {/if}
|
|
*
|
|
* <p>Viewport: {responsiveManager.width}x{responsiveManager.height}</p>
|
|
* <p>Breakpoint: {responsiveManager.currentBreakpoint}</p>
|
|
* ```
|
|
*/
|
|
|
|
/**
|
|
* Breakpoint definitions for responsive design
|
|
*
|
|
* Values represent the minimum width (in pixels) for each breakpoint.
|
|
* Customize to match your design system's breakpoints.
|
|
*/
|
|
export interface Breakpoints {
|
|
/**
|
|
* Mobile devices - default 640px
|
|
*/
|
|
mobile: number;
|
|
/**
|
|
* Tablet portrait - default 768px
|
|
*/
|
|
tabletPortrait: number;
|
|
/**
|
|
* Tablet landscape - default 1024px
|
|
*/
|
|
tablet: number;
|
|
/**
|
|
* Desktop - default 1280px
|
|
*/
|
|
desktop: number;
|
|
/**
|
|
* Large desktop - default 1536px
|
|
*/
|
|
desktopLarge: number;
|
|
}
|
|
|
|
/**
|
|
* Default breakpoint values (Tailwind CSS compatible)
|
|
*/
|
|
const DEFAULT_BREAKPOINTS: Breakpoints = {
|
|
mobile: 640,
|
|
tabletPortrait: 768,
|
|
tablet: 1024,
|
|
desktop: 1280,
|
|
desktopLarge: 1536,
|
|
};
|
|
|
|
/**
|
|
* Device orientation type
|
|
*/
|
|
export type Orientation = 'portrait' | 'landscape';
|
|
|
|
/**
|
|
* Creates a responsive manager for tracking viewport state
|
|
*
|
|
* Tracks viewport dimensions, calculates breakpoint states, and detects
|
|
* device capabilities (touch, orientation). Uses ResizeObserver for
|
|
* accurate tracking and falls back to window resize events.
|
|
*
|
|
* @param customBreakpoints - Optional custom breakpoint values
|
|
* @returns Responsive manager instance with reactive properties
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* // Use defaults
|
|
* const responsive = createResponsiveManager();
|
|
*
|
|
* // Custom breakpoints
|
|
* const custom = createResponsiveManager({
|
|
* mobile: 480,
|
|
* desktop: 1024
|
|
* });
|
|
*
|
|
* // In component
|
|
* $: isMobile = responsive.isMobile;
|
|
* $: cols = responsive.isDesktop ? 3 : 1;
|
|
* ```
|
|
*/
|
|
export function createResponsiveManager(customBreakpoints?: Partial<Breakpoints>) {
|
|
const breakpoints: Breakpoints = {
|
|
...DEFAULT_BREAKPOINTS,
|
|
...customBreakpoints,
|
|
};
|
|
|
|
// Reactive viewport dimensions
|
|
let width = $state(typeof window !== 'undefined' ? window.innerWidth : 0);
|
|
let height = $state(typeof window !== 'undefined' ? window.innerHeight : 0);
|
|
|
|
// Derived breakpoint states
|
|
const isMobile = $derived(width < breakpoints.mobile);
|
|
const isTabletPortrait = $derived(
|
|
width >= breakpoints.mobile && width < breakpoints.tabletPortrait,
|
|
);
|
|
const isTablet = $derived(
|
|
width >= breakpoints.tabletPortrait && width < breakpoints.desktop,
|
|
);
|
|
const isDesktop = $derived(
|
|
width >= breakpoints.desktop && width < breakpoints.desktopLarge,
|
|
);
|
|
const isDesktopLarge = $derived(width >= breakpoints.desktopLarge);
|
|
|
|
// Convenience groupings
|
|
const isMobileOrTablet = $derived(width < breakpoints.desktop);
|
|
const isTabletOrDesktop = $derived(width >= breakpoints.tabletPortrait);
|
|
|
|
// Orientation detection
|
|
const orientation = $derived<Orientation>(height > width ? 'portrait' : 'landscape');
|
|
const isPortrait = $derived(orientation === 'portrait');
|
|
const isLandscape = $derived(orientation === 'landscape');
|
|
|
|
// Touch device detection (best effort heuristic)
|
|
const isTouchDevice = $derived(
|
|
typeof window !== 'undefined'
|
|
&& ('ontouchstart' in window || navigator.maxTouchPoints > 0),
|
|
);
|
|
|
|
/**
|
|
* Initialize responsive tracking
|
|
*
|
|
* Sets up ResizeObserver on document.documentElement and falls back
|
|
* to window resize event listener. Returns cleanup function.
|
|
*
|
|
* @returns Cleanup function to remove listeners
|
|
*/
|
|
function init() {
|
|
if (typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
const handleResize = () => {
|
|
width = window.innerWidth;
|
|
height = window.innerHeight;
|
|
};
|
|
|
|
// Use ResizeObserver for more accurate tracking
|
|
const resizeObserver = new ResizeObserver(handleResize);
|
|
resizeObserver.observe(document.documentElement);
|
|
|
|
// Fallback to window resize event
|
|
window.addEventListener('resize', handleResize, { passive: true });
|
|
|
|
// Initial measurement
|
|
handleResize();
|
|
|
|
return () => {
|
|
resizeObserver.disconnect();
|
|
window.removeEventListener('resize', handleResize);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if current viewport matches a custom breakpoint range
|
|
*
|
|
* @param min - Minimum width (inclusive)
|
|
* @param max - Optional maximum width (exclusive)
|
|
* @returns true if viewport width matches the range
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* responsive.matches(768, 1024); // true for tablet only
|
|
* responsive.matches(1280); // true for desktop and larger
|
|
* ```
|
|
*/
|
|
function matches(min: number, max?: number): boolean {
|
|
if (max !== undefined) {
|
|
return width >= min && width < max;
|
|
}
|
|
return width >= min;
|
|
}
|
|
|
|
/**
|
|
* Current breakpoint name based on viewport width
|
|
*/
|
|
const currentBreakpoint = $derived<keyof Breakpoints | 'xs'>(
|
|
(() => {
|
|
switch (true) {
|
|
case isMobile:
|
|
return 'mobile';
|
|
case isTabletPortrait:
|
|
return 'tabletPortrait';
|
|
case isTablet:
|
|
return 'tablet';
|
|
case isDesktop:
|
|
return 'desktop';
|
|
case isDesktopLarge:
|
|
return 'desktopLarge';
|
|
default:
|
|
return 'xs';
|
|
}
|
|
})(),
|
|
);
|
|
|
|
return {
|
|
/**
|
|
* Current viewport width in pixels (reactive)
|
|
*/
|
|
get width() {
|
|
return width;
|
|
},
|
|
/**
|
|
* Current viewport height in pixels (reactive)
|
|
*/
|
|
get height() {
|
|
return height;
|
|
},
|
|
|
|
/**
|
|
* True if viewport width is below the mobile threshold
|
|
*/
|
|
get isMobile() {
|
|
return isMobile;
|
|
},
|
|
/**
|
|
* True if viewport width is between mobile and tablet portrait thresholds
|
|
*/
|
|
get isTabletPortrait() {
|
|
return isTabletPortrait;
|
|
},
|
|
/**
|
|
* True if viewport width is between tablet portrait and desktop thresholds
|
|
*/
|
|
get isTablet() {
|
|
return isTablet;
|
|
},
|
|
/**
|
|
* True if viewport width is between desktop and large desktop thresholds
|
|
*/
|
|
get isDesktop() {
|
|
return isDesktop;
|
|
},
|
|
/**
|
|
* True if viewport width is at or above the large desktop threshold
|
|
*/
|
|
get isDesktopLarge() {
|
|
return isDesktopLarge;
|
|
},
|
|
|
|
/**
|
|
* True if viewport width is below the desktop threshold
|
|
*/
|
|
get isMobileOrTablet() {
|
|
return isMobileOrTablet;
|
|
},
|
|
/**
|
|
* True if viewport width is at or above the tablet portrait threshold
|
|
*/
|
|
get isTabletOrDesktop() {
|
|
return isTabletOrDesktop;
|
|
},
|
|
|
|
/**
|
|
* Current screen orientation (portrait | landscape)
|
|
*/
|
|
get orientation() {
|
|
return orientation;
|
|
},
|
|
/**
|
|
* True if screen height is greater than width
|
|
*/
|
|
get isPortrait() {
|
|
return isPortrait;
|
|
},
|
|
/**
|
|
* True if screen width is greater than height
|
|
*/
|
|
get isLandscape() {
|
|
return isLandscape;
|
|
},
|
|
|
|
/**
|
|
* True if the device supports touch interaction
|
|
*/
|
|
get isTouchDevice() {
|
|
return isTouchDevice;
|
|
},
|
|
|
|
/**
|
|
* Name of the currently active breakpoint (reactive)
|
|
*/
|
|
get currentBreakpoint() {
|
|
return currentBreakpoint;
|
|
},
|
|
|
|
/**
|
|
* Initialization function to start event listeners
|
|
*/
|
|
init,
|
|
/**
|
|
* Helper to check for custom width ranges
|
|
*/
|
|
matches,
|
|
|
|
/**
|
|
* Underlying breakpoint pixel values
|
|
*/
|
|
breakpoints,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Singleton responsive manager instance
|
|
*
|
|
* Auto-initializes on the client side. Use this throughout the app
|
|
* rather than creating multiple instances.
|
|
*/
|
|
export const responsiveManager = createResponsiveManager();
|
|
|
|
if (typeof window !== 'undefined') {
|
|
responsiveManager.init();
|
|
}
|
|
|
|
/**
|
|
* Type for the responsive manager instance
|
|
*/
|
|
export type ResponsiveManager = ReturnType<typeof createResponsiveManager>;
|