diff --git a/src/app/ui/Layout.svelte b/src/app/ui/Layout.svelte index 1e84e61..6ca820e 100644 --- a/src/app/ui/Layout.svelte +++ b/src/app/ui/Layout.svelte @@ -12,6 +12,7 @@ */ import { BreadcrumbHeader } from '$entities/Breadcrumb'; import favicon from '$shared/assets/favicon.svg'; +import { ResponsiveProvider } from '$shared/lib'; import { ScrollArea } from '$shared/shadcn/ui/scroll-area'; import { Provider as TooltipProvider } from '$shared/shadcn/ui/tooltip'; import { TypographyMenu } from '$widgets/TypographySettings'; @@ -42,18 +43,20 @@ let { children }: Props = $props(); > -
-
- -
+ +
+
+ +
- -
- - - {@render children?.()} - -
- -
-
+ +
+ + + {@render children?.()} + +
+ + +
+ diff --git a/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts b/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts index 134e71e..d8dcded 100644 --- a/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts +++ b/src/features/SetupFont/lib/controlManager/controlManager.svelte.ts @@ -15,6 +15,7 @@ export interface Control { export class TypographyControlManager { #controls = new SvelteMap(); + #sizeMultiplier = $state(1); constructor(configs: ControlModel[]) { configs.forEach(({ id, increaseLabel, decreaseLabel, controlLabel, ...config }) => { @@ -37,7 +38,8 @@ export class TypographyControlManager { } 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() { @@ -47,6 +49,10 @@ export class TypographyControlManager { get spacing() { return this.#controls.get('letter_spacing')?.instance.value; } + + set multiplier(value: number) { + this.#sizeMultiplier = value; + } } /** diff --git a/src/features/SetupFont/ui/SetupFontMenu.svelte b/src/features/SetupFont/ui/SetupFontMenu.svelte index 5384878..197c0a2 100644 --- a/src/features/SetupFont/ui/SetupFontMenu.svelte +++ b/src/features/SetupFont/ui/SetupFontMenu.svelte @@ -3,8 +3,32 @@ Contains controls for setting up font properties. -->
diff --git a/src/shared/lib/helpers/createResponsiveManager/createResponsiveManager.svelte.ts b/src/shared/lib/helpers/createResponsiveManager/createResponsiveManager.svelte.ts new file mode 100644 index 0000000..1332c22 --- /dev/null +++ b/src/shared/lib/helpers/createResponsiveManager/createResponsiveManager.svelte.ts @@ -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 + * + * + * {#if responsive.isMobile} + * + * {:else if responsive.isTablet} + * + * {:else} + * + * {/if} + * + *

Width: {responsive.width}px

+ *

Orientation: {responsive.orientation}

+ * ``` + */ +export function createResponsiveManager(customBreakpoints?: Partial) { + 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(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( + (() => { + 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; diff --git a/src/shared/lib/helpers/index.ts b/src/shared/lib/helpers/index.ts index f325d4f..a5bcd1c 100644 --- a/src/shared/lib/helpers/index.ts +++ b/src/shared/lib/helpers/index.ts @@ -33,3 +33,9 @@ export { } from './createCharacterComparison/createCharacterComparison.svelte'; export { createPersistentStore } from './createPersistentStore/createPersistentStore.svelte'; + +export { + createResponsiveManager, + type ResponsiveManager, + responsiveManager, +} from './createResponsiveManager/createResponsiveManager.svelte'; diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts index 7cde5c5..56882c0 100644 --- a/src/shared/lib/index.ts +++ b/src/shared/lib/index.ts @@ -6,6 +6,7 @@ export { createEntityStore, createFilter, createPersistentStore, + createResponsiveManager, createTypographyControl, createVirtualizer, type Entity, @@ -14,6 +15,8 @@ export { type FilterModel, type LineData, type Property, + type ResponsiveManager, + responsiveManager, type TypographyControl, type VirtualItem, type Virtualizer, @@ -23,3 +26,5 @@ export { export { splitArray } from './utils'; export { springySlideFade } from './transitions'; + +export { ResponsiveProvider } from './providers'; diff --git a/src/shared/lib/providers/ResponsiveProvider/ResponsiveProvider.svelte b/src/shared/lib/providers/ResponsiveProvider/ResponsiveProvider.svelte new file mode 100644 index 0000000..6f205b1 --- /dev/null +++ b/src/shared/lib/providers/ResponsiveProvider/ResponsiveProvider.svelte @@ -0,0 +1,30 @@ + + + +{@render children()} diff --git a/src/shared/lib/providers/index.ts b/src/shared/lib/providers/index.ts new file mode 100644 index 0000000..768deb7 --- /dev/null +++ b/src/shared/lib/providers/index.ts @@ -0,0 +1 @@ +export { default as ResponsiveProvider } from './ResponsiveProvider/ResponsiveProvider.svelte';