Refactor/reacrhitecture to fsd+ #49

Merged
ilia merged 70 commits from refactor/reacrhitecture-to-fsd+ into main 2026-06-03 09:55:47 +00:00

70 Commits

Author SHA1 Message Date
Ilia Mashkov eafe89b313 test: change old test to work with new grapheme split mechanism
Workflow / build (pull_request) Successful in 1m16s
Workflow / e2e (pull_request) Successful in 1m11s
Workflow / publish (pull_request) Has been skipped
2026-06-03 12:50:03 +03:00
Ilia Mashkov 724b00d3d5 test(layoutStore): silence expected warn in invalid-JSON case
The "defaults to list mode when localStorage has invalid data" test feeds
invalid JSON on purpose; createPersistentStore logs and swallows the parse
error, so its warning (with stack) was polluting CI output. Spy on
console.warn to silence it and assert it fired, matching the equivalent
test in createPersistentStore.test.ts.
2026-06-03 11:45:13 +03:00
Ilia Mashkov c09ca93f4e perf(test): stop typographySettings pulling @tanstack for four constants
Workflow / build (pull_request) Successful in 1m15s
Workflow / e2e (pull_request) Failing after 1m33s
Workflow / publish (pull_request) Has been skipped
typographySettingsStore (and its spec) imported DEFAULT_FONT_* from the
$entities/Font root barrel, which re-exports FontVirtualList -> stores ->
@tanstack/query-core. Under Vitest (no tree-shaking) that loaded the entire
UI + TanStack graph just to read four constants.

Import them from the pure $entities/Font/model/const/const module instead.
Deep path is deliberate: it pulls only the constants, not the entity store
graph. Mitigates the test-side cost of audit D-1 (root barrel not yet
inert); the structural fix (inject stores into FontVirtualList) stays
parked.
2026-06-03 10:52:29 +03:00
Ilia Mashkov 99ab7e9e08 refactor(shared/ui): stop deep-importing sibling config/types (C-3)
Badge and TechText reached into $shared/ui/Label/config, and SearchBar into
$shared/ui/Input/types — bypassing the siblings public surface.

- Label/config.ts was explicitly shared text-styling config, not Labels
  internals; relocate it to shared/ui/labelConfig.ts (a neutral peer module)
  and point Label/Badge/TechText at it relatively.
- inputIconSize is a consumer-facing map; export it from Input/index.ts so
  SearchBar imports it through the barrel alongside Input.
2026-06-03 10:36:15 +03:00
Ilia Mashkov ec488cf1ce docs(ComparisonView): document the injected store fields
#fontCatalog / #typography / #lifecycle lacked the per-field doc the rest
of the class uses.
2026-06-03 10:26:00 +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 0aae710e35 fix: dispose persistent-store effects; close comparisonStore effect leak
Thread the new createPersistentStore.destroy() through its owners so the
save effect.root is actually torn down: LayoutManager gains destroy();
ThemeManager and typographySettings dispose their #store in their existing
destroy().

comparisonStore had its own leak — a constructor $effect.root whose disposer
was discarded, and a module-level persistent storage created at import. Move
storage into the instance (#storage, created lazily per instance), capture
the effect.root disposer, add destroy(), and adopt createSingleton so reset
runs resetAll() + destroy(). Re-export createSingleton from the $shared/lib
barrel; give the test storage mock a destroy().
2026-06-03 10:16:47 +03:00
Ilia Mashkov ded9606c30 fix(shared): give createPersistentStore a destroy() to dispose its effect.root
The store created an $effect.root for the save-on-change sync but returned no
disposer, so the effect leaked for the life of the process — contradicting
the rule that $effect.root owners must expose destroy(). Capture and expose
the disposer.

- add destroy() to the returned store; covered by tests (flushSync proves the
  save effect runs before destroy and stops after)
- trim the bloated header (two near-duplicate @example blocks) to one concise
  JSDoc — no fluff
- update typographySettings test mocks to satisfy the now-required destroy()

Consumers (LayoutManager, ThemeManager, typographySettings, comparisonStore)
do not yet call it — threading + the createSingleton migration follow.
2026-06-03 10:00:21 +03:00
Ilia Mashkov f0736f4d35 feat(shared): add createSingleton lazy-singleton accessor helper
Standardizes the getX() / __resetX() pattern hand-rolled identically across
every store: lazy construction on first get(), memoized thereafter, and a
reset() that runs an optional teardown (e.g. destroy()) and clears so the
next get() rebuilds. Lazy by construction, so owning modules stay inert at
import. Covered by unit tests (laziness, memoization, rebuild-after-reset,
teardown-once-with-live-instance, reset-before-get no-op, falsy-value
caching).

Not yet adopted by the stores — that migration is a separate step.
2026-06-03 09:39:51 +03:00
Ilia Mashkov 5eb458eabb refactor(ComparisonView): import typography store via root barrel
comparisonStore reached getTypographySettingsStore through
$features/AdjustTypography/model (deep, past the public API) while the
catalog/lifecycle accessors already go through the $entities/Font root
barrel. Re-path to $features/AdjustTypography so all three composed-store
imports go through public APIs uniformly. Direction was always legal
(widget -> feature, downward); this only closes the deep-import
inconsistency.

Consolidate the spec mock onto the root module accordingly and drop the
now-dead /model mock.
2026-06-03 08:57:04 +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 028853aff5 chore: drop stale bindings.svelte.ts from sideEffects allowlist
bindings.svelte.ts no longer has a top-level side effect: the $effect.root
bridge was moved into startFilterBindings(), wired explicitly by the
app-layer AppBindings provider (onMount). Nothing imports it
side-effect-only anymore, so the allowlist entry falsely marked a now-pure
module as impure. Stores and queryClient are lazy getX() accessors, so they
correctly need no entry either.

Allowlist is now just *.css (style injection) and **/router.ts
(createRouter at eval). Verified: production build succeeds and
startFilterBindings is retained as a used export.
2026-06-02 23:34:08 +03:00
Ilia Mashkov 1c6427c586 chore: drop vestigial $lib alias
$lib pointed at src/lib/, which does not exist, and nothing imported it.
Removed the dead alias from all five declaration sites (tsconfig plus the
vite and three vitest configs). A stray $lib import now fails fast as an
unknown alias instead of resolving to a missing path.
2026-06-02 23:17:27 +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 771bda745c refactor: replace export* barrels with explicit named exports
Wildcard re-exports obscure each slice public surface and weaken
tree-shaking. Convert to explicit named re-exports with export/export
type split (B-1) for ComparisonView, ChangeAppTheme, Breadcrumb/model,
and FilterAndSortFonts/api barrels.
2026-06-02 23:02:18 +03:00
Ilia Mashkov c6c8497906 fix: break import cycles
import/no-cycle (now active) flagged 17 cycles across 12 files:

- shared/ui self-barrel cycles (Logo/Stat/StatGroup/ComboControl/
  FilterGroup/SectionHeader): import siblings relatively instead of
  through the $shared/ui barrel that re-exports them
- shared/lib/utils: roundToStepPrecision imports getDecimalPlaces
  relatively instead of via the utils barrel
- routes: lazy-load Redirect in the router so it no longer statically
  imports a component that imports navigate back from it
2026-06-02 23:02:07 +03:00
Ilia Mashkov f3a10e38df refactor: clear remaining lint errors (comma operator, bind:this ref)
- splitArray: replace the comma-operator reduce body with an explicit
  block + return (no-sequences); behaviour unchanged
- BreadcrumbHeaderSeeded: declare the bind:this ref with $state() so it
  is not flagged as never-assigned (oxlint cannot see template bindings),
  matching the rest of the codebase; guard the onMount use
2026-06-02 23:01:59 +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 deefb51b57 chore(lint): repair oxlint config and enforce FSD boundaries
oxlint was never loading its config: the file was named oxlint.json but
oxlint only auto-discovers .oxlintrc.json/.jsonc, and the `ignore` field
was invalid (should be `ignorePatterns`). So import/no-cycle and every
other rule silently never ran.

- rename oxlint.json -> .oxlintrc.json, fix ignore -> ignorePatterns
- turn off the restriction/style category grab-bags (opt-in, partly
  contradictory); enable wanted rules individually
- add overrides enforcing FSD layer direction and the interior
  ui -> model -> domain law via no-restricted-imports (oxlint has no
  zone rule); import/no-cycle resolves $-aliases via tsconfig discovery
2026-06-02 23:01:33 +03:00
Ilia Mashkov 431fb41a7f chore: merged with main, conflict resolved 2026-06-02 21:52:33 +03:00
Ilia Mashkov 7ddf232e3a refactor(sample-list): replace layoutManager singleton with lazy accessor
Convert the eager layoutManager singleton to getLayoutManager() (+ __resetLayoutManager
for tests), so its persisted layout preference is read on first access rather than at
module load. Update the model barrels and consumers (LayoutSwitch, SampleListSection,
SampleList) with $derived reads; the LayoutSwitch test resolves via the accessor.
2026-06-02 09:09:20 +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 839460726e refactor(breadcrumb): replace scrollBreadcrumbsStore singleton with lazy accessor
Convert the eager scrollBreadcrumbsStore singleton to getScrollBreadcrumbsStore()
(+ __resetScrollBreadcrumbsStore for tests) and add a public destroy() that
disconnects the IntersectionObserver and scroll listener. Update the feature
barrel and consumers (BreadcrumbHeader, BreadcrumbHeaderSeeded, NavigationWrapper).
2026-06-01 18:46:10 +03:00
Ilia Mashkov 6877807aaf refactor(typography): lazy getTypographySettingsStore + fix effect leak
Convert the eager typographySettingsStore singleton to getTypographySettingsStore()
(+ __resetTypographySettingsStore for tests) and update barrels and consumers
(TypographyMenu, FontSampler, SampleList, ComparisonView Line/SliderArea,
comparisonStore + its mock).

Fix a latent leak while here: the store ran $effect.root and discarded the
returned cleanup, so its storage-sync effects outlived every instance. Capture
the disposer and expose destroy(), which __reset now calls.
2026-06-01 18:44:17 +03:00
Ilia Mashkov 3dca11fea8 refactor(theme): replace themeManager singleton with lazy getThemeManager
Convert the eager themeManager singleton to a getThemeManager() lazy accessor
(+ __resetThemeManager for tests), so the persistent-store subscription is set
up on first access rather than at module load. Update the barrel and consumers
(Layout init/destroy, ThemeSwitch, story, test).
2026-06-01 18:39:17 +03:00
Ilia Mashkov 0b675635b3 refactor(filters): replace filter/sort store singletons with lazy accessors
Convert appliedFilterStore, availableFilterStore and sortStore from eager
module-level singletons to getAppliedFilterStore/getAvailableFilterStore/
getSortStore lazy accessors (+ __reset* helpers for tests), so the
availableFilterStore QueryObserver is built on first use rather than at import.

Update barrels, the startFilterBindings bridge, and all consumers. Reactive
reads in components are wrapped in $derived; two-way bind:value targets resolve
the accessor once and bind directly (a $derived is read-only).
2026-06-01 18:37:18 +03:00
Ilia Mashkov 9780ff9358 refactor(filters): mount-scope store bindings and fix effect-update loop
Replace the side-effect-on-import $effect.root in bindings with an explicit
startFilterBindings() started from an AppBindings provider in onMount, so the
filters/sort -> font-catalog bridge has a lifecycle tied to the app tree and a
returned cleanup. bindings now consumes getFontCatalog().

Fix the effect-update loop this surfaced: setGroups populated the reactive
groups array in place via `groups.length = 0; groups.push(...)`. push reads the
array's length signal, so the populating effect both read and wrote
groups.length each run and re-triggered itself forever
(effect_update_depth_exceeded). setGroups now reassigns the array (groups is
`let`), which does not read length.

Extract mapFilterMetadataToGroups to own the metadata -> group-config mapping,
including sorting a copy of options (the source is TanStack-cached data; an
in-place sort corrupts the cache and writes into the effect's read dependency).
2026-06-01 17:25:26 +03:00
Ilia Mashkov 1ad015aed6 refactor(comparison): replace comparisonStore singleton with lazy getComparisonStore
Mirror the font-catalog change in ComparisonView: expose getComparisonStore()
(plus __resetComparisonStore for tests) instead of an eager comparisonStore
singleton, and consume getFontCatalog() internally. Update the model barrel and
all UI consumers (Sidebar, FontList, Header, Line, SliderArea); Character no
longer needs the store and reads everything from props.

Update both specs to the accessor: comparisonStore.test mocks getFontCatalog
with a writable stub (the real store's fonts is getter-only) and resets the
catalog between cases; Sidebar.svelte.test resolves the store via the accessor.

Also document Character's props.
2026-06-01 17:25:05 +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 39d1ce4c37 refactor(FontSampler): remove $derived, since props are already reactive 2026-06-01 16:49:59 +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 43e8507144 Merge branch 'main' into refactor/reacrhitecture-to-fsd+ 2026-06-01 10:43:29 +03:00
Ilia Mashkov a677dc6b0b Merge branch 'main' into refactor/reacrhitecture-to-fsd+ 2026-06-01 10:21:42 +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