Commit Graph

208 Commits

Author SHA1 Message Date
Ilia Mashkov 600b905e01 feat(Font): add windowSizeForLine crossfade-window policy 2026-06-03 16:00:29 +03:00
Ilia Mashkov fe07c60dd4 refactor: adopt createSingleton across the remaining stores
Replace the hand-rolled let _x / getX / __resetX boilerplate with the
createSingleton helper in all nine remaining singleton stores. Exposed
accessor names (getX, __resetX) are unchanged, so consumers and specs are
unaffected. Teardown wired to each stores destroy() where it has one
(fontCatalog, fontLifecycle, typography, availableFilter, theme, layout,
scrollBreadcrumbs); sort and appliedFilter have no teardown. Also merges
layoutStores duplicate $shared/lib imports.
2026-06-03 10:22:41 +03:00
Ilia Mashkov a428eac309 docs: remove decorative separator comments
Strip box-drawing (──) section dividers and ===/banner headers — visual
noise with no information. Where a divider label carried a non-obvious
why (VirtualList owns scrolling; mobile footer is md:hidden because header
stats take over) it is kept as a plain one-line comment; pure restatements
of the markup (Header bar, Red hover line, Bottom: fixed controls) are
dropped. Single comment style, no fluff.
2026-06-03 08:45:36 +03:00
Ilia Mashkov 09869aed00 refactor(entities/Font): relocate FontSampler from DisplayFont, invert typography
DisplayFont was not a feature (FSD+ A-6): the whole slice was one
presentational component that renders a Font styled by typography, with no
model/domain/action. To get typography it reached sideways into a sibling
feature (`$features/AdjustTypography/model`) — a feature->feature edge
(C-1), the symptom of the mislayering, not the disease.

Fix by inversion, mirroring the existing `status` prop pattern:

- move FontSampler into entities/Font/ui (it now uses only entity siblings
  + $shared/ui)
- it accepts a `typography` prop typed to a minimal contract defined in the
  component; the AdjustTypography store satisfies it structurally, so the
  entity has no dependency on the feature
- SampleList (owns both) injects its typographySettingsStore as the prop
- delete the DisplayFont slice; export FontSampler from the Font barrel;
  relocate the story (now passes a mock typography)

Resolves A-6, A-7, and the FontSampler half of C-1. Verified: 0 type
errors, 0 lint (boundary rule satisfied), 905 unit + 213 component tests,
production build OK.
2026-06-03 08:34:49 +03:00
Ilia Mashkov 60e115309c refactor(shared/api): lazify queryClient to remove eager-construction footgun
queryClient.ts constructed the TanStack client at module eval but was not
in the sideEffects allowlist, so Rollup treated the module as pure — safe
only while a value-importer keeps it alive (D-3).

- replace the eager `queryClient` singleton with a memoized getQueryClient()
  factory; construction is deferred to first call
- the module is now genuinely side-effect-free, so no sideEffects exception
  is needed and construction can never be legally tree-shaken away
- route all consumers through getQueryClient() (QueryProvider as first
  caller; stores via class fields/observers; tests via a local alias; the
  fontCatalogStore spec mock now overrides getQueryClient)
2026-06-02 23:13:03 +03:00
Ilia Mashkov b390efdabe refactor(entities/Font): named public API and expose stores via root barrel
Stores were only reachable by deep-importing $entities/Font/model, so
consumers reached past the slice public API (FSD anti-pattern D, B-3/D-2).

- convert the Font barrels (root, model, model/store, model/types) to
  explicit named exports with export/export type split (B-1)
- re-export the lazy store accessors/classes from the root barrel so the
  entity public API is complete and inert at import (construction stays
  lazy; the root already loads tanstack via ./ui)
- repoint all consumers (SampleList, SampleListSection, FontList,
  comparisonStore, bindings) from $entities/Font/model to $entities/Font
2026-06-02 23:02:30 +03:00
Ilia Mashkov 9788f07dec refactor: remove unused vars and dead code
Cleanup surfaced once the oxlint config actually loads (no-unused-vars).

- drop dead locals/imports/params (cachedOffsetTop, elasticOut, key,
  unused type imports, unused test imports; _-prefix unused mock params)
- createVirtualizer: keep the _version read (reactive subscription inside
  $derived.by) but bind it to _v so it is not flagged
- scrollBreadcrumbsStore.test: keep the removeEventListener mock side
  effect, drop the unread spy binding
2026-06-02 23:01:48 +03:00
Ilia Mashkov b3bc40b76c refactor(font): replace fontLifecycleManager singleton with lazy accessor
Convert the eager fontLifecycleManager singleton to getFontLifecycleManager()
(+ __resetFontLifecycleManager for tests), so its AbortController/FontFace
bookkeeping is set up on first use rather than at module load. Update consumers
(FontVirtualList, FontList, SampleList) and resolve it once as a field in
comparisonStore; the comparisonStore mock now exposes getFontLifecycleManager.
2026-06-01 18:49:47 +03:00
Ilia Mashkov 10603d18bf refactor(font): replace fontCatalogStore singleton with lazy getFontCatalog
Swap the eagerly-constructed fontCatalogStore singleton for a lazy
getFontCatalog() accessor (plus __resetFontCatalog for tests), so the
InfiniteQueryObserver is created on first use rather than at module load.
Update the model barrels and all consumers (FontVirtualList, SampleList,
SampleListSection) to the accessor.

Also extract createFontLoadRequestConfig from FontVirtualList: it resolves a
font's URL for a weight and returns a 0-or-1 element array, letting callers
flatMap over a list to build load requests and drop unresolvable fonts in one
pass.
2026-06-01 17:24:55 +03:00
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 28a8e49915 refactor(font): keep @tanstack out of the entity public API barrel
Extract NonRetryableError into its own tanstack-free module and drop the
./api re-export from the Font slice barrel. Importing $entities/Font no
longer transitively loads @tanstack/query-core or constructs the QueryClient
singleton via the ./api and ./lib (errors) chains — light consumers (domain,
types, consts) and unit specs stop paying for TanStack.

Note: ./ui still pulls the stores; addressed separately.
2026-06-01 11:49:53 +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 c72b51b1c7 refactor(shared): keep BaseQueryStore out of the lib barrels
BaseQueryStore pulls @tanstack/query-core. Re-exporting it through the
broad $shared/lib and $shared/lib/helpers barrels made every consumer of
those barrels eager-load TanStack at module-eval time (no tree-shaking in
vitest/vite-node), which is what surfaced the queryClient mock init-order
failure. Its single consumer now imports it by path.
2026-05-31 19:16:44 +03:00
Ilia Mashkov 6888f67f14 test(Font): make queryClient mock factory self-contained
The vi.mock factory referenced the top-level QueryClient import. Once
BaseQueryStore landed in the $shared/lib barrel, importing that barrel
for createFilter eagerly pulled @tanstack/query-core into the init
cycle, so the hoisted factory hit the import before initialization
(Cannot access '__vi_import_2__' before initialization). Import
QueryClient inside the factory to keep it independent of module order.
2026-05-31 17:51:34 +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 e49148008b refactor(Font): remove typography-control config, keep render defaults 2026-05-31 17:08:48 +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 7d66b0bc92 refactor(font): extract glyph-comparison logic into a domain segment
Move DualFontLayout and computeLineRenderModel from lib/ to a new
domain/ segment. This is the pure glyph-comparison algorithm — no
framework, no UI, no shared/model dependencies — so it belongs in
domain per the FSD+ ui -> model -> domain interior rule, shielded from
UI-kit and API churn.

Public API is unchanged: the slice index re-exports domain/, so
$entities/Font consumers (ComparisonView SliderArea, Line) are
unaffected.
2026-05-31 14:08:09 +03:00
Ilia Mashkov 6a07b89773 refactor(font): remove CharacterComparisonEngine, superseded by DualFontLayout 2026-05-31 13:36:39 +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 3e568685b3 refactor(font): drop unnecessary PretextInternals shim, use pretext type directly 2026-05-30 22:27:44 +03:00
Ilia Mashkov 581ffb5887 refactor(font): re-export dualFontLayout from Font entity public API 2026-05-30 22:13:26 +03:00
Ilia Mashkov 2ece4c5559 test(font): port DualFontLayout tests, drop per-char-state cases 2026-05-30 22:12:29 +03:00
Ilia Mashkov 1fa099bef5 refactor(font): port engine to DualFontLayout with typed pretext internals 2026-05-30 22:12:00 +03:00
Ilia Mashkov 50238e12c3 refactor(findSplitIndex): remove one for cycle 2026-05-30 22:10:17 +03:00
Ilia Mashkov f13dfe1caf chore: add jsdoc comment 2026-05-30 22:06:47 +03:00
Ilia Mashkov f4edb67acb test(font): window centering, clamping, and key stability 2026-05-30 21:50:34 +03:00
Ilia Mashkov ccf51c645e feat(font): compute split index and 3-region slice 2026-05-30 21:50:02 +03:00
Ilia Mashkov efbc464b14 test(font): computeLineRenderModel handles empty line 2026-05-30 21:49:25 +03:00
Ilia Mashkov c5092a488b refactor(font): scaffold dualFontLayout module with shared types 2026-05-30 21:48:56 +03:00
Ilia Mashkov ddadac8686 reafactor: move CharacterComparisonEngine into Font entity 2026-05-30 18:48:53 +03:00
Ilia Mashkov 5d72bb7a4c refactor(fontCatalogStore): single source of truth for query params
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.
2026-05-28 21:38:17 +03:00
Ilia Mashkov 7f20f36d0a fix(api): mark schema-validation errors as non-retryable
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.
2026-05-28 21:37:23 +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 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 ccef3cf7bb refactor: extract magic constants — wave 2 (TanStack Query defaults)
Promote the duplicated query lifecycle constants in \$shared/api/queryClient.ts:

- staleTime (5 minutes) -> DEFAULT_QUERY_STALE_TIME_MS
- gcTime (10 minutes)   -> DEFAULT_QUERY_GC_TIME_MS
- retry (3)             -> QUERY_RETRY_COUNT
- retryDelay (1s base, 30s cap) -> QUERY_RETRY_BASE_DELAY_MS + QUERY_RETRY_MAX_DELAY_MS

fontCatalogStore and availableFilterStore now import the stale/gc
constants instead of re-deriving '5 * 60 * 1000' / '10 * 60 * 1000'.

fontCatalogStore.svelte.spec.ts's queryClient mock now passes through
the new named exports via importOriginal so the consumer's imports
resolve.
2026-05-24 20:33:46 +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 f92577608a refactor(Font): use pretext layout() directly in row size resolver
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.
2026-05-24 20:12:48 +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 1573950605 chore(Font): move batchFontStore to separate directory 2026-05-24 13:54:15 +03:00
Ilia Mashkov 0d4356b8f1 chore: remove @ts-expect-error since scheduler was added in new TS release 2026-05-05 17:03:18 +03:00
Ilia Mashkov 54087b7b2a feat: replace clsx with cn util 2026-04-23 14:59:32 +03:00
Ilia Mashkov ecdb1e016d feat(FontApplicator): add skeleton snippet prop to replace blur loading state 2026-04-22 09:02:32 +03:00
Ilia Mashkov 092b58e651 feat(FontVirtualList): suppress font loading during jump scroll catch-up 2026-04-22 09:02:32 +03:00
Ilia Mashkov d6914f8179 feat(FontStore): add fetchAllPagesTo for parallel batch page loading 2026-04-22 09:01:45 +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 bb65f1c8d6 feat: add missing storybook files and type template arguments properly 2026-04-17 18:01:24 +03:00
Ilia Mashkov 8a93c7b545 chore: purge shadcn from codebase. Replace with bits-ui components and other tools 2026-04-17 13:37:44 +03:00