Part of the code-health / complexity review of src/. A bundle of small,
low-risk duplication/robustness cleanups. Each is independently shippable.
Why a bundle
These are individually minor (each <~40 LOC) but share a theme: the same logic is
written twice, or a fragile manual pattern is repeated. Grouped so one focused PR
(or a few commits) can clear them.
Item 1 — Duplicated material filtering (~40 LOC)
entity-selection/material-tab.svelte and entity-selection/picker-sheet.svelte
both implement the same element/compound category filtering
(inElements() / inCompounds() + density filtering).
- Why: two copies of selection-filtering logic drift apart over time; a fix to
one (e.g. how externals are categorized) silently misses the other.
- Fix: extract to
src/lib/utils/material-filters.ts (pure functions) and
call from both; add direct unit tests for the predicates.
Item 2 — displayElements recomputed in mobile sheet
compound-editor/mobile-sheet.svelte (~lines 29–39) re-derives displayElements
(weight→atom conversion) that the parent compound-editor-modal.svelte
(~lines 127–135) already computes.
- Why: the mobile sheet already receives a shared
EditorController from the
parent; recomputing the same derivation risks the two views disagreeing if the
conversion logic changes in one place only.
- Fix: expose
displayElements on the controller and consume it in the child
instead of recomputing.
Item 3 — Manual input-snapshot pattern in async calc effects
multi-program-calc.svelte.ts, multi-entity-calc.svelte.ts, and
inverse-calc.svelte.ts each snapshot all inputs into a local object before a
debounced setTimeout(... , 300) to avoid stale-closure races.
- Why: correct today, but every new dependency must be hand-copied into the
snapshot; forgetting one yields stale results with no compile-time error. The
pattern is duplicated three times.
- Fix: extract a small shared helper (e.g.
src/lib/utils/debounced-snapshot.ts) that captures a typed input object and
runs a debounced async callback with cancellation, so the contract is in one
place and each call site just declares its input object.
Acceptance criteria
Out of scope
- Restructuring the compound editor or entity-selection components beyond these
extractions.
Files
src/lib/components/entity-selection/material-tab.svelte,
entity-selection/picker-sheet.svelte, new src/lib/utils/material-filters.ts
src/lib/components/compound-editor/mobile-sheet.svelte,
compound-editor-modal.svelte, compound-editor/types.ts (controller type)
src/lib/state/multi-program-calc.svelte.ts,
multi-entity-calc.svelte.ts, inverse-calc.svelte.ts, new helper + test
AI logging
Why a bundle
These are individually minor (each <~40 LOC) but share a theme: the same logic is
written twice, or a fragile manual pattern is repeated. Grouped so one focused PR
(or a few commits) can clear them.
Item 1 — Duplicated material filtering (~40 LOC)
entity-selection/material-tab.svelteandentity-selection/picker-sheet.svelteboth implement the same element/compound category filtering
(
inElements()/inCompounds()+ density filtering).one (e.g. how externals are categorized) silently misses the other.
src/lib/utils/material-filters.ts(pure functions) andcall from both; add direct unit tests for the predicates.
Item 2 —
displayElementsrecomputed in mobile sheetcompound-editor/mobile-sheet.svelte(~lines 29–39) re-derivesdisplayElements(weight→atom conversion) that the parent
compound-editor-modal.svelte(~lines 127–135) already computes.
EditorControllerfrom theparent; recomputing the same derivation risks the two views disagreeing if the
conversion logic changes in one place only.
displayElementson the controller and consume it in the childinstead of recomputing.
Item 3 — Manual input-snapshot pattern in async calc effects
multi-program-calc.svelte.ts,multi-entity-calc.svelte.ts, andinverse-calc.svelte.tseach snapshot all inputs into a local object before adebounced
setTimeout(... , 300)to avoid stale-closure races.snapshot; forgetting one yields stale results with no compile-time error. The
pattern is duplicated three times.
src/lib/utils/debounced-snapshot.ts) that captures a typed input object andruns a debounced async callback with cancellation, so the contract is in one
place and each call site just declares its input object.
Acceptance criteria
material-tabandpicker-sheetboth consume it; behaviour unchanged.displayElementsfrom the controller (noindependent recomputation); desktop/mobile show identical derived elements.
behaviour (latest input wins, stale results cancelled) is preserved and
covered by a unit test.
pnpm check,pnpm lint,pnpm testpass.Out of scope
extractions.
Files
src/lib/components/entity-selection/material-tab.svelte,entity-selection/picker-sheet.svelte, newsrc/lib/utils/material-filters.tssrc/lib/components/compound-editor/mobile-sheet.svelte,compound-editor-modal.svelte,compound-editor/types.ts(controller type)src/lib/state/multi-program-calc.svelte.ts,multi-entity-calc.svelte.ts,inverse-calc.svelte.ts, new helper + testAI logging