feature/responsive #22
@@ -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();
|
||||
>
|
||||
</svelte:head>
|
||||
|
||||
<div id="app-root" class="min-h-screen flex flex-col bg-background">
|
||||
<header>
|
||||
<BreadcrumbHeader />
|
||||
</header>
|
||||
<ResponsiveProvider>
|
||||
<div id="app-root" class="min-h-screen flex flex-col bg-background">
|
||||
<header>
|
||||
<BreadcrumbHeader />
|
||||
</header>
|
||||
|
||||
<!-- <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">
|
||||
<TooltipProvider>
|
||||
<TypographyMenu />
|
||||
{@render children?.()}
|
||||
</TooltipProvider>
|
||||
</main>
|
||||
<!-- </ScrollArea> -->
|
||||
<footer></footer>
|
||||
</div>
|
||||
<!-- <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">
|
||||
<TooltipProvider>
|
||||
<TypographyMenu />
|
||||
{@render children?.()}
|
||||
</TooltipProvider>
|
||||
</main>
|
||||
<!-- </ScrollArea> -->
|
||||
<footer></footer>
|
||||
</div>
|
||||
</ResponsiveProvider>
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface Control {
|
||||
|
||||
export class TypographyControlManager {
|
||||
#controls = new SvelteMap<string, Control>();
|
||||
#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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,8 +3,32 @@
|
||||
Contains controls for setting up font properties.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { ResponsiveManager } from '$shared/lib';
|
||||
import { ComboControl } from '$shared/ui';
|
||||
import { getContext } from 'svelte';
|
||||
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>
|
||||
|
||||
<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';
|
||||
|
||||
export { createPersistentStore } from './createPersistentStore/createPersistentStore.svelte';
|
||||
|
||||
export {
|
||||
createResponsiveManager,
|
||||
type ResponsiveManager,
|
||||
responsiveManager,
|
||||
} from './createResponsiveManager/createResponsiveManager.svelte';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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