chore(app): update config, dependencies, storybook, and app shell
This commit is contained in:
@@ -2,9 +2,15 @@
|
|||||||
Component: ThemeDecorator
|
Component: ThemeDecorator
|
||||||
Storybook decorator that initializes ThemeManager for theme-related stories.
|
Storybook decorator that initializes ThemeManager for theme-related stories.
|
||||||
Ensures theme management works correctly in Storybook's iframe environment.
|
Ensures theme management works correctly in Storybook's iframe environment.
|
||||||
|
Includes a floating theme toggle for universal theme switching across all stories.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { themeManager } from '$features/ChangeAppTheme';
|
import { themeManager } from '$features/ChangeAppTheme';
|
||||||
|
import type { ResponsiveManager } from '$shared/lib';
|
||||||
|
import { IconButton } from '$shared/ui';
|
||||||
|
import MoonIcon from '@lucide/svelte/icons/moon';
|
||||||
|
import SunIcon from '@lucide/svelte/icons/sun';
|
||||||
|
import { getContext } from 'svelte';
|
||||||
import {
|
import {
|
||||||
onDestroy,
|
onDestroy,
|
||||||
onMount,
|
onMount,
|
||||||
@@ -16,15 +22,58 @@ interface Props {
|
|||||||
|
|
||||||
let { children }: Props = $props();
|
let { children }: Props = $props();
|
||||||
|
|
||||||
|
// Get responsive context (set by Decorator)
|
||||||
|
const responsive = getContext<ResponsiveManager>('responsive');
|
||||||
|
|
||||||
// Initialize themeManager on mount
|
// Initialize themeManager on mount
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
themeManager.init();
|
themeManager.init();
|
||||||
|
|
||||||
|
// Add keyboard shortcut for theme toggle (Cmd/Ctrl+Shift+D)
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'D') {
|
||||||
|
e.preventDefault();
|
||||||
|
themeManager.toggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up themeManager when story unmounts
|
// Clean up themeManager when story unmounts
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
themeManager.destroy();
|
themeManager.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const theme = $derived(themeManager.value);
|
||||||
|
const themeLabel = $derived(theme === 'light' ? 'Light' : 'Dark');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Floating Theme Toggle -->
|
||||||
|
<div
|
||||||
|
class="fixed top-4 right-4 z-50 flex items-center gap-2 px-3 py-2 bg-card border border-border shadow-lg rounded-lg"
|
||||||
|
title="Toggle theme (Cmd/Ctrl+Shift+D)"
|
||||||
|
>
|
||||||
|
<span class="text-xs font-medium text-muted-foreground">Theme: {themeLabel}</span>
|
||||||
|
<IconButton
|
||||||
|
onclick={() => themeManager.toggle()}
|
||||||
|
size={responsive?.isMobile ? 'sm' : 'md'}
|
||||||
|
variant="ghost"
|
||||||
|
title="Toggle theme"
|
||||||
|
>
|
||||||
|
{#snippet icon()}
|
||||||
|
{#if theme === 'light'}
|
||||||
|
<MoonIcon class="size-4" />
|
||||||
|
{:else}
|
||||||
|
<SunIcon class="size-4" />
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Story Content -->
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|||||||
@@ -1,11 +1,74 @@
|
|||||||
import type { Preview } from '@storybook/svelte-vite';
|
import type { Preview } from '@storybook/svelte-vite';
|
||||||
import Decorator from './Decorator.svelte';
|
import Decorator from './Decorator.svelte';
|
||||||
import StoryStage from './StoryStage.svelte';
|
import StoryStage from './StoryStage.svelte';
|
||||||
|
import ThemeDecorator from './ThemeDecorator.svelte';
|
||||||
import '../src/app/styles/app.css';
|
import '../src/app/styles/app.css';
|
||||||
|
|
||||||
const preview: Preview = {
|
const preview: Preview = {
|
||||||
|
globalTypes: {
|
||||||
|
viewport: {
|
||||||
|
description: 'Viewport size for responsive design',
|
||||||
|
defaultValue: 'widgetWide',
|
||||||
|
toolbar: {
|
||||||
|
icon: 'view',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
value: 'reset',
|
||||||
|
icon: 'refresh',
|
||||||
|
title: 'Reset viewport',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'mobile1',
|
||||||
|
icon: 'mobile',
|
||||||
|
title: 'iPhone 5/SE',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'mobile2',
|
||||||
|
icon: 'mobile',
|
||||||
|
title: 'iPhone 14 Pro Max',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'tablet',
|
||||||
|
icon: 'tablet',
|
||||||
|
title: 'iPad (Portrait)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'desktop',
|
||||||
|
icon: 'desktop',
|
||||||
|
title: 'Desktop (Small)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'widgetMedium',
|
||||||
|
icon: 'view',
|
||||||
|
title: 'Widget Medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'widgetWide',
|
||||||
|
icon: 'view',
|
||||||
|
title: 'Widget Wide',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'widgetExtraWide',
|
||||||
|
icon: 'view',
|
||||||
|
title: 'Widget Extra Wide',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'fullWidth',
|
||||||
|
icon: 'view',
|
||||||
|
title: 'Full Width',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'fullScreen',
|
||||||
|
icon: 'expand',
|
||||||
|
title: 'Full Screen',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dynamicTitle: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: 'fullscreen',
|
layout: 'padded',
|
||||||
controls: {
|
controls: {
|
||||||
matchers: {
|
matchers: {
|
||||||
color: /(background|color)$/i,
|
color: /(background|color)$/i,
|
||||||
@@ -23,7 +86,79 @@ const preview: Preview = {
|
|||||||
docs: {
|
docs: {
|
||||||
story: {
|
story: {
|
||||||
// This sets the default height for the iframe in Autodocs
|
// This sets the default height for the iframe in Autodocs
|
||||||
iframeHeight: '400px',
|
iframeHeight: '600px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
viewport: {
|
||||||
|
viewports: {
|
||||||
|
// Mobile devices
|
||||||
|
mobile1: {
|
||||||
|
name: 'iPhone 5/SE',
|
||||||
|
styles: {
|
||||||
|
width: '320px',
|
||||||
|
height: '568px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mobile2: {
|
||||||
|
name: 'iPhone 14 Pro Max',
|
||||||
|
styles: {
|
||||||
|
width: '414px',
|
||||||
|
height: '896px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Tablet
|
||||||
|
tablet: {
|
||||||
|
name: 'iPad (Portrait)',
|
||||||
|
styles: {
|
||||||
|
width: '834px',
|
||||||
|
height: '1112px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
desktop: {
|
||||||
|
name: 'Desktop (Small)',
|
||||||
|
styles: {
|
||||||
|
width: '1024px',
|
||||||
|
height: '1280px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Widget-specific viewports
|
||||||
|
widgetMedium: {
|
||||||
|
name: 'Widget Medium',
|
||||||
|
styles: {
|
||||||
|
width: '768px',
|
||||||
|
height: '800px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
widgetWide: {
|
||||||
|
name: 'Widget Wide',
|
||||||
|
styles: {
|
||||||
|
width: '1024px',
|
||||||
|
height: '800px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
widgetExtraWide: {
|
||||||
|
name: 'Widget Extra Wide',
|
||||||
|
styles: {
|
||||||
|
width: '1280px',
|
||||||
|
height: '800px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Full-width viewports
|
||||||
|
fullWidth: {
|
||||||
|
name: 'Full Width',
|
||||||
|
styles: {
|
||||||
|
width: '100%',
|
||||||
|
height: '800px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fullScreen: {
|
||||||
|
name: 'Full Screen',
|
||||||
|
styles: {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -45,6 +180,13 @@ const preview: Preview = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
decorators: [
|
decorators: [
|
||||||
|
// Outermost: initialize ThemeManager for all stories
|
||||||
|
story => ({
|
||||||
|
Component: ThemeDecorator,
|
||||||
|
props: {
|
||||||
|
children: story(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
// Wrap with providers (TooltipProvider, ResponsiveManager)
|
// Wrap with providers (TooltipProvider, ResponsiveManager)
|
||||||
story => ({
|
story => ({
|
||||||
Component: Decorator,
|
Component: Decorator,
|
||||||
|
|||||||
@@ -61,7 +61,6 @@
|
|||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vaul-svelte": "^1.0.0-next.7",
|
|
||||||
"vite": "^7.2.6",
|
"vite": "^7.2.6",
|
||||||
"vitest": "^4.0.16",
|
"vitest": "^4.0.16",
|
||||||
"vitest-browser-svelte": "^2.0.1"
|
"vitest-browser-svelte": "^2.0.1"
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
<!--
|
||||||
|
Component: App
|
||||||
|
Application root with query provider and layout
|
||||||
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
/**
|
/**
|
||||||
* App Component
|
* App Component
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import { QueryClientProvider } from '@tanstack/svelte-query';
|
|||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
/**
|
||||||
|
* Content snippet
|
||||||
|
*/
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -306,81 +306,72 @@
|
|||||||
animation: nudge 10s ease-in-out infinite;
|
animation: nudge 10s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
/* ============================================
|
||||||
scrollbar-width: thin;
|
SCROLLBAR STYLES
|
||||||
scrollbar-color: hsl(0 0% 70% / 0.4) transparent;
|
============================================ */
|
||||||
|
|
||||||
|
/* ---- Modern API: color + width (Chrome 121+, FF 64+) ---- */
|
||||||
|
@supports (scrollbar-width: auto) {
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: hsl(0 0% 70% / 0.4) var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark * {
|
||||||
|
scrollbar-color: hsl(0 0% 40% / 0.5) var(--color-surface);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark * {
|
/* ---- Webkit layer: runs ON TOP in Chrome, standalone in old Safari ---- */
|
||||||
scrollbar-color: hsl(0 0% 40% / 0.5) transparent;
|
/* Handles things scrollbar-width can't: hiding buttons, exact sizing */
|
||||||
|
@supports selector(::-webkit-scrollbar) {
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button {
|
||||||
|
display: none; /* kills arrows */
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: hsl(0 0% 70% / 0.4);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: hsl(0 0% 50% / 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:active {
|
||||||
|
background: hsl(0 0% 40% / 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-thumb { background: hsl(0 0% 40% / 0.5); }
|
||||||
|
.dark ::-webkit-scrollbar-thumb:hover { background: hsl(0 0% 55% / 0.6); }
|
||||||
|
.dark ::-webkit-scrollbar-thumb:active { background: hsl(0 0% 65% / 0.7); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Webkit / Blink ---- */
|
html {
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: hsl(0 0% 70% / 0);
|
|
||||||
border-radius: 3px;
|
|
||||||
transition: background 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Show thumb when container is hovered or actively scrolling */
|
|
||||||
:hover > ::-webkit-scrollbar-thumb,
|
|
||||||
::-webkit-scrollbar-thumb:hover,
|
|
||||||
*:hover::-webkit-scrollbar-thumb {
|
|
||||||
background: hsl(0 0% 70% / 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: hsl(0 0% 50% / 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:active {
|
|
||||||
background: hsl(0 0% 40% / 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-corner {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode */
|
|
||||||
.dark ::-webkit-scrollbar-thumb {
|
|
||||||
background: hsl(0 0% 40% / 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark :hover > ::-webkit-scrollbar-thumb,
|
|
||||||
.dark ::-webkit-scrollbar-thumb:hover,
|
|
||||||
.dark *:hover::-webkit-scrollbar-thumb {
|
|
||||||
background: hsl(0 0% 40% / 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark ::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: hsl(0 0% 55% / 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark ::-webkit-scrollbar-thumb:active {
|
|
||||||
background: hsl(0 0% 65% / 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Behavior ---- */
|
|
||||||
* {
|
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
scrollbar-gutter: stable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
html {
|
html { scroll-behavior: auto; }
|
||||||
scroll-behavior: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
overscroll-behavior-y: none;
|
overscroll-behavior-y: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scroll-stable {
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
<!--
|
||||||
|
Component: Layout
|
||||||
|
Application shell with providers and page wrapper
|
||||||
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
/**
|
/**
|
||||||
* Layout Component
|
* Layout Component
|
||||||
@@ -11,13 +15,9 @@
|
|||||||
* - Footer area (currently empty, reserved for future use)
|
* - Footer area (currently empty, reserved for future use)
|
||||||
*/
|
*/
|
||||||
import { BreadcrumbHeader } from '$entities/Breadcrumb';
|
import { BreadcrumbHeader } from '$entities/Breadcrumb';
|
||||||
import {
|
import { themeManager } from '$features/ChangeAppTheme';
|
||||||
ThemeSwitch,
|
|
||||||
themeManager,
|
|
||||||
} from '$features/ChangeAppTheme';
|
|
||||||
import GD from '$shared/assets/GD.svg';
|
import GD from '$shared/assets/GD.svg';
|
||||||
import { ResponsiveProvider } from '$shared/lib';
|
import { ResponsiveProvider } from '$shared/lib';
|
||||||
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 { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
import {
|
import {
|
||||||
@@ -27,36 +27,16 @@ import {
|
|||||||
} from 'svelte';
|
} from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
/**
|
||||||
|
* Content snippet
|
||||||
|
*/
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { children }: Props = $props();
|
let { children }: Props = $props();
|
||||||
let fontsReady = $state(false);
|
let fontsReady = $state(true);
|
||||||
const theme = $derived(themeManager.value);
|
const theme = $derived(themeManager.value);
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets fontsReady flag to true when font for the page logo is loaded.
|
|
||||||
*/
|
|
||||||
onMount(async () => {
|
|
||||||
if (!('fonts' in document)) {
|
|
||||||
fontsReady = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const required = ['100'];
|
|
||||||
|
|
||||||
const missing = required.filter(
|
|
||||||
w => !document.fonts.check(`${w} 1em Barlow`),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (missing.length > 0) {
|
|
||||||
await Promise.all(
|
|
||||||
missing.map(w => document.fonts.load(`${w} 1em Barlow`)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fontsReady = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(() => themeManager.init());
|
onMount(() => themeManager.init());
|
||||||
onDestroy(() => themeManager.destroy());
|
onDestroy(() => themeManager.destroy());
|
||||||
</script>
|
</script>
|
||||||
@@ -94,30 +74,29 @@ onDestroy(() => themeManager.destroy());
|
|||||||
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&family=Syne:wght@800&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Space+Grotesk:wght@300..700&family=Space+Mono:ital,wght@0,400;0,700;1,400;1,700&family=Syne:wght@800&display=swap"
|
||||||
/>
|
/>
|
||||||
</noscript>
|
</noscript>
|
||||||
<title>Compare Typography & Typefaces | GlyphDiff</title>
|
<title>GlyphDiff | Typography & Typefaces</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<ResponsiveProvider>
|
<ResponsiveProvider>
|
||||||
<div
|
<div
|
||||||
id="app-root"
|
id="app-root"
|
||||||
class={cn(
|
class={cn(
|
||||||
'min-h-screen flex flex-col bg-surface dark:bg-dark-bg',
|
'min-h-screen w-auto flex flex-col bg-surface dark:bg-dark-bg',
|
||||||
theme === 'dark' ? 'dark' : '',
|
theme === 'dark' ? 'dark' : '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<header>
|
<header>
|
||||||
<BreadcrumbHeader />
|
<BreadcrumbHeader />
|
||||||
<ThemeSwitch />
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- <ScrollArea class="h-screen w-screen"> -->
|
<!-- <ScrollArea class="h-screen w-screen"> -->
|
||||||
<main class="flex-1 w-full mx-auto px-4 pt-0 pb-10 sm:px-6 sm:pt-8 sm:pb-12 md:px-8 md:pt-10 md:pb-16 lg:px-10 lg:pt-12 lg:pb-20 xl:px-16 relative">
|
<!-- <main class="flex-1 w-full mx-auto relative"> -->
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
{#if fontsReady}
|
{#if fontsReady}
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{/if}
|
{/if}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</main>
|
<!-- </main> -->
|
||||||
<!-- </ScrollArea> -->
|
<!-- </ScrollArea> -->
|
||||||
<footer></footer>
|
<footer></footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,149 +4,22 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { scrollBreadcrumbsStore } from '$entities/Breadcrumb';
|
import { scrollBreadcrumbsStore } from '$entities/Breadcrumb';
|
||||||
import type { ResponsiveManager } from '$shared/lib';
|
import { ComparisonView } from '$widgets/ComparisonView';
|
||||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
import { FontSearchSection } from '$widgets/FontSearch';
|
||||||
import {
|
import { SampleListSection } from '$widgets/SampleList';
|
||||||
Logo,
|
|
||||||
Section,
|
|
||||||
} from '$shared/ui';
|
|
||||||
import ComparisonSlider from '$widgets/ComparisonSlider/ui/ComparisonSlider/ComparisonSlider.svelte';
|
|
||||||
import { FontSearch } from '$widgets/FontSearch';
|
|
||||||
import { SampleList } from '$widgets/SampleList';
|
|
||||||
import CodeIcon from '@lucide/svelte/icons/code';
|
|
||||||
import EyeIcon from '@lucide/svelte/icons/eye';
|
|
||||||
import LineSquiggleIcon from '@lucide/svelte/icons/line-squiggle';
|
|
||||||
import ScanSearchIcon from '@lucide/svelte/icons/search';
|
|
||||||
import {
|
|
||||||
type Snippet,
|
|
||||||
getContext,
|
|
||||||
} from 'svelte';
|
|
||||||
import { cubicIn } from 'svelte/easing';
|
import { cubicIn } from 'svelte/easing';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
let searchContainer: HTMLElement;
|
|
||||||
|
|
||||||
let isExpanded = $state(true);
|
|
||||||
const responsive = getContext<ResponsiveManager>('responsive');
|
|
||||||
|
|
||||||
function handleTitleStatusChanged(
|
|
||||||
index: number,
|
|
||||||
isPast: boolean,
|
|
||||||
title?: Snippet<[{ className?: string }]>,
|
|
||||||
id?: string,
|
|
||||||
) {
|
|
||||||
if (isPast && title) {
|
|
||||||
scrollBreadcrumbsStore.add({ index, title, id });
|
|
||||||
} else {
|
|
||||||
scrollBreadcrumbsStore.remove(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
scrollBreadcrumbsStore.remove(index);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Font List -->
|
|
||||||
<div
|
<div
|
||||||
class="p-2 sm:p-3 md:p-4 h-full grid gap-3 sm:gap-4 grid-cols-[max-content_1fr]"
|
class="h-full flex flex-col gap-3 sm:gap-4"
|
||||||
in:fade={{ duration: 500, delay: 150, easing: cubicIn }}
|
in:fade={{ duration: 500, delay: 150, easing: cubicIn }}
|
||||||
>
|
>
|
||||||
<Section
|
<section class="w-auto">
|
||||||
class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-8"
|
<ComparisonView />
|
||||||
onTitleStatusChange={handleTitleStatusChanged}
|
</section>
|
||||||
>
|
<main class="w-full pt-0 pb-10 sm:px-6 sm:pt-16 sm:pb-12 md:px-8 md:pt-32 md:pb-16 lg:px-10 lg:pt-48 lg:pb-20 xl:px-16">
|
||||||
{#snippet icon({ className })}
|
<FontSearchSection />
|
||||||
<CodeIcon class={className} />
|
<SampleListSection index={1} />
|
||||||
{/snippet}
|
</main>
|
||||||
{#snippet description({ className })}
|
|
||||||
<span class={className}> Project_Codename </span>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet content({ className })}
|
|
||||||
<div class={cn(className, 'col-start-0 col-span-2')}>
|
|
||||||
<Logo />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section
|
|
||||||
class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-x-12 sm:gap-y-8"
|
|
||||||
index={1}
|
|
||||||
id="optical_comparator"
|
|
||||||
onTitleStatusChange={handleTitleStatusChanged}
|
|
||||||
stickyTitle={responsive.isDesktopLarge}
|
|
||||||
stickyOffset="4rem"
|
|
||||||
>
|
|
||||||
{#snippet icon({ className })}
|
|
||||||
<EyeIcon class={className} />
|
|
||||||
{/snippet}
|
|
||||||
{#snippet title({ className })}
|
|
||||||
<h1 class={className}>
|
|
||||||
Optical<br />Comparator
|
|
||||||
</h1>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet content({ className })}
|
|
||||||
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
|
|
||||||
<ComparisonSlider />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section
|
|
||||||
class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-x-12 sm:gap-y-8"
|
|
||||||
index={2}
|
|
||||||
id="query_module"
|
|
||||||
onTitleStatusChange={handleTitleStatusChanged}
|
|
||||||
stickyTitle={responsive.isDesktopLarge}
|
|
||||||
stickyOffset="4rem"
|
|
||||||
>
|
|
||||||
{#snippet icon({ className })}
|
|
||||||
<ScanSearchIcon class={className} />
|
|
||||||
{/snippet}
|
|
||||||
{#snippet title({ className })}
|
|
||||||
<h2 class={className}>
|
|
||||||
Query<br />Module
|
|
||||||
</h2>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet content({ className })}
|
|
||||||
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
|
|
||||||
<FontSearch bind:showFilters={isExpanded} />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section
|
|
||||||
class="py-4 sm:py-10 md:py-12 gap-6 sm:gap-x-12 sm:gap-y-8"
|
|
||||||
index={3}
|
|
||||||
id="sample_set"
|
|
||||||
onTitleStatusChange={handleTitleStatusChanged}
|
|
||||||
stickyTitle={responsive.isDesktopLarge}
|
|
||||||
stickyOffset="4rem"
|
|
||||||
>
|
|
||||||
{#snippet icon({ className })}
|
|
||||||
<LineSquiggleIcon class={className} />
|
|
||||||
{/snippet}
|
|
||||||
{#snippet title({ className })}
|
|
||||||
<h2 class={className}>
|
|
||||||
Sample<br />Set
|
|
||||||
</h2>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet content({ className })}
|
|
||||||
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
|
|
||||||
<SampleList />
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
</Section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.content {
|
|
||||||
/* Tells the browser to skip rendering off-screen content */
|
|
||||||
content-visibility: auto;
|
|
||||||
/* Helps the browser reserve space without calculating everything */
|
|
||||||
contain-intrinsic-size: 1px 1000px;
|
|
||||||
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export default defineConfig({
|
|||||||
statements: 70,
|
statements: 70,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setupFiles: [],
|
setupFiles: ['./vitest.setup.unit.ts'],
|
||||||
globals: false,
|
globals: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
83
vitest.setup.unit.ts
Normal file
83
vitest.setup.unit.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Setup file for unit tests
|
||||||
|
*
|
||||||
|
* This file runs before all unit tests to set up global mocks
|
||||||
|
* that are needed before any module imports.
|
||||||
|
*
|
||||||
|
* IMPORTANT: This runs in Node environment BEFORE jsdom is initialized
|
||||||
|
* for test files that use @vitest-environment jsdom.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
// Create a storage map that persists through the test session
|
||||||
|
// This is used for the localStorage mock
|
||||||
|
// We make it global so tests can clear it
|
||||||
|
(globalThis as any).__testStorageMap = new Map<string, string>();
|
||||||
|
|
||||||
|
// Mock ResizeObserver for tests that import modules using responsiveManager
|
||||||
|
// This must be done at setup time because the responsiveManager singleton
|
||||||
|
// is instantiated when the module is first imported
|
||||||
|
globalThis.ResizeObserver = class {
|
||||||
|
observe() {}
|
||||||
|
unobserve() {}
|
||||||
|
disconnect() {}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// Mock MediaQueryListEvent for tests that need to simulate system theme changes
|
||||||
|
// @ts-expect-error - Mocking a DOM API
|
||||||
|
globalThis.MediaQueryListEvent = class MediaQueryListEvent extends Event {
|
||||||
|
matches: boolean;
|
||||||
|
media: string;
|
||||||
|
|
||||||
|
constructor(type: string, eventInitDict: { matches: boolean; media: string }) {
|
||||||
|
super(type);
|
||||||
|
this.matches = eventInitDict.matches;
|
||||||
|
this.media = eventInitDict.media;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock window.matchMedia for tests that import modules using media queries
|
||||||
|
// Some modules (like createPerspectiveManager) use matchMedia during import
|
||||||
|
Object.defineProperty(globalThis, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
value: vi.fn().mockImplementation((query: string) => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: vi.fn(),
|
||||||
|
removeListener: vi.fn(),
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock localStorage for tests that use it during module import
|
||||||
|
// Some modules (like ThemeManager via createPersistentStore) access localStorage during initialization
|
||||||
|
// This MUST be a fully functional mock since it's used during module load
|
||||||
|
const getStorageMap = () => (globalThis as any).__testStorageMap;
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'localStorage', {
|
||||||
|
writable: true,
|
||||||
|
value: {
|
||||||
|
get length() {
|
||||||
|
return getStorageMap().size;
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
getStorageMap().clear();
|
||||||
|
},
|
||||||
|
getItem(key: string) {
|
||||||
|
return getStorageMap().get(key) ?? null;
|
||||||
|
},
|
||||||
|
setItem(key: string, value: string) {
|
||||||
|
getStorageMap().set(key, value);
|
||||||
|
},
|
||||||
|
removeItem(key: string) {
|
||||||
|
getStorageMap().delete(key);
|
||||||
|
},
|
||||||
|
key(index: number) {
|
||||||
|
return Array.from(getStorageMap().keys())[index] ?? null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
37
yarn.lock
37
yarn.lock
@@ -2470,7 +2470,6 @@ __metadata:
|
|||||||
tailwindcss: "npm:^4.1.18"
|
tailwindcss: "npm:^4.1.18"
|
||||||
tw-animate-css: "npm:^1.4.0"
|
tw-animate-css: "npm:^1.4.0"
|
||||||
typescript: "npm:^5.9.3"
|
typescript: "npm:^5.9.3"
|
||||||
vaul-svelte: "npm:^1.0.0-next.7"
|
|
||||||
vite: "npm:^7.2.6"
|
vite: "npm:^7.2.6"
|
||||||
vitest: "npm:^4.0.16"
|
vitest: "npm:^4.0.16"
|
||||||
vitest-browser-svelte: "npm:^2.0.1"
|
vitest-browser-svelte: "npm:^2.0.1"
|
||||||
@@ -3626,17 +3625,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"runed@npm:^0.23.2":
|
|
||||||
version: 0.23.4
|
|
||||||
resolution: "runed@npm:0.23.4"
|
|
||||||
dependencies:
|
|
||||||
esm-env: "npm:^1.0.0"
|
|
||||||
peerDependencies:
|
|
||||||
svelte: ^5.7.0
|
|
||||||
checksum: 10c0/e27400af9e69b966dca449b851e82e09b3d2ddde4095ba72237599aa80fc248a23d0737c0286f751ca6c12721a5e09eb21b9d8cc872cbd70e7b161442818eece
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"runed@npm:^0.35.1":
|
"runed@npm:^0.35.1":
|
||||||
version: 0.35.1
|
version: 0.35.1
|
||||||
resolution: "runed@npm:0.35.1"
|
resolution: "runed@npm:0.35.1"
|
||||||
@@ -3920,19 +3908,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"svelte-toolbelt@npm:^0.7.1":
|
|
||||||
version: 0.7.1
|
|
||||||
resolution: "svelte-toolbelt@npm:0.7.1"
|
|
||||||
dependencies:
|
|
||||||
clsx: "npm:^2.1.1"
|
|
||||||
runed: "npm:^0.23.2"
|
|
||||||
style-to-object: "npm:^1.0.8"
|
|
||||||
peerDependencies:
|
|
||||||
svelte: ^5.0.0
|
|
||||||
checksum: 10c0/a50db97c851fa65af7fbf77007bd76730a179ac0239c0121301bd26682c1078a4ffea77835492550b133849a42d3dffee0714ae076154d86be8d0b3a84c9a9bf
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"svelte2tsx@npm:^0.7.44, svelte2tsx@npm:~0.7.46":
|
"svelte2tsx@npm:^0.7.44, svelte2tsx@npm:~0.7.46":
|
||||||
version: 0.7.46
|
version: 0.7.46
|
||||||
resolution: "svelte2tsx@npm:0.7.46"
|
resolution: "svelte2tsx@npm:0.7.46"
|
||||||
@@ -4257,18 +4232,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"vaul-svelte@npm:^1.0.0-next.7":
|
|
||||||
version: 1.0.0-next.7
|
|
||||||
resolution: "vaul-svelte@npm:1.0.0-next.7"
|
|
||||||
dependencies:
|
|
||||||
runed: "npm:^0.23.2"
|
|
||||||
svelte-toolbelt: "npm:^0.7.1"
|
|
||||||
peerDependencies:
|
|
||||||
svelte: ^5.0.0
|
|
||||||
checksum: 10c0/7a459122b39c9ef6bd830b525d5f6acbc07575491e05c758d9dfdb993cc98ab4dee4a9c022e475760faaf1d7bd8460a1434965431d36885a3ee48315ffa54eb3
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"vite@npm:^6.0.0 || ^7.0.0, vite@npm:^7.2.6":
|
"vite@npm:^6.0.0 || ^7.0.0, vite@npm:^7.2.6":
|
||||||
version: 7.3.0
|
version: 7.3.0
|
||||||
resolution: "vite@npm:7.3.0"
|
resolution: "vite@npm:7.3.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user