Commit Graph

941 Commits

Author SHA1 Message Date
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 43e8507144 Merge branch 'main' into refactor/reacrhitecture-to-fsd+ 2026-06-01 10:43:29 +03:00
ilia 67af3d946a Merge pull request 'chore: introduce font files caching and compressing' (#46) from chore/font-files-caching into main
Workflow / build (push) Successful in 1m19s
Workflow / e2e (push) Successful in 1m8s
Workflow / publish (push) Successful in 14s
Reviewed-on: #46
2026-06-01 07:42:51 +00:00
Ilia Mashkov c6d0270072 chore: introduce font files caching and compressing
Workflow / build (pull_request) Successful in 1m27s
Workflow / e2e (pull_request) Successful in 1m19s
Workflow / publish (pull_request) Has been skipped
2026-06-01 10:37:50 +03:00
Ilia Mashkov a677dc6b0b Merge branch 'main' into refactor/reacrhitecture-to-fsd+ 2026-06-01 10:21:42 +03:00
ilia f7cd6b5081 Merge pull request 'Feature/local fonts' (#45) from feature/local-fonts into main
Workflow / build (push) Successful in 1m20s
Workflow / e2e (push) Successful in 1m7s
Workflow / publish (push) Successful in 28s
Reviewed-on: #45
2026-06-01 07:20:21 +00:00
Ilia Mashkov dda8ef6368 docs(styles): strip decorative comment banners from app.css
Workflow / build (pull_request) Successful in 1m48s
Workflow / e2e (pull_request) Successful in 1m17s
Workflow / publish (pull_request) Has been skipped
Replace ASCII-art separators (====, box-drawing rules, ---- dashes) with
plain section labels and rewrite the casual one-liners as terse, factual
comments.
2026-06-01 10:11:42 +03:00
Ilia Mashkov d77b51736a fix(styles): default body font to Inter, drop unloaded Karla
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).
2026-06-01 10:06:18 +03:00
Ilia Mashkov 1e16330097 refactor(fonts): drop Google Fonts CDN links, preload self-hosted faces
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.
2026-06-01 10:06:12 +03:00
Ilia Mashkov c41016ac5d feat(fonts): self-host interface fonts as vendored latin woff2
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.
2026-06-01 10:06:07 +03:00
Ilia Mashkov aa4189f6a8 chore(app): declare *.woff2?url module type for asset imports 2026-06-01 10:05:33 +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 a9f3b990ab chore: declare sideEffects allowlist for tree-shaking
Without the field, Rollup treats every module as potentially side-effectful
and cannot drop unused re-exports pulled through barrels. Audited all
import-time side effects: only CSS and the two bare side-effect imports
(router.ts, bindings.svelte.ts) must be preserved; module-level store
singletons ride their export usage and need no listing. Trims the bundle
~8 KB raw / ~2.4 KB gzip.
2026-05-31 20:06:22 +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 42bcc915c7 chore(lint): enable import/no-cycle
Guards against circular-dependency regressions in barrels. Requires the
import plugin, which is not enabled by default.
2026-05-31 19:16:44 +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 4d8dcf52e0 refactor: move BaseQueryStore into separate directory, adjust exports/imports 2026-05-31 17:33:06 +03:00
Ilia Mashkov 907145c655 refactor(shared): drop createTypographyControl from shared/lib 2026-05-31 17:08:55 +03:00
Ilia Mashkov e49148008b refactor(Font): remove typography-control config, keep render defaults 2026-05-31 17:08:48 +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 7834c7cbf2 refactor(AdjustTypography): add typography-control module (factory, types, constants) 2026-05-31 16:52:37 +03:00
Ilia Mashkov 4640d6e521 test(shared): test ComboControl against NumericControl mock, not the factory 2026-05-31 16:50:55 +03:00
Ilia Mashkov 8adf5cd7b3 refactor(shared): ComboControl owns its NumericControl + ControlLabels contract 2026-05-31 16:45:04 +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 ecdb2f1b7f refactor(shared): remove deprecated TextLayoutEngine and its re-exports 2026-05-31 13:36:39 +03:00
Ilia Mashkov 6a07b89773 refactor(font): remove CharacterComparisonEngine, superseded by DualFontLayout 2026-05-31 13:36:39 +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 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 f6911fbcca feature: add sv-router, page structure and redirect to home from any other page 2026-05-30 18:31:56 +03:00
ilia eb5a8d1e5b Merge pull request 'Fixes/request deduplication' (#44) from fixes/request-deduplication into main
Workflow / build (push) Successful in 1m28s
Workflow / e2e (push) Successful in 1m14s
Workflow / publish (push) Successful in 15s
Reviewed-on: #44
2026-05-28 19:54:57 +00:00
Ilia Mashkov e698dc6e07 docs: remove GIT_WORKFLOW.md
Workflow / build (pull_request) Successful in 1m35s
Workflow / e2e (pull_request) Successful in 1m22s
Workflow / publish (pull_request) Has been skipped
2026-05-28 22:29:02 +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