feat(responsiveManager): add a manager to monitor responsive state and give access to responsive state flags
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
import { BreadcrumbHeader } from '$entities/Breadcrumb';
|
import { BreadcrumbHeader } from '$entities/Breadcrumb';
|
||||||
import favicon from '$shared/assets/favicon.svg';
|
import favicon from '$shared/assets/favicon.svg';
|
||||||
|
import { ResponsiveProvider } from '$shared/lib';
|
||||||
import { ScrollArea } from '$shared/shadcn/ui/scroll-area';
|
import { ScrollArea } from '$shared/shadcn/ui/scroll-area';
|
||||||
import { Provider as TooltipProvider } from '$shared/shadcn/ui/tooltip';
|
import { Provider as TooltipProvider } from '$shared/shadcn/ui/tooltip';
|
||||||
import { TypographyMenu } from '$widgets/TypographySettings';
|
import { TypographyMenu } from '$widgets/TypographySettings';
|
||||||
@@ -42,18 +43,20 @@ let { children }: Props = $props();
|
|||||||
>
|
>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div id="app-root" class="min-h-screen flex flex-col bg-background">
|
<ResponsiveProvider>
|
||||||
<header>
|
<div id="app-root" class="min-h-screen flex flex-col bg-background">
|
||||||
<BreadcrumbHeader />
|
<header>
|
||||||
</header>
|
<BreadcrumbHeader />
|
||||||
|
</header>
|
||||||
|
|
||||||
<!-- <ScrollArea class="h-screen w-screen"> -->
|
<!-- <ScrollArea class="h-screen w-screen"> -->
|
||||||
<main class="flex-1 h-full w-full max-w-6xl mx-auto px-4 pt-6 pb-10 md:px-8 lg:pt-10 lg:pb-20 relative overflow-x-hidden">
|
<main class="flex-1 h-full w-full max-w-6xl mx-auto px-4 pt-6 pb-10 md:px-8 lg:pt-10 lg:pb-20 relative overflow-x-hidden">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<TypographyMenu />
|
<TypographyMenu />
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</main>
|
</main>
|
||||||
<!-- </ScrollArea> -->
|
<!-- </ScrollArea> -->
|
||||||
<footer></footer>
|
<footer></footer>
|
||||||
</div>
|
</div>
|
||||||
|
</ResponsiveProvider>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface Control {
|
|||||||
|
|
||||||
export class TypographyControlManager {
|
export class TypographyControlManager {
|
||||||
#controls = new SvelteMap<string, Control>();
|
#controls = new SvelteMap<string, Control>();
|
||||||
|
#sizeMultiplier = $state(1);
|
||||||
|
|
||||||
constructor(configs: ControlModel[]) {
|
constructor(configs: ControlModel[]) {
|
||||||
configs.forEach(({ id, increaseLabel, decreaseLabel, controlLabel, ...config }) => {
|
configs.forEach(({ id, increaseLabel, decreaseLabel, controlLabel, ...config }) => {
|
||||||
@@ -37,7 +38,8 @@ export class TypographyControlManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get size() {
|
get size() {
|
||||||
return this.#controls.get('font_size')?.instance.value;
|
const size = this.#controls.get('font_size')?.instance.value;
|
||||||
|
return size === undefined ? undefined : size * this.#sizeMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
get height() {
|
get height() {
|
||||||
@@ -47,6 +49,10 @@ export class TypographyControlManager {
|
|||||||
get spacing() {
|
get spacing() {
|
||||||
return this.#controls.get('letter_spacing')?.instance.value;
|
return this.#controls.get('letter_spacing')?.instance.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set multiplier(value: number) {
|
||||||
|
this.#sizeMultiplier = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,8 +3,32 @@
|
|||||||
Contains controls for setting up font properties.
|
Contains controls for setting up font properties.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
import { ComboControl } from '$shared/ui';
|
import { ComboControl } from '$shared/ui';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
import { controlManager } from '../model';
|
import { controlManager } from '../model';
|
||||||
|
const responsive = getContext<ResponsiveManager>('responsive');
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!responsive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case responsive.isMobile:
|
||||||
|
controlManager.multiplier = 0.5;
|
||||||
|
break;
|
||||||
|
case responsive.isTablet:
|
||||||
|
controlManager.multiplier = 0.75;
|
||||||
|
break;
|
||||||
|
case responsive.isDesktop:
|
||||||
|
controlManager.multiplier = 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
controlManager.multiplier = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="py-2 px-10 flex flex-row items-center gap-2">
|
<div class="py-2 px-10 flex flex-row items-center gap-2">
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
// $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>;
|
||||||
@@ -33,3 +33,9 @@ export {
|
|||||||
} from './createCharacterComparison/createCharacterComparison.svelte';
|
} from './createCharacterComparison/createCharacterComparison.svelte';
|
||||||
|
|
||||||
export { createPersistentStore } from './createPersistentStore/createPersistentStore.svelte';
|
export { createPersistentStore } from './createPersistentStore/createPersistentStore.svelte';
|
||||||
|
|
||||||
|
export {
|
||||||
|
createResponsiveManager,
|
||||||
|
type ResponsiveManager,
|
||||||
|
responsiveManager,
|
||||||
|
} from './createResponsiveManager/createResponsiveManager.svelte';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export {
|
|||||||
createEntityStore,
|
createEntityStore,
|
||||||
createFilter,
|
createFilter,
|
||||||
createPersistentStore,
|
createPersistentStore,
|
||||||
|
createResponsiveManager,
|
||||||
createTypographyControl,
|
createTypographyControl,
|
||||||
createVirtualizer,
|
createVirtualizer,
|
||||||
type Entity,
|
type Entity,
|
||||||
@@ -14,6 +15,8 @@ export {
|
|||||||
type FilterModel,
|
type FilterModel,
|
||||||
type LineData,
|
type LineData,
|
||||||
type Property,
|
type Property,
|
||||||
|
type ResponsiveManager,
|
||||||
|
responsiveManager,
|
||||||
type TypographyControl,
|
type TypographyControl,
|
||||||
type VirtualItem,
|
type VirtualItem,
|
||||||
type Virtualizer,
|
type Virtualizer,
|
||||||
@@ -23,3 +26,5 @@ export {
|
|||||||
export { splitArray } from './utils';
|
export { splitArray } from './utils';
|
||||||
|
|
||||||
export { springySlideFade } from './transitions';
|
export { springySlideFade } from './transitions';
|
||||||
|
|
||||||
|
export { ResponsiveProvider } from './providers';
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<!--
|
||||||
|
Component: ResponsiveProvider
|
||||||
|
Provides a responsive manager to all children
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
type ResponsiveManager,
|
||||||
|
createResponsiveManager,
|
||||||
|
} from '$shared/lib/helpers';
|
||||||
|
import { setContext } from 'svelte';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { children }: Props = $props();
|
||||||
|
|
||||||
|
const responsive = createResponsiveManager();
|
||||||
|
|
||||||
|
// Initialize and cleanup
|
||||||
|
$effect(() => {
|
||||||
|
return responsive.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Provide to all children
|
||||||
|
setContext('responsive', responsive);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@render children()}
|
||||||
1
src/shared/lib/providers/index.ts
Normal file
1
src/shared/lib/providers/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as ResponsiveProvider } from './ResponsiveProvider/ResponsiveProvider.svelte';
|
||||||
Reference in New Issue
Block a user