Commit Graph

929 Commits

Author SHA1 Message Date
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
ilia dec83c93d0 Merge pull request 'Feature/playwright' (#43) from feature/playwright into main
Workflow / build (push) Successful in 1m28s
Workflow / e2e (push) Successful in 1m13s
Workflow / publish (push) Successful in 15s
Reviewed-on: #43
2026-05-28 13:31:16 +00:00
Ilia Mashkov b9560336d5 ci/cd: remove artifact, add build to e2e step
Workflow / build (pull_request) Successful in 1m32s
Workflow / e2e (pull_request) Successful in 2m10s
Workflow / publish (pull_request) Has been skipped
2026-05-28 16:18:25 +03:00
Ilia Mashkov 18f1d109ab fix: add single root level
Workflow / build (pull_request) Failing after 1m33s
Workflow / e2e (pull_request) Has been skipped
Workflow / publish (pull_request) Has been skipped
2026-05-28 16:11:07 +03:00
Ilia Mashkov 24f084ae77 test: add e2e suite for core comparison flows
Workflow / build (pull_request) Failing after 3m47s
Workflow / e2e (pull_request) Has been skipped
Workflow / publish (pull_request) Has been skipped
Adds 17 new specs across six files plus the supporting POM
infrastructure:

- fixtures with auto-opened comparison + typography helpers
- TypographyMenu POM reading values from ComboControl aria-labels
- ComparisonPage extended with side selection, font picking,
  slider-value reader, FontFaceSet inspection, storage snapshot
- compare-flow: A/B selection, aria-pressed state, storage write
- preview-text: input binding + slider character rendering
- slider: keyboard ARIA contract (Arrow / Shift+Arrow / Page / Home / End)
- font-loading: FontFaceSet contains selected, excludes unrelated
- persistence: font selection + typography settings survive reload
- typography: symmetric increase/decrease for all four controls
2026-05-28 15:52:40 +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 f79b24272c ci/cd: add e2e tests with playwright into gitea actions workflow 2026-05-28 13:20:25 +03:00
Ilia Mashkov a9229342e6 test: base playwrignt setup for firefox and chrome 2026-05-28 12:55:10 +03:00
Ilia Mashkov 05cab5f892 fix(cicd): fix yarn version, surface the real failure, bust cache
Workflow / build (push) Successful in 1m45s
Workflow / publish (push) Successful in 35s
2026-05-25 14:11:23 +03:00
Ilia Mashkov 0518c84230 fix: add .dockerignore to prevent glibc yarn cache leaking into musl build
Workflow / build (push) Successful in 1m45s
Workflow / publish (push) Failing after 9s
2026-05-25 13:57:35 +03:00
ilia 5afb9c5d5d Merge pull request 'Chore/architecture refactoring' (#42) from chore/architecture-refactoring into main
Workflow / build (push) Successful in 12s
Workflow / publish (push) Failing after 14s
Reviewed-on: #42
2026-05-25 08:43:06 +00: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