/** * 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 * * * {#if responsiveManager.isMobile} * * {:else} * * {/if} * *

Viewport: {responsiveManager.width}x{responsiveManager.height}

*

Breakpoint: {responsiveManager.currentBreakpoint}

* ``` */ /** * 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) { 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(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( (() => { 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;