Commit Graph

74 Commits

Author SHA1 Message Date
Ilia Mashkov fcd61be4fa refactor(font): inject font-load status as a prop, decoupling UI from the store
FontApplicator and FontSampler no longer read fontLifecycleManager. They take a
`status` prop (FontLoadStatus | undefined) supplied by the composing widget;
FontList and SampleList resolve status once per visible row and pass it down.

FSD+ dependency inversion: the entity/feature UI depends on a value, not the
lifecycle store. Removes FontApplicator's value-import of the store (one step
toward an inert ./ui barrel) and drops the duplicate getFontStatus read per row
in FontList. FontSampler is now status-decoupled and trivially relocatable to
entities/Font/ui.
2026-06-01 12:06:30 +03:00
Ilia Mashkov 17c022470e refactor(font): expose stores via model segment, not the top barrel
Re-exporting the store singletons (fontCatalogStore, fontLifecycleManager,
FontsByIdsStore) through entities/Font/index.ts meant every consumer of the
barrel eager-instantiated stores and pulled @tanstack/query-core — in dev,
test, and as retained code. Drop the store re-export from the top barrel;
keep the pure surface (types, constants, domain, lib, ui) there for
convenience. Consumers that need stores import $entities/Font/model.
Aligns with the BaseQueryStore carve-out: barrel by default, segment path
when it would drag a heavy or side-effectful dependency.
2026-05-31 20:06:33 +03:00
Ilia Mashkov 36673597f7 refactor(breadcrumb): relocate Breadcrumb slice from entities to features
Breadcrumb is not a business aggregate — it is a scroll-tracking navigation
capability (NavigationWrapper registers page sections into a store), so it
belongs in the features layer, not entities. Move the whole slice and
repoint its three widget consumers. entities/ now holds only Font, a true
aggregate.
2026-05-31 19:30:56 +03:00
Ilia Mashkov a651d3d16f refactor(font): absorb FetchFontsByIds store into Font entity
FetchFontsByIds was data-access for the Font aggregate wearing a feature
costume: a single FontsByIdsStore (BaseQueryStore over Font's proxy API),
no UI, consumed only by comparisonStore. Move it beside fontCatalogStore
in the Font entity, collapse its deep $entities/Font imports to relative
paths, expose via the Font public API, and delete the feature slice.
2026-05-31 17:49:59 +03:00
Ilia Mashkov c613d4cf88 refactor(AdjustTypography): consume typography-control module from its new home 2026-05-31 17:04:51 +03:00
Ilia Mashkov b8edeff86f refactor(font): move test fixtures into a dedicated testing segment
Mock data lived in lib/ and was re-exported through the slice's
production index.ts, advertising fixtures as part of the public API.
Move it to a testing/ segment and drop it from index.ts; consumers now
import explicitly via the $entities/Font/testing subpath.

Repoints the four mock consumers (FontApplicator story, sizeResolver
and fontCatalogStore specs, comparisonStore test) to the new subpath.
2026-05-31 14:08:25 +03:00
Ilia Mashkov 02aa27dc48 refactor(comparison): drop no-op displayChar mapping
The space→non-breaking-space swap guarded against a lone space collapsing
to zero width, but the line container now uses white-space: pre (inherited
by Character), so spaces keep their width. The derived had been reduced to
a space→space identity; render `char` directly.
2026-05-31 13:25:33 +03:00
Ilia Mashkov 4652857512 fix(comparison): lay out lines to the content box, not a padding guess
availableWidth was containerWidth minus a flat 48/96px constant, but the
slider track's gutters are responsive CSS padding (px-4/8/12/lg:px-24,
per side). At lg the real padding is 192px while only 96 was subtracted,
so lines were broken ~96px too wide and overflowed the container.

Measure the container's content box (ResizeObserver contentBoxSize) and
use it directly as availableWidth; keep the border box for the slider and
split math. The content box already excludes the gutters, so it tracks the
breakpoints with no constant to maintain.
2026-05-31 13:25:13 +03:00
Ilia Mashkov d5f0814efc fix(comparison): stabilize line rendering, cut per-tick re-renders
Extract findSplitIndex; computeLineRenderModel now takes the split index
as a primitive. Line derives its model from `split`, so the $derived
short-circuits on value equality and skips recomputation on spring ticks
that don't move the split (previously every tick rebuilt the model and
re-rendered the line).

Lay the three regions out as inline boxes on a shared baseline. fontA and
fontB now align on the typographic baseline despite differing metrics,
and an always-present overflow:hidden strut pins the line-box baseline so
the line no longer jumps when a bulk run mounts/unmounts or the last
window char morphs to a font of different ascent.
2026-05-31 13:24:14 +03:00
Ilia Mashkov 6153769317 refactor(comparison): switch to 3-section render model via DualFontLayout
Rewrite Line.svelte to render leftText / windowChars / rightText regions
from a LineRenderModel. Bulk regions render as native shaped text runs so
the browser applies kerning and ligatures; per-char DOM is reserved for
the N-char crossfade window straddling the slider.

Slim Character.svelte: drop the unused proximity prop and the redundant
font-size/font-weight/letter-spacing styles now inherited from the line
container.

Switch SliderArea.svelte to instantiate DualFontLayout and derive each
line's render model via computeLineRenderModel(line, sliderPos,
containerWidth, WINDOW_SIZE).
2026-05-30 22:29:43 +03:00
Ilia Mashkov ddadac8686 reafactor: move CharacterComparisonEngine into Font entity 2026-05-30 18:48:53 +03:00
Ilia Mashkov c90a258f6c feat(font-list): show empty state when search yields no fonts
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.
2026-05-28 21:36:23 +03:00
Ilia Mashkov b9e21a66d3 fix(comparisonStore): preserve stored selection on cold load
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.
2026-05-28 14:58:18 +03:00
Ilia Mashkov 7a9422b574 test: add aria attributes to tested components 2026-05-28 14:05:14 +03:00
Ilia Mashkov 4126275c4d refactor(SliderArea): extract grid overlay into bg-grid utilities
Workflow / build (pull_request) Successful in 37s
Workflow / publish (pull_request) Has been skipped
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.
2026-05-25 11:09:26 +03:00
Ilia Mashkov ffc28f78f5 refactor(SliderArea): bump the padding to avoid overlap with TypographyMenu 2026-05-25 11:08:04 +03:00
Ilia Mashkov 80241aa352 refactor(SliderArea): remove $derived className 2026-05-25 11:07:20 +03:00
Ilia Mashkov 410a7cd37e feat(SliderArea): keyboard accessibility for the comparison slider
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)
2026-05-25 10:57:54 +03:00
Ilia Mashkov b5fec3a1ba fix(SliderArea): inset paper with padding instead of scale for even gaps
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.
2026-05-25 10:57:23 +03:00
Ilia Mashkov 8eee815e9a refactor(styles): improve light-mode contrast across surfaces and muted text
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.
2026-05-25 10:56:51 +03:00
Ilia Mashkov 5b7ec03973 refactor: sweep call sites onto design-system utilities + bug fixes
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.
2026-05-25 10:20:40 +03:00
Ilia Mashkov 0c59262a59 refactor: extract magic constants — wave 4 (UX timings + physics)
Name throttle/debounce intervals, spring presets, and layout paddings
that were inline numeric literals:

- VirtualList: VISIBLE_CHANGE_THROTTLE_MS (150), NEAR_BOTTOM_THROTTLE_MS
  (200), JUMP_THROTTLE_MS (200)
- SampleList: CHECK_POSITION_THROTTLE_MS (100)
- SliderArea: SLIDER_SPRING_CONFIG ({stiffness: 0.2, damping: 0.7}),
  SLIDER_PERSIST_DEBOUNCE_MS (100), SLIDER_PADDING_MOBILE_PX (48),
  SLIDER_PADDING_DESKTOP_PX (96)
- FontVirtualList: TOUCH_DEBOUNCE_MS (150)
- createPerspectiveManager: PERSPECTIVE_SPRING_CONFIG ({stiffness: 0.2,
  damping: 0.8})

No behavior changes — values preserved exactly.
2026-05-24 21:13:46 +03:00
Ilia Mashkov 2bb43797f0 refactor: extract magic constants — wave 3 (font lifecycle)
Promote font-loading scheduling and lifecycle tunables to named
module-level constants:

- comparisonStore: FONT_READY_FALLBACK_MS (1000ms) — UI unblock safety net
- fontLifecycleManager:
  - PURGE_INTERVAL_MS (60000) — periodic eviction sweep
  - IDLE_CALLBACK_TIMEOUT_MS (150) — requestIdleCallback timeout
  - SCHEDULE_FALLBACK_MS (16) — setTimeout fallback (~60fps)
  - YIELD_INTERVAL_MS (8) — parse-loop yield budget for non-Chromium
  - CRITICAL_FONT_WEIGHTS ([400, 700]) — data-saver allowlist
- FontEvictionPolicy: DEFAULT_FONT_TTL_MS (5 minutes)
- FontLoadQueue: FONT_LOAD_MAX_RETRIES (3)

No behavior changes — values preserved exactly. Class-private fields
that mirrored these constants are removed in favor of module scope.
2026-05-24 21:13:38 +03:00
Ilia Mashkov e3b489f173 refactor: extract magic constants — wave 1 (UX, API, storage)
- 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).
2026-05-24 20:30:26 +03:00
Ilia Mashkov 728380498b refactor(Font): rename fontStore and appliedFontsManager
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.
2026-05-24 20:00:43 +03:00
Ilia Mashkov 07d044f4d6 refactor: extract BatchFontStore into new FetchFontsByIds feature
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.
2026-05-24 19:41:40 +03:00
Ilia Mashkov df59dfda02 refactor(features): rename SetupFont to AdjustTypography + reorganize
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.
2026-05-24 18:27:10 +03:00
Ilia Mashkov ca382fd43d refactor(features): rename GetFonts to FilterAndSortFonts
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.
2026-05-24 18:16:16 +03:00
Ilia Mashkov e0d39d861f refactor(GetFonts): rename filters/filterManager to available/appliedFilterStore
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.
2026-05-24 18:08:05 +03:00
Ilia Mashkov cc218934f4 fix(ComparisonView): update batchFontStore import path in test
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.
2026-05-24 16:05:59 +03:00
Ilia Mashkov e60309af78 refactor(GetFonts): centralize filterManager/sortStore → fontStore bridge
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.
2026-05-24 15:05:28 +03:00
Ilia Mashkov 5ca7a433ff fix: use dvh units to prevent ComparisonView from being covered with address bar on mobile 2026-05-23 21:19:51 +03:00
Ilia Mashkov f762a09c23 fix(SliderArea): temporarily replace pretext measurements with canvas
Workflow / build (pull_request) Successful in 1m53s
Workflow / publish (pull_request) Has been skipped
2026-05-23 20:07:39 +03:00
Ilia Mashkov 95ae72719e chore: move getPretextFontString into separate directory 2026-05-23 20:03:13 +03:00
Ilia Mashkov d1eb83fa90 fix: wire the search to the store 2026-05-23 16:59:59 +03:00
Ilia Mashkov 3894912a22 feat(FontList): add a small gap for elements of ComparisonView sidebar font list 2026-05-05 12:05:19 +03:00
Ilia Mashkov 4eafb96d35 feat(ComparisonView): replace window resize listener with ResiseObserver on the container to catch the container size change on sidebar open/close 2026-04-23 14:59:32 +03:00
Ilia Mashkov 54087b7b2a feat: replace clsx with cn util 2026-04-23 14:59:32 +03:00
Ilia Mashkov cffebf05e3 feat(SliderArea): tweak the styles 2026-04-23 14:59:32 +03:00
Ilia Mashkov 6c91d570ec chore: remove usused code 2026-04-22 12:31:35 +03:00
Ilia Mashkov 6664beec25 feat(FontList): unified skeleton — rows stay skeletal until font file loaded 2026-04-22 09:02:32 +03:00
Ilia Mashkov a801903fd3 feat(FontList): use getSkeletonWidth utility for skeleton row widths 2026-04-22 09:02:32 +03:00
Ilia Mashkov 67fc9dee72 fix(FontList): address the bug with selected font transition animations 2026-04-20 13:36:05 +03:00
Ilia Mashkov a73bd75947 refactor(ComparisonView): unify pretext font string generation with a utility function 2026-04-20 11:13:54 +03:00
Ilia Mashkov 141126530d fix(ComparisonView): fix character morphing thresholds and add tracking support 2026-04-20 10:52:28 +03:00
Ilia Mashkov f9f96e2797 fix(ComparisonView): add correct line-height calculation 2026-04-20 10:51:41 +03:00
Ilia Mashkov 2a51f031cc chore: add missing aria labels 2026-04-19 19:14:49 +03:00
Ilia Mashkov b792dde7cb fix(FontList): overwrite css rule 2026-04-19 19:14:15 +03:00
Ilia Mashkov 66dcffa448 chore(storybook): replace viewport with defaultViewport 2026-04-18 11:04:10 +03:00
Ilia Mashkov 6c69d7a5b3 test(ComparisonView): cover parts of the widget with tests 2026-04-18 01:19:01 +03:00