feat(html): read a sheet in a quieter grid, under a ruler that stays put - #670
Draft
andiwand wants to merge 10 commits into
Draft
feat(html): read a sheet in a quieter grid, under a ruler that stays put#670andiwand wants to merge 10 commits into
andiwand wants to merge 10 commits into
Conversation
Dropping odr.js made the css and js string constants written into every document, which is what a self-contained view wants but not what a corpus of them wants: the ~20 KB frontend is repeated in each file, and a one-character stylesheet change rewrites all of them. `HtmlConfig::embed_shipped_resources`, `resource_path`, `relative_resource_paths` and `HtmlResource::is_shipped` were left accepted and inert by that change. They mean again what they used to, except for what made them painful: nothing has to be installed next to the library, because the bytes are still the compiled-in constants. Each asset is registered as a shipped `HtmlResource` over a `MemoryFile`, so the existing locator decides between `<style>`/`<script>` and `<link>`/`<src>`, and `bring_offline` writes the file out — the same path images have always taken. `GlobalParams::odr_core_data_path` stays inert and stays on the list to remove; it is about finding a data directory, which is the part that is gone for good. `HtmlConfig` no longer seeds `resource_path` from it, so reviving the option cannot silently aim output at it. The reference-output suite links them from one `resources/` directory per test repository, the layout it already had in the odr.js era, so that a change to the frontend moves those files rather than every document under them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HcgniS5pgVa4rV1yHxuKm7
The column letters and row numbers were plain `<td>`s carrying their alignment as an inline style repeated on every row, so nothing in css or js could tell the grid's ruler from its contents, and the sheet table itself was addressable only as `table`. They are `<th>`s in a `<thead>`/`<tbody>` now, under `.odr-sheet` with `-corner`, `-column-header`, `-row-header` and a `-gutter` `<col>`, and the constant styling moves into the stylesheet — which makes the output smaller, since what was repeated per row is now written once. Only the row's own height stays inline, being the one part that varies. The letters and numbers get no `scope`: they label positions rather than the contents of what they head, and a screen reader announcing "A" ahead of every cell in a column would be noise. Appearance is unchanged; this is the handle the styling and the scripts need. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HcgniS5pgVa4rV1yHxuKm7
The sheet was the browser's default table: hairline #c0c0c0 rules of the same weight everywhere, Arial 10pt, text set flush against the gridline, and a ruler that scrolled away with the content it labelled. The ruler now sticks to the edges it labels, so the column letters and row numbers stay put however far down or across a sheet goes — the one change that makes a large sheet navigable at all. Sticky cells in a collapsed border model do not repaint their borders in Chrome or WebKit, so it draws its own separators with inset shadows and leaves `border` to the grid. The rest is restraint: lighter gridlines, a firmer rule where the ruler meets the grid, the ruler set smaller and greyer than the data so the eye skips it, a system font stack, and 6px of horizontal padding so text no longer touches the line beside it. Vertical padding stays at a hair, because a row's height is the document's to state and cell padding would fight it. The gutter is sized from the longest row number rather than the flat 30px that truncated anything past four digits. Under `table-layout:fixed` the first row sizes the columns, so the width goes on the corner cell, where `ch` resolves against the ruler's own font. A workbook opens as one file per sheet, so a view titled `odr` left every browser tab of it identical; a sheet view is titled with its sheet's name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HcgniS5pgVa4rV1yHxuKm7
`sheet_name` read a `name` attribute off the worksheet part's root element, which has none — the name is on the `<sheet>` entry in `workbook.xml` that points at the part. Every xlsx sheet was therefore nameless: `Sheet::name()` returned "", the html views were named "" and the sheet titles came out blank. The registry's `Sheet` carries the name now, set from that entry while the workbook's children are parsed. `.ods` and `.xls` already did this correctly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HcgniS5pgVa4rV1yHxuKm7
`body{padding:5px}` sat in the shared document stylesheet, so every view kept a
five pixel border against the window. On a sheet that is exactly the wrong
place for it: the ruler pins itself to the scrollport edge, and the padding
left a sliver of page showing outside it.
The rule goes, rather than being overridden per document type — paged content
already sets its own padding through `.odr-background`, so what it held back
was the two kinds of view that reflow.
Past the last row and column a sheet now shows canvas instead of more white,
which is where the grid ends rather than where the window does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcgniS5pgVa4rV1yHxuKm7
Reading a wide row meant tracking a line of cells by eye across a grid with no markings, and there was no way to say "this one" at all. The row and column under the pointer are now washed, and a click pins them — on a cell, its row and its column; on a ruler cell, just what it labels. The pinned cell is outlined, clicking it again or pressing Escape lets go. The ruler washes harder than the cells, since the label is what the eye is looking for at the end of a column. A row is `tr:hover` in css. A column has no such selector, and walking every cell of one on each pointer move would not survive a sheet of thousands, so a single generated `:nth-child` rule lights the whole column at once — free per cell, but only correct while cell and column line up. A sheet containing a merged cell therefore gets the row half and not the column half, rather than a column highlight pointing one cell off. The wash goes on through `background-image`, which layers over whatever background the document gave a cell instead of replacing it, and leaves `box-shadow` to the ruler's separators. This is the first script a spreadsheet view carries, so `write_spreadsheet_script` joins the style beside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HcgniS5pgVa4rV1yHxuKm7
A rendered sheet could only be read in the order it was written in. Each column header grows a control on hover that sorts by that column, ascending, then descending, then back to the document's own order. The header itself still pins the column, so a sort is never something a click lands on by accident, and the control is the only thing that reorders anything. Values sort the way a spreadsheet sorts them: numbers first, then text, then blanks, with blanks last in both directions because a blank is the absence of a value rather than the smallest one. That ordering is per cell rather than per column, so a column of figures under a label still sorts by its figures. The sort is stable, so equal values keep the order the document had them in. The number behind a cell is not in the markup, only the text it rendered as, so a cell the document calls numeric is read back with the later of `.` and `,` taken as the decimal separator — enough for 1,234.56 and 1.234,56 without knowing the locale — and anything that will not read back stays text. The row numbers do not renumber: they say which row of the document this is, which is the only thing that makes a sorted view legible. A sheet with a merged cell gets no sort control at all. A `rowspan` reaches into a row that would no longer be under it, and there is no honest way to reorder around that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HcgniS5pgVa4rV1yHxuKm7
An archive listed as a run of `<p>`s in monospace, each a row of `<span>`s reading "path file 1048576 download" — the size in bytes with no separators, the kind spelled out per entry, and no alignment between one line and the next. It is a table now, because it always was one: a path, a size and a download control, with a row under the pointer picking itself out and the sizes right-aligned in tabular figures so they can be compared down the column. A directory says so with the trailing separator every file listing has used and carries no size, rather than spending a column on the distinction. Sizes read as binary multiples and are named as such, so 1.4 MiB is not a number whose base the reader has to guess. Nothing in it is written in a language. There is no header row, because a path, a size and a download arrow do not need to be labelled and the labels would have been the only words on the page. Download is a glyph rather than the word — U+2193, not one of the download arrows, which are missing from enough system fonts to leave a tofu box where a control with no text has nothing to fall back on. Its title is the file's name, which is what a tooltip and a screen reader read and is not a word in anyone's language. The listing gets its own stylesheet rather than the one rule it had inline, so it travels the same way the rest of the frontend does. No archive reaches the reference-output suite — every zip in the test data is opened as the document inside it — so this comes with the test that renders one by forcing the type. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HcgniS5pgVa4rV1yHxuKm7
11px was small enough that a column letter took a second look to read, which is the opposite of what a ruler is for. The letters, the row numbers and the sheet tabs go to 12px, and the header row grows the two pixels that keeps them off its edges. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HcgniS5pgVa4rV1yHxuKm7
Every html the generic renderer writes changed: the css and js are linked rather than inlined, a sheet's ruler is `<thead>`/`<th>`, and an archive is a table. The reference repos carry the regenerated output and a `filesystem.css` that did not exist before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nk1S12YmjBsmksSJ4tVB3X
andiwand
force-pushed
the
feat/html-sheet-chrome
branch
from
August 9, 2026 12:25
d24ec7f to
5fb7907
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Generated with Claude Code
A spreadsheet rendered as the browser's default table: one weight of hairline
rule everywhere, Arial 10pt flush against the gridline, and a ruler of column
letters and row numbers that scrolled away with the data it labelled. It now has
a ruler that stays put and reads as chrome, lighter gridlines, room around the
text, canvas past the last row and column, a row and column wash under the
pointer, and a sort control per column. An archive, listed until now as a run of
<p>s in monospace, becomes a table: path, size in binary multiples, downloadglyph.
Two things underneath.
embed_shipped_resourcesand friends, left inert by #648,work again — without them every stylesheet change rewrites every reference file.
And
sheet_nameread an attribute the worksheet part does not have, so everyxlsx sheet was nameless; it comes from the workbook now.
Worth a second opinion:
or WebKit, so the ruler draws its separators with inset shadows and leaves
borderto the grid.the highlight is one generated
:nth-childrule rather than a pass over everycell, which is what keeps it free on a large sheet and wrong under a
colspan.text-overflow:ellipsiswas dropped, not fixed — it sat ontd, whoseoverflow is visible, so it never did anything.
Still base64'd: every file of an archive, whole, into the listing.
Reference output regenerated and pinned. Full suite: 817 passed, 8 skipped, no
failures; new
Document.xlsx_sheet_namesandhtml.archive_listing. Highlightand sort verified in Chrome against
sampledatainsurance.xlsx.