Skip to content

fix: stop a width the layout cannot draw, and say when two rows share an id - #45

Merged
ndlabdev merged 4 commits into
devfrom
fix/43-column-snapshot-widths
Sep 3, 2026
Merged

ndlabdev merged 4 commits into
devfrom
fix/43-column-snapshot-widths

Conversation

@ndlabdev

@ndlabdev ndlabdev commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

Two defects reached the same place from different directions, so they travel together.

A column width that the layout cannot draw destroyed the grid, silently. A NaN or Infinity width reached the CSS custom property as NaNpx. A custom property accepts that, but grid-template-columns: var(...) then resolves to an invalid value, the declaration is dropped at computed-value time, every column folds into a single track and the cells stack down the page. Nothing threw and nothing was logged, and widthOverrides still read as a plausible record while the layout was already gone.

setState separately threw on a corrupt columns slice: an order that was not an array reached .filter inside the caller's own call.

Two rows sharing an id failed silently in the data: the row index keeps the last row for a repeated id, so an edit addressed to the row the user opened was written to a different one.

Closes #43
Closes #44

Changes

Where a number becomes CSS. buildColumnCssVars and columnTrackSize refuse a non-finite value and fall back to the column's minimum. This is the gate that closes the failure rather than a list of routes, and while writing it three more routes turned up that the issue had not named: a container measured as NaN, which makes every flex column NaNpx at once, and a definition written with width: NaN or flex: NaN. None of them pass through a setter or a snapshot.

Where an unusable value enters the model. setWidth and setWidths refuse a width that is not finite, where clamp and Math.round had been carrying NaN straight through. setWidth returns the width the column still has. These stay alongside the gate above: they keep junk out of the model rather than papering over it at the end.

Where a snapshot is read. resolveColumnSnapshot now reads values as carefully as it already read keys. Only string ids order the columns, only finite numbers set a width, only real booleans hide a column or fold a group, and a columns slice that is not an object is read as nothing. The fields are typed as unknown while they are read, because naming the type there would be the same promise that let the broken snapshot in.

Where rows are indexed. nodesById compares index.size against nodes.length, and a development build names the ids that collided. Production pays that one integer comparison for the whole data set; working out which ids repeated costs a second pass and only a build that will print it pays for that.

Behaviour changes

  • A width that cannot be drawn no longer reaches the layout. A column given one keeps its minimum instead, so a column can come out narrower than intended where it used to take the whole grid down with it.
  • setWidth returns the current width instead of NaN when handed one. It still returns 0 for a column that is not there.
  • A corrupt snapshot value is dropped rather than applied, so a column falls back to what its definition says.
  • One console.warn in development, the library's only console statement. no-console is disabled on exactly that line: an error would take an app down over data the grid can still draw, and there is no logger to route it to.
  • No valid input changes. Healthy widths, flex weights, minimums and pin offsets are written exactly as before.

Checklist

  • pnpm check: 1503 files, 0 errors, 0 warnings
  • pnpm lint: clean
  • pnpm test: 106 files, 1323 passed, 13 skipped
  • pnpm bench: ran clean, and budgets.test.ts passes inside the suite. The added work is one comparison per data set and one finite check per track, not per row per frame
  • The browser regression test asserts the computed grid-template-columns, not the override record. Backing the fix out turns it red with one track where there should be two, which is how the collapse was confirmed in the first place

Note

This branch and #42 both add to ## [Unreleased] in CHANGELOG.md, so whichever merges second needs a short conflict resolved there.

A `NaN` or `Infinity` width reached the CSS custom property as `NaNpx`. A
custom property accepts that, but `grid-template-columns: var(...)` then
resolves to an invalid value, the declaration is dropped at computed-value
time, and every column folds into a single track with the cells stacked down
the page. Nothing threw and nothing was logged: the grid simply looked broken,
and `widthOverrides` still read as a plausible record while the layout was
already gone.

Two ways in, both closed. The snapshot boundary now reads values as carefully
as it already read keys, and `setWidth`/`setWidths` refuse a width that is not
finite, where `clamp` and `Math.round` had been carrying `NaN` straight
through. An app computing a width from an empty input field reached the second
without going near a snapshot.

`setState` also stopped throwing on a corrupt `columns` slice: an `order` that
was not an array reached `.filter` inside the caller's own call. Only string
ids order the columns now, only real booleans hide one or fold a group, and a
slice that is not an object is read as nothing at all. The fields are typed as
unknown while they are read, because naming the type there would be the same
promise that let the broken snapshot in.

The regression test asserts the computed `grid-template-columns` in the
browser, not the override record. Backing the fix out turns it red with one
track where there should be two.

Closes #43
The row index keeps the last row for a repeated id, so an edit addressed to
the row the user opened was written to a different one, and a selection stood
for two rows at once. Nothing reported it. The id belongs to the app and the
grid cannot mend it, but failing at it silently, in the data, is the worst of
the available behaviours.

A development build now names the ids that collided. Production pays one
integer comparison for the whole data set, `index.size` against
`nodes.length`; working out which ids repeated costs a second pass and only a
build that will print it pays for that.

This is the library's only `console` statement, and the lint rule that
forbids them is disabled on exactly that line. An error would take an app down
over data the grid can still draw, and there is no logger to route it to.

Closes #44
Closing the width setters and the snapshot boundary closed the routes the
issue named, not the failure itself. Three more reach the same line without
passing either: a container measured as `NaN`, which makes every flex column
`NaNpx` at once, and a column definition written with `width: NaN` or
`flex: NaN`.

They all converge on `buildColumnCssVars`, so the check belongs there. A value
that is not finite falls back to the column's minimum, and an unusable minimum
or flex weight falls back to a width that can be drawn. A track of the wrong
size is a much smaller failure than a grid that will not lay out at all.

The earlier guards stay. They keep unusable values out of the model rather
than papering over them at the end, and they let `setWidth` return an honest
answer about the width a column still has.
@ndlabdev ndlabdev added the bug Something isn't working label Sep 2, 2026
@ndlabdev ndlabdev self-assigned this Sep 2, 2026
Both branches added under `## [Unreleased]`, which is the one place two open
fix branches always collide. Resolved by keeping both blocks: changelog
entries are additive, and taking either side would have dropped the other
branch's lines. Ordered by when they landed, so the filter and sort entries
that reached `dev` first lead.
@ndlabdev
ndlabdev merged commit fcc57f5 into dev Sep 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant