Pull the baseline-strut height math into a documented computeStrutHeight
util (named constants for the empirical 0.34 / 1.1 factors, with a unit
test) and the per-side native text runs into a SettledText component.
Strut statics move to Tailwind classes with only the height as style:height;
drop the now-redundant style-string $derived bindings.
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.
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.
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.
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.
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().
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.
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.
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.
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.
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.
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.
$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.
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)
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
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.
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
- 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
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
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
The native Popover always renders its content (the vertical slider), so the
slider's value label is in the DOM even when closed, and opening is driven by
the browser's declarative popovertarget invoker (not simulated by jsdom on
click). Update the tests to scope value assertions to the trigger and drive
open via showPopover(), matching Popover.svelte.test.ts.