232 lines
6.2 KiB
TypeScript
232 lines
6.2 KiB
TypeScript
|
|
// $shared/lib/createResponsiveManager.svelte.ts
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Breakpoint definitions following common device sizes
|
||
|
|
* Customize these values to match your design system
|
||
|
|
*/
|
||
|
|
export interface Breakpoints {
|
||
|
|
/** Mobile devices (portrait phones) */
|
||
|
|
mobile: number;
|
||
|
|
/** Tablet portrait */
|
||
|
|
tabletPortrait: number;
|
||
|
|
/** Tablet landscape */
|
||
|
|
tablet: number;
|
||
|
|
/** Desktop */
|
||
|
|
desktop: number;
|
||
|
|
/** Large desktop */
|
||
|
|
desktopLarge: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Default breakpoints (matches common Tailwind-like breakpoints)
|
||
|
|
*/
|
||
|
|
const DEFAULT_BREAKPOINTS: Breakpoints = {
|
||
|
|
mobile: 640, // sm
|
||
|
|
tabletPortrait: 768, // md
|
||
|
|
tablet: 1024, // lg
|
||
|
|
desktop: 1280, // xl
|
||
|
|
desktopLarge: 1536, // 2xl
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Orientation type
|
||
|
|
*/
|
||
|
|
export type Orientation = 'portrait' | 'landscape';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Creates a reactive responsive manager that tracks viewport size and breakpoints.
|
||
|
|
*
|
||
|
|
* Provides reactive getters for:
|
||
|
|
* - Current breakpoint detection (isMobile, isTablet, etc.)
|
||
|
|
* - Viewport dimensions (width, height)
|
||
|
|
* - Device orientation (portrait/landscape)
|
||
|
|
* - Custom breakpoint matching
|
||
|
|
*
|
||
|
|
* @param customBreakpoints - Optional custom breakpoint values
|
||
|
|
* @returns Responsive manager instance with reactive properties
|
||
|
|
*
|
||
|
|
* @example
|
||
|
|
* ```svelte
|
||
|
|
* <script lang="ts">
|
||
|
|
* const responsive = createResponsiveManager();
|
||
|
|
* </script>
|
||
|
|
*
|
||
|
|
* {#if responsive.isMobile}
|
||
|
|
* <MobileNav />
|
||
|
|
* {:else if responsive.isTablet}
|
||
|
|
* <TabletNav />
|
||
|
|
* {:else}
|
||
|
|
* <DesktopNav />
|
||
|
|
* {/if}
|
||
|
|
*
|
||
|
|
* <p>Width: {responsive.width}px</p>
|
||
|
|
* <p>Orientation: {responsive.orientation}</p>
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
export function createResponsiveManager(customBreakpoints?: Partial<Breakpoints>) {
|
||
|
|
const breakpoints: Breakpoints = {
|
||
|
|
...DEFAULT_BREAKPOINTS,
|
||
|
|
...customBreakpoints,
|
||
|
|
};
|
||
|
|
|
||
|
|
// Reactive state
|
||
|
|
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
|
||
|
|
const orientation = $derived<Orientation>(height > width ? 'portrait' : 'landscape');
|
||
|
|
const isPortrait = $derived(orientation === 'portrait');
|
||
|
|
const isLandscape = $derived(orientation === 'landscape');
|
||
|
|
|
||
|
|
// Touch device detection (best effort)
|
||
|
|
const isTouchDevice = $derived(
|
||
|
|
typeof window !== 'undefined'
|
||
|
|
&& ('ontouchstart' in window || navigator.maxTouchPoints > 0),
|
||
|
|
);
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize responsive tracking
|
||
|
|
* Call this in an $effect or component mount
|
||
|
|
*/
|
||
|
|
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 width matches a custom breakpoint
|
||
|
|
* @param min - Minimum width (inclusive)
|
||
|
|
* @param max - Maximum width (exclusive)
|
||
|
|
*/
|
||
|
|
function matches(min: number, max?: number): boolean {
|
||
|
|
if (max !== undefined) {
|
||
|
|
return width >= min && width < max;
|
||
|
|
}
|
||
|
|
return width >= min;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the current breakpoint name
|
||
|
|
*/
|
||
|
|
const currentBreakpoint = $derived<keyof Breakpoints | 'xs'>(
|
||
|
|
(() => {
|
||
|
|
if (isMobile) return 'mobile';
|
||
|
|
if (isTabletPortrait) return 'tabletPortrait';
|
||
|
|
if (isTablet) return 'tablet';
|
||
|
|
if (isDesktop) return 'desktop';
|
||
|
|
if (isDesktopLarge) return 'desktopLarge';
|
||
|
|
return 'xs'; // Fallback for very small screens
|
||
|
|
})(),
|
||
|
|
);
|
||
|
|
|
||
|
|
return {
|
||
|
|
// Dimensions
|
||
|
|
get width() {
|
||
|
|
return width;
|
||
|
|
},
|
||
|
|
get height() {
|
||
|
|
return height;
|
||
|
|
},
|
||
|
|
|
||
|
|
// Standard breakpoints
|
||
|
|
get isMobile() {
|
||
|
|
return isMobile;
|
||
|
|
},
|
||
|
|
get isTabletPortrait() {
|
||
|
|
return isTabletPortrait;
|
||
|
|
},
|
||
|
|
get isTablet() {
|
||
|
|
return isTablet;
|
||
|
|
},
|
||
|
|
get isDesktop() {
|
||
|
|
return isDesktop;
|
||
|
|
},
|
||
|
|
get isDesktopLarge() {
|
||
|
|
return isDesktopLarge;
|
||
|
|
},
|
||
|
|
|
||
|
|
// Convenience groupings
|
||
|
|
get isMobileOrTablet() {
|
||
|
|
return isMobileOrTablet;
|
||
|
|
},
|
||
|
|
get isTabletOrDesktop() {
|
||
|
|
return isTabletOrDesktop;
|
||
|
|
},
|
||
|
|
|
||
|
|
// Orientation
|
||
|
|
get orientation() {
|
||
|
|
return orientation;
|
||
|
|
},
|
||
|
|
get isPortrait() {
|
||
|
|
return isPortrait;
|
||
|
|
},
|
||
|
|
get isLandscape() {
|
||
|
|
return isLandscape;
|
||
|
|
},
|
||
|
|
|
||
|
|
// Device capabilities
|
||
|
|
get isTouchDevice() {
|
||
|
|
return isTouchDevice;
|
||
|
|
},
|
||
|
|
|
||
|
|
// Current breakpoint
|
||
|
|
get currentBreakpoint() {
|
||
|
|
return currentBreakpoint;
|
||
|
|
},
|
||
|
|
|
||
|
|
// Methods
|
||
|
|
init,
|
||
|
|
matches,
|
||
|
|
|
||
|
|
// Breakpoint values (for custom logic)
|
||
|
|
breakpoints,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export const responsiveManager = createResponsiveManager();
|
||
|
|
|
||
|
|
if (typeof window !== 'undefined') {
|
||
|
|
responsiveManager.init();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Type for the responsive manager instance
|
||
|
|
*/
|
||
|
|
export type ResponsiveManager = ReturnType<typeof createResponsiveManager>;
|