scale-[0.94] shrinks proportionally — on wide viewports this produced
visibly larger horizontal gaps than vertical ones when the sidebar
opens, and it left the text engine measuring the un-scaled width
(causing the thumb-to-character morph boundary to drift).
Switch to outer-container padding (p-6 when sidebar is open on desktop)
so the paper inherits an equal pixel inset on all four sides. The
ResizeObserver picks up the new dimensions and the layout engine
re-wraps text at the actual rendered width.
Dark mode unchanged. Targets that were reported as "barely visible" in
light theme:
Surfaces / dividers
- --color-border-subtle (light) bumped from rgb(0 0 0 / 0.05) to
--neutral-300 (matches the Input underline variant's border color and
yields a visible hairline on bg-surface / bg-paper).
- New bg-subtle utility (same color as border-subtle but as
background-color) — used by Divider component and the TypographyMenu
inline column separator. Replaces ad-hoc 'bg-black/5 dark:bg-white/10'
and 'bg-black/10 dark:bg-white/10' bands.
- FontSearch + ComparisonView Search wrapper borders switched from
hand-written 'border-swiss-black/5 dark:border-white/10' to
border-subtle so they participate in the palette.
Muted text
- Button tertiary inactive text (light) bumped neutral-400 → neutral-600
(~2.7:1 → ~7.5:1 contrast). Covers the A/B toggle and the font-list
rows in the sidebar.
- Label/TechText muted variant (light) bumped neutral-400 → neutral-600.
Covers the ComboControl value text.
- Link text aligned to neutral-500 / neutral-400 (subtle but visible).
No behavior changes; pure styling.
Replace inline class clusters with the design-system utilities and
tokens established in the prior two commits. No behavior changes
intended beyond two real bug fixes.
Bug fixes:
- SampleList.svelte: 'border-border-subtle bg-background-40' was a
silent no-op (both classes mis-spelled). Now 'border-subtle
bg-background/40' applies as intended.
- FontList.svelte: 'h-[44px]' → 'h-11' (44px = 2.75rem = spacing-11,
no need for arbitrary value).
Sweeps:
- TypographyMenu: popover + floating bar now use surface-popover /
surface-floating + shadow-popover.
- FontList + FilterGroup: tertiary list buttons use the new
Button layout="block-list-row" variant; skeleton fills use
the skeleton-fill utility.
- Footer / BreadcrumbHeader: surface-floating absorbs the
bg-surface/blur/border cluster. Footer bumped to z-20 with a
comment explaining the stacking against SidebarContainer (z-40/50).
- FontSampler: surface-card + hover shadow-stamp-card token.
- SliderArea: surface-canvas, flex-center, shadow-floating-panel
tokens (light + dark variants).
- Sidebar / Header / ButtonGroup / Layout / SidebarContainer:
bg-surface dark:bg-dark-bg → surface-canvas (8 sites);
SidebarContainer mobile panel uses shadow-overlay.
- Loader / Thumb: flex items-center justify-center → flex-center;
Thumb durations → duration-fast.
- ComboControl: trigger uses surface-card-elevated when open,
popover uses surface-card-elevated, label cluster → text-label-mono,
flex-center for the trigger interior.
- Slider: shadow-sm → shadow-rest, duration-150 → duration-fast.
- text-secondary → text-subtle across Input, Slider, ComboControl
(matches the rename in the styles commit).
- Link: reverted earlier surface-floating attempt — Link's original
bg-surface/80 backdrop-blur pattern was thinner than surface-floating
(no border, smaller blur), and the Footer was overlaying its own
border-subtle on top, fighting the utility. Kept the original style.
- New layout prop with values 'inline' (default) and 'block-list-row'.
The block-list-row variant bakes in full-width, left-aligned content
with trailing icon and text-sm, replacing the ~10-class override
duplicated across FilterGroup, FontList, and similar list-row sites.
- primary variant's three hard-offset shadows now reference the
shadow-stamp-{rest,hover,pressed} tokens; the 0.0625rem translate
becomes translate-{x,y}-px.
- Base classes use text-label-mono and duration-normal utilities
instead of inline 'font-primary font-bold tracking-tight uppercase'
and 'duration-200'.
- The icon variant's background uses surface-canvas (semantic naming;
picks up dark-mode automatically via --color-surface).
- text-secondary → text-subtle (avoids collision with the @theme
--color-secondary token; see earlier styles commit).
New exported type: ButtonLayout.
Establish a real design system foundation by moving the project from
inline arbitrary-value classes to named tokens and reusable utilities.
Tokens added to @theme (auto-generate Tailwind utilities):
- Shadows: shadow-rest, shadow-stamp-{rest,hover,pressed,card},
shadow-popover, shadow-floating-panel{,-dark}, shadow-overlay
- Motion: duration-{fast,normal,slow,slower};
ease-{standard,out-soft,spring-overshoot}
Semantic mode-switching colors added to :root / .dark so utilities
auto-adapt without dark: variants:
- --color-border-subtle, --color-text-subtle, --color-skeleton
Utilities migrated to Tailwind v4's @utility directive with direct CSS
properties (previously @layer utilities with @apply chains, which
silently failed when chaining to other user-defined utilities):
- border-subtle, text-subtle, focus-ring
- surface-canvas, surface-card, surface-card-elevated, surface-popover,
surface-floating
- flex-center, skeleton-fill, text-label-mono
Notes:
- text-secondary was renamed to text-subtle because --color-secondary is
registered in @theme (a near-white shadcn surface token), which made
Tailwind v4 auto-generate a colliding text-secondary utility that won
over the user-defined one — every consumer effectively rendered as
near-white text. The text-subtle name pairs cleanly with border-subtle
and avoids any @theme collisions.
- Dead --space-* variable scale removed (was defined but never wired
into @theme; Tailwind's default spacing scale is used everywhere).
Long-tail cleanup: threshold and default-value literals in shared
helpers get named module-level constants.
- CharacterComparisonEngine: CHAR_PROXIMITY_RANGE_PCT (5),
DEFAULT_RENDER_SIZE_PX (16) — kept local instead of importing
DEFAULT_FONT_SIZE from \$entities/Font because \$shared/lib cannot
legally upward-import from \$entities per FSD (also avoids an init
cycle through the mocks barrel).
- typographySettingsStore: BASE_SIZE_EPSILON (0.01) — rounding-jitter
guard for baseSize reconciliation.
- createDebouncedState: DEFAULT_DEBOUNCE_MS (300) — exported so callers
can mirror the default.
- createVirtualizer: MEASUREMENT_EPSILON_PX (0.5) — minimum height
delta before committing a re-measured row.
- createPerspectiveManager: PERSPECTIVE_TOGGLE_THRESHOLD (0.5) — the
halfway point on the 0-1 spring that flips isBack/isFront.
Skipped #19 (PerspectivePlan defaults) per review — marginal gain.
- Use existing MULTIPLIER_S/M/L from \$entities/Font in SliderArea instead
of inlining the 0.5/0.75/1 literals (constants already existed but were
duplicated at the call site).
- Centralize API base URL in \$shared/api/endpoints.ts (was duplicated
between proxyFonts and FilterAndSortFonts filters api).
- Promote every 'glyphdiff:...' localStorage key to a named module-level
STORAGE_KEY constant. Test files now import the source constant rather
than redeclaring it (eliminates silent-typo divergence risk).
createFontRowSizeResolver was reaching into TextLayoutEngine, which
internally called pretext's heavy layoutWithLines and then walked
per-grapheme cursors to build chars arrays. The resolver discarded all
that work and used only totalHeight.
Replace with direct prepare + layout from @chenglou/pretext — pretext
docs explicitly recommend layout() (not layoutWithLines) for the resize
hot path: pure arithmetic on cached segment widths, no canvas calls, no
string allocations.
Test spies on TextLayoutEngine.prototype.layout migrated to vi.mock-ed
pretext layout (pretext's ESM exports are frozen — vi.spyOn fails with
"Cannot redefine property").
TextLayoutEngine marked @deprecated since it has no remaining
consumers; slated for removal after a release cycle.
Both names were vague or overloaded:
- fontStore / FontStore -> fontCatalogStore / FontCatalogStore
Three font-related stores live in this slice; the new name names the
paginated catalog specifically.
- appliedFontsManager / AppliedFontsManager -> fontLifecycleManager /
FontLifecycleManager
"Applied" collided with the filter-side appliedFilterStore (different
meaning). The class actually orchestrates a load-use-evict lifecycle
with FontBufferCache + FontEvictionPolicy + FontLoadQueue
collaborators, so "Manager" is justified. Companion types file moved
alongside (appliedFonts.ts -> fontLifecycle.ts).
Directories, file basenames, factory (createFontStore ->
createFontCatalogStore), and the AppliedFontsManagerDeps interface all
renamed. All consumers (ComparisonView, SampleList, FontList,
FontApplicator, FontVirtualList, FilterAndSortFonts bindings,
createFontRowSizeResolver, mocks) updated.
The byId font fetch was a verb-oriented capability with a single
consumer driven by a feature need (materializing comparison picks).
That shape belongs at the feature layer, not on the entity.
Move:
- entities/Font/model/store/batchFontStore -> features/FetchFontsByIds/model/store/fontsByIdsStore
- Class BatchFontStore -> FontsByIdsStore
entities/Font retains the transport primitives (fetchFontsByIds,
seedFontCache) and the keyspace (fontKeys); the feature wraps them in
the reactive store. comparisonStore now imports FontsByIdsStore from
the new feature. The proxy API is imported via direct path so vi.spyOn
on the source module still observes the call.
Structural:
- Merge factory + singleton from lib/settingsManager and model/state into
one model/store/typographySettingsStore/ slice
- Drop now-empty lib/ and model/state/ directories
Semantic:
- Rename feature SetupFont -> AdjustTypography (the feature owns
continuous typography adjustment, not one-time font setup)
- Drop "Manager" from TypographySettingsManager -> TypographySettingsStore
(class + factory); singleton typographySettingsStore unchanged
All consumers (Character, Line, SampleList, SliderArea, FontSampler,
comparisonStore) updated. Public barrel signature changed: now exports
createTypographySettingsStore and type TypographySettingsStore.
The feature does not fetch fonts — that lives in \$entities/Font's
fontStore. It owns the user's filter selections, sort preference, and
search-by-name query that drive the listing. The new name describes what
it actually does.
Directory + every \$features/GetFonts import path updated; no symbol
renames in this commit.
The 'filters' + 'filterManager' pair didn't reveal the schema-vs-selection
split. Rename to reflect the actual roles:
- FiltersStore / filtersStore → AvailableFilterStore / availableFilterStore
- createFilterManager / FilterManager → createAppliedFilterStore / AppliedFilterStore
- filterManager singleton → appliedFilterStore
- mapManagerToParams → mapAppliedFiltersToParams
Directories and file basenames follow the new singleton names. Public
barrel signature updated; all consumers (Search, FontSearch, Filters,
FilterControls) point at the new identifiers.
Export createSortStore and FiltersStore so per-test instances can be
constructed without sharing singleton state. Add unit tests covering:
- sortStore: default + custom init, display→API value mapping, set()
idempotency, singleton shape
- filters: empty initial state, fetch population, single-call dedup,
error path, cached-fetch reuse across observers
Group each store with its tests under its own directory to match the
filterManager layout.
Dynamic import inside the vi.mock('$entities/Font') factory was missed
when batchFontStore was relocated into its own subdirectory in 1573950.
Restores the previously-failing comparisonStore test suite (9 tests) and
clears the lingering TS error in svelte-check.
Collapse the three duplicated getGroup/map/length-guard chains into a
single selectedIn helper. Drop the unnecessary `as string[]` casts —
Property<TValue extends string>.value already yields string at the call
site.
Add unit tests covering empty query, populated query, missing group,
empty group, single + multi selection, unknown group ids, and the
combined param shape.
Align the slice with the project-wide convention (entities/Font,
entities/Breadcrumb, features/ChangeAppTheme all use model/store/;
CLAUDE.md spec calls for store/). Move bindings, filters, and the
filterManager subdir out of the now-removed model/state/ directory.
Merge the factory previously in lib/filterManager/ with the singleton
previously in model/state/manager.svelte.ts into a single
model/state/filterManager/ slice. The factory builds stateful runes-backed
objects, so it belongs alongside the singleton in model/, not in lib/.
lib/ now contains only the pure mapManagerToParams transform.
Public barrel signature unchanged.
Move the filtersStore → filterManager.setGroups $effect.root out of
manager.svelte.ts into bindings.svelte.ts so all cross-store reactive
wiring for the feature lives in one place. manager.svelte.ts now only
constructs and exports the singleton.
Move the duplicated $effect blocks that mapped filterManager and sortStore
into fontStore params out of Search, FontSearch and FilterControls into a
single $effect.root in features/GetFonts/model/state/bindings.svelte.ts.
Consumers now bind to the manager/store directly; the bridge is installed
once via a side-effect import from the feature barrel.