Compare commits

...

4 Commits

Author SHA1 Message Date
Ilia Mashkov 8a93c7b545 chore: purge shadcn from codebase. Replace with bits-ui components and other tools 2026-04-17 13:37:44 +03:00
Ilia Mashkov 0004b81e40 chore(ComboControl): replace shadcn tooltip with the one from bits-ui 2026-04-17 13:20:47 +03:00
Ilia Mashkov fb1d2765d0 chore: purge TooltipProvider 2026-04-17 13:20:01 +03:00
Ilia Mashkov 12e8bc0a89 chore: enforce brackets for if clause and for/while loops 2026-04-17 13:05:36 +03:00
74 changed files with 315 additions and 450 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
run: yarn lint run: yarn lint
- name: Type Check - name: Type Check
run: yarn check:shadcn-excluded run: yarn check
publish: publish:
needs: build # Only runs if tests/lint pass needs: build # Only runs if tests/lint pass
+1 -4
View File
@@ -4,12 +4,11 @@
This provides: This provides:
- ResponsiveManager context for breakpoint tracking - ResponsiveManager context for breakpoint tracking
- TooltipProvider for shadcn Tooltip components - TooltipProvider for tooltip components
--> -->
<script lang="ts"> <script lang="ts">
import { createResponsiveManager } from '$shared/lib'; import { createResponsiveManager } from '$shared/lib';
import type { ResponsiveManager } from '$shared/lib'; import type { ResponsiveManager } from '$shared/lib';
import { Provider as TooltipProvider } from '$shared/shadcn/ui/tooltip';
import { setContext } from 'svelte'; import { setContext } from 'svelte';
interface Props { interface Props {
@@ -24,6 +23,4 @@ $effect(() => responsiveManager.init());
setContext<ResponsiveManager>('responsive', responsiveManager); setContext<ResponsiveManager>('responsive', responsiveManager);
</script> </script>
<TooltipProvider delayDuration={200} skipDelayDuration={300}>
{@render children()} {@render children()}
</TooltipProvider>
+2 -2
View File
@@ -8,14 +8,14 @@ A modern font exploration and comparison tool for browsing fonts from Google Fon
- **Side-by-Side Comparison**: Compare up to 4 fonts simultaneously with customizable text, size, and typography settings - **Side-by-Side Comparison**: Compare up to 4 fonts simultaneously with customizable text, size, and typography settings
- **Advanced Filtering**: Filter by category, provider, character subsets, and weight - **Advanced Filtering**: Filter by category, provider, character subsets, and weight
- **Virtual Scrolling**: Fast, smooth browsing of thousands of fonts - **Virtual Scrolling**: Fast, smooth browsing of thousands of fonts
- **Responsive UI**: Beautiful interface built with shadcn components and Tailwind CSS - **Responsive UI**: Beautiful interface built with Tailwind CSS
- **Type-Safe**: Full TypeScript coverage with strict mode enabled - **Type-Safe**: Full TypeScript coverage with strict mode enabled
## Tech Stack ## Tech Stack
- **Framework**: Svelte 5 with reactive primitives (runes) - **Framework**: Svelte 5 with reactive primitives (runes)
- **Styling**: Tailwind CSS v4 - **Styling**: Tailwind CSS v4
- **Components**: shadcn-svelte (via bits-ui) - **Components**: Bits UI primitives
- **State Management**: TanStack Query for async data - **State Management**: TanStack Query for async data
- **Architecture**: Feature-Sliced Design (FSD) - **Architecture**: Feature-Sliced Design (FSD)
- **Quality**: oxlint (linting), dprint (formatting), lefthook (git hooks) - **Quality**: oxlint (linting), dprint (formatting), lefthook (git hooks)
-16
View File
@@ -1,16 +0,0 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"tailwind": {
"css": "src/app.css",
"baseColor": "zinc"
},
"aliases": {
"components": "$shared/shadcn/ui",
"utils": "$shared/shadcn/utils/shadcn-utils",
"ui": "$shared/shadcn/ui",
"hooks": "$shared/shadcn/hooks",
"lib": "$shared"
},
"typescript": true,
"registry": "https://shadcn-svelte.com/registry"
}
+6 -1
View File
@@ -31,7 +31,12 @@
"importDeclaration.forceMultiLine": "whenMultiple", "importDeclaration.forceMultiLine": "whenMultiple",
"importDeclaration.forceSingleLine": false, "importDeclaration.forceSingleLine": false,
"exportDeclaration.forceMultiLine": "whenMultiple", "exportDeclaration.forceMultiLine": "whenMultiple",
"exportDeclaration.forceSingleLine": false "exportDeclaration.forceSingleLine": false,
"ifStatement.useBraces": "always",
"whileStatement.useBraces": "always",
"forStatement.useBraces": "always",
"forInStatement.useBraces": "always",
"forOfStatement.useBraces": "always"
}, },
"json": { "json": {
"indentWidth": 2, "indentWidth": 2,
+1 -1
View File
@@ -17,7 +17,7 @@ pre-push:
run: yarn tsc --noEmit run: yarn tsc --noEmit
svelte-check: svelte-check:
run: yarn check:shadcn-excluded --threshold warning run: yarn check --threshold warning
format-check: format-check:
glob: "*.{ts,js,svelte,json,md}" glob: "*.{ts,js,svelte,json,md}"
-1
View File
@@ -11,7 +11,6 @@
"prepare": "svelte-check --tsconfig ./tsconfig.json || echo ''", "prepare": "svelte-check --tsconfig ./tsconfig.json || echo ''",
"check": "svelte-check", "check": "svelte-check",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"check:shadcn-excluded": "svelte-check --no-tsconfig --ignore \"src/shared/shadcn\"",
"lint": "oxlint", "lint": "oxlint",
"format": "dprint fmt", "format": "dprint fmt",
"format:check": "dprint check", "format:check": "dprint check",
+2 -6
View File
@@ -14,12 +14,10 @@
* *
* - Footer area (currently empty, reserved for future use) * - Footer area (currently empty, reserved for future use)
*/ */
import { BreadcrumbHeader } from '$entities/Breadcrumb';
import { themeManager } from '$features/ChangeAppTheme'; import { 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 { Provider as TooltipProvider } from '$shared/shadcn/ui/tooltip'; import clsx from 'clsx';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { import {
type Snippet, type Snippet,
onDestroy, onDestroy,
@@ -80,16 +78,14 @@ onDestroy(() => themeManager.destroy());
<ResponsiveProvider> <ResponsiveProvider>
<div <div
id="app-root" id="app-root"
class={cn( class={clsx(
'min-h-screen w-auto 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' : '',
)} )}
> >
<TooltipProvider>
{#if fontsReady} {#if fontsReady}
{@render children?.()} {@render children?.()}
{/if} {/if}
</TooltipProvider>
<footer></footer> <footer></footer>
</div> </div>
</ResponsiveProvider> </ResponsiveProvider>
@@ -105,13 +105,17 @@ class ScrollBreadcrumbsStore {
* (fires as soon as any part of element crosses viewport edge). * (fires as soon as any part of element crosses viewport edge).
*/ */
#initObserver(): void { #initObserver(): void {
if (this.#observer) return; if (this.#observer) {
return;
}
this.#observer = new IntersectionObserver( this.#observer = new IntersectionObserver(
entries => { entries => {
for (const entry of entries) { for (const entry of entries) {
const item = this.#items.find(i => i.element === entry.target); const item = this.#items.find(i => i.element === entry.target);
if (!item) continue; if (!item) {
continue;
}
if (!entry.isIntersecting && this.#isScrollingDown) { if (!entry.isIntersecting && this.#isScrollingDown) {
// Element exited viewport while scrolling DOWN - add to breadcrumbs // Element exited viewport while scrolling DOWN - add to breadcrumbs
@@ -199,7 +203,9 @@ class ScrollBreadcrumbsStore {
* @param offset - Scroll offset in pixels (for sticky headers) * @param offset - Scroll offset in pixels (for sticky headers)
*/ */
add(item: BreadcrumbItem, offset = 0): void { add(item: BreadcrumbItem, offset = 0): void {
if (this.#items.find(i => i.index === item.index)) return; if (this.#items.find(i => i.index === item.index)) {
return;
}
this.#scrollOffset = offset; this.#scrollOffset = offset;
this.#items.push(item); this.#items.push(item);
@@ -216,7 +222,9 @@ class ScrollBreadcrumbsStore {
*/ */
remove(index: number): void { remove(index: number): void {
const item = this.#items.find(i => i.index === index); const item = this.#items.find(i => i.index === index);
if (!item) return; if (!item) {
return;
}
this.#observer?.unobserve(item.element); this.#observer?.unobserve(item.element);
this.#items = this.#items.filter(i => i.index !== index); this.#items = this.#items.filter(i => i.index !== index);
@@ -237,7 +245,9 @@ class ScrollBreadcrumbsStore {
*/ */
scrollTo(index: number, container: HTMLElement | Window = window): void { scrollTo(index: number, container: HTMLElement | Window = window): void {
const item = this.#items.find(i => i.index === index); const item = this.#items.find(i => i.index === index);
if (!item) return; if (!item) {
return;
}
const rect = item.element.getBoundingClientRect(); const rect = item.element.getBoundingClientRect();
const scrollTop = container === window ? window.scrollY : (container as HTMLElement).scrollTop; const scrollTop = container === window ? window.scrollY : (container as HTMLElement).scrollTop;
@@ -26,7 +26,9 @@ class MockIntersectionObserver implements IntersectionObserver {
constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) { constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) {
this.callbacks.push(callback); this.callbacks.push(callback);
if (options?.rootMargin) this.rootMargin = options.rootMargin; if (options?.rootMargin) {
this.rootMargin = options.rootMargin;
}
if (options?.threshold) { if (options?.threshold) {
this.thresholds = Array.isArray(options.threshold) ? options.threshold : [options.threshold]; this.thresholds = Array.isArray(options.threshold) ? options.threshold : [options.threshold];
} }
@@ -120,7 +122,9 @@ describe('ScrollBreadcrumbsStore', () => {
(event: string, listener: EventListenerOrEventListenerObject) => { (event: string, listener: EventListenerOrEventListenerObject) => {
if (event === 'scroll') { if (event === 'scroll') {
const index = scrollListeners.indexOf(listener as () => void); const index = scrollListeners.indexOf(listener as () => void);
if (index > -1) scrollListeners.splice(index, 1); if (index > -1) {
scrollListeners.splice(index, 1);
}
} }
return undefined; return undefined;
}, },
+3 -1
View File
@@ -197,7 +197,9 @@ export async function fetchProxyFontById(
* @returns Promise resolving to an array of fonts * @returns Promise resolving to an array of fonts
*/ */
export async function fetchFontsByIds(ids: string[]): Promise<UnifiedFont[]> { export async function fetchFontsByIds(ids: string[]): Promise<UnifiedFont[]> {
if (ids.length === 0) return []; if (ids.length === 0) {
return [];
}
const queryString = ids.join(','); const queryString = ids.join(',');
const url = `${PROXY_API_URL}/batch?ids=${queryString}`; const url = `${PROXY_API_URL}/batch?ids=${queryString}`;
+6 -2
View File
@@ -529,8 +529,12 @@ export function createMockStore<T>(config: {
* Returns semantic status string * Returns semantic status string
*/ */
get status() { get status() {
if (isLoading) return 'pending'; if (isLoading) {
if (isError) return 'error'; return 'pending';
}
if (isError) {
return 'error';
}
return 'success'; return 'success';
}, },
}; };
@@ -93,12 +93,16 @@ export function createFontRowSizeResolver(options: FontRowSizeResolverOptions):
return function resolveRowHeight(rowIndex: number): number { return function resolveRowHeight(rowIndex: number): number {
const fonts = options.getFonts(); const fonts = options.getFonts();
const font = fonts[rowIndex]; const font = fonts[rowIndex];
if (!font) return options.fallbackHeight; if (!font) {
return options.fallbackHeight;
}
const containerWidth = options.getContainerWidth(); const containerWidth = options.getContainerWidth();
const previewText = options.getPreviewText(); const previewText = options.getPreviewText();
if (containerWidth <= 0 || !previewText) return options.fallbackHeight; if (containerWidth <= 0 || !previewText) {
return options.fallbackHeight;
}
const weight = options.getWeight(); const weight = options.getWeight();
// generateFontKey: '{id}@{weight}' for static fonts, '{id}@vf' for variable fonts. // generateFontKey: '{id}@{weight}' for static fonts, '{id}@vf' for variable fonts.
@@ -107,7 +111,9 @@ export function createFontRowSizeResolver(options: FontRowSizeResolverOptions):
// Reading via getStatus() allows the caller to pass appliedFontsManager.statuses.get(), // Reading via getStatus() allows the caller to pass appliedFontsManager.statuses.get(),
// which creates a Svelte 5 reactive dependency when called inside $derived.by. // which creates a Svelte 5 reactive dependency when called inside $derived.by.
const status = options.getStatus(fontKey); const status = options.getStatus(fontKey);
if (status !== 'loaded') return options.fallbackHeight; if (status !== 'loaded') {
return options.fallbackHeight;
}
const fontSizePx = options.getFontSizePx(); const fontSizePx = options.getFontSizePx();
const lineHeightPx = options.getLineHeightPx(); const lineHeightPx = options.getLineHeightPx();
@@ -116,7 +122,9 @@ export function createFontRowSizeResolver(options: FontRowSizeResolverOptions):
const cacheKey = `${fontCssString}|${previewText}|${contentWidth}|${lineHeightPx}`; const cacheKey = `${fontCssString}|${previewText}|${contentWidth}|${lineHeightPx}`;
const cached = cache.get(cacheKey); const cached = cache.get(cacheKey);
if (cached !== undefined) return cached; if (cached !== undefined) {
return cached;
}
const { totalHeight } = engine.layout(previewText, fontCssString, contentWidth, lineHeightPx); const { totalHeight } = engine.layout(previewText, fontCssString, contentWidth, lineHeightPx);
const result = totalHeight + options.chromeHeight; const result = totalHeight + options.chromeHeight;
@@ -246,12 +246,16 @@ export class AppliedFontsManager {
); );
for (const result of results) { for (const result of results) {
if (result.ok) continue; if (result.ok) {
continue;
}
const { key, config, reason } = result; const { key, config, reason } = result;
const isAbort = reason instanceof FontFetchError const isAbort = reason instanceof FontFetchError
&& reason.cause instanceof Error && reason.cause instanceof Error
&& reason.cause.name === 'AbortError'; && reason.cause.name === 'AbortError';
if (isAbort) continue; if (isAbort) {
continue;
}
if (reason instanceof FontFetchError) { if (reason instanceof FontFetchError) {
console.error(`Font fetch failed: ${config.name}`, reason); console.error(`Font fetch failed: ${config.name}`, reason);
} }
@@ -293,7 +297,9 @@ export class AppliedFontsManager {
// Remove FontFace from document to free memory // Remove FontFace from document to free memory
const font = this.#loadedFonts.get(key); const font = this.#loadedFonts.get(key);
if (font) document.fonts.delete(font); if (font) {
document.fonts.delete(font);
}
// Evict from cache and cleanup URL mapping // Evict from cache and cleanup URL mapping
const url = this.#urlByKey.get(key); const url = this.#urlByKey.get(key);
@@ -15,7 +15,9 @@ import type { UnifiedFont } from '../../model/types';
* Standalone function to avoid 'this' issues during construction. * Standalone function to avoid 'this' issues during construction.
*/ */
async function fetchAndSeed(ids: string[]): Promise<UnifiedFont[]> { async function fetchAndSeed(ids: string[]): Promise<UnifiedFont[]> {
if (ids.length === 0) return []; if (ids.length === 0) {
return [];
}
let response: UnifiedFont[]; let response: UnifiedFont[];
try { try {
@@ -166,7 +166,9 @@ export class FontStore {
const data = this.#qc.getQueryData<InfiniteData<ProxyFontsResponse, PageParam>>( const data = this.#qc.getQueryData<InfiniteData<ProxyFontsResponse, PageParam>>(
this.buildQueryKey(this.#params), this.buildQueryKey(this.#params),
); );
if (!data) return undefined; if (!data) {
return undefined;
}
return data.pages.flatMap(p => p.fonts); return data.pages.flatMap(p => p.fonts);
} }
@@ -336,9 +338,15 @@ export class FontStore {
throw new FontNetworkError(cause); throw new FontNetworkError(cause);
} }
if (!response) throw new FontResponseError('response', response); if (!response) {
if (!response.fonts) throw new FontResponseError('response.fonts', response.fonts); throw new FontResponseError('response', response);
if (!Array.isArray(response.fonts)) throw new FontResponseError('response.fonts', response.fonts); }
if (!response.fonts) {
throw new FontResponseError('response.fonts', response.fonts);
}
if (!Array.isArray(response.fonts)) {
throw new FontResponseError('response.fonts', response.fonts);
}
return { return {
fonts: response.fonts, fonts: response.fonts,
@@ -6,7 +6,7 @@
- Adds smooth transition when font appears - Adds smooth transition when font appears
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { prefersReducedMotion } from 'svelte/motion'; import { prefersReducedMotion } from 'svelte/motion';
import { import {
@@ -64,7 +64,7 @@ const transitionClasses = $derived(
style:font-family={shouldReveal style:font-family={shouldReveal
? `'${font.name}'` ? `'${font.name}'`
: 'system-ui, -apple-system, sans-serif'} : 'system-ui, -apple-system, sans-serif'}
class={cn( class={clsx(
transitionClasses, transitionClasses,
// If reduced motion is on, we skip the transform/blur entirely // If reduced motion is on, we skip the transform/blur entirely
!shouldReveal !shouldReveal
@@ -65,7 +65,9 @@ function handleInternalVisibleChange(items: UnifiedFont[]) {
$effect(() => { $effect(() => {
const configs: FontLoadRequestConfig[] = visibleFonts.flatMap(item => { const configs: FontLoadRequestConfig[] = visibleFonts.flatMap(item => {
const url = getFontUrl(item, weight); const url = getFontUrl(item, weight);
if (!url) return []; if (!url) {
return [];
}
return [{ id: item.id, name: item.name, weight, url, isVariable: item.features?.isVariable }]; return [{ id: item.id, name: item.name, weight, url, isVariable: item.features?.isVariable }];
}); });
if (configs.length > 0) { if (configs.length > 0) {
@@ -6,10 +6,10 @@
<script lang="ts"> <script lang="ts">
import { fontStore } from '$entities/Font'; import { fontStore } from '$entities/Font';
import type { ResponsiveManager } from '$shared/lib'; import type { ResponsiveManager } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Button } from '$shared/ui'; import { Button } from '$shared/ui';
import { Label } from '$shared/ui'; import { Label } from '$shared/ui';
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw'; import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
import clsx from 'clsx';
import { import {
getContext, getContext,
untrack, untrack,
@@ -45,7 +45,7 @@ function handleReset() {
</script> </script>
<div <div
class={cn( class={clsx(
'flex flex-col md:flex-row justify-between items-start md:items-center', 'flex flex-col md:flex-row justify-between items-start md:items-center',
'gap-1 md:gap-6', 'gap-1 md:gap-6',
'pt-6 mt-6 md:pt-8 md:mt-8', 'pt-6 mt-6 md:pt-8 md:mt-8',
@@ -77,7 +77,7 @@ function handleReset() {
variant="ghost" variant="ghost"
size={isMobileOrTabletPortrait ? 'xs' : 'sm'} size={isMobileOrTabletPortrait ? 'xs' : 'sm'}
onclick={handleReset} onclick={handleReset}
class={cn('group font-mono tracking-widest text-neutral-400', isMobileOrTabletPortrait && 'px-0')} class={clsx('group font-mono tracking-widest text-neutral-400', isMobileOrTabletPortrait && 'px-0')}
iconPosition="left" iconPosition="left"
> >
{#snippet icon()} {#snippet icon()}
@@ -128,7 +128,9 @@ export class TypographySettingsManager {
// This handles the "Multiplier" logic specifically for the Font Size Control // This handles the "Multiplier" logic specifically for the Font Size Control
$effect(() => { $effect(() => {
const ctrl = this.#controls.get('font_size')?.instance; const ctrl = this.#controls.get('font_size')?.instance;
if (!ctrl) return; if (!ctrl) {
return;
}
// If the user moves the slider/clicks buttons in the UI: // If the user moves the slider/clicks buttons in the UI:
// We update the baseSize (User Intent) // We update the baseSize (User Intent)
@@ -147,10 +149,18 @@ export class TypographySettingsManager {
* Gets initial value for a control from storage or defaults * Gets initial value for a control from storage or defaults
*/ */
#getInitialValue(id: string, saved: TypographySettings): number { #getInitialValue(id: string, saved: TypographySettings): number {
if (id === 'font_size') return saved.fontSize * this.#multiplier; if (id === 'font_size') {
if (id === 'font_weight') return saved.fontWeight; return saved.fontSize * this.#multiplier;
if (id === 'line_height') return saved.lineHeight; }
if (id === 'letter_spacing') return saved.letterSpacing; if (id === 'font_weight') {
return saved.fontWeight;
}
if (id === 'line_height') {
return saved.lineHeight;
}
if (id === 'letter_spacing') {
return saved.letterSpacing;
}
return 0; return 0;
} }
@@ -165,7 +175,9 @@ export class TypographySettingsManager {
* Updates the multiplier and recalculates dependent control values * Updates the multiplier and recalculates dependent control values
*/ */
set multiplier(value: number) { set multiplier(value: number) {
if (this.#multiplier === value) return; if (this.#multiplier === value) {
return;
}
this.#multiplier = value; this.#multiplier = value;
// When multiplier changes, we must update the Font Size Control's display value // When multiplier changes, we must update the Font Size Control's display value
@@ -192,7 +204,9 @@ export class TypographySettingsManager {
set baseSize(val: number) { set baseSize(val: number) {
this.#baseSize = val; this.#baseSize = val;
const ctrl = this.#controls.get('font_size')?.instance; const ctrl = this.#controls.get('font_size')?.instance;
if (ctrl) ctrl.value = val * this.#multiplier; if (ctrl) {
ctrl.value = val * this.#multiplier;
}
} }
/** /**
@@ -268,9 +282,15 @@ export class TypographySettingsManager {
// Map storage key to control id // Map storage key to control id
const key = c.id.replace('_', '') as keyof TypographySettings; const key = c.id.replace('_', '') as keyof TypographySettings;
// Simplified for brevity, you'd map these properly: // Simplified for brevity, you'd map these properly:
if (c.id === 'font_weight') c.instance.value = defaults.fontWeight; if (c.id === 'font_weight') {
if (c.id === 'line_height') c.instance.value = defaults.lineHeight; c.instance.value = defaults.fontWeight;
if (c.id === 'letter_spacing') c.instance.value = defaults.letterSpacing; }
if (c.id === 'line_height') {
c.instance.value = defaults.lineHeight;
}
if (c.id === 'letter_spacing') {
c.instance.value = defaults.letterSpacing;
}
} }
}); });
} }
@@ -12,7 +12,6 @@ import {
MULTIPLIER_S, MULTIPLIER_S,
} from '$entities/Font'; } from '$entities/Font';
import type { ResponsiveManager } from '$shared/lib'; import type { ResponsiveManager } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { import {
ComboControl, ComboControl,
ControlGroup, ControlGroup,
@@ -21,6 +20,7 @@ import {
import Settings2Icon from '@lucide/svelte/icons/settings-2'; import Settings2Icon from '@lucide/svelte/icons/settings-2';
import XIcon from '@lucide/svelte/icons/x'; import XIcon from '@lucide/svelte/icons/x';
import { Popover } from 'bits-ui'; import { Popover } from 'bits-ui';
import clsx from 'clsx';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
@@ -48,7 +48,9 @@ let isOpen = $state(false);
* Sets the common font size multiplier based on the current responsive state. * Sets the common font size multiplier based on the current responsive state.
*/ */
$effect(() => { $effect(() => {
if (!responsive) return; if (!responsive) {
return;
}
switch (true) { switch (true) {
case responsive.isMobile: case responsive.isMobile:
typographySettingsStore.multiplier = MULTIPLIER_S; typographySettingsStore.multiplier = MULTIPLIER_S;
@@ -72,7 +74,7 @@ $effect(() => {
{#snippet child({ props })} {#snippet child({ props })}
<button <button
{...props} {...props}
class={cn( class={clsx(
'inline-flex items-center justify-center', 'inline-flex items-center justify-center',
'size-8 p-0', 'size-8 p-0',
'border border-transparent rounded-none', 'border border-transparent rounded-none',
@@ -93,7 +95,7 @@ $effect(() => {
side="top" side="top"
align="start" align="start"
sideOffset={8} sideOffset={8}
class={cn( class={clsx(
'z-50 w-72', 'z-50 w-72',
'bg-surface dark:bg-dark-card', 'bg-surface dark:bg-dark-card',
'border border-subtle', 'border border-subtle',
@@ -147,11 +149,11 @@ $effect(() => {
</Popover.Root> </Popover.Root>
{:else} {:else}
<div <div
class={cn('w-full md:w-auto', className)} class={clsx('w-full md:w-auto', className)}
transition:fly={{ y: 100, duration: 200, easing: cubicOut }} transition:fly={{ y: 100, duration: 200, easing: cubicOut }}
> >
<div <div
class={cn( class={clsx(
'flex items-center gap-1 md:gap-2 p-1.5 md:p-2', 'flex items-center gap-1 md:gap-2 p-1.5 md:p-2',
'bg-surface/95 dark:bg-dark-bg/95 backdrop-blur-xl', 'bg-surface/95 dark:bg-dark-bg/95 backdrop-blur-xl',
'border border-subtle', 'border border-subtle',
@@ -171,7 +171,9 @@ export class CharacterComparisonEngine {
for (let sIdx = start.segmentIndex; sIdx <= end.segmentIndex; sIdx++) { for (let sIdx = start.segmentIndex; sIdx <= end.segmentIndex; sIdx++) {
const segmentText = this.#preparedA!.segments[sIdx]; const segmentText = this.#preparedA!.segments[sIdx];
if (segmentText === undefined) continue; if (segmentText === undefined) {
continue;
}
// PERFORMANCE: Reuse segmenter results if possible, but for now just optimize the loop // PERFORMANCE: Reuse segmenter results if possible, but for now just optimize the loop
const graphemes = Array.from(this.#segmenter.segment(segmentText), s => s.segment); const graphemes = Array.from(this.#segmenter.segment(segmentText), s => s.segment);
@@ -242,7 +244,9 @@ export class CharacterComparisonEngine {
const line = this.#lastResult.lines[lineIndex]; const line = this.#lastResult.lines[lineIndex];
const char = line.chars[charIndex]; const char = line.chars[charIndex];
if (!char) return { proximity: 0, isPast: false }; if (!char) {
return { proximity: 0, isPast: false };
}
// Center the comparison on the unified width // Center the comparison on the unified width
// In the UI, lines are centered. So we need to calculate the global X. // In the UI, lines are centered. So we need to calculate the global X.
@@ -279,9 +283,15 @@ export class CharacterComparisonEngine {
unified.breakableFitAdvances = intA.breakableFitAdvances.map((advA: number[] | null, i: number) => { unified.breakableFitAdvances = intA.breakableFitAdvances.map((advA: number[] | null, i: number) => {
const advB = intB.breakableFitAdvances[i]; const advB = intB.breakableFitAdvances[i];
if (!advA && !advB) return null; if (!advA && !advB) {
if (!advA) return advB; return null;
if (!advB) return advA; }
if (!advA) {
return advB;
}
if (!advB) {
return advA;
}
return advA.map((w: number, j: number) => Math.max(w, advB[j])); return advA.map((w: number, j: number) => Math.max(w, advB[j]));
}); });
@@ -127,7 +127,9 @@ export class TextLayoutEngine {
// Both cursors are grapheme-level: start is inclusive, end is exclusive. // Both cursors are grapheme-level: start is inclusive, end is exclusive.
for (let sIdx = start.segmentIndex; sIdx <= end.segmentIndex; sIdx++) { for (let sIdx = start.segmentIndex; sIdx <= end.segmentIndex; sIdx++) {
const segmentText = prepared.segments[sIdx]; const segmentText = prepared.segments[sIdx];
if (segmentText === undefined) continue; if (segmentText === undefined) {
continue;
}
const graphemes = Array.from(this.#segmenter.segment(segmentText), s => s.segment); const graphemes = Array.from(this.#segmenter.segment(segmentText), s => s.segment);
const advances = breakableFitAdvances[sIdx]; const advances = breakableFitAdvances[sIdx];
@@ -150,7 +150,9 @@ export function createResponsiveManager(customBreakpoints?: Partial<Breakpoints>
* @returns Cleanup function to remove listeners * @returns Cleanup function to remove listeners
*/ */
function init() { function init() {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') {
return;
}
const handleResize = () => { const handleResize = () => {
width = window.innerWidth; width = window.innerWidth;
@@ -175,7 +175,9 @@ export function createVirtualizer<T>(
const { count, data } = options; const { count, data } = options;
// Implicit dependency // Implicit dependency
const v = _version; const v = _version;
if (count === 0 || containerHeight === 0 || !data) return []; if (count === 0 || containerHeight === 0 || !data) {
return [];
}
const overscan = options.overscan ?? 5; const overscan = options.overscan ?? 5;
@@ -264,7 +266,9 @@ export function createVirtualizer<T>(
containerHeight = window.innerHeight; containerHeight = window.innerHeight;
const handleScroll = () => { const handleScroll = () => {
if (rafId !== null) return; if (rafId !== null) {
return;
}
rafId = requestAnimationFrame(() => { rafId = requestAnimationFrame(() => {
// Get current position of element relative to viewport // Get current position of element relative to viewport
@@ -323,7 +327,9 @@ export function createVirtualizer<T>(
}; };
const resizeObserver = new ResizeObserver(([entry]) => { const resizeObserver = new ResizeObserver(([entry]) => {
if (entry) containerHeight = entry.contentRect.height; if (entry) {
containerHeight = entry.contentRect.height;
}
}); });
node.addEventListener('scroll', handleScroll, { passive: true }); node.addEventListener('scroll', handleScroll, { passive: true });
@@ -423,7 +429,9 @@ export function createVirtualizer<T>(
* ``` * ```
*/ */
function scrollToIndex(index: number, align: 'start' | 'center' | 'end' | 'auto' = 'auto') { function scrollToIndex(index: number, align: 'start' | 'center' | 'end' | 'auto' = 'auto') {
if (!elementRef || index < 0 || index >= options.count) return; if (!elementRef || index < 0 || index >= options.count) {
return;
}
const itemStart = offsets[index]; const itemStart = offsets[index];
const itemSize = measuredSizes[index] ?? options.estimateSize(index); const itemSize = measuredSizes[index] ?? options.estimateSize(index);
@@ -431,16 +439,24 @@ export function createVirtualizer<T>(
const { useWindowScroll } = optionsGetter(); const { useWindowScroll } = optionsGetter();
if (useWindowScroll) { if (useWindowScroll) {
if (align === 'center') target = itemStart - window.innerHeight / 2 + itemSize / 2; if (align === 'center') {
if (align === 'end') target = itemStart - window.innerHeight + itemSize; target = itemStart - window.innerHeight / 2 + itemSize / 2;
}
if (align === 'end') {
target = itemStart - window.innerHeight + itemSize;
}
// Add container offset to target to get absolute document position // Add container offset to target to get absolute document position
const absoluteTarget = target + elementOffsetTop; const absoluteTarget = target + elementOffsetTop;
window.scrollTo({ top: absoluteTarget, behavior: 'smooth' }); window.scrollTo({ top: absoluteTarget, behavior: 'smooth' });
} else { } else {
if (align === 'center') target = itemStart - containerHeight / 2 + itemSize / 2; if (align === 'center') {
if (align === 'end') target = itemStart - containerHeight + itemSize; target = itemStart - containerHeight / 2 + itemSize / 2;
}
if (align === 'end') {
target = itemStart - containerHeight + itemSize;
}
elementRef.scrollTo({ top: target, behavior: 'smooth' }); elementRef.scrollTo({ top: target, behavior: 'smooth' });
} }
+2 -2
View File
@@ -7,7 +7,7 @@
correctly via the HTML element's class attribute. correctly via the HTML element's class attribute.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
import type { import type {
Component, Component,
Snippet, Snippet,
@@ -32,7 +32,7 @@ let { icon: Icon, class: className, attrs = {} }: Props = $props();
</script> </script>
{#if Icon} {#if Icon}
{@const __iconClass__ = cn('size-4', className)} {@const __iconClass__ = clsx('size-4', className)}
<!-- Render icon component dynamically with class prop --> <!-- Render icon component dynamically with class prop -->
<Icon <Icon
class={__iconClass__} class={__iconClass__}
@@ -4,13 +4,11 @@
Provides: Provides:
- responsive: ResponsiveManager context for breakpoint tracking - responsive: ResponsiveManager context for breakpoint tracking
- tooltip: Tooltip.Provider context for shadcn Tooltip components
- Additional Radix UI providers can be added here as needed - Additional Radix UI providers can be added here as needed
--> -->
<script lang="ts"> <script lang="ts">
import { createResponsiveManager } from '$shared/lib'; import { createResponsiveManager } from '$shared/lib';
import type { ResponsiveManager } from '$shared/lib'; import type { ResponsiveManager } from '$shared/lib';
import { Provider as TooltipProvider } from '$shared/shadcn/ui/tooltip';
import { setContext } from 'svelte'; import { setContext } from 'svelte';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
@@ -60,10 +58,5 @@ setContext<ResponsiveManager>('responsive', responsiveManager);
</script> </script>
<div class="storybook-providers" style:width="100%" style:height="100%"> <div class="storybook-providers" style:width="100%" style:height="100%">
<TooltipProvider
delayDuration={tooltipDelayDuration}
skipDelayDuration={tooltipSkipDelayDuration}
>
{@render children()} {@render children()}
</TooltipProvider>
</div> </div>
@@ -18,7 +18,9 @@ export function smoothScroll(node: HTMLAnchorElement) {
event.preventDefault(); event.preventDefault();
const hash = node.getAttribute('href'); const hash = node.getAttribute('href');
if (!hash || hash === '#') return; if (!hash || hash === '#') {
return;
}
const targetElement = document.querySelector(hash); const targetElement = document.querySelector(hash);
+3 -1
View File
@@ -35,7 +35,9 @@ export function throttle<T extends (...args: any[]) => any>(
fn(...args); fn(...args);
} else { } else {
// Schedule for end of wait period (trailing edge) // Schedule for end of wait period (trailing edge)
if (timeoutId) clearTimeout(timeoutId); if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
lastCall = Date.now(); lastCall = Date.now();
fn(...args); fn(...args);
@@ -1,9 +0,0 @@
import { MediaQuery } from 'svelte/reactivity';
const DEFAULT_MOBILE_BREAKPOINT = 768;
export class IsMobile extends MediaQuery {
constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) {
super(`max-width: ${breakpoint - 1}px`);
}
}
-19
View File
@@ -1,19 +0,0 @@
import Close from './popover-close.svelte';
import Content from './popover-content.svelte';
import Portal from './popover-portal.svelte';
import Trigger from './popover-trigger.svelte';
import Root from './popover.svelte';
export {
Close,
Close as PopoverClose,
Content,
Content as PopoverContent,
Portal,
Portal as PopoverPortal,
Root,
//
Root as Popover,
Trigger,
Trigger as PopoverTrigger,
};
@@ -1,7 +0,0 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from 'bits-ui';
let { ref = $bindable(null), ...restProps }: PopoverPrimitive.CloseProps = $props();
</script>
<PopoverPrimitive.Close bind:ref data-slot="popover-close" {...restProps} />
@@ -1,34 +0,0 @@
<script lang="ts">
import {
type WithoutChildrenOrChild,
cn,
} from '$shared/shadcn/utils/shadcn-utils.js';
import { Popover as PopoverPrimitive } from 'bits-ui';
import type { ComponentProps } from 'svelte';
import PopoverPortal from './popover-portal.svelte';
let {
ref = $bindable(null),
class: className,
sideOffset = 4,
align = 'center',
portalProps,
...restProps
}: PopoverPrimitive.ContentProps & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof PopoverPortal>>;
} = $props();
</script>
<PopoverPortal {...portalProps}>
<PopoverPrimitive.Content
bind:ref
data-slot="popover-content"
{sideOffset}
{align}
class={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--bits-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
className,
)}
{...restProps}
/>
</PopoverPortal>
@@ -1,7 +0,0 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from 'bits-ui';
let { ...restProps }: PopoverPrimitive.PortalProps = $props();
</script>
<PopoverPrimitive.Portal {...restProps} />
@@ -1,17 +0,0 @@
<script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
import { Popover as PopoverPrimitive } from 'bits-ui';
let {
ref = $bindable(null),
class: className,
...restProps
}: PopoverPrimitive.TriggerProps = $props();
</script>
<PopoverPrimitive.Trigger
bind:ref
data-slot="popover-trigger"
class={cn('', className)}
{...restProps}
/>
@@ -1,7 +0,0 @@
<script lang="ts">
import { Popover as PopoverPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: PopoverPrimitive.RootProps = $props();
</script>
<PopoverPrimitive.Root bind:open {...restProps} />
-19
View File
@@ -1,19 +0,0 @@
import Content from './tooltip-content.svelte';
import Portal from './tooltip-portal.svelte';
import Provider from './tooltip-provider.svelte';
import Trigger from './tooltip-trigger.svelte';
import Root from './tooltip.svelte';
export {
Content,
Content as TooltipContent,
Portal,
Portal as TooltipPortal,
Provider,
Provider as TooltipProvider,
Root,
//
Root as Tooltip,
Trigger,
Trigger as TooltipTrigger,
};
@@ -1,53 +0,0 @@
<script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils.js';
import type { WithoutChildrenOrChild } from '$shared/shadcn/utils/shadcn-utils.js';
import { Tooltip as TooltipPrimitive } from 'bits-ui';
import type { ComponentProps } from 'svelte';
import TooltipPortal from './tooltip-portal.svelte';
let {
ref = $bindable(null),
class: className,
sideOffset = 0,
side = 'top',
children,
arrowClasses,
portalProps,
...restProps
}: TooltipPrimitive.ContentProps & {
arrowClasses?: string;
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof TooltipPortal>>;
} = $props();
</script>
<TooltipPortal {...portalProps}>
<TooltipPrimitive.Content
bind:ref
data-slot="tooltip-content"
{sideOffset}
{side}
class={cn(
'bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--bits-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
className,
)}
{...restProps}
>
{@render children?.()}
<TooltipPrimitive.Arrow>
{#snippet child({ props })}
<div
class={cn(
'bg-primary z-50 size-2.5 rotate-45 rounded-[2px]',
'data-[side=top]:translate-x-1/2 data-[side=top]:translate-y-[calc(-50%_+_2px)]',
'data-[side=bottom]:-translate-x-1/2 data-[side=bottom]:-translate-y-[calc(-50%_+_1px)]',
'data-[side=right]:translate-x-[calc(50%_+_2px)] data-[side=right]:translate-y-1/2',
'data-[side=left]:-translate-y-[calc(50%_-_3px)]',
arrowClasses,
)}
{...props}
>
</div>
{/snippet}
</TooltipPrimitive.Arrow>
</TooltipPrimitive.Content>
</TooltipPortal>
@@ -1,7 +0,0 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from 'bits-ui';
let { ...restProps }: TooltipPrimitive.PortalProps = $props();
</script>
<TooltipPrimitive.Portal {...restProps} />
@@ -1,7 +0,0 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from 'bits-ui';
let { ...restProps }: TooltipPrimitive.ProviderProps = $props();
</script>
<TooltipPrimitive.Provider {...restProps} />
@@ -1,7 +0,0 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from 'bits-ui';
let { ref = $bindable(null), ...restProps }: TooltipPrimitive.TriggerProps = $props();
</script>
<TooltipPrimitive.Trigger bind:ref data-slot="tooltip-trigger" {...restProps} />
@@ -1,7 +0,0 @@
<script lang="ts">
import { Tooltip as TooltipPrimitive } from 'bits-ui';
let { open = $bindable(false), ...restProps }: TooltipPrimitive.RootProps = $props();
</script>
<TooltipPrimitive.Root bind:open {...restProps} />
-40
View File
@@ -1,40 +0,0 @@
import {
type ClassValue,
clsx,
} from 'clsx';
import { twMerge } from 'tailwind-merge';
/**
* Utility function to merge Tailwind CSS classes
* Combines clsx for conditional classes and tailwind-merge to handle conflicts
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* Type utility to add a ref property to HTML element attributes
* Used in shadcn-svelte components to support element references
* @template T - The attributes type (e.g., HTMLAttributes<HTMLDivElement>)
* @template E - The element type (e.g., HTMLDivElement)
*/
export type WithElementRef<T, E extends HTMLElement = HTMLElement> = T & {
/**
* Reference to the DOM element
*/
ref?: E | null;
};
/**
* Type utility to remove 'children' and 'child' properties from a type
* Used in shadcn-svelte components that use Snippets instead of children
* @template T - The type to remove children from
*/
export type WithoutChildren<T> = Omit<T, 'children'>;
/**
* Type utility to remove 'children' and 'child' properties from a type
* Used in shadcn-svelte components that use Snippets instead of children
* @template T - The type to remove children and child from
*/
export type WithoutChildrenOrChild<T> = Omit<T, 'children' | 'child'>;
+2 -2
View File
@@ -3,11 +3,11 @@
Pill badge with border and optional status dot. Pill badge with border and optional status dot.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { import {
type LabelSize, type LabelSize,
labelSizeConfig, labelSizeConfig,
} from '$shared/ui/Label/config'; } from '$shared/ui/Label/config';
import clsx from 'clsx';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from 'svelte/elements';
@@ -64,7 +64,7 @@ let {
</script> </script>
<span <span
class={cn( class={clsx(
'inline-flex items-center gap-1 px-2 py-0.5 border rounded-full', 'inline-flex items-center gap-1 px-2 py-0.5 border rounded-full',
'font-mono uppercase tracking-wide', 'font-mono uppercase tracking-wide',
labelSizeConfig[size], labelSizeConfig[size],
+9 -9
View File
@@ -3,7 +3,7 @@
design-system button. Uppercase, zero border-radius, Space Grotesk. design-system button. Uppercase, zero border-radius, Space Grotesk.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements'; import type { HTMLButtonAttributes } from 'svelte/elements';
import type { import type {
@@ -71,7 +71,7 @@ let {
const isIconOnly = $derived(!!icon && !children); const isIconOnly = $derived(!!icon && !children);
const variantStyles: Record<ButtonVariant, string> = { const variantStyles: Record<ButtonVariant, string> = {
primary: cn( primary: clsx(
'bg-swiss-red text-white', 'bg-swiss-red text-white',
'hover:bg-swiss-red/90', 'hover:bg-swiss-red/90',
'active:bg-swiss-red/80', 'active:bg-swiss-red/80',
@@ -87,7 +87,7 @@ const variantStyles: Record<ButtonVariant, string> = {
'disabled:cursor-not-allowed', 'disabled:cursor-not-allowed',
'disabled:transform-none', 'disabled:transform-none',
), ),
secondary: cn( secondary: clsx(
'bg-surface dark:bg-paper', 'bg-surface dark:bg-paper',
'text-swiss-black dark:text-neutral-200', 'text-swiss-black dark:text-neutral-200',
'border border-black/10 dark:border-white/10', 'border border-black/10 dark:border-white/10',
@@ -98,7 +98,7 @@ const variantStyles: Record<ButtonVariant, string> = {
'disabled:text-neutral-400 dark:disabled:text-neutral-600', 'disabled:text-neutral-400 dark:disabled:text-neutral-600',
'disabled:cursor-not-allowed', 'disabled:cursor-not-allowed',
), ),
outline: cn( outline: clsx(
'bg-transparent', 'bg-transparent',
'text-swiss-black dark:text-neutral-200', 'text-swiss-black dark:text-neutral-200',
'border border-black/20 dark:border-white/20', 'border border-black/20 dark:border-white/20',
@@ -109,7 +109,7 @@ const variantStyles: Record<ButtonVariant, string> = {
'disabled:text-neutral-400 dark:disabled:text-neutral-600', 'disabled:text-neutral-400 dark:disabled:text-neutral-600',
'disabled:cursor-not-allowed', 'disabled:cursor-not-allowed',
), ),
ghost: cn( ghost: clsx(
'bg-transparent', 'bg-transparent',
'text-secondary', 'text-secondary',
'border border-transparent', 'border border-transparent',
@@ -119,7 +119,7 @@ const variantStyles: Record<ButtonVariant, string> = {
'disabled:text-neutral-400 dark:disabled:text-neutral-600', 'disabled:text-neutral-400 dark:disabled:text-neutral-600',
'disabled:cursor-not-allowed', 'disabled:cursor-not-allowed',
), ),
icon: cn( icon: clsx(
'bg-surface dark:bg-dark-bg', 'bg-surface dark:bg-dark-bg',
'text-secondary', 'text-secondary',
'border border-transparent', 'border border-transparent',
@@ -130,8 +130,8 @@ const variantStyles: Record<ButtonVariant, string> = {
'disabled:text-neutral-400 dark:disabled:text-neutral-600', 'disabled:text-neutral-400 dark:disabled:text-neutral-600',
'disabled:cursor-not-allowed', 'disabled:cursor-not-allowed',
), ),
tertiary: cn( tertiary: clsx(
// Font override — must come after base in cn() to win via tailwind-merge // Font override — must come after base in clsx() to win via tailwind-merge
'font-secondary font-medium normal-case tracking-normal', 'font-secondary font-medium normal-case tracking-normal',
// Inactive state // Inactive state
'bg-transparent', 'bg-transparent',
@@ -175,7 +175,7 @@ const activeStyles: Partial<Record<ButtonVariant, string>> = {
icon: 'bg-paper dark:bg-paper text-brand border-subtle', icon: 'bg-paper dark:bg-paper text-brand border-subtle',
}; };
const classes = $derived(cn( const classes = $derived(clsx(
// Base // Base
'inline-flex items-center justify-center', 'inline-flex items-center justify-center',
'font-primary font-bold tracking-tight uppercase', 'font-primary font-bold tracking-tight uppercase',
+2 -2
View File
@@ -4,7 +4,7 @@
Use for segmented controls, view toggles, or any mutually exclusive button set. Use for segmented controls, view toggles, or any mutually exclusive button set.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from 'svelte/elements';
@@ -23,7 +23,7 @@ let { children, class: className, ...rest }: Props = $props();
</script> </script>
<div <div
class={cn( class={clsx(
'flex items-center gap-1 p-1', 'flex items-center gap-1 p-1',
'bg-surface dark:bg-dark-bg', 'bg-surface dark:bg-dark-bg',
'border border-subtle', 'border border-subtle',
+14 -16
View File
@@ -5,16 +5,12 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { TypographyControl } from '$shared/lib'; import type { TypographyControl } from '$shared/lib';
import {
Content as PopoverContent,
Root as PopoverRoot,
Trigger as PopoverTrigger,
} from '$shared/shadcn/ui/popover';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Slider } from '$shared/ui'; import { Slider } from '$shared/ui';
import { Button } from '$shared/ui/Button'; import { Button } from '$shared/ui/Button';
import MinusIcon from '@lucide/svelte/icons/minus'; import MinusIcon from '@lucide/svelte/icons/minus';
import PlusIcon from '@lucide/svelte/icons/plus'; import PlusIcon from '@lucide/svelte/icons/plus';
import { Popover } from 'bits-ui';
import clsx from 'clsx';
import TechText from '../TechText/TechText.svelte'; import TechText from '../TechText/TechText.svelte';
interface Props { interface Props {
@@ -66,7 +62,9 @@ let open = $state(false);
// Smart value formatting matching the Figma design // Smart value formatting matching the Figma design
const formattedValue = $derived(() => { const formattedValue = $derived(() => {
const v = control.value; const v = control.value;
if (Number.isInteger(v)) return String(v); if (Number.isInteger(v)) {
return String(v);
}
return control.step < 0.1 ? v.toFixed(2) : v.toFixed(1); return control.step < 0.1 ? v.toFixed(2) : v.toFixed(1);
}); });
@@ -80,7 +78,7 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
--> -->
{#if reduced} {#if reduced}
<div <div
class={cn( class={clsx(
'flex gap-4 items-end w-full', 'flex gap-4 items-end w-full',
className, className,
)} )}
@@ -100,7 +98,7 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
<!-- ── FULL MODE ──────────────────────────────────────────────────────────────── --> <!-- ── FULL MODE ──────────────────────────────────────────────────────────────── -->
{:else} {:else}
<div class={cn('flex items-center px-1 relative', className)}> <div class={clsx('flex items-center px-1 relative', className)}>
<!-- Decrease button --> <!-- Decrease button -->
<Button <Button
variant="icon" variant="icon"
@@ -116,12 +114,12 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
<!-- Trigger --> <!-- Trigger -->
<div class="relative mx-1"> <div class="relative mx-1">
<PopoverRoot bind:open> <Popover.Root bind:open>
<PopoverTrigger> <Popover.Trigger>
{#snippet child({ props })} {#snippet child({ props })}
<button <button
{...props} {...props}
class={cn( class={clsx(
'flex flex-col items-center justify-center w-14 py-1', 'flex flex-col items-center justify-center w-14 py-1',
'select-none rounded-none transition-all duration-150', 'select-none rounded-none transition-all duration-150',
'border border-transparent', 'border border-transparent',
@@ -151,10 +149,10 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
</TechText> </TechText>
</button> </button>
{/snippet} {/snippet}
</PopoverTrigger> </Popover.Trigger>
<!-- Vertical slider popover --> <!-- Vertical slider popover -->
<PopoverContent <Popover.Content
class="w-auto py-4 px-3 h-64 flex items-center justify-center rounded-none border border-subtle shadow-sm bg-paper dark:bg-dark-card" class="w-auto py-4 px-3 h-64 flex items-center justify-center rounded-none border border-subtle shadow-sm bg-paper dark:bg-dark-card"
align="center" align="center"
side="top" side="top"
@@ -167,8 +165,8 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
step={control.step} step={control.step}
orientation="vertical" orientation="vertical"
/> />
</PopoverContent> </Popover.Content>
</PopoverRoot> </Popover.Root>
</div> </div>
<!-- Increase button --> <!-- Increase button -->
@@ -3,7 +3,7 @@
Labeled container for form controls Labeled container for form controls
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
interface Props { interface Props {
@@ -24,7 +24,7 @@ interface Props {
const { label, children, class: className }: Props = $props(); const { label, children, class: className }: Props = $props();
</script> </script>
<div class={cn('flex flex-col gap-3 py-6 border-b border-subtle last:border-0', className)}> <div class={clsx('flex flex-col gap-3 py-6 border-b border-subtle last:border-0', className)}>
<div class="flex justify-between items-center text-xs font-primary font-bold tracking-tight text-neutral-900 dark:text-neutral-100 uppercase leading-none"> <div class="flex justify-between items-center text-xs font-primary font-bold tracking-tight text-neutral-900 dark:text-neutral-100 uppercase leading-none">
{label} {label}
</div> </div>
+2 -2
View File
@@ -3,7 +3,7 @@
1px separator line, horizontal or vertical. 1px separator line, horizontal or vertical.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
interface Props { interface Props {
/** /**
@@ -24,7 +24,7 @@ let {
</script> </script>
<div <div
class={cn( class={clsx(
'bg-black/10 dark:bg-white/10', 'bg-black/10 dark:bg-white/10',
orientation === 'horizontal' ? 'w-full h-px' : 'w-px h-full', orientation === 'horizontal' ? 'w-full h-px' : 'w-px h-full',
className, className,
+2 -2
View File
@@ -4,11 +4,11 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { Filter } from '$shared/lib'; import type { Filter } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Button } from '$shared/ui'; import { Button } from '$shared/ui';
import { Label } from '$shared/ui'; import { Label } from '$shared/ui';
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up'; import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
import EllipsisIcon from '@lucide/svelte/icons/ellipsis'; import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
import clsx from 'clsx';
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
import { import {
draw, draw,
@@ -68,7 +68,7 @@ $effect(() => {
</svg> </svg>
{/snippet} {/snippet}
<div class={cn('flex flex-col', className)}> <div class={clsx('flex flex-col', className)}>
<Label <Label
variant="default" variant="default"
size="sm" size="sm"
+3 -3
View File
@@ -3,7 +3,7 @@
Provides classes for styling footnotes Provides classes for styling footnotes
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
interface Props { interface Props {
@@ -26,14 +26,14 @@ const { children, class: className, render }: Props = $props();
{#if render} {#if render}
{@render render({ {@render render({
class: cn( class: clsx(
'font-mono text-3xs sm:text-2xs lowercase tracking-wider-mono text-text-soft', 'font-mono text-3xs sm:text-2xs lowercase tracking-wider-mono text-text-soft',
className, className,
), ),
})} })}
{:else if children} {:else if children}
<span <span
class={cn( class={clsx(
'font-mono text-3xs sm:text-2xs lowercase tracking-wider-mono text-text-soft', 'font-mono text-3xs sm:text-2xs lowercase tracking-wider-mono text-text-soft',
className, className,
)} )}
+5 -5
View File
@@ -3,8 +3,8 @@
design-system input. Zero border-radius, Space Grotesk, precise states. design-system input. Zero border-radius, Space Grotesk, precise states.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import XIcon from '@lucide/svelte/icons/x'; import XIcon from '@lucide/svelte/icons/x';
import clsx from 'clsx';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
import type { HTMLInputAttributes } from 'svelte/elements'; import type { HTMLInputAttributes } from 'svelte/elements';
@@ -90,7 +90,7 @@ const hasRightSlot = $derived(!!rightIcon || showClearButton);
const cfg = $derived(inputSizeConfig[size]); const cfg = $derived(inputSizeConfig[size]);
const styles = $derived(inputVariantConfig[variant]); const styles = $derived(inputVariantConfig[variant]);
const inputClasses = $derived(cn( const inputClasses = $derived(clsx(
'font-primary rounded-none outline-none transition-all duration-200', 'font-primary rounded-none outline-none transition-all duration-200',
'text-neutral-900 dark:text-neutral-100', 'text-neutral-900 dark:text-neutral-100',
'placeholder:text-neutral-400 dark:placeholder:text-neutral-600', 'placeholder:text-neutral-400 dark:placeholder:text-neutral-600',
@@ -107,8 +107,8 @@ const inputClasses = $derived(cn(
)); ));
</script> </script>
<div class={cn('flex flex-col gap-1', fullWidth && 'w-full')}> <div class={clsx('flex flex-col gap-1', fullWidth && 'w-full')}>
<div class={cn('relative group', fullWidth && 'w-full')}> <div class={clsx('relative group', fullWidth && 'w-full')}>
<!-- Left icon slot --> <!-- Left icon slot -->
{#if leftIcon} {#if leftIcon}
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400 dark:text-neutral-600 pointer-events-none z-10 flex items-center"> <div class="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400 dark:text-neutral-600 pointer-events-none z-10 flex items-center">
@@ -147,7 +147,7 @@ const inputClasses = $derived(cn(
<!-- Helper / error text --> <!-- Helper / error text -->
{#if helperText} {#if helperText}
<span <span
class={cn( class={clsx(
'text-2xs font-mono tracking-wide px-1', 'text-2xs font-mono tracking-wide px-1',
error ? 'text-brand ' : 'text-secondary', error ? 'text-brand ' : 'text-secondary',
)} )}
+2 -2
View File
@@ -3,7 +3,7 @@
Inline monospace label. The base primitive for all micrographic text. Inline monospace label. The base primitive for all micrographic text.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { import {
type LabelFont, type LabelFont,
@@ -72,7 +72,7 @@ let {
</script> </script>
<span <span
class={cn( class={clsx(
'font-mono tracking-widest leading-none', 'font-mono tracking-widest leading-none',
'inline-flex items-center gap-1.5', 'inline-flex items-center gap-1.5',
font === 'primary' && 'font-primary tracking-tight', font === 'primary' && 'font-primary tracking-tight',
+2 -2
View File
@@ -3,8 +3,8 @@
Project logo with apropriate styles Project logo with apropriate styles
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Badge } from '$shared/ui'; import { Badge } from '$shared/ui';
import clsx from 'clsx';
interface Props { interface Props {
/** /**
@@ -18,7 +18,7 @@ const { class: className }: Props = $props();
const title = 'GLYPHDIFF'; const title = 'GLYPHDIFF';
</script> </script>
<div class={cn('flex items-center gap-2 md:gap-3 select-none', className)}> <div class={clsx('flex items-center gap-2 md:gap-3 select-none', className)}>
<h1 class="font-logo font-extrabold text-base md:text-xl tracking-tight text-swiss-black dark:text-neutral-200"> <h1 class="font-logo font-extrabold text-base md:text-xl tracking-tight text-swiss-black dark:text-neutral-200">
{title} {title}
</h1> </h1>
@@ -5,7 +5,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { PerspectiveManager } from '$shared/lib'; import type { PerspectiveManager } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
import { type Snippet } from 'svelte'; import { type Snippet } from 'svelte';
interface Props { interface Props {
@@ -61,7 +61,9 @@ const style = $derived.by(() => {
// Calculate horizontal constraints based on region // Calculate horizontal constraints based on region
const regionStyleStr = $derived(() => { const regionStyleStr = $derived(() => {
if (region === 'full') return ''; if (region === 'full') {
return '';
}
const side = region === 'left' ? 'left' : 'right'; const side = region === 'left' ? 'left' : 'right';
return `position: absolute; ${side}: 0; width: ${regionWidth}%; top: 0; bottom: 0;`; return `position: absolute; ${side}: 0; width: ${regionWidth}%; top: 0; bottom: 0;`;
}); });
@@ -71,7 +73,7 @@ const isVisible = $derived(manager.isFront);
</script> </script>
<div <div
class={cn('will-change-transform', className)} class={clsx('will-change-transform', className)}
style:transform-style="preserve-3d" style:transform-style="preserve-3d"
style:transform={style?.transform} style:transform={style?.transform}
style:filter={style?.filter} style:filter={style?.filter}
@@ -3,8 +3,8 @@
Numbered section heading with optional subtitle and pulse dot. Numbered section heading with optional subtitle and pulse dot.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Label } from '$shared/ui'; import { Label } from '$shared/ui';
import clsx from 'clsx';
interface Props { interface Props {
/** /**
@@ -41,7 +41,7 @@ let {
const indexStr = $derived(String(index).padStart(2, '0')); const indexStr = $derived(String(index).padStart(2, '0'));
</script> </script>
<div class={cn('flex items-center gap-3 md:gap-4 mb-2', className)}> <div class={clsx('flex items-center gap-3 md:gap-4 mb-2', className)}>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{#if pulse} {#if pulse}
<span class="w-1.5 h-1.5 bg-brand rounded-full animate-pulse"></span> <span class="w-1.5 h-1.5 bg-brand rounded-full animate-pulse"></span>
@@ -3,7 +3,7 @@
A horizontal separator line used to visually separate sections within a page. A horizontal separator line used to visually separate sections within a page.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
interface Props { interface Props {
/** /**
@@ -15,4 +15,4 @@ interface Props {
const { class: className = '' }: Props = $props(); const { class: className = '' }: Props = $props();
</script> </script>
<div class={cn('w-full h-px bg-swiss-black/5 dark:bg-white/10 my-8 md:my-12', className)}></div> <div class={clsx('w-full h-px bg-swiss-black/5 dark:bg-white/10 my-8 md:my-12', className)}></div>
@@ -4,7 +4,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { ResponsiveManager } from '$shared/lib'; import type { ResponsiveManager } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
@@ -79,7 +79,7 @@ function close() {
The inner div stays w-80 so Sidebar layout never reflows mid-animation. The inner div stays w-80 so Sidebar layout never reflows mid-animation.
--> -->
<div <div
class={cn( class={clsx(
'shrink-0 z-30 h-full relative', 'shrink-0 z-30 h-full relative',
'overflow-hidden', 'overflow-hidden',
'will-change-[width]', 'will-change-[width]',
+2 -2
View File
@@ -3,7 +3,7 @@
Generic loading placeholder with shimmer animation. Generic loading placeholder with shimmer animation.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from 'svelte/elements';
interface Props extends HTMLAttributes<HTMLDivElement> { interface Props extends HTMLAttributes<HTMLDivElement> {
@@ -18,7 +18,7 @@ let { class: className, animate = true, ...rest }: Props = $props();
</script> </script>
<div <div
class={cn( class={clsx(
'rounded-md bg-background-subtle/50 backdrop-blur-sm', 'rounded-md bg-background-subtle/50 backdrop-blur-sm',
animate && 'animate-pulse', animate && 'animate-pulse',
className, className,
+2 -2
View File
@@ -3,8 +3,8 @@
A single key:value pair in Space Mono. Optional trailing divider. A single key:value pair in Space Mono. Optional trailing divider.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Label } from '$shared/ui'; import { Label } from '$shared/ui';
import clsx from 'clsx';
import type { ComponentProps } from 'svelte'; import type { ComponentProps } from 'svelte';
interface Props extends Pick<ComponentProps<typeof Label>, 'variant'> { interface Props extends Pick<ComponentProps<typeof Label>, 'variant'> {
@@ -36,7 +36,7 @@ let {
}: Props = $props(); }: Props = $props();
</script> </script>
<div class={cn('flex items-center gap-1', className)}> <div class={clsx('flex items-center gap-1', className)}>
<Label variant="muted" size="xs">{label}:</Label> <Label variant="muted" size="xs">{label}:</Label>
<Label {variant} size="xs" bold>{value}</Label> <Label {variant} size="xs" bold>{value}</Label>
</div> </div>
+2 -2
View File
@@ -3,8 +3,8 @@
Renders multiple Stat components in a row with auto-separators. Renders multiple Stat components in a row with auto-separators.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Stat } from '$shared/ui'; import { Stat } from '$shared/ui';
import clsx from 'clsx';
import type { ComponentProps } from 'svelte'; import type { ComponentProps } from 'svelte';
interface StatItem extends Partial<Pick<ComponentProps<typeof Stat>, 'variant'>> { interface StatItem extends Partial<Pick<ComponentProps<typeof Stat>, 'variant'>> {
@@ -26,7 +26,7 @@ interface Props {
let { stats, class: className }: Props = $props(); let { stats, class: className }: Props = $props();
</script> </script>
<div class={cn('flex items-center gap-4', className)}> <div class={clsx('flex items-center gap-4', className)}>
{#each stats as stat, i} {#each stats as stat, i}
<Stat <Stat
label={stat.label} label={stat.label}
+2 -2
View File
@@ -3,13 +3,13 @@
Monospace <code> element for technical values, measurements, identifiers. Monospace <code> element for technical values, measurements, identifiers.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { import {
type LabelSize, type LabelSize,
type LabelVariant, type LabelVariant,
labelSizeConfig, labelSizeConfig,
labelVariantConfig, labelVariantConfig,
} from '$shared/ui/Label/config'; } from '$shared/ui/Label/config';
import clsx from 'clsx';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
interface Props { interface Props {
@@ -42,7 +42,7 @@ let {
</script> </script>
<code <code
class={cn( class={clsx(
'font-mono tracking-tight tabular-nums', 'font-mono tracking-tight tabular-nums',
labelSizeConfig[size], labelSizeConfig[size],
labelVariantConfig[variant], labelVariantConfig[variant],
+6 -4
View File
@@ -11,7 +11,7 @@
<script lang="ts" generics="T"> <script lang="ts" generics="T">
import { createVirtualizer } from '$shared/lib'; import { createVirtualizer } from '$shared/lib';
import { throttle } from '$shared/lib/utils'; import { throttle } from '$shared/lib/utils';
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements'; import type { HTMLAttributes } from 'svelte/elements';
@@ -136,7 +136,9 @@ const estimatedTotalSize = $derived.by(() => {
// Add estimated size for unloaded rows // Add estimated size for unloaded rows
const unloadedRows = totalRows - rowCount; const unloadedRows = totalRows - rowCount;
if (unloadedRows <= 0) return loadedSize; if (unloadedRows <= 0) {
return loadedSize;
}
// Estimate the size of unloaded rows // Estimate the size of unloaded rows
const estimateFn = typeof itemHeight === 'function' const estimateFn = typeof itemHeight === 'function'
@@ -293,13 +295,13 @@ $effect(() => {
{/snippet} {/snippet}
{#if useWindowScroll} {#if useWindowScroll}
<div class={cn('relative w-full', className)} bind:this={viewportRef} {...rest}> <div class={clsx('relative w-full', className)} bind:this={viewportRef} {...rest}>
{@render content()} {@render content()}
</div> </div>
{:else} {:else}
<div <div
bind:this={viewportRef} bind:this={viewportRef}
class={cn( class={clsx(
'relative overflow-y-auto overflow-x-hidden', 'relative overflow-y-auto overflow-x-hidden',
'rounded-md bg-background', 'rounded-md bg-background',
'w-full', 'w-full',
@@ -95,16 +95,22 @@ export class ComparisonStore {
// Effect 1: Sync batch results → fontA / fontB // Effect 1: Sync batch results → fontA / fontB
$effect(() => { $effect(() => {
const fonts = this.#batchStore.fonts; const fonts = this.#batchStore.fonts;
if (fonts.length === 0) return; if (fonts.length === 0) {
return;
}
const { fontAId: aId, fontBId: bId } = storage.value; const { fontAId: aId, fontBId: bId } = storage.value;
if (aId) { if (aId) {
const fa = fonts.find(f => f.id === aId); const fa = fonts.find(f => f.id === aId);
if (fa) this.#fontA = fa; if (fa) {
this.#fontA = fa;
}
} }
if (bId) { if (bId) {
const fb = fonts.find(f => f.id === bId); const fb = fonts.find(f => f.id === bId);
if (fb) this.#fontB = fb; if (fb) {
this.#fontB = fb;
}
} }
}); });
@@ -114,7 +120,9 @@ export class ComparisonStore {
const fb = this.#fontB; const fb = this.#fontB;
const weight = typographySettingsStore.weight; const weight = typographySettingsStore.weight;
if (!fa || !fb) return; if (!fa || !fb) {
return;
}
const configs: FontLoadRequestConfig[] = []; const configs: FontLoadRequestConfig[] = [];
[fa, fb].forEach(f => { [fa, fb].forEach(f => {
@@ -138,7 +146,9 @@ export class ComparisonStore {
// Effect 3: Set default fonts when storage is empty // Effect 3: Set default fonts when storage is empty
$effect(() => { $effect(() => {
if (this.#fontA && this.#fontB) return; if (this.#fontA && this.#fontB) {
return;
}
const fonts = fontStore.fonts; const fonts = fontStore.fonts;
if (fonts.length >= 2) { if (fonts.length >= 2) {
@@ -156,11 +166,19 @@ export class ComparisonStore {
const fa = this.#fontA; const fa = this.#fontA;
const fb = this.#fontB; const fb = this.#fontB;
const w = typographySettingsStore.weight; const w = typographySettingsStore.weight;
if (fa) appliedFontsManager.pin(fa.id, w, fa.features?.isVariable); if (fa) {
if (fb) appliedFontsManager.pin(fb.id, w, fb.features?.isVariable); appliedFontsManager.pin(fa.id, w, fa.features?.isVariable);
}
if (fb) {
appliedFontsManager.pin(fb.id, w, fb.features?.isVariable);
}
return () => { return () => {
if (fa) appliedFontsManager.unpin(fa.id, w, fa.features?.isVariable); if (fa) {
if (fb) appliedFontsManager.unpin(fb.id, w, fb.features?.isVariable); appliedFontsManager.unpin(fa.id, w, fa.features?.isVariable);
}
if (fb) {
appliedFontsManager.unpin(fb.id, w, fb.features?.isVariable);
}
}; };
}); });
}); });
@@ -183,7 +201,9 @@ export class ComparisonStore {
const fontAName = this.#fontA?.name; const fontAName = this.#fontA?.name;
const fontBName = this.#fontB?.name; const fontBName = this.#fontB?.name;
if (!fontAName || !fontBName) return; if (!fontAName || !fontBName) {
return;
}
const fontAString = `${weight} ${size}px "${fontAName}"`; const fontAString = `${weight} ${size}px "${fontAName}"`;
const fontBString = `${weight} ${size}px "${fontBName}"`; const fontBString = `${weight} ${size}px "${fontBName}"`;
@@ -4,7 +4,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { typographySettingsStore } from '$features/SetupFont'; import { typographySettingsStore } from '$features/SetupFont';
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
import { comparisonStore } from '../../model'; import { comparisonStore } from '../../model';
interface Props { interface Props {
@@ -35,7 +35,9 @@ const displayChar = $derived(char === ' ' ? '\u00A0' : char);
const targetFont = $derived(isPast ? fontA?.name ?? '' : fontB?.name ?? ''); const targetFont = $derived(isPast ? fontA?.name ?? '' : fontB?.name ?? '');
$effect(() => { $effect(() => {
if (!targetFont || slotFonts[slot] === targetFont) return; if (!targetFont || slotFonts[slot] === targetFont) {
return;
}
const next = slot === 0 ? 1 : 0; const next = slot === 0 ? 1 : 0;
slotFonts[next] = targetFont; slotFonts[next] = targetFont;
slot = next; slot = next;
@@ -50,7 +52,7 @@ $effect(() => {
> >
{#each [0, 1] as s (s)} {#each [0, 1] as s (s)}
<span <span
class={cn( class={clsx(
'char-inner', 'char-inner',
'transition-colors duration-300', 'transition-colors duration-300',
isPast isPast
@@ -5,7 +5,6 @@
<script lang="ts"> <script lang="ts">
import { ThemeSwitch } from '$features/ChangeAppTheme'; import { ThemeSwitch } from '$features/ChangeAppTheme';
import type { ResponsiveManager } from '$shared/lib'; import type { ResponsiveManager } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { import {
Badge, Badge,
Divider, Divider,
@@ -17,6 +16,7 @@ import {
} from '$shared/ui'; } from '$shared/ui';
import PanelLeftClose from '@lucide/svelte/icons/panel-left-close'; import PanelLeftClose from '@lucide/svelte/icons/panel-left-close';
import PanelLeftOpen from '@lucide/svelte/icons/panel-left-open'; import PanelLeftOpen from '@lucide/svelte/icons/panel-left-open';
import clsx from 'clsx';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { comparisonStore } from '../../model'; import { comparisonStore } from '../../model';
@@ -49,7 +49,7 @@ const fontBName = $derived(comparisonStore.fontB?.name ?? '');
</script> </script>
<header <header
class={cn( class={clsx(
'flex items-center justify-between', 'flex items-center justify-between',
'px-4 md:px-8 py-4 md:py-6', 'px-4 md:px-8 py-4 md:py-6',
'h-16 md:h-20 z-20', 'h-16 md:h-20 z-20',
@@ -5,12 +5,12 @@
Content (font list, controls) is injected via snippets. Content (font list, controls) is injected via snippets.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { import {
ButtonGroup, ButtonGroup,
Label, Label,
ToggleButton, ToggleButton,
} from '$shared/ui'; } from '$shared/ui';
import clsx from 'clsx';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { import {
type Side, type Side,
@@ -40,7 +40,7 @@ let {
</script> </script>
<div <div
class={cn( class={clsx(
'flex flex-col h-full', 'flex flex-col h-full',
'w-80', 'w-80',
'bg-surface dark:bg-dark-bg', 'bg-surface dark:bg-dark-bg',
@@ -17,8 +17,8 @@ import {
import { import {
CharacterComparisonEngine, CharacterComparisonEngine,
} from '$shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.svelte'; } from '$shared/lib/helpers/CharacterComparisonEngine/CharacterComparisonEngine.svelte';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Loader } from '$shared/ui'; import { Loader } from '$shared/ui';
import clsx from 'clsx';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { Spring } from 'svelte/motion'; import { Spring } from 'svelte/motion';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
@@ -65,7 +65,9 @@ const sliderSpring = new Spring(50, {
const sliderPos = $derived(sliderSpring.current); const sliderPos = $derived(sliderSpring.current);
function handleMove(e: PointerEvent) { function handleMove(e: PointerEvent) {
if (!isDragging || !container) return; if (!isDragging || !container) {
return;
}
const rect = container.getBoundingClientRect(); const rect = container.getBoundingClientRect();
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width)); const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
const percentage = (x / rect.width) * 100; const percentage = (x / rect.width) * 100;
@@ -87,7 +89,9 @@ $effect(() => {
}); });
$effect(() => { $effect(() => {
if (!responsive) return; if (!responsive) {
return;
}
switch (true) { switch (true) {
case responsive.isMobile: case responsive.isMobile:
typography.multiplier = 0.5; typography.multiplier = 0.5;
@@ -143,7 +147,9 @@ $effect(() => {
}); });
$effect(() => { $effect(() => {
if (typeof window === 'undefined') return; if (typeof window === 'undefined') {
return;
}
const handleResize = () => { const handleResize = () => {
if (container && fontA && fontB) { if (container && fontA && fontB) {
const width = container.offsetWidth; const width = container.offsetWidth;
@@ -180,10 +186,10 @@ const scaleClass = $derived(
Outer flex container — fills parent. Outer flex container — fills parent.
The paper div inside scales down when the sidebar opens on desktop. The paper div inside scales down when the sidebar opens on desktop.
--> -->
<div class={cn('flex-1 relative flex items-center justify-center p-0 overflow-hidden bg-surface dark:bg-dark-bg', className)}> <div class={clsx('flex-1 relative flex items-center justify-center p-0 overflow-hidden bg-surface dark:bg-dark-bg', className)}>
<!-- Paper surface --> <!-- Paper surface -->
<div <div
class={cn( class={clsx(
'w-full h-full flex flex-col items-center justify-center relative', 'w-full h-full flex flex-col items-center justify-center relative',
'bg-paper dark:bg-dark-card', 'bg-paper dark:bg-dark-card',
'shadow-2xl shadow-black/5 dark:shadow-black/20', 'shadow-2xl shadow-black/5 dark:shadow-black/20',
@@ -247,7 +253,7 @@ const scaleClass = $derived(
</div> </div>
<TypographyMenu <TypographyMenu
class={cn( class={clsx(
'absolute bottom-4 sm:bottom-5 right-4 sm:left-1/2 sm:right-[unset] sm:-translate-x-1/2 z-50', 'absolute bottom-4 sm:bottom-5 right-4 sm:left-1/2 sm:right-[unset] sm:-translate-x-1/2 z-50',
)} )}
/> />
@@ -4,7 +4,7 @@
1px red vertical rule with square handles at top and bottom. 1px red vertical rule with square handles at top and bottom.
--> -->
<script lang="ts"> <script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils'; import clsx from 'clsx';
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
@@ -31,7 +31,7 @@ let { sliderPos, isDragging }: Props = $props();
> >
<!-- Top handle --> <!-- Top handle -->
<div <div
class={cn( class={clsx(
'w-5 h-5 md:w-6 md:h-6', 'w-5 h-5 md:w-6 md:h-6',
'-ml-2.5 md:-ml-3', '-ml-2.5 md:-ml-3',
'mt-2 md:mt-4', 'mt-2 md:mt-4',
@@ -47,7 +47,7 @@ let { sliderPos, isDragging }: Props = $props();
<!-- Bottom handle --> <!-- Bottom handle -->
<div <div
class={cn( class={clsx(
'w-5 h-5 md:w-6 md:h-6', 'w-5 h-5 md:w-6 md:h-6',
'-ml-2.5 md:-ml-3', '-ml-2.5 md:-ml-3',
'mb-2 md:mb-4', 'mb-2 md:mb-4',
@@ -5,8 +5,8 @@
<script lang="ts"> <script lang="ts">
import { NavigationWrapper } from '$entities/Breadcrumb'; import { NavigationWrapper } from '$entities/Breadcrumb';
import type { ResponsiveManager } from '$shared/lib'; import type { ResponsiveManager } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { Section } from '$shared/ui'; import { Section } from '$shared/ui';
import clsx from 'clsx';
import { import {
getContext, getContext,
untrack, untrack,
@@ -38,7 +38,7 @@ $effect(() => {
headerAction={registerAction} headerAction={registerAction}
> >
{#snippet content({ className })} {#snippet content({ className })}
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}> <div class={clsx(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
<FontSearch bind:showFilters={isExpanded} /> <FontSearch bind:showFilters={isExpanded} />
</div> </div>
{/snippet} {/snippet}
@@ -46,7 +46,9 @@ let isAboveMiddle = $state(false);
let containerWidth = $state(0); let containerWidth = $state(0);
const checkPosition = throttle(() => { const checkPosition = throttle(() => {
if (!wrapper) return; if (!wrapper) {
return;
}
const rect = wrapper.getBoundingClientRect(); const rect = wrapper.getBoundingClientRect();
const viewportMiddle = innerHeight / 2; const viewportMiddle = innerHeight / 2;
@@ -6,11 +6,11 @@
import { NavigationWrapper } from '$entities/Breadcrumb'; import { NavigationWrapper } from '$entities/Breadcrumb';
import { fontStore } from '$entities/Font'; import { fontStore } from '$entities/Font';
import type { ResponsiveManager } from '$shared/lib'; import type { ResponsiveManager } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { import {
Label, Label,
Section, Section,
} from '$shared/ui'; } from '$shared/ui';
import clsx from 'clsx';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { layoutManager } from '../../model'; import { layoutManager } from '../../model';
import LayoutSwitch from '../LayoutSwitch/LayoutSwitch.svelte'; import LayoutSwitch from '../LayoutSwitch/LayoutSwitch.svelte';
@@ -50,7 +50,7 @@ const responsive = getContext<ResponsiveManager>('responsive');
{/snippet} {/snippet}
{#snippet content({ className })} {#snippet content({ className })}
<div class={cn(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}> <div class={clsx(className, !responsive.isDesktopLarge && 'col-start-0 col-span-2')}>
<SampleList /> <SampleList />
</div> </div>
{/snippet} {/snippet}
-1
View File
@@ -43,7 +43,6 @@
"exclude": [ "exclude": [
"node_modules", "node_modules",
"dist", "dist",
"src/shared/shadcn/**/*",
"node_modules/**/*" "node_modules/**/*"
] ]
} }
-2
View File
@@ -20,7 +20,6 @@ export default defineConfig({
'dist', 'dist',
'e2e', 'e2e',
'.storybook', '.storybook',
'src/shared/shadcn/**/*',
], ],
restoreMocks: true, restoreMocks: true,
coverage: { coverage: {
@@ -38,7 +37,6 @@ export default defineConfig({
'**/*.spec.ts', '**/*.spec.ts',
'**/*.d.ts', '**/*.d.ts',
'**/*.stories.svelte', '**/*.stories.svelte',
'src/shared/shadcn/**/*',
'vitest.config.ts', 'vitest.config.ts',
], ],
thresholds: { thresholds: {