14 Commits

Author SHA1 Message Date
Ilia Mashkov
816d4b89ce refactor: tailwind tier 1 — border-subtle/text-secondary/focus-ring utilities + Input config extraction 2026-04-16 16:32:41 +03:00
Ilia Mashkov
aa1379c15b chore: remove unused code 2026-04-16 15:59:58 +03:00
Ilia Mashkov
33e589f041 feat: remove widgets from page 2026-04-16 15:58:33 +03:00
Ilia Mashkov
b12dc6257d feat(ComparisonView): add wrapper for search bar 2026-04-16 15:58:10 +03:00
Ilia Mashkov
35e0f06a77 feat(ComparisonView): add color transition for each character 2026-04-16 15:55:57 +03:00
Ilia Mashkov
dde187e0b2 chore: move ControlId type to the entities/Font layer 2026-04-16 11:19:17 +03:00
Ilia Mashkov
5a7c61ade7 feat(FontVirtualList): re-touch on weight change and pin visible fonts 2026-04-16 11:05:09 +03:00
Ilia Mashkov
d2bce85f9c feat(ComparisonStore): pin fontA/fontB to prevent eviction while on-screen 2026-04-16 10:55:41 +03:00
Ilia Mashkov
e509463911 chore: remove unused 2026-04-16 09:07:46 +03:00
Ilia Mashkov
db08f523f6 chore: move typography constants to the entity/Font layer 2026-04-16 09:05:34 +03:00
Ilia Mashkov
c5fa159c14 fix(FontList): remove weight prop, use default weight for FontList 2026-04-16 08:51:18 +03:00
Ilia Mashkov
8645c7dcc8 feat: use typographySettingsStore everywhere for the typography settings 2026-04-16 08:44:49 +03:00
Ilia Mashkov
fbeb84270b feat(Layout): remove breadcrumbs 2026-04-16 08:40:16 +03:00
Ilia Mashkov
c1ac9b5bc4 chore(SetupFont): rename controlManager to typographySettingsStore for better semantic 2026-04-16 08:22:08 +03:00
41 changed files with 353 additions and 336 deletions

View File

@@ -265,6 +265,21 @@
}
}
@layer utilities {
/* 21× border-black/5 dark:border-white/10 → single token */
.border-subtle {
@apply border-black/5 dark:border-white/10;
}
/* Secondary text pair */
.text-secondary {
@apply text-neutral-500 dark:text-neutral-400;
}
/* Standard focus ring */
.focus-ring {
@apply focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2;
}
}
/* Global utility - useful across your app */
@media (prefers-reduced-motion: reduce) {
* {

View File

@@ -85,19 +85,11 @@ onDestroy(() => themeManager.destroy());
theme === 'dark' ? 'dark' : '',
)}
>
<header>
<BreadcrumbHeader />
</header>
<!-- <ScrollArea class="h-screen w-screen"> -->
<!-- <main class="flex-1 w-full mx-auto relative"> -->
<TooltipProvider>
{#if fontsReady}
{@render children?.()}
{/if}
</TooltipProvider>
<!-- </main> -->
<!-- </ScrollArea> -->
<footer></footer>
</div>
</ResponsiveProvider>

View File

@@ -44,7 +44,7 @@ function createButtonText(item: BreadcrumbItem) {
flex items-center justify-between
z-40
bg-surface/90 dark:bg-dark-bg/90 backdrop-blur-md
border-b border-black/5 dark:border-white/10
border-b border-subtle
"
>
<div class="max-w-8xl px-4 sm:px-6 h-full w-full flex items-center justify-between gap-2 sm:gap-4">

View File

@@ -1,5 +1,5 @@
import type { ControlModel } from '$shared/lib';
import type { ControlId } from '..';
import type { ControlId } from '../types/typography';
/**
* Font size constants

View File

@@ -1,2 +1,3 @@
export * from './const/const';
export * from './store';
export * from './types';

View File

@@ -1,5 +1,5 @@
// Applied fonts manager
export { appliedFontsManager } from './appliedFontsStore/appliedFontsStore.svelte';
export * from './appliedFontsStore/appliedFontsStore.svelte';
// Batch font store
export { BatchFontStore } from './batchFontStore.svelte';

View File

@@ -33,3 +33,4 @@ export type {
} from './store';
export * from './store/appliedFonts';
export * from './typography';

View File

@@ -0,0 +1 @@
export type ControlId = 'font_size' | 'font_weight' | 'line_height' | 'letter_spacing';

View File

@@ -10,6 +10,7 @@ import { cn } from '$shared/shadcn/utils/shadcn-utils';
import type { Snippet } from 'svelte';
import { prefersReducedMotion } from 'svelte/motion';
import {
DEFAULT_FONT_WEIGHT,
type UnifiedFont,
appliedFontsManager,
} from '../../model';
@@ -36,7 +37,7 @@ interface Props {
let {
font,
weight = 400,
weight = DEFAULT_FONT_WEIGHT,
className,
children,
}: Props = $props();

View File

@@ -18,8 +18,8 @@ import {
type FontLoadRequestConfig,
type UnifiedFont,
appliedFontsManager,
fontStore,
} from '../../model';
import { fontStore } from '../../model/store';
interface Props extends
Omit<
@@ -53,30 +53,42 @@ const isLoading = $derived(
fontStore.isFetching || fontStore.isLoading,
);
function handleInternalVisibleChange(visibleItems: UnifiedFont[]) {
const configs: FontLoadRequestConfig[] = [];
visibleItems.forEach(item => {
const url = getFontUrl(item, weight);
if (url) {
configs.push({
id: item.id,
name: item.name,
weight,
url,
isVariable: item.features?.isVariable,
});
}
});
// Auto-register fonts with the manager
appliedFontsManager.touch(configs);
let visibleFonts = $state<UnifiedFont[]>([]);
function handleInternalVisibleChange(items: UnifiedFont[]) {
visibleFonts = items;
// Forward the call to any external listener
// onVisibleItemsChange?.(visibleItems);
onVisibleItemsChange?.(items);
}
// Re-touch whenever visible set or weight changes — fixes weight-change gap
$effect(() => {
const configs: FontLoadRequestConfig[] = visibleFonts.flatMap(item => {
const url = getFontUrl(item, weight);
if (!url) return [];
return [{ id: item.id, name: item.name, weight, url, isVariable: item.features?.isVariable }];
});
if (configs.length > 0) {
appliedFontsManager.touch(configs);
}
});
// Pin visible fonts so the eviction policy never removes on-screen entries.
// Cleanup captures the snapshot values, so a weight change unpins the old
// weight before pinning the new one.
$effect(() => {
const w = weight;
const fonts = visibleFonts;
for (const f of fonts) {
appliedFontsManager.pin(f.id, w, f.features?.isVariable);
}
return () => {
for (const f of fonts) {
appliedFontsManager.unpin(f.id, w, f.features?.isVariable);
}
};
});
/**
* Load more fonts by moving to the next page
*/

View File

@@ -35,7 +35,6 @@ const { Story } = defineMeta({
<script lang="ts">
import type { UnifiedFont } from '$entities/Font';
import { controlManager } from '$features/SetupFont';
// Mock fonts for testing
const mockArial: UnifiedFont = {

View File

@@ -8,14 +8,13 @@ import {
FontApplicator,
type UnifiedFont,
} from '$entities/Font';
import { controlManager } from '$features/SetupFont';
import { typographySettingsStore } from '$features/SetupFont/model';
import {
Badge,
ContentEditable,
Divider,
Footnote,
Stat,
StatGroup,
} from '$shared/ui';
import { fly } from 'svelte/transition';
@@ -37,11 +36,6 @@ interface Props {
let { font, text = $bindable(), index = 0 }: Props = $props();
const fontWeight = $derived(controlManager.weight);
const fontSize = $derived(controlManager.renderedSize);
const lineHeight = $derived(controlManager.height);
const letterSpacing = $derived(controlManager.spacing);
// Adjust the property name to match your UnifiedFont type
const fontType = $derived((font as any).type ?? (font as any).category ?? '');
@@ -52,10 +46,10 @@ const providerBadge = $derived(
);
const stats = $derived([
{ label: 'SZ', value: `${fontSize}PX` },
{ label: 'WGT', value: `${fontWeight}` },
{ label: 'LH', value: lineHeight?.toFixed(2) },
{ label: 'LTR', value: `${letterSpacing}` },
{ label: 'SZ', value: `${typographySettingsStore.renderedSize}PX` },
{ label: 'WGT', value: `${typographySettingsStore.weight}` },
{ label: 'LH', value: typographySettingsStore.height?.toFixed(2) },
{ label: 'LTR', value: `${typographySettingsStore.spacing}` },
]);
</script>
@@ -65,7 +59,7 @@ const stats = $derived([
group relative
w-full h-full
bg-paper dark:bg-dark-card
border border-black/5 dark:border-white/10
border border-subtle
hover:border-brand dark:hover:border-brand
hover:shadow-brand/10
hover:shadow-[5px_5px_0px_0px]
@@ -75,14 +69,14 @@ const stats = $derived([
min-h-60
rounded-none
"
style:font-weight={fontWeight}
style:font-weight={typographySettingsStore.weight}
>
<!-- ── Header bar ─────────────────────────────────────────────────── -->
<div
class="
flex items-center justify-between
px-4 sm:px-5 md:px-6 py-3 sm:py-4
border-b border-black/5 dark:border-white/10
border-b border-subtle
bg-paper dark:bg-dark-card
"
>
@@ -140,18 +134,18 @@ const stats = $derived([
<!-- ── Main content area ──────────────────────────────────────────── -->
<div class="flex-1 p-4 sm:p-5 md:p-8 flex items-center overflow-hidden bg-paper dark:bg-dark-card relative z-10">
<FontApplicator {font} weight={fontWeight}>
<FontApplicator {font} weight={typographySettingsStore.weight}>
<ContentEditable
bind:text
{fontSize}
{lineHeight}
{letterSpacing}
fontSize={typographySettingsStore.renderedSize}
lineHeight={typographySettingsStore.height}
letterSpacing={typographySettingsStore.spacing}
/>
</FontApplicator>
</div>
<!-- ── Mobile stats footer (md:hidden — header stats take over above) -->
<div class="md:hidden px-4 sm:px-5 py-1.5 sm:py-2 border-t border-black/5 dark:border-white/10 flex gap-2 sm:gap-4 bg-paper dark:bg-dark-card mt-auto">
<div class="md:hidden px-4 sm:px-5 py-1.5 sm:py-2 border-t border-subtle flex gap-2 sm:gap-4 bg-paper dark:bg-dark-card mt-auto">
{#each stats as stat, i}
<Footnote class="text-[0.4375rem] sm:text-[0.5rem] tracking-wider {i === 0 ? 'ml-auto' : ''}">
{stat.label}:{stat.value}

View File

@@ -1,28 +1,6 @@
export { TypographyMenu } from './ui';
export {
type ControlId,
controlManager,
DEFAULT_FONT_SIZE,
DEFAULT_FONT_WEIGHT,
DEFAULT_LETTER_SPACING,
DEFAULT_LINE_HEIGHT,
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
FONT_SIZE_STEP,
FONT_WEIGHT_STEP,
LINE_HEIGHT_STEP,
MAX_FONT_SIZE,
MAX_FONT_WEIGHT,
MAX_LINE_HEIGHT,
MIN_FONT_SIZE,
MIN_FONT_WEIGHT,
MIN_LINE_HEIGHT,
MULTIPLIER_L,
MULTIPLIER_M,
MULTIPLIER_S,
} from './model';
export {
createTypographyControlManager,
type TypographyControlManager,
createTypographySettingsManager,
type TypographySettingsManager,
} from './lib';
export { typographySettingsStore } from './model';
export { TypographyMenu } from './ui';

View File

@@ -1,4 +1,4 @@
export {
createTypographyControlManager,
type TypographyControlManager,
} from './controlManager/controlManager.svelte';
createTypographySettingsManager,
type TypographySettingsManager,
} from './settingsManager/settingsManager.svelte';

View File

@@ -10,6 +10,13 @@
* when displaying/editing, but the base size is what's stored.
*/
import {
type ControlId,
DEFAULT_FONT_SIZE,
DEFAULT_FONT_WEIGHT,
DEFAULT_LETTER_SPACING,
DEFAULT_LINE_HEIGHT,
} from '$entities/Font';
import {
type ControlDataModel,
type ControlModel,
@@ -19,13 +26,6 @@ import {
createTypographyControl,
} from '$shared/lib';
import { SvelteMap } from 'svelte/reactivity';
import {
type ControlId,
DEFAULT_FONT_SIZE,
DEFAULT_FONT_WEIGHT,
DEFAULT_LETTER_SPACING,
DEFAULT_LINE_HEIGHT,
} from '../../model';
type ControlOnlyFields<T extends string = string> = Omit<ControlModel<T>, keyof ControlDataModel>;
@@ -52,7 +52,7 @@ export interface TypographySettings {
* Manages multiple typography controls with persistent storage and
* responsive scaling support for font size.
*/
export class TypographyControlManager {
export class TypographySettingsManager {
/** Map of controls keyed by ID */
#controls = new SvelteMap<string, Control>();
/** Responsive multiplier for font size display */
@@ -242,7 +242,7 @@ export class TypographyControlManager {
* @param storageId - Persistent storage identifier
* @returns Typography control manager instance
*/
export function createTypographyControlManager(
export function createTypographySettingsManager(
configs: ControlModel<ControlId>[],
storageId: string = 'glyphdiff:typography',
) {
@@ -252,5 +252,5 @@ export function createTypographyControlManager(
lineHeight: DEFAULT_LINE_HEIGHT,
letterSpacing: DEFAULT_LETTER_SPACING,
});
return new TypographyControlManager(configs, storage);
return new TypographySettingsManager(configs, storage);
}

View File

@@ -1,6 +1,12 @@
/** @vitest-environment jsdom */
import {
afterEach,
DEFAULT_FONT_SIZE,
DEFAULT_FONT_WEIGHT,
DEFAULT_LETTER_SPACING,
DEFAULT_LINE_HEIGHT,
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
} from '$entities/Font';
import {
beforeEach,
describe,
expect,
@@ -8,21 +14,14 @@ import {
vi,
} from 'vitest';
import {
DEFAULT_FONT_SIZE,
DEFAULT_FONT_WEIGHT,
DEFAULT_LETTER_SPACING,
DEFAULT_LINE_HEIGHT,
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
} from '../../model';
import {
TypographyControlManager,
type TypographySettings,
} from './controlManager.svelte';
TypographySettingsManager,
} from './settingsManager.svelte';
/**
* Test Strategy for TypographyControlManager
* Test Strategy for TypographySettingsManager
*
* This test suite validates the TypographyControlManager state management logic.
* This test suite validates the TypographySettingsManager state management logic.
* These are unit tests for the manager logic, separate from component rendering.
*
* NOTE: Svelte 5's $effect runs in microtasks, so we need to flush effects
@@ -45,7 +44,7 @@ async function flushEffects() {
await Promise.resolve();
}
describe('TypographyControlManager - Unit Tests', () => {
describe('TypographySettingsManager - Unit Tests', () => {
let mockStorage: TypographySettings;
let mockPersistentStore: {
value: TypographySettings;
@@ -85,7 +84,7 @@ describe('TypographyControlManager - Unit Tests', () => {
describe('Initialization', () => {
it('creates manager with default values from storage', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -105,7 +104,7 @@ describe('TypographyControlManager - Unit Tests', () => {
};
mockPersistentStore = createMockPersistentStore(mockStorage);
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -117,7 +116,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('initializes font size control with base size multiplied by current multiplier (1)', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -126,7 +125,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('returns all controls via controls getter', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -142,7 +141,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('returns individual controls via specific getters', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -160,7 +159,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('control instances have expected interface', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -179,7 +178,7 @@ describe('TypographyControlManager - Unit Tests', () => {
describe('Multiplier System', () => {
it('has default multiplier of 1', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -188,7 +187,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('updates multiplier when set', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -201,7 +200,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('does not update multiplier if set to same value', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -217,7 +216,7 @@ describe('TypographyControlManager - Unit Tests', () => {
mockStorage = { fontSize: 48, fontWeight: 400, lineHeight: 1.5, letterSpacing: 0 };
mockPersistentStore = createMockPersistentStore(mockStorage);
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -241,7 +240,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('updates font size control display value when multiplier increases', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -262,7 +261,7 @@ describe('TypographyControlManager - Unit Tests', () => {
describe('Base Size Setter', () => {
it('updates baseSize when set directly', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -273,7 +272,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('updates size control value when baseSize is set', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -284,7 +283,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('applies multiplier to size control when baseSize is set', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -298,7 +297,7 @@ describe('TypographyControlManager - Unit Tests', () => {
describe('Rendered Size Calculation', () => {
it('calculates renderedSize as baseSize * multiplier', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -307,7 +306,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('updates renderedSize when multiplier changes', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -320,7 +319,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('updates renderedSize when baseSize changes', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -340,7 +339,7 @@ describe('TypographyControlManager - Unit Tests', () => {
// proxy effect behavior should be tested in E2E tests.
it('does NOT immediately update baseSize from control change (effect is async)', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -355,7 +354,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('updates baseSize via direct setter (synchronous)', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -380,7 +379,7 @@ describe('TypographyControlManager - Unit Tests', () => {
};
mockPersistentStore = createMockPersistentStore(mockStorage);
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -393,7 +392,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('syncs to storage after effect flush (async)', async () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -409,7 +408,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('syncs control changes to storage after effect flush (async)', async () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -422,7 +421,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('syncs height control changes to storage after effect flush (async)', async () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -434,7 +433,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('syncs spacing control changes to storage after effect flush (async)', async () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -448,7 +447,7 @@ describe('TypographyControlManager - Unit Tests', () => {
describe('Control Value Getters', () => {
it('returns current weight value', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -460,7 +459,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('returns current height value', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -472,7 +471,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('returns current spacing value', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -485,7 +484,7 @@ describe('TypographyControlManager - Unit Tests', () => {
it('returns default value when control is not found', () => {
// Create a manager with empty configs (no controls)
const manager = new TypographyControlManager([], mockPersistentStore);
const manager = new TypographySettingsManager([], mockPersistentStore);
expect(manager.weight).toBe(DEFAULT_FONT_WEIGHT);
expect(manager.height).toBe(DEFAULT_LINE_HEIGHT);
@@ -503,7 +502,7 @@ describe('TypographyControlManager - Unit Tests', () => {
};
mockPersistentStore = createMockPersistentStore(mockStorage);
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -536,7 +535,7 @@ describe('TypographyControlManager - Unit Tests', () => {
clear: clearSpy,
};
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -547,7 +546,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('respects multiplier when resetting font size control', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -565,7 +564,7 @@ describe('TypographyControlManager - Unit Tests', () => {
describe('Complex Scenarios', () => {
it('handles changing multiplier then modifying baseSize', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -586,7 +585,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('maintains correct renderedSize throughout changes', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -608,7 +607,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('handles multiple control changes in sequence', async () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -633,7 +632,7 @@ describe('TypographyControlManager - Unit Tests', () => {
mockStorage = { fontSize: 48, fontWeight: 400, lineHeight: 1.5, letterSpacing: 0 };
mockPersistentStore = createMockPersistentStore(mockStorage);
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -645,7 +644,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('handles very small multiplier', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -658,7 +657,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('handles large base size with multiplier', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -671,7 +670,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('handles floating point precision in multiplier', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -690,7 +689,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('handles control methods (increase/decrease)', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);
@@ -704,7 +703,7 @@ describe('TypographyControlManager - Unit Tests', () => {
});
it('handles control boundary conditions', () => {
const manager = new TypographyControlManager(
const manager = new TypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
mockPersistentStore,
);

View File

@@ -1,24 +1 @@
export {
DEFAULT_FONT_SIZE,
DEFAULT_FONT_WEIGHT,
DEFAULT_LETTER_SPACING,
DEFAULT_LINE_HEIGHT,
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
FONT_SIZE_STEP,
FONT_WEIGHT_STEP,
LINE_HEIGHT_STEP,
MAX_FONT_SIZE,
MAX_FONT_WEIGHT,
MAX_LINE_HEIGHT,
MIN_FONT_SIZE,
MIN_FONT_WEIGHT,
MIN_LINE_HEIGHT,
MULTIPLIER_L,
MULTIPLIER_M,
MULTIPLIER_S,
} from './const/const';
export {
type ControlId,
controlManager,
} from './state/manager.svelte';
export { typographySettingsStore } from './state/typographySettingsStore';

View File

@@ -1,6 +0,0 @@
import { createTypographyControlManager } from '../../lib';
import { DEFAULT_TYPOGRAPHY_CONTROLS_DATA } from '../const/const';
export type ControlId = 'font_size' | 'font_weight' | 'line_height' | 'letter_spacing';
export const controlManager = createTypographyControlManager(DEFAULT_TYPOGRAPHY_CONTROLS_DATA);

View File

@@ -0,0 +1,7 @@
import { DEFAULT_TYPOGRAPHY_CONTROLS_DATA } from '$entities/Font';
import { createTypographySettingsManager } from '../../lib';
export const typographySettingsStore = createTypographySettingsManager(
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
'glyphdiff:comparison:typography',
);

View File

@@ -6,10 +6,14 @@
Desktop: inline bar with combo controls.
-->
<script lang="ts">
import {
MULTIPLIER_L,
MULTIPLIER_M,
MULTIPLIER_S,
} from '$entities/Font';
import type { ResponsiveManager } from '$shared/lib';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import {
Button,
ComboControl,
ControlGroup,
Slider,
@@ -20,12 +24,7 @@ import { Popover } from 'bits-ui';
import { getContext } from 'svelte';
import { cubicOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
import {
MULTIPLIER_L,
MULTIPLIER_M,
MULTIPLIER_S,
controlManager,
} from '../../model';
import { typographySettingsStore } from '../../model';
interface Props {
/**
@@ -52,16 +51,16 @@ $effect(() => {
if (!responsive) return;
switch (true) {
case responsive.isMobile:
controlManager.multiplier = MULTIPLIER_S;
typographySettingsStore.multiplier = MULTIPLIER_S;
break;
case responsive.isTablet:
controlManager.multiplier = MULTIPLIER_M;
typographySettingsStore.multiplier = MULTIPLIER_M;
break;
case responsive.isDesktop:
controlManager.multiplier = MULTIPLIER_L;
typographySettingsStore.multiplier = MULTIPLIER_L;
break;
default:
controlManager.multiplier = MULTIPLIER_L;
typographySettingsStore.multiplier = MULTIPLIER_L;
}
});
</script>
@@ -80,7 +79,7 @@ $effect(() => {
'transition-colors duration-150',
'hover:bg-white/50 dark:hover:bg-white/5',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30',
isOpen && 'bg-paper dark:bg-dark-card border-black/5 dark:border-white/10 shadow-sm',
isOpen && 'bg-paper dark:bg-dark-card border-subtle shadow-sm',
className,
)}
>
@@ -97,7 +96,7 @@ $effect(() => {
class={cn(
'z-50 w-72',
'bg-surface dark:bg-dark-card',
'border border-black/5 dark:border-white/10',
'border border-subtle',
'shadow-[0_20px_40px_-10px_rgba(0,0,0,0.15)]',
'rounded-none p-4',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
@@ -110,7 +109,7 @@ $effect(() => {
escapeKeydownBehavior="close"
>
<!-- Header -->
<div class="flex items-center justify-between mb-3 pb-3 border-b border-black/5 dark:border-white/10">
<div class="flex items-center justify-between mb-3 pb-3 border-b border-subtle">
<div class="flex items-center gap-1.5">
<Settings2Icon size={12} class="text-swiss-red" />
<span
@@ -133,7 +132,7 @@ $effect(() => {
</div>
<!-- Controls -->
{#each controlManager.controls as control (control.id)}
{#each typographySettingsStore.controls as control (control.id)}
<ControlGroup label={control.controlLabel ?? ''}>
<Slider
bind:value={control.instance.value}
@@ -155,13 +154,13 @@ $effect(() => {
class={cn(
'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',
'border border-black/5 dark:border-white/10',
'border border-subtle',
'shadow-[0_20px_40px_-10px_rgba(0,0,0,0.1)]',
'rounded-none ring-1 ring-black/5 dark:ring-white/5',
)}
>
<!-- Header: icon + label -->
<div class="px-2 md:px-3 flex items-center gap-1.5 md:gap-2 border-r border-black/5 dark:border-white/10 mr-1 text-swiss-black dark:text-neutral-200 shrink-0">
<div class="px-2 md:px-3 flex items-center gap-1.5 md:gap-2 border-r border-subtle mr-1 text-swiss-black dark:text-neutral-200 shrink-0">
<Settings2Icon
size={14}
class="text-swiss-red"
@@ -174,7 +173,7 @@ $effect(() => {
</div>
<!-- Controls with dividers between each -->
{#each controlManager.controls as control, i (control.id)}
{#each typographySettingsStore.controls as control, i (control.id)}
{#if i > 0}
<div class="w-px h-6 md:h-8 bg-black/5 dark:bg-white/10 mx-0.5 md:mx-1 shrink-0"></div>
{/if}

View File

@@ -3,10 +3,7 @@
Description: The main page component of the application.
-->
<script lang="ts">
import { scrollBreadcrumbsStore } from '$entities/Breadcrumb';
import { ComparisonView } from '$widgets/ComparisonView';
import { FontSearchSection } from '$widgets/FontSearch';
import { SampleListSection } from '$widgets/SampleList';
import { cubicIn } from 'svelte/easing';
import { fade } from 'svelte/transition';
</script>
@@ -18,8 +15,4 @@ import { fade } from 'svelte/transition';
<section class="w-auto">
<ComparisonView />
</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">
<FontSearchSection />
<SampleListSection index={1} />
</main>
</div>

View File

@@ -111,7 +111,7 @@ const variantStyles: Record<ButtonVariant, string> = {
),
ghost: cn(
'bg-transparent',
'text-neutral-500 dark:text-neutral-400',
'text-secondary',
'border border-transparent',
'hover:bg-transparent dark:hover:bg-transparent',
'hover:text-brand dark:hover:text-brand',
@@ -121,7 +121,7 @@ const variantStyles: Record<ButtonVariant, string> = {
),
icon: cn(
'bg-surface dark:bg-dark-bg',
'text-neutral-500 dark:text-neutral-400',
'text-secondary',
'border border-transparent',
'hover:bg-paper dark:hover:bg-paper',
'hover:text-brand',
@@ -172,7 +172,7 @@ const activeStyles: Partial<Record<ButtonVariant, string>> = {
'bg-paper dark:bg-dark-card border-black/10 dark:border-white/10 shadow-sm text-neutral-900 dark:text-neutral-100',
ghost: 'bg-transparent dark:bg-transparent text-brand dark:text-brand',
outline: 'bg-surface dark:bg-paper border-brand',
icon: 'bg-paper dark:bg-paper text-brand border-black/5 dark:border-white/10',
icon: 'bg-paper dark:bg-paper text-brand border-subtle',
};
const classes = $derived(cn(
@@ -184,7 +184,7 @@ const classes = $derived(cn(
'select-none',
'outline-none',
'cursor-pointer',
'focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2',
'focus-ring',
'focus-visible:ring-offset-surface dark:focus-visible:ring-offset-dark-bg',
'disabled:cursor-not-allowed disabled:pointer-events-none',
// Variant

View File

@@ -26,7 +26,7 @@ let { children, class: className, ...rest }: Props = $props();
class={cn(
'flex items-center gap-1 p-1',
'bg-surface dark:bg-dark-bg',
'border border-black/5 dark:border-white/10',
'border border-subtle',
'rounded-none',
'transition-colors duration-500',
className,

View File

@@ -93,9 +93,7 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
step={control.step}
orientation="horizontal"
/>
<span
class="font-mono text-[0.6875rem] text-neutral-500 dark:text-neutral-400 tabular-nums w-10 text-right shrink-0"
>
<span class="font-mono text-[0.6875rem] text-secondary tabular-nums w-10 text-right shrink-0">
{formattedValue()}
</span>
</div>
@@ -129,7 +127,7 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
'border border-transparent',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/30',
open
? 'bg-paper dark:bg-dark-card shadow-sm border-black/5 dark:border-white/10'
? 'bg-paper dark:bg-dark-card shadow-sm border-subtle'
: 'hover:bg-paper/50 dark:hover:bg-dark-card/50',
)}
aria-label={controlLabel}
@@ -157,7 +155,7 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
<!-- Vertical slider popover -->
<PopoverContent
class="w-auto py-4 px-3 h-64 flex items-center justify-center rounded-none border border-black/5 dark:border-white/10 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"
side="top"
>

View File

@@ -24,7 +24,7 @@ interface Props {
const { label, children, class: className }: Props = $props();
</script>
<div class={cn('flex flex-col gap-3 py-6 border-b border-black/5 dark:border-white/10 last:border-0', className)}>
<div class={cn('flex flex-col gap-3 py-6 border-b border-subtle last:border-0', className)}>
<div class="flex justify-between items-center text-[0.6875rem] font-primary font-bold tracking-tight text-neutral-900 dark:text-neutral-100 uppercase leading-none">
{label}
</div>

View File

@@ -9,6 +9,10 @@ import type { Snippet } from 'svelte';
import { cubicOut } from 'svelte/easing';
import type { HTMLInputAttributes } from 'svelte/elements';
import { scale } from 'svelte/transition';
import {
inputSizeConfig,
inputVariantConfig,
} from './config';
import type {
InputSize,
InputVariant,
@@ -80,36 +84,11 @@ let {
...rest
}: Props = $props();
const sizeConfig: Record<InputSize, { input: string; text: string; height: string; clearIcon: number }> = {
sm: { input: 'px-3 py-1.5', text: 'text-sm', height: 'h-8', clearIcon: 12 },
md: { input: 'px-4 py-2', text: 'text-base', height: 'h-10', clearIcon: 14 },
lg: { input: 'px-4 py-3', text: 'text-lg md:text-xl', height: 'h-12', clearIcon: 16 },
xl: { input: 'px-4 py-3', text: 'text-xl md:text-2xl', height: 'h-14', clearIcon: 18 },
};
const variantConfig: Record<InputVariant, { base: string; focus: string; error: string }> = {
default: {
base: 'bg-paper dark:bg-paper border border-black/5 dark:border-white/10',
focus: 'focus:border-brand focus:ring-1 focus:ring-brand/20',
error: 'border-brand ring-1 ring-brand/20',
},
underline: {
base: 'bg-transparent border-0 border-b border-neutral-300 dark:border-neutral-700',
focus: 'focus:border-brand',
error: 'border-brand',
},
filled: {
base: 'bg-surface dark:bg-paper border border-transparent',
focus: 'focus:border-brand focus:ring-1 focus:ring-brand/20',
error: 'border-brand ring-1 ring-brand/20',
},
};
const hasValue = $derived(value !== undefined && value !== '');
const showClear = $derived(showClearButton && hasValue && !!onclear);
const hasRightSlot = $derived(!!rightIcon || showClearButton);
const cfg = $derived(sizeConfig[size]);
const styles = $derived(variantConfig[variant]);
const cfg = $derived(inputSizeConfig[size]);
const styles = $derived(inputVariantConfig[variant]);
const inputClasses = $derived(cn(
'font-primary rounded-none outline-none transition-all duration-200',
@@ -170,7 +149,7 @@ const inputClasses = $derived(cn(
<span
class={cn(
'text-[0.625rem] font-mono tracking-wide px-1',
error ? 'text-brand ' : 'text-neutral-500 dark:text-neutral-400',
error ? 'text-brand ' : 'text-secondary',
)}
>
{helperText}

View File

@@ -0,0 +1,31 @@
import type {
InputSize,
InputVariant,
} from './types';
/** Size-specific layout classes: padding, text size, height, and clear-icon pixel size. */
export const inputSizeConfig: Record<InputSize, { input: string; text: string; height: string; clearIcon: number }> = {
sm: { input: 'px-3 py-1.5', text: 'text-sm', height: 'h-8', clearIcon: 12 },
md: { input: 'px-4 py-2', text: 'text-base', height: 'h-10', clearIcon: 14 },
lg: { input: 'px-4 py-3', text: 'text-lg md:text-xl', height: 'h-12', clearIcon: 16 },
xl: { input: 'px-4 py-3', text: 'text-xl md:text-2xl', height: 'h-14', clearIcon: 18 },
};
/** Variant-specific classes: base background/border, focus ring, and error state. */
export const inputVariantConfig: Record<InputVariant, { base: string; focus: string; error: string }> = {
default: {
base: 'bg-paper dark:bg-paper border border-subtle',
focus: 'focus:border-brand focus:ring-1 focus:ring-brand/20',
error: 'border-brand ring-1 ring-brand/20',
},
underline: {
base: 'bg-transparent border-0 border-b border-neutral-300 dark:border-neutral-700',
focus: 'focus:border-brand',
error: 'border-brand',
},
filled: {
base: 'bg-surface dark:bg-paper border border-transparent',
focus: 'focus:border-brand focus:ring-1 focus:ring-brand/20',
error: 'border-brand ring-1 ring-brand/20',
},
};

View File

@@ -20,7 +20,7 @@ let {
}: Props = $props();
</script>
<Input bind:value variant="underline" {...rest}>
<Input bind:value variant="default" {...rest}>
{#snippet rightIcon(size)}
<SearchIcon size={inputIconSize[size]} />
{/snippet}

View File

@@ -84,7 +84,7 @@ function close() {
'overflow-hidden',
'will-change-[width]',
'transition-[width] duration-300 ease-out',
'border-r border-black/5 dark:border-white/10',
'border-r border-subtle',
'bg-surface dark:bg-dark-bg',
isOpen ? 'w-80 opacity-100' : 'w-0 opacity-0',
'transition-[width,opacity] duration-300 ease-out',

View File

@@ -70,7 +70,7 @@ let {
const isVertical = $derived(orientation === 'vertical');
const labelClasses = `font-mono text-[0.625rem] tabular-nums shrink-0
text-neutral-500 dark:text-neutral-400
text-secondary
group-hover:text-neutral-700 dark:group-hover:text-neutral-300
transition-colors`;

View File

@@ -21,10 +21,7 @@ import {
fontStore,
getFontUrl,
} from '$entities/Font';
import {
DEFAULT_TYPOGRAPHY_CONTROLS_DATA,
createTypographyControlManager,
} from '$features/SetupFont';
import { typographySettingsStore } from '$features/SetupFont/model';
import { createPersistentStore } from '$shared/lib';
import { untrack } from 'svelte';
@@ -68,8 +65,8 @@ export class ComparisonStore {
#side = $state<Side>('A');
/** Slider position for character morphing (0-100) */
#sliderPosition = $state(50);
/** Typography controls for this comparison */
#typography = createTypographyControlManager(DEFAULT_TYPOGRAPHY_CONTROLS_DATA, 'glyphdiff:comparison:typography');
// /** Typography controls for this comparison */
// #typography = createTypographyControlManager(DEFAULT_TYPOGRAPHY_CONTROLS_DATA, 'glyphdiff:comparison:typography');
/** TanStack Query-backed batch font fetcher */
#batchStore: BatchFontStore;
@@ -99,7 +96,7 @@ export class ComparisonStore {
$effect(() => {
const fa = this.#fontA;
const fb = this.#fontB;
const weight = this.#typography.weight;
const weight = typographySettingsStore.weight;
if (!fa || !fb) return;
@@ -137,6 +134,19 @@ export class ComparisonStore {
});
}
});
// Effect 4: Pin fontA/fontB so eviction never removes on-screen fonts
$effect(() => {
const fa = this.#fontA;
const fb = this.#fontB;
const w = typographySettingsStore.weight;
if (fa) appliedFontsManager.pin(fa.id, w, fa.features?.isVariable);
if (fb) appliedFontsManager.pin(fb.id, w, fb.features?.isVariable);
return () => {
if (fa) appliedFontsManager.unpin(fa.id, w, fa.features?.isVariable);
if (fb) appliedFontsManager.unpin(fb.id, w, fb.features?.isVariable);
};
});
});
}
@@ -152,8 +162,8 @@ export class ComparisonStore {
return;
}
const weight = this.#typography.weight;
const size = this.#typography.renderedSize;
const weight = typographySettingsStore.weight;
const size = typographySettingsStore.renderedSize;
const fontAName = this.#fontA?.name;
const fontBName = this.#fontB?.name;
@@ -201,12 +211,12 @@ export class ComparisonStore {
};
}
// ── Getters / Setters ─────────────────────────────────────────────────────
// // ── Getters / Setters ─────────────────────────────────────────────────────
/** Typography control manager */
get typography() {
return this.#typography;
}
// /** Typography control manager */
// get typography() {
// return typographySettingsStore;
// }
/** Font for side A */
get fontA() {
@@ -273,7 +283,7 @@ export class ComparisonStore {
this.#fontB = undefined;
this.#batchStore.setIds([]);
storage.clear();
this.#typography.reset();
typographySettingsStore.reset();
}
}

View File

@@ -53,15 +53,19 @@ vi.mock('$shared/lib/helpers/createPersistentStore/createPersistentStore.svelte'
// ── $entities/Font mock — keep real BatchFontStore, stub singletons ───────────
vi.mock('$entities/Font', async () => {
vi.mock('$entities/Font', async importOriginal => {
const actual = await importOriginal<typeof import('$entities/Font')>();
const { BatchFontStore } = await import(
'$entities/Font/model/store/batchFontStore.svelte'
);
return {
...actual,
BatchFontStore,
fontStore: { fonts: [] },
appliedFontsManager: {
touch: vi.fn(),
pin: vi.fn(),
unpin: vi.fn(),
getFontStatus: vi.fn(),
ready: vi.fn(() => Promise.resolve()),
},
@@ -80,9 +84,20 @@ vi.mock('$features/SetupFont', () => ({
})),
}));
vi.mock('$features/SetupFont/model', () => ({
typographySettingsStore: {
weight: 400,
renderedSize: 48,
reset: vi.fn(),
},
}));
// ── Imports (after mocks) ─────────────────────────────────────────────────────
import { fontStore } from '$entities/Font';
import {
appliedFontsManager,
fontStore,
} from '$entities/Font';
import * as proxyFonts from '$entities/Font/api/proxy/proxyFonts';
import { ComparisonStore } from './comparisonStore.svelte';
@@ -209,4 +224,55 @@ describe('ComparisonStore', () => {
expect(store.fontB).toBeUndefined();
});
});
// ── Pin / Unpin ───────────────────────────────────────────────────────────
describe('Pin / Unpin (eviction guard)', () => {
it('pins fontA and fontB when they are loaded', async () => {
mockStorage._value.fontAId = mockFontA.id;
mockStorage._value.fontBId = mockFontB.id;
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([mockFontA, mockFontB]);
new ComparisonStore();
await vi.waitFor(() => {
expect(appliedFontsManager.pin).toHaveBeenCalledWith(
mockFontA.id,
400,
mockFontA.features?.isVariable,
);
expect(appliedFontsManager.pin).toHaveBeenCalledWith(
mockFontB.id,
400,
mockFontB.features?.isVariable,
);
}, { timeout: 2000 });
});
it('unpins the old font when fontA is replaced', async () => {
mockStorage._value.fontAId = mockFontA.id;
mockStorage._value.fontBId = mockFontB.id;
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([mockFontA, mockFontB]);
const store = new ComparisonStore();
await vi.waitFor(() => expect(store.fontA?.id).toBe(mockFontA.id), { timeout: 2000 });
const mockFontC: typeof mockFontA = { ...mockFontA, id: 'playfair', name: 'Playfair Display' };
vi.spyOn(proxyFonts, 'fetchFontsByIds').mockResolvedValue([mockFontC, mockFontB]);
store.fontA = mockFontC;
await vi.waitFor(() => {
expect(appliedFontsManager.unpin).toHaveBeenCalledWith(
mockFontA.id,
400,
mockFontA.features?.isVariable,
);
expect(appliedFontsManager.pin).toHaveBeenCalledWith(
mockFontC.id,
400,
mockFontC.features?.isVariable,
);
}, { timeout: 2000 });
});
});
});

View File

@@ -3,6 +3,7 @@
Renders a single character with morphing animation
-->
<script lang="ts">
import { typographySettingsStore } from '$features/SetupFont';
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import { comparisonStore } from '../../model';
@@ -25,7 +26,7 @@ let { char, proximity, isPast }: Props = $props();
const fontA = $derived(comparisonStore.fontA);
const fontB = $derived(comparisonStore.fontB);
const typography = $derived(comparisonStore.typography);
const typography = $derived(typographySettingsStore);
let slot = $state<0 | 1>(0);
let slotFonts = $state<[string, string]>(['', '']);
@@ -51,6 +52,7 @@ $effect(() => {
<span
class={cn(
'char-inner',
'transition-colors duration-300',
isPast
? 'text-swiss-black/75 dark:text-brand/75'
: 'text-neutral-950 dark:text-white',

View File

@@ -6,23 +6,18 @@
<script lang="ts">
import { NavigationWrapper } from '$entities/Breadcrumb';
import type { ResponsiveManager } from '$shared/lib';
import {
ControlGroup,
SidebarContainer,
Slider,
} from '$shared/ui';
import { SidebarContainer } from '$shared/ui';
import {
getContext,
untrack,
} from 'svelte';
import { comparisonStore } from '../../model';
import FontList from '../FontList/FontList.svelte';
import Header from '../Header/Header.svelte';
import Search from '../Search/Search.svelte';
import Sidebar from '../Sidebar/Sidebar.svelte';
import SliderArea from '../SliderArea/SliderArea.svelte';
const responsive = getContext<ResponsiveManager>('responsive');
const typography = $derived(comparisonStore.typography);
const isMobileOrTabletPortrait = $derived(responsive.isMobile || responsive.isTabletPortrait);
let isSidebarOpen = $state(!isMobileOrTabletPortrait);
@@ -43,52 +38,9 @@ $effect(() => {
{#snippet sidebar()}
<Sidebar class="w-full h-full border-none">
{#snippet main()}
<Search />
<FontList />
{/snippet}
{#snippet controls()}
{#if typography.sizeControl && typography.weightControl && typography.heightControl && typography.spacingControl}
<ControlGroup label="Size">
<Slider
bind:value={typography.sizeControl.value}
min={typography.sizeControl.min}
max={typography.sizeControl.max}
step={typography.sizeControl.step}
/>
</ControlGroup>
<ControlGroup label="Weight">
<Slider
bind:value={typography.weightControl.value}
min={typography.weightControl.min}
max={typography.weightControl.max}
step={typography.weightControl.step}
/>
</ControlGroup>
<div class="grid grid-cols-2 gap-6 mt-4">
<ControlGroup label="Leading" class="border-0 py-0">
<Slider
bind:value={typography.heightControl.value}
min={typography.heightControl.min}
max={typography.heightControl.max}
step={typography.heightControl.step}
format={(v => v.toFixed(1))}
/>
</ControlGroup>
<ControlGroup label="Tracking" class="border-0 py-0">
<Slider
bind:value={typography.spacingControl.value}
min={typography.spacingControl.min}
max={typography.spacingControl.max}
step={typography.spacingControl.step}
format={(v => v.toFixed(2))}
/>
</ControlGroup>
</div>
{/if}
{/snippet}
</Sidebar>
{/snippet}
</SidebarContainer>

View File

@@ -4,6 +4,7 @@
-->
<script lang="ts">
import {
DEFAULT_FONT_WEIGHT,
FontApplicator,
FontVirtualList,
type UnifiedFont,
@@ -18,8 +19,6 @@ import { crossfade } from 'svelte/transition';
import { comparisonStore } from '../../model';
const side = $derived(comparisonStore.side);
const typography = $derived(comparisonStore.typography);
let prevIndexA: number | null = null;
let prevIndexB: number | null = null;
let selectedIndexA: number | null = null;
@@ -71,17 +70,17 @@ $effect(() => {
</script>
<div class="flex-1 min-h-0 h-full">
<div class="py-2 pl-4 relative flex flex-col min-h-0 h-full">
<div class="px-2 py-4 mr-4 sticky border-b border-black/5 dark:border-white/10 mb-2">
<div class="py-2 relative flex flex-col min-h-0 h-full">
<div class="py-2 mx-6 sticky border-b border-subtle">
<Label class="font-primary text-neutral-400" bold variant="default" size="sm" uppercase>
Typeface Selection
</Label>
</div>
<FontVirtualList
data-font-list
weight={typography.weight}
weight={DEFAULT_FONT_WEIGHT}
itemHeight={45}
class="bg-transparent min-h-0 h-full scroll-stable pr-4"
class="bg-transparent min-h-0 h-full scroll-stable py-2 pl-6 pr-4"
>
{#snippet children({ item: font, index })}
{@const isSelectedA = font.id === comparisonStore.fontA?.id}
@@ -95,7 +94,7 @@ $effect(() => {
class="w-full px-3 md:px-4 py-2.5 md:py-3 justify-between text-left text-sm flex"
iconPosition="right"
>
<FontApplicator {font} weight={typography.weight}>{font.name}</FontApplicator>
<FontApplicator {font}>{font.name}</FontApplicator>
{#snippet icon()}
{#if active}

View File

@@ -53,7 +53,7 @@ const fontBName = $derived(comparisonStore.fontB?.name ?? '');
'flex items-center justify-between',
'px-4 md:px-8 py-4 md:py-6',
'h-16 md:h-20 z-20',
'border-b border-black/5 dark:border-white/10',
'border-b border-subtle',
'bg-surface dark:bg-dark-bg',
className,
)}

View File

@@ -3,8 +3,8 @@
Renders a line of text in the SliderArea
-->
<script lang="ts">
import { typographySettingsStore } from '$features/SetupFont';
import type { Snippet } from 'svelte';
import { comparisonStore } from '../../model';
interface LineChar {
char: string;
@@ -26,7 +26,7 @@ interface Props {
*/
character: Snippet<[{ char: string; index: number }]>;
}
const typography = $derived(comparisonStore.typography);
const typography = $derived(typographySettingsStore);
let { chars, character }: Props = $props();
</script>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import { filterManager } from '$features/GetFonts';
import { SearchBar } from '$shared/ui';
</script>
<div class="p-6 border-b border-black/5">
<SearchBar
id="font-search"
class="w-full"
placeholder="Typeface Search"
bind:value={filterManager.queryValue}
fullWidth
/>
</div>

View File

@@ -44,7 +44,7 @@ let {
'flex flex-col h-full',
'w-80',
'bg-surface dark:bg-dark-bg',
'border-r border-black/5 dark:border-white/10',
'border-r border-subtle',
'transition-colors duration-500',
className,
)}
@@ -53,7 +53,7 @@ let {
<div
class="
p-6 shrink-0
border-b border-black/5 dark:border-white/10
border-b border-subtle
bg-surface dark:bg-dark-bg
"
>
@@ -100,7 +100,7 @@ let {
class="
shrink-0 p-6
bg-surface dark:bg-dark-bg
border-t border-black/5 dark:border-white/10
border-t border-subtle
z-10
"
>

View File

@@ -8,6 +8,8 @@
- Performance optimized using offscreen canvas for measurements and transform-based animations.
-->
<script lang="ts">
import { TypographyMenu } from '$features/SetupFont';
import { typographySettingsStore } from '$features/SetupFont/model';
import {
type ResponsiveManager,
debounce,
@@ -42,7 +44,7 @@ let { isSidebarOpen = false, class: className }: Props = $props();
const fontA = $derived(comparisonStore.fontA);
const fontB = $derived(comparisonStore.fontB);
const isLoading = $derived(comparisonStore.isLoading || !comparisonStore.isReady);
const typography = $derived(comparisonStore.typography);
const typography = $derived(typographySettingsStore);
let container = $state<HTMLElement>();
@@ -179,12 +181,7 @@ const scaleClass = $derived(
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)}>
<!--
Paper surface.
Replaces the old glassmorphism card with a clean white/dark sheet.
Scale transition replaces motion.div spring — CSS transition-transform
is smooth enough here; a JS spring would add ~4kb for minimal gain.
-->
<!-- Paper surface -->
<div
class={cn(
'w-full h-full flex flex-col items-center justify-center relative',
@@ -248,4 +245,10 @@ const scaleClass = $derived(
{/if}
</div>
</div>
<TypographyMenu
class={cn(
'absolute bottom-4 sm:bottom-5 right-4 sm:left-1/2 sm:right-[unset] sm:-translate-x-1/2 z-50',
)}
/>
</div>

View File

@@ -14,7 +14,7 @@ import {
import { FontSampler } from '$features/DisplayFont';
import {
TypographyMenu,
controlManager,
typographySettingsStore,
} from '$features/SetupFont';
import { throttle } from '$shared/lib/utils';
import { Skeleton } from '$shared/ui';
@@ -60,11 +60,11 @@ const checkPosition = throttle(() => {
const fontRowHeight = $derived.by(() =>
createFontRowSizeResolver({
getFonts: () => fontStore.fonts,
getWeight: () => controlManager.weight,
getWeight: () => typographySettingsStore.weight,
getPreviewText: () => text,
getContainerWidth: () => containerWidth,
getFontSizePx: () => controlManager.renderedSize,
getLineHeightPx: () => controlManager.height * controlManager.renderedSize,
getFontSizePx: () => typographySettingsStore.renderedSize,
getLineHeightPx: () => typographySettingsStore.height * typographySettingsStore.renderedSize,
getStatus: key => appliedFontsManager.statuses.get(key),
contentHorizontalPadding: SAMPLER_CONTENT_PADDING_X,
chromeHeight: SAMPLER_CHROME_HEIGHT,
@@ -97,7 +97,7 @@ const fontRowHeight = $derived.by(() =>
<FontVirtualList
itemHeight={fontRowHeight}
useWindowScroll={true}
weight={controlManager.weight}
weight={typographySettingsStore.weight}
columns={layoutManager.columns}
gap={layoutManager.gap}
{skeleton}