oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– Sheets β€” design review, 2026-09-01
2
3**Method:** dual-agent Impeccable `critique`. Assessment A (design review)
4and Assessment B (deterministic detector) ran isolated and did not see
5each other's output. Both were synthesised against screenshots of the
6real binary, driven in Chromium at 1440Γ—900 and 390Γ—844, in both colour
7schemes. Branch `design-catchup`, cut from `core` at b54989d, with
8rastrillo v0.23.0.
9
10**Mode:** Operate. Scanability, consistency and the real usage scene
11outrank expression.
12
13**Design health: 17/40.** Below the 20–32 band that most working
14interfaces land in. Three of the five surfaces carry a defect that stops
15the screen working.
16
17---
18
19## The headline
20
21The prose is composed for a sealed-at-rest team spreadsheet. The
22composition is not.
23
24The specificity in this app is almost entirely in its copy, and the copy
25is genuinely good β€” `sheets.html:5`, "Everyone on this instance can open
26every sheet here", is a line no ordinary spreadsheet would dare write,
27placed on the screen where a person decides what to put in a sheet.
28
29But copy is the cheapest layer to make specific, and every *structural*
30decision is generic. The one place the product is licensed to invent β€”
31a spreadsheet's own chrome, per the suite rule that the chrome is
32estilo's and the grid is ours β€” got a title label and three ghost
33buttons. There is no cell-reference box, which is the most recognisable
34piece of spreadsheet furniture there is.
35
36The deterministic detector found **nothing** (0 findings across all six
37templates and both JS modules). Every defect below is a semantic one: the
38markup is well-formed, correctly labelled and correctly structured. It is
39pointed at the wrong names.
40
41---
42
43## P0 β€” three screens that do not work
44
45### 1. The dark-mode grid is white-on-white
46
47`sheets.css:66-82` themes `.jexcel`, `.jexcel > tbody > tr > td` and
48`.jexcel > thead > tr > td`. Those are **Jspreadsheet v4** class names.
49The vendored blob is **v5**, whose DOM is `.jss_container`,
50`.jss_content` and `.jss_worksheet` β€” verified against
51`static/vendor/jspreadsheet.css`, which contains no `.jexcel` selector at
52all. Not one of those overrides matches anything.
53
54In light mode this is invisible: `--rst-bg` is white and so is the
55vendor's default. In dark mode the cells keep the vendor's hard-coded
56`#ffffff` while `color` inherits `--rst-text` from `<body>`. The result
57is near-white text on white. The numbers are ghosts.
58
59The file's own opening comment says one hard-coded grey in the grid
60chrome "is a grey that stays wrong in two themes out of three". The rule
61was written correctly and the selector missed.
62
63It also silently kills the selection tint at `sheets.css:80`, which is
64the only feedback the app gives about which cell is selected.
65
66**Fix:** replace `.jexcel` with `.jss_worksheet` throughout, and add
67`.jss_container` / `.jss_content` so the surface behind the last column
68is themed too. Then add a guard test asserting that at least one `.jss_*`
69selector in `sheets.css` also appears in `vendor/jspreadsheet.css` β€” this
70is a vendor-upgrade trap that will recur on the next bump, and it is
71exactly the class of failure that survives a green suite.
72
73### 2. Below 800px there is no way to sign out
74
75`tokens.css:1401` sets `[rst-shell-topbar] [rst-shell-tail] { display:
76none }`. It is revealed by an `[rst-shell-menu][open]` that immediately
77precedes it (`tokens.css:1409`), or by the viewport reaching 800px
78(`tokens.css:1428`). `layout.html` never renders an `[rst-shell-menu]`.
79
80So on every phone β€” and on any half-width desktop window β€” the
81navigation and the entire account dropdown are `display:none` with
82nothing that can reveal them. No sign out. No theme switcher.
83
84That second one matters more than it looks: the theme switcher is a form
85rather than a script specifically because "a person who needs a
86high-contrast theme is not well served by being told to enable scripting
87first". Being told to find a laptop is no better.
88
89**Fix:** render `<details rst-shell-menu name="rst-menus"><summary>Menu
90</summary></details>` as the immediate previous sibling of `<div
91rst-shell-tail>`, so the `+` combinator fires. Sharing the `rst-menus`
92name group means opening it closes the account dropdown. rastrillo hides
93it itself above 800px, so no media query of our own is needed.
94
95### 3. The sign-in help sentence renders inside a 28px icon badge
96
97`signin.html:31` writes `<p rst-help>`. In rastrillo, `rst-help` is not
98helper text β€” it is an **icon badge**: `tokens.css:1245` gives it
99`display:inline-flex; width:28px; height:28px; border:1px solid;
100border-radius:7px`.
101
102A 90-character sentence is being poured into a 28px box that centres its
103overflow. Hence the one-word-per-line column and the stray border stroke
104across the text on desktop, and text running off the left edge of the
105viewport on mobile.
106
107First screen, every visitor, every viewport β€” and it is the sentence
108carrying the sign-in reassurance. Reassurance delivered inside a visibly
109broken component reads as incompetence, not honesty.
110
111**Fix:** the attribute for field help text is `rst-field-help`
112(`tokens.css:1056`). Move the paragraph inside the `rst-field` so it sits
113under the input.
114
115---
116
117## P1 β€” the scriptless table view is a link farm
118
119`table.html:45-46` wraps every cell's content in a bare `<a>`, and
120`sheets.css` styles the table but never the anchor. The result is roughly
121650 default-blue underlined links per page. Empty cells render `&nbsp;`
122inside an anchor, producing a grid of tiny underline stubs β€” 650 tab
123stops, with no visual difference between `94200` and nothing at all.
124
125Compounding it, `--rst-border` is undefined (below), so the table has no
126cell borders whatsoever. Columns E–Z collapse while A–D stretch, so the
127grid geometry a spreadsheet depends on is gone.
128
129This is the screen the spec calls the product, and "fallback" the wrong
130word for. Right now it is the fallback in the apologetic sense β€”
131including in its own subtitle, which says "with no JavaScript at all":
132implementation vocabulary aimed at the wrong reader, describing what the
133view lacks rather than what it is.
134
135**Fix:** style `[data-cell-table] td > a` to inherit colour, drop the
136underline, and fill the cell (`display:block`, a min block size) so the
137target exists without being a mark; show the affordance on `:hover` and
138`:focus-visible` with `--rst-accent-soft`. Drop the `&nbsp;`. Right-align
139numeric cells and set `table-layout: fixed` so sparse data keeps its
140geometry.
141
142---
143
144## P1 β€” five invented token names
145
146`sheets.css` uses five `--rst-*` custom properties that are defined
147nowhere in `tokens.css` or `theme.css`:
148
149| Token | Real name | Sites | Fallback? |
150|---|---|---|---|
151| `--rst-border` | `--rst-line` | 26, 71, 111 | **no** |
152| `--rst-text-mut` | `--rst-text-muted` | 49, 53, 77, 119, 135 | **no** |
153| `--rst-warning` | β€” (none ships) | 53, 93 | yes |
154| `--rst-font-mono` | β€” | 40 | yes |
155| `--rst-shell-bar-h` | β€” | 16 | yes |
156
157The three without a fallback are the ones that bite. `border: 1px solid
158var(--rst-border)` is invalid at computed-value time, so the whole
159shorthand resolves to `initial` β€” `border-style: none`. That is why the
160table view has no cell borders and the sheet bar has no divider. The five
161`--rst-text-mut` sites lose their colour and inherit full-strength text,
162so nothing muted is actually muted.
163
164`--rst-shell-bar-h` always falls back to the 3.5rem literal, so
165`calc(100dvh - …)` at `sheets.css:16` is wrong whenever the bar's real
166height differs β€” on mobile the sheet bar wraps to two rows and the last
167grid row is clipped under the fold.
168
169**Already in flight.** The session working in the primary `sheets-core`
170checkout has an uncommitted `internal/guardstest/csstokens_test.go` whose
171comment reads "the check that would have caught five invented token names
172shipping to a canary". It is these five. This review confirms them
173independently; the fix belongs to that branch, not this one.
174
175---
176
177## P2 β€” smaller, but real
178
179- **`data-contrast` is never set.** `sheets.css:88-99`'s low-contrast
180 corner flag is well-conceived and well-commented, and it is dead code:
181 nothing in Go, JS or any template emits the attribute. `contrast.go`
182 computes the warned list and the notice fires, so a person is told
183 which cells are unreadable and given no way to see which. The one
184 Sheets-specific visual mark in the app never appears.
185- **"Fill down" fills one cell.** `grid.js:183-188` writes to
186 `selection.r + 1` and stops. Every spreadsheet a collaborator has used
187 means "fill the selected range" by that label, so they will press it
188 repeatedly and conclude the app is broken. Either relabel to `Copy
189 down` / `Copy right` until the range version exists, or build the
190 range version.
191- **Nothing has an undo.** A cell write is a POST; browser-back does not
192 reverse it; version history is deferred. With cell-level last-write-
193 wins, one person can silently replace another's formula with no record
194 either of them can see. See the open question below.
195- **No cell reference on screen.** To use a fill command you must
196 remember which cell you selected. The only cue is Jspreadsheet's own
197 header shading.
198- **Raw doubles as display values.** `0.10514018691588785` in the table
199 view, clipped mid-digit in the grid. Three renderings of D2 across
200 three surfaces, only one of them labelled.
201- **The grid page has no `<h1>`.** The title is a `<span
202 data-sheet-title>`, so the document outline is empty on the app's
203 primary screen.
204- **`table.html:5` reads "Table view β€” rows 1 of 20 pages."** It splices
205 a row count and a page count into one ungrammatical phrase.
206- **`table.html:12` uses an inline `style="overflow-x:auto"`** β€” the only
207 raw style declaration in an app that is otherwise entirely attribute
208 grammar and tokens.
209- **Sheet list order is unstable on tied `UpdatedAt`.** Two sheets
210 created in the same minute swap places between renders. No secondary
211 sort key.
212- **Three names for one app in three screens:** `Sheets` (topbar brand),
213 `Oficina Sheets` (`index.html`), `Sign in to Oficina Sheets`.
214- **`sheets.html:4` "Your sheets" is contradicted by its own subtitle**
215 one line below. If they are the instance's sheets, say so in the
216 heading rather than correcting it underneath.
217
218---
219
220## What is working
221
2221. **The list row overlay is used correctly.** `sheets.html:22-27` writes
223 `rst-lrow` / `rst-row-main` / `rst-row-sub`, which picks up
224 rastrillo's `::after { inset: 0; position: absolute }` β€” the whole row
225 is a click target without nesting anchors. It is the only screen that
226 feels finished, and that is because the framework was trusted rather
227 than fought.
2282. **The trust copy lands where it matters.** Not in a settings page: in
229 the subtitle of the screen where you decide what to put in a sheet.
2303. **The formula/display split.** Display strings in the cells, raw input
231 in a side map, the formula bar as the reliable read path with the
232 in-cell swap as best effort. That is the correct model of what a
233 spreadsheet is, and it is why the app can honestly claim the server
234 owns calc.
2354. **`autofocus` on the table view's editing input.** A person clicks a
236 cell and can type immediately, with no script. More of that thinking,
237 applied to how the table view *looks*, is most of what P1 asks for.
238
239---
240
241## Three questions
242
2431. **If the table view is the product and the grid is the enhancement,
244 why did the grid get a formula bar, fill commands, live sync and a
245 contrast marker, while the table view got a `title` tooltip?** Every
246 affordance flowed downhill from the grid. What does the table view
247 look like if it is designed *first*, and does the grid then become a
248 skin over it rather than a second app?
249
2502. **The honest scoping lives on `/`** β€” the page a working collaborator
251 passes once and never returns to. The sheet page, where numbers are
252 actually typed, says nothing about what is and is not protected. If
253 the trust model is the differentiator, does it belong on the marketing
254 surface or the working surface?
255
2563. **Is history the deferred feature, or is undo the missing primitive?**
257 In a cell-level last-write-wins grid shared by two people, undo may be
258 what makes last-write-wins survivable at all, with history merely its
259 readable form. The deferred table asks every deferral for a written
260 trigger. The honest trigger here might be "the first time one of us
261 silently overwrote the other's formula" β€” and that fires the day the
262 second person joins.
263