The native Popover always renders its content (the vertical slider), so the
slider's value label is in the DOM even when closed, and opening is driven by
the browser's declarative popovertarget invoker (not simulated by jsdom on
click). Update the tests to scope value assertions to the trigger and drive
open via showPopover(), matching Popover.svelte.test.ts.
The body font-family referenced "Karla", which was never loaded, so
body text silently fell back to system-ui. Point it at the existing
--font-secondary token (Inter + system fallbacks).
Remove the googleapis stylesheet, both google preconnects, and the two
dead fontshare preconnects from the document head. Preload the two
render-critical faces (Inter, Space Grotesk) via Vite ?url imports.
Eliminates two third-party origins and the IP leak to Google.
Replace Google Fonts CDN delivery of the four UI typefaces (Inter,
Space Grotesk, Space Mono, Syne) with latin-subset woff2 vendored into
app/assets/fonts and wired via a hand-authored @font-face stylesheet.
Variable faces keep wght (Inter also opsz). Vite content-hashes the
binaries for immutable caching.
On initial load, two separate $effects in bindings.svelte.ts — one for
filters, one for sort — each issued its own setOptions with a different
queryKey on the first flush, producing an orphaned
`/fonts?limit=50&offset=0` request immediately followed by the real
`/fonts?limit=50&sort=popularity&offset=0`. Hardcoding the default sort
on the singleton would have papered over the symptom while leaving the
sortStore default and the catalog-store default coupled by hand.
Make bindings the sole emitter of query params:
- features/.../bindings: merge filter + sort effects into one. The effect
reads both stores, builds the merged param object, and issues a single
setParams. No more interleaved setOptions on mount.
- entities/.../fontCatalogStore: gate the observer with `enabled: false`
on construction. The first setParams flips `#enabled` on and triggers
exactly one fetch with the correct queryKey. Removes the need for a
hardcoded default sort on the singleton.
- isEmpty is also gated on `#enabled` so the brief pre-config window
doesn't render "no results" before bindings configures the query.
- The constructor seeds #result from observer.getCurrentResult() because
subscribe may not fire synchronously when the observer is disabled.
The proxy returned `{fonts: null, total: 0}` for empty results, which
fetchProxyFonts surfaced as a generic Error. fontCatalogStore wrapped it
as FontNetworkError, and TanStack retried 3× with exponential backoff —
pinning the loading skeleton for ~7s before settling on an empty list.
Schema mismatches are deterministic; retrying only delays surfacing the
contract violation.
- shared/api/queryClient: introduce NonRetryableError marker class.
The default retry handler short-circuits when it sees this so any
store using the shared client gets fail-fast behavior for free.
- entities/Font/lib/errors: FontResponseError extends NonRetryableError.
- entities/Font/api/proxy/proxyFonts: throw FontResponseError (was a
bare Error). Document that ProxyFontsResponse.fonts is always an array.
- entities/Font/.../fontCatalogStore.fetchPage: preserve a
FontResponseError raised lower in the stack instead of re-wrapping
it as FontNetworkError.
- features/FilterAndSortFonts/api/filters: throw NonRetryableError on
invalid filters payloads and document the array-never-null contract.
Adds an `empty` snippet prop to FontVirtualList and supplies it from the
sidebar FontList. Settled queries with zero results now render a centered
"No typefaces found" label instead of a blank list area.
The seed-defaults effect fired whenever fontA/fontB were still
undefined, including the window between constructor reading storage
and the per-id batch resolving. On a slow batch or fast catalog the
effect clobbered storage with catalog[0]/catalog[N-1], losing the
user's pick on reload.
Now bails when storage already holds IDs, and reads storage via
untrack so per-font selection writes don't re-trigger the effect.
Adds a deterministic regression test that controls catalog/batch
ordering via mockImplementation timing.
The decorative dotted-grid background on the paper surface was a
6-line $derived gridStyle string applied via inline style="" plus four
extra utility classes for color and opacity. Replace with two named
utilities and let CSS handle the responsive switch.
app.css:
- New --color-grid-line CSS var (light + dark) so the grid colour and
intensity auto-switch without consumers needing a dark: variant or an
opacity layer.
- @utility bg-grid (20px cells) and @utility bg-grid-sm (10px cells).
Both reference --color-grid-line, so the same markup paints correctly
in light and dark mode.
SliderArea.svelte:
- Drop the gridStyle $derived block and the inline style= attribute.
- Overlay becomes a single line:
<div class="absolute inset-0 pointer-events-none bg-grid-sm md:bg-grid"
aria-hidden="true" />
Mobile picks the tight 10px grid; the md: breakpoint flips to 20px,
matching the prior JS-driven behaviour with no extra runtime cost.
The slider element had role="slider" and tabindex="0" but no keyboard
handler — the focus ring appeared but the slider could not be moved.
Add a keydown handler implementing the standard ARIA slider contract:
- ArrowLeft / ArrowDown — step left by 1 percent
- ArrowRight / ArrowUp — step right by 1 percent
- Shift + arrow — coarse step (10 percent)
- PageUp / PageDown — coarse step (10 percent)
- Home — jump to 0
- End — jump to 100
Bounds and step sizes extracted as named constants (SLIDER_MIN,
SLIDER_MAX, SLIDER_STEP_FINE, SLIDER_STEP_COARSE). Position updates go
through sliderSpring.target so keyboard moves animate the same way as
pointer drags.
Also adds the missing ARIA attributes that screen readers need:
- aria-valuemin / aria-valuemax (bounds)
- aria-orientation (horizontal)
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.