Spotted while shipping #487 phase 1 (src/core/left-nav-layout.ts), deferred as out of scope.
The problem
clamp (src/core/format.ts:8) is not NaN-safe — Math.max(lo, Math.min(hi, NaN)) is NaN.
Four createState decoders in src/state.ts feed a parsed localStorage string straight into
it, so a corrupt, truncated or hand-edited value decodes to NaN rather than to the
documented default:
| Field |
Key |
Site |
editorPct |
asb:editorPct |
src/state.ts — via the local num() helper |
sideSplitPct |
asb:sideSplitPct |
src/state.ts — via num() |
cellDrawerPx |
asb:cellDrawerPx |
src/state.ts — clamp(parseInt(...), 320, Infinity) |
docPanePx |
asb:docPanePx |
src/state.ts — clamp(parseInt(...), 320, Infinity) |
A NaN geometry value reaches the DOM as width: NaNpx / height: NaN%, which the browser
drops — so the pane collapses or takes an unstyled default with nothing in the UI to explain
it, and the bad value stays persisted across reloads.
sidebarPx had the same hole and was fixed in #487 phase 1 (clampWideWidthPx) because that
key is the left navigation's wide width, which the phase owns. The other four were left
alone deliberately — they belong to the editor/results splitter, the sidebar's vertical split,
the cell-detail drawer and the docs pane, none of which #487 touches.
Suggested fix
Either make clamp itself NaN-safe (it has many callers — needs an audit of whether any
depend on NaN propagating), or give these four decoders an explicit finite-check-and-default
the way clampWideWidthPx / clampDrawerWidthPx do. The latter is the smaller change and
keeps each key's documented default next to the key.
Each fix needs a createState test case with a non-numeric stored value, alongside the
existing "reads + clamps persisted prefs" case in tests/unit/state.test.ts.
Spotted while shipping #487 phase 1 (
src/core/left-nav-layout.ts), deferred as out of scope.The problem
clamp(src/core/format.ts:8) is not NaN-safe —Math.max(lo, Math.min(hi, NaN))isNaN.Four
createStatedecoders insrc/state.tsfeed a parsed localStorage string straight intoit, so a corrupt, truncated or hand-edited value decodes to
NaNrather than to thedocumented default:
editorPctasb:editorPctsrc/state.ts— via the localnum()helpersideSplitPctasb:sideSplitPctsrc/state.ts— vianum()cellDrawerPxasb:cellDrawerPxsrc/state.ts—clamp(parseInt(...), 320, Infinity)docPanePxasb:docPanePxsrc/state.ts—clamp(parseInt(...), 320, Infinity)A
NaNgeometry value reaches the DOM aswidth: NaNpx/height: NaN%, which the browserdrops — so the pane collapses or takes an unstyled default with nothing in the UI to explain
it, and the bad value stays persisted across reloads.
sidebarPxhad the same hole and was fixed in #487 phase 1 (clampWideWidthPx) because thatkey is the left navigation's wide width, which the phase owns. The other four were left
alone deliberately — they belong to the editor/results splitter, the sidebar's vertical split,
the cell-detail drawer and the docs pane, none of which #487 touches.
Suggested fix
Either make
clampitself NaN-safe (it has many callers — needs an audit of whether anydepend on NaN propagating), or give these four decoders an explicit finite-check-and-default
the way
clampWideWidthPx/clampDrawerWidthPxdo. The latter is the smaller change andkeeps each key's documented default next to the key.
Each fix needs a
createStatetest case with a non-numeric stored value, alongside theexisting "reads + clamps persisted prefs" case in
tests/unit/state.test.ts.