Skip to content

Spreadsheet row identity, header rows under a title, and a Mermaid pair that opens as a picture - #51

Merged
mindaugaskasp merged 3 commits into
mainfrom
improvement/spreadsheet-row-identity
Aug 9, 2026
Merged

Spreadsheet row identity, header rows under a title, and a Mermaid pair that opens as a picture#51
mindaugaskasp merged 3 commits into
mainfrom
improvement/spreadsheet-row-identity

Conversation

@mindaugaskasp

Copy link
Copy Markdown
Owner

Closes the Spreadsheet track's two now items on docs/roadmap.md, adds the cross-panel row hover, and fixes a Mermaid pair that came back as text.

Row identity — key columns

utils/matchRowsByKey.js pairs rows by a composite key across the whole sheet instead of by whole-row LCS, so the same export sorted differently reads as the figures that moved rather than a rewrite. Duplicate keys pair in order of occurrence and are counted, never hidden — a cap that hides is worse than a cap. Chosen per sheet in the new Match-rows popover; Auto keeps today's LCS exactly, because changing the default matcher for every existing diff has no failing case behind it.

Display order follows the RIGHT file, with a removed row emitted just before the first surviving left row that followed it, so it keeps its context.

Header row offset

headerPairing scans the first ten rows for a usable header instead of reading rows[0] and dropping silently to positional pairing — the exact failure alignColumns exists to prevent. Each side scans independently, since a title row added on one side only is the common case, and the status band names the row it used when it was not the first.

Row hover across both grids

The hovered row is marked in both panels — they are two <table>s, so :hover in one cannot reach the row aligned with it in the other. An outline, not a veil: every row and cell state here is already a background, and a wider border would move the rows the virtualization measures. 2px inset, because 1px read as a different border on beacon and contrast, whose hard --border is a contract.

A restored Mermaid pair opens as a picture

Every load path already defaulted to the diagram — file dialog, drop, diffbro compare, relaunch — all verified against a real launch. restore() was the gap: it replayed payload.semanticView === true, so a snapshot that recorded nothing came back as text. The view is now a tri-state on the way in; an explicitly recorded view is kept either way.

Bugs found and fixed

  • keyColumnsOf accepted an out-of-range column (undefined !== null is true) and threw.
  • The Match-rows panel was unclickable: .content carries isolation: isolate, so the body-teleported backdrop outranked the whole diff subtree.
  • Three of seven new header tests passed before the fix — positional pairing coincided with the right answer. Rewritten so the two pairings disagree.
  • toolbar-view-menu.spec.mjs still selected .count after the chip moved to ui.css as .btn-count.
  • The first restore() attempt made a new tab inherit the previous tab's toggle; tabsStore.test.js caught it.

Verification

  • npm run check green: 3088 tests, coverage 95.34 / 88.47 / 95.76 / 96.33 over the 93 / 86 / 92 / 95 floors, theme depth ok on 20 themes, i18n and raw-text clean.
  • Full e2e in the container, plus targeted host runs of every spec touching the changed contracts.
  • The hover mark read off real frames on light, matrix, beacon, contrast, sepia, nyan and volcano.
  • useSpreadsheetDiff hit its 73-line ratchet and split into useToleranceChoice + useKeyColumns; entry retightened 73 → 69, never raised.

Spec: specs/2026-08-09-spreadsheet-row-identity/plan.md.

🤖 Generated with Claude Code

mindaugaskasp and others added 3 commits August 9, 2026 21:45
… row

The Spreadsheet track's two "now" items, plus a cross-panel row hover.

Row identity — matchRowsByKey.js pairs rows by a composite key across the
whole sheet, so a re-sorted export reads as the figures that moved rather
than as a rewrite. Duplicate keys pair in order of occurrence and are
COUNTED, never hidden. Chosen per sheet in the new Match-rows popover;
Auto keeps today's whole-row LCS exactly.

Header row — headerPairing scans the first ten rows for a usable header
instead of reading rows[0] and dropping silently to positional pairing,
which is the failure alignColumns exists to prevent. The band names the
row it used when it was not the first.

Row hover — the hovered row is marked in BOTH grids; they are two tables,
so :hover in one cannot reach the row aligned with it in the other. An
outline, not a veil: every row and cell state here is already a
background, and a wider border would move the rows the virtualization
measures. 2px inset, because 1px read as a different border on beacon and
contrast, whose hard --border is a contract.

Three bugs found and fixed on the way, two by tests written first:
keyColumnsOf let an out-of-range column through (undefined !== null);
the Match-rows panel was unclickable because .content carries
isolation: isolate and the body-teleported backdrop outranked the whole
diff subtree; toolbar-view-menu.spec.mjs still selected .count after the
chip moved to ui.css as .btn-count for its second consumer.

useSpreadsheetDiff hit its 73-line ratchet and split into the two choices
it carried — useToleranceChoice and useKeyColumns. Entry retightened
73 to 69, never raised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every load path already defaulted to the diagram view — the file dialog,
a drop, `diffbro compare`, a relaunched session — because receive() and
comparePasted() both ask shouldOpenSemantic. restore() did not: it
replayed `payload.semanticView === true`, so a snapshot that recorded
NOTHING (saved before the diagram view existed, or carried by a bundle
that never wrote the field) came back as text.

The view is now a tri-state on the way in: an explicitly recorded view is
kept, either way, and only an absent one is taken from the files. That
needed session.js to stop coercing an unrecorded view to false, or the
distinction never survived being read back.

restoredSemanticView lives beside shouldOpenSemantic in viewChrome.js:
inlining it put restore() over the complexity cap and diffStore over its
line ratchet, and the rule belongs with the one it defers to.

tabsStore._withCurrentView no longer forces semanticView at all. It must
not INHERIT it either — the toggle belongs to the comparison, not to the
reader's last tab (tabsStore.test.js already guarded that, and caught the
first attempt) — so an unrecorded view is left for restore() to resolve
from the files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mindaugaskasp
mindaugaskasp merged commit 772acad into main Aug 9, 2026
6 checks passed
@mindaugaskasp
mindaugaskasp deleted the improvement/spreadsheet-row-identity branch August 9, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant