fix: the selection had a fill and no outline in dark
Reported from the canary: a selected range in dark mode is a navy slab
with one bright white line down its left side and no edge anywhere else.
Every colour in the vendored Jspreadsheet stylesheet is a literal, and
sheets.css restated the ones for the grid, the headers and the range
fill. It stopped there, and the outline is where the rest of them live:
.highlight-right/-bottom border: 1px solid #000
.highlight-top/-left the same, plus box-shadow: -1px 0 #ccc
In dark that renders backwards. The black borders vanish into a #111418
grid. The two that would not are overwritten by our own
`td { border-top-color: var(--rst-line) }`, which at (0,2,3) outranks
the library's (0,2,0) `.jss_worksheet .highlight-top` — so they resolve
to ordinary grid lines. What survives is the #ccc box-shadow, a shim
that exists only to mask the grid line the border sits on, and it
becomes the only visible edge. Hence the white L.
Four more of the same shape:
- The selected row's header was #dcdcdc carrying --rst-text-muted
(#a8b0ba) at 1.60:1. sheets.css DID have an override for it,
`tbody > tr > td:first-child.selected`, but the library puts
.selected on the <tr>, not on its first cell, so the rule matched
nothing and looked like a fix. A CSS rule that matches nothing fails
exactly like a rule nobody wrote.
- The fill handle is a black knob ringed in white: inverted in dark.
- The copy marquee and the fill-handle drag preview are dashed and
dotted #000, both invisible.
- jsuites' .jcontextmenu is bound to `contextmenu` whether or not the
host app configures one, so a right-click on any cell opened a #fff
card with #555 text on the dark grid — the last unthemed surface on
this screen.
The fix is one outline colour, --rst-accent, on all four sides of all
three range states, and the #ccc shadow dropped: with an outline that
reads, masking the line beneath it buys nothing and costs the selection
its symmetry. The library gives each state a different border STYLE
(solid, dashed, dotted) and the style survives a colour-only override,
so that is what tells them apart. --rst-accent on --rst-accent-soft
measures 6.59:1 in dark and 4.75:1 in light, both past 1.4.11's 3:1 for
a component boundary.
Every rule is scoped `> tbody > tr > td`, for two reasons and the second
is the one that bites. The grid-line rule above is (0,2,3), so the
library's own specificity loses to it. And sheet.html links the vendored
stylesheets into a head block layout.html renders AFTER sheets.css, so
the library is later in source order and an EQUAL-specificity
restatement loses on order alone. Ours must be strictly higher, never
merely equal — which is also the real reason the vendored #dcdcdc beat
the header rule: both are (0,3,3), and the vendor came last.
One change is not dark-specific and is called out as such.
.highlight-selected — the anchor cell the keyboard is on — is
transparent in the library so it reads out of the fill, and our opaque
.highlight rule took that back. A range had no visible cursor in either
scheme. It is restored, and placed last on purpose: it ties .highlight
and .selection at (0,3,3) and sits inside both mid-drag, so on a tie it
has to be the later rule or the cursor vanishes exactly while you are
aiming with it.
Three guards, and the first one had to be fixed before it was worth
anything. It matched the bare string `.highlight-right` against the
whole of sheets.css — and the paragraph above the rules contains the
line `.highlight-right/-bottom border: 1px solid #000`. Delete the
real rule and every guard stayed green: the prose describing the bug
matched the test looking for its fix. Comments are now stripped before
matching, and each entry anchors on the full scoped selector so a
restatement written at the vendor's specificity fails too.
TestVendorPaintIsRestated 24 vendored rules across both
stylesheets, checked in BOTH
directions — that each vendored
selector still exists (v5 renamed
every .jexcel to .jss_* once
already) and that we restate it in
a rule, not in prose. Rules the app
cannot render are named with a
checkable reason.
TestOurStylesheetHasNoColourLiterals
csstokens_test.go proves every
var(--rst-…) we use is defined, and
says nothing about a declaration
that skips var() altogether.
TestVendorLinksLoadAfterOurs pins the load order the specificity
counting depends on.
Proved by mutation rather than by reading. Four reintroductions, each
caught: the .highlight-right rule deleted; the outline written at
`.jss_worksheet .highlight-top`; `border-right-color: #7ba7ff` in place
of the token; the row-header override put back on the td.
Stated limit, written into the test so nobody trusts it further than it
goes: TestVendorPaintIsRestated compares strings, not specificity. If
the grid-line rule ever gains a step of its own, these selectors stay
green and stop winning.
Reviewed by Claude Opus 5 (1M context) — five findings taken, including
the two above that would have blocked a merge, and one overcall recorded
with its measurement: .jss_content's scrollbar rules are dead here,
because .jss_content computes `overflow: visible` with scrollWidth ==
clientWidth and the grid scrolls on our own [data-sheet-grid]. Review
record and the three things deliberately not fixed are in discussion #9.
Verified in Chromium in both schemes on the real binary, before and
after the rebase onto core: outline #7ba7ff dark / #2464e0 light on all
four sides, box-shadow none, row header on --rst-accent-soft with
--rst-text, handle on the accent, marquee dashed and visible, context
menu #1a1f26/#e8eaed dark and white/#1a1d21 light. make test green
including vet, fmt, migrations and race.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UiPVt2xfkKStDgkv4qLCm1
3 files changed,
+390
−7
AGENTS.md+19 −0internal/guardstest/vendorpaint_test.go+208 −0internal/sheets/static/sheets.css+163 −7
CI passed — run details
diff --git a/AGENTS.md b/AGENTS.md| index 0b48e98..8d9691d 100644 |
| --- a/AGENTS.md |
| +++ b/AGENTS.md |
| @@ -142,6 +142,25 @@ relying on remembering one, make it a test instead. |
| **Frontend** |
| - No build step. Jspreadsheet CE is vendored as a static asset, not fetched |
| from a CDN and not bundled. |
| +- **Both vendored stylesheets paint in literals, and every rule the app |
| + can render must be restated in `sheets.css`.** `TestVendorPaintIsRestated` |
| + holds the list and checks it in both directions — that each vendored |
| + selector still exists (v5 renamed every `.jexcel` to `.jss_*` once |
| + already) and that we restate it. Rules the app cannot render are named |
| + in that test with a checkable reason, never silently dropped. |
| + Two more guards sit beside it: no colour literal may appear in |
| + `sheets.css` at all, and `sheets.css` must stay linked before the head |
| + block that carries the vendored links. |
| +- **Restate at `> tbody > tr > td.foo`, never at the library's own |
| + `.jss_worksheet .foo`.** Two independent reasons, and the second is the |
| + one that bites: our grid-line rule is one specificity step higher than |
| + the library's, *and* `sheet.html` links the vendored stylesheets into a |
| + head block that `layout.html` renders after `sheets.css`, so the |
| + library is later in source order and an equal-specificity restatement |
| + loses on order alone. Ours must be strictly higher, never merely equal. |
| + Written short, a restatement parses, uses a token, and paints nothing — |
| + which is how the grid's selection outline went missing in dark for the |
| + whole of the `core` branch. |
| - **Write the attribute grammar: `rst-btn="primary compact"`, not |
| `class="rst-btn rst-btn--primary"`.** This reverses the earlier rule in |
| this file, which was written against v0.20.0 where the attribute |
diff --git a/internal/guardstest/vendorpaint_test.go b/internal/guardstest/vendorpaint_test.go| new file mode 100644 |
| index 0000000..47703d9 |
| --- /dev/null |
| +++ b/internal/guardstest/vendorpaint_test.go |
| @@ -0,0 +1,208 @@ |
| +package guards |
| + |
| +import ( |
| + "os" |
| + "path/filepath" |
| + "regexp" |
| + "strings" |
| + "testing" |
| +) |
| + |
| +// The vendored stylesheets, and ours. |
| +const ( |
| + vendorGrid = "internal/sheets/static/vendor/jspreadsheet.css" |
| + vendorMenu = "internal/sheets/static/vendor/jsuites.css" |
| + ourSheet = "internal/sheets/static/sheets.css" |
| +) |
| + |
| +// vendorPaint is every vendored rule that paints in a hard-coded colour |
| +// and that this app can actually render. |
| +// |
| +// sheets.css restated the vendored greys for the grid, the headers and |
| +// the range fill, and stopped there. It did not restate the four |
| +// highlight-* borders, the four copying-* borders, the four selection-* |
| +// borders, .jss_corner, or the selected row's header — so in dark mode |
| +// the range fill was drawn and its outline was not: |
| +// |
| +// - highlight-right/-bottom border 1px solid #000 → invisible on #111418. |
| +// - highlight-top/-left their border is overwritten by our own |
| +// `td { border-top-color: var(--rst-line) }`, one specificity step |
| +// higher, so the outline became an ordinary grid line — while their |
| +// decorative `box-shadow: -1px 0 #ccc` survived and became the one |
| +// bright edge on screen. A selection lit down its left side and |
| +// nowhere else. |
| +// - tr.selected > td:first-child #dcdcdc → a light slab in a dark |
| +// grid, carrying --rst-text-muted (#a8b0ba) at 1.60:1. |
| +// - .jss_corner a black knob ringed in white → reads inverted. |
| +// - jsuites' .jcontextmenu #fff with #555 text → a white card on a |
| +// dark grid, on every cell's right-click. |
| +// |
| +// The row header is the instructive one. sheets.css DID write an |
| +// override for it — `tbody > tr > td:first-child.selected` — but |
| +// Jspreadsheet puts `.selected` on the <tr>, not on its first cell, so |
| +// the rule matched nothing and looked like a fix. A CSS rule that |
| +// matches nothing fails exactly like a rule nobody wrote. |
| +// |
| +// Each entry is checked in BOTH directions: the vendored selector must |
| +// still exist (v5 renamed every `.jexcel` to `.jss_*` once already, and a |
| +// stale list like this one is how that rename goes unnoticed a second |
| +// time), and ours must restate it. |
| +// |
| +// Vendored rules with hard-coded colours that are deliberately NOT here. |
| +// Each reason is checkable against the code, and if one stops holding, |
| +// the rule belongs in the list: |
| +// |
| +// .readonly no column sets readOnly and nothing calls |
| +// setReadOnly; grid.js contains neither word. |
| +// .jss_freezed grid.js passes no freezeColumns or freezeRows. |
| +// .jss_pagination a sheet is one page; pagination is off. |
| +// .jupload, editors we do not enable — the cell editor is the |
| +// .jss_richtext plain text one. |
| +// .jss_content ::-webkit-scrollbar-track/-thumb, #eee and #666. |
| +// scrollbar Measured in Chromium: .jss_content computes |
| +// `overflow: visible` with scrollWidth == |
| +// clientWidth, so it never has a scrollbar of its |
| +// own. The grid scrolls on OUR [data-sheet-grid] |
| +// (overflow: auto), whose bars the browser paints |
| +// from color-scheme. |
| +var vendorPaint = []struct { |
| + file string // the vendored stylesheet it lives in |
| + vendor string // must still exist in that stylesheet |
| + restated string // sheets.css must carry this selector, in a RULE not a comment |
| +}{ |
| + {vendorGrid, ".jss_worksheet > thead > tr > td.selected", "> thead > tr > td.selected"}, |
| + {vendorGrid, ".jss_worksheet > tbody > tr.selected > td:first-child", "> tbody > tr.selected > td:first-child"}, |
| + |
| + {vendorGrid, ".jss_worksheet .highlight ", "> tbody > tr > td.highlight "}, |
| + {vendorGrid, ".jss_worksheet .highlight-top", "> tbody > tr > td.highlight-top"}, |
| + {vendorGrid, ".jss_worksheet .highlight-left", "> tbody > tr > td.highlight-left"}, |
| + {vendorGrid, ".jss_worksheet .highlight-right", "> tbody > tr > td.highlight-right"}, |
| + {vendorGrid, ".jss_worksheet .highlight-bottom", "> tbody > tr > td.highlight-bottom"}, |
| + {vendorGrid, ".jss_worksheet .highlight-selected", "> tbody > tr > td.highlight-selected"}, |
| + {vendorGrid, ".jss_worksheet > tbody > tr > td.highlight > a", "> tbody > tr > td.highlight > a"}, |
| + |
| + {vendorGrid, ".jss_worksheet .selection\n", "> tbody > tr > td.selection "}, |
| + {vendorGrid, ".jss_worksheet .selection-top", "> tbody > tr > td.selection-top"}, |
| + {vendorGrid, ".jss_worksheet .selection-bottom", "> tbody > tr > td.selection-bottom"}, |
| + {vendorGrid, ".jss_worksheet .selection-left", "> tbody > tr > td.selection-left"}, |
| + {vendorGrid, ".jss_worksheet .selection-right", "> tbody > tr > td.selection-right"}, |
| + |
| + {vendorGrid, ".jss_worksheet .copying-top", "> tbody > tr > td.copying-top"}, |
| + {vendorGrid, ".jss_worksheet .copying-bottom", "> tbody > tr > td.copying-bottom"}, |
| + {vendorGrid, ".jss_worksheet .copying-left", "> tbody > tr > td.copying-left"}, |
| + {vendorGrid, ".jss_worksheet .copying-right", "> tbody > tr > td.copying-right"}, |
| + |
| + {vendorGrid, ".jss_corner", "[data-sheet-grid] .jss_corner"}, |
| + |
| + {vendorMenu, ".jcontextmenu {", "[data-sheet-grid] .jcontextmenu {"}, |
| + {vendorMenu, ".jcontextmenu > div a", "[data-sheet-grid] .jcontextmenu > div a"}, |
| + {vendorMenu, ".jcontextmenu > div:hover", "[data-sheet-grid] .jcontextmenu > div:hover"}, |
| + {vendorMenu, ".jcontextmenu .jcontextmenu-disabled a", ".jcontextmenu .jcontextmenu-disabled a"}, |
| + {vendorMenu, ".jcontextmenu hr", "[data-sheet-grid] .jcontextmenu hr"}, |
| +} |
| + |
| +// comments strips /* … */ so a selector NAMED in an explanatory comment |
| +// cannot stand in for the rule itself. |
| +// |
| +// This is not hypothetical tidying. The first draft of these tests |
| +// matched bare `.highlight-right` against the whole file, and the |
| +// paragraph above the rules contains the line |
| +// `.highlight-right/-bottom border: 1px solid #000` — so deleting the |
| +// actual rule left every guard green and the reported bug back. The |
| +// prose describing a bug matched the test looking for its fix. |
| +var comments = regexp.MustCompile(`(?s)/\*.*?\*/`) |
| + |
| +func rulesOnly(t *testing.T, rel string) string { |
| + t.Helper() |
| + body, err := os.ReadFile(filepath.Join(repoRoot(t), rel)) |
| + if err != nil { |
| + t.Fatalf("guards: read %s: %v", rel, err) |
| + } |
| + // The vendored files are CRLF and ours is LF; normalise so an entry |
| + // above can anchor on a line break without caring which. |
| + return comments.ReplaceAllString(strings.ReplaceAll(string(body), "\r\n", "\n"), "") |
| +} |
| + |
| +func TestVendorPaintIsRestated(t *testing.T) { |
| + ours := rulesOnly(t, ourSheet) |
| + vendors := map[string]string{ |
| + vendorGrid: rulesOnly(t, vendorGrid), |
| + vendorMenu: rulesOnly(t, vendorMenu), |
| + } |
| + |
| + for _, c := range vendorPaint { |
| + if !strings.Contains(vendors[c.file], c.vendor) { |
| + t.Errorf("%s: no rule for %q — the vendored library renamed or dropped it "+ |
| + "(v5 renamed every .jexcel to .jss_*). Re-derive this list from the "+ |
| + "vendored stylesheet; deleting the entry hides the rule, it does not fix it", |
| + c.file, c.vendor) |
| + continue |
| + } |
| + if !strings.Contains(ours, c.restated) { |
| + t.Errorf("%s: %s is painted in a hard-coded colour by %s and no RULE here restates it — "+ |
| + "a literal does not follow the person's light/dark choice, so this surface "+ |
| + "is wrong in one of the two schemes. (Comments are stripped before this "+ |
| + "check: naming the selector in prose does not count.)", |
| + ourSheet, c.restated, c.file) |
| + } |
| + } |
| +} |
| + |
| +// The vendored stylesheets are linked from sheet.html's head block, |
| +// which layout.html renders AFTER sheets.css — so the library is later |
| +// in source order and an EQUAL-specificity restatement loses. Ours has |
| +// to be strictly higher, which for the grid means carrying the same |
| +// `> tbody > tr > td` the grid-line rule carries. That is why every |
| +// `restated` string above is the full scoped selector rather than the |
| +// bare class: written short, the rule parses, uses a token, and paints |
| +// nothing. |
| +// |
| +// The limit of this check, stated so nobody trusts it further than it |
| +// goes: it compares strings, not specificity. If the grid-line rule at |
| +// the top of sheets.css ever gains a step of its own — an added class, |
| +// a `:not()`, a misused `:where()` — these selectors stay green and stop |
| +// winning. Recount by hand when you touch that rule. |
| +func TestVendorLinksLoadAfterOurs(t *testing.T) { |
| + root := repoRoot(t) |
| + layout, err := os.ReadFile(filepath.Join(root, "internal/sheets/templates/layout.html")) |
| + if err != nil { |
| + t.Skipf("guards: read layout.html: %v", err) |
| + } |
| + sheet, err := os.ReadFile(filepath.Join(root, "internal/sheets/templates/sheet.html")) |
| + if err != nil { |
| + t.Skipf("guards: read sheet.html: %v", err) |
| + } |
| + ours := strings.Index(string(layout), "sheets.css") |
| + head := strings.Index(string(layout), `"head"`) |
| + if ours < 0 || head < 0 { |
| + t.Skip("guards: layout.html no longer links sheets.css before a head block") |
| + } |
| + if !strings.Contains(string(sheet), "jspreadsheet.css") { |
| + t.Skip("guards: sheet.html no longer links the vendored stylesheet") |
| + } |
| + if ours > head { |
| + t.Errorf("layout.html: sheets.css is linked after the head block that carries the "+ |
| + "vendored stylesheets (%d > %d). That reverses the load order these overrides "+ |
| + "were counted against — recheck every restatement in %s, since ties now go "+ |
| + "to us and some may be over-specified", ours, head, ourSheet) |
| + } |
| +} |
| + |
| +// TestOurStylesheetHasNoColourLiterals is the other half of the token |
| +// rule, and the one the token-name check cannot make. |
| +// |
| +// csstokens_test.go proves every `var(--rst-…)` we use is defined. It |
| +// says nothing about a declaration that skips var() altogether: |
| +// `border-right-color: #7ba7ff` passes that check, passes the |
| +// restatement check above, and is wrong in light mode forever. |
| +func TestOurStylesheetHasNoColourLiterals(t *testing.T) { |
| + css := rulesOnly(t, ourSheet) |
| + literal := regexp.MustCompile(`(?i)(#[0-9a-f]{3,8}\b|\brgba?\(|\bhsla?\(|:\s*(red|blue|green|black|white|gray|grey|orange|yellow|purple)\b)`) |
| + for i, line := range strings.Split(css, "\n") { |
| + if m := literal.FindString(line); m != "" { |
| + t.Errorf("%s:%d: colour literal %q — every colour here is a token, never a "+ |
| + "literal, because a literal cannot follow the person's light/dark choice. "+ |
| + "Line: %s", ourSheet, i+1, m, strings.TrimSpace(line)) |
| + } |
| + } |
| +} |
diff --git a/internal/sheets/static/sheets.css b/internal/sheets/static/sheets.css| index d5a439e..e03af32 100644 |
| --- a/internal/sheets/static/sheets.css |
| +++ b/internal/sheets/static/sheets.css |
| @@ -98,12 +98,13 @@ |
| * chrome. Check the vendored stylesheet, not a memory of the last major |
| * version, when these stop working. |
| * |
| - * Every rule that sets a background also sets a color. The library sets |
| - * background-color on the headers and no color, so the text is |
| - * inherited from the page: in dark mode that put light grey text on a |
| - * light grey header and the row and column names became invisible. A |
| - * background without a foreground beside it is the bug, not the |
| - * colour. */ |
| + * Every rule that sets a background on something carrying text also |
| + * sets a color. The library sets background-color on the headers and no |
| + * color, so the text is inherited from the page: in dark mode that put |
| + * light grey text on a light grey header and the row and column names |
| + * became invisible. A background without a foreground beside it is the |
| + * bug, not the colour. (The exceptions are the things with no text in |
| + * them — the fill handle is a 1px knob.) */ |
| [data-sheet-grid] .jss_worksheet { |
| background-color: var(--rst-bg); |
| color: var(--rst-text); |
| @@ -128,17 +129,172 @@ |
| font-weight: 600; |
| } |
| +/* The names of the selected column and row. The library puts .selected |
| + * on the thead td but on the tbody TR — not on its first cell — so the |
| + * two halves need different selectors. Written as `td:first-child.selected` |
| + * the second half matched nothing at all, and a rule that matches nothing |
| + * fails exactly like a rule nobody wrote: the vendored #dcdcdc stood, and |
| + * a dark grid grew a light-grey slab down its left edge carrying |
| + * --rst-text-muted (#a8b0ba) at 1.60:1. */ |
| [data-sheet-grid] .jss_worksheet > thead > tr > td.selected, |
| -[data-sheet-grid] .jss_worksheet > tbody > tr > td:first-child.selected { |
| +[data-sheet-grid] .jss_worksheet > tbody > tr.selected > td:first-child { |
| background-color: var(--rst-accent-soft); |
| color: var(--rst-text); |
| } |
| +/* ── The selection ─────────────────────────────────────────────────── |
| + * |
| + * A range is a fill plus an outline. This file used to restate only the |
| + * fill, and the outline is where every hard-coded colour in the vendored |
| + * stylesheet lives: |
| + * |
| + * .highlight-right/-bottom border: 1px solid #000 |
| + * .highlight-top/-left border: 1px solid #000, plus a decorative |
| + * box-shadow of #ccc just outside it |
| + * |
| + * In dark that renders backwards. The black borders disappear into the |
| + * grid; the two that do not are overwritten by our own |
| + * `td { border-top-color: var(--rst-line) }` above, which is one |
| + * specificity step higher than the library's `.jss_worksheet .highlight-top` |
| + * — so they become ordinary grid lines, while the #ccc shadow, which |
| + * exists only to mask the grid line the border sits on, survives as the |
| + * one bright thing on screen. What you get is a navy slab lit down its |
| + * top and left and nowhere else, with no way to see where it ends. |
| + * |
| + * ── Why every rule below is scoped `> tbody > tr > td` ──────────────── |
| + * Two reasons, and the second is the one that bites. |
| + * |
| + * 1. The grid-line rule above is (0,2,3). A restatement written at the |
| + * library's own `.jss_worksheet .highlight-top` is (0,2,0) and loses |
| + * to it — a rule that parses, uses a token, and paints nothing. |
| + * 2. sheet.html links the vendored stylesheets in the head block, which |
| + * layout.html renders AFTER sheets.css. The library is later in |
| + * source order, so equal specificity loses too. Ours must be |
| + * strictly higher, never merely equal. That is also the real reason |
| + * the vendored #dcdcdc beat the header rule above: both are (0,3,3), |
| + * and the vendor came last. |
| + * |
| + * So: one outline colour, --rst-accent, on all four sides, above both. |
| + * Measured on the fill it sits on, 6.59:1 in dark and 4.75:1 in light — |
| + * both past 1.4.11's 3:1 for a component boundary. The shadow goes: |
| + * with the outline in a colour that reads, masking the line underneath |
| + * it buys nothing and costs the selection its symmetry. |
| + * |
| + * The library gives each of its three range states a different border |
| + * STYLE — solid for the selection, dashed for the copy marquee, dotted |
| + * for the fill-handle drag preview — and the style survives our |
| + * colour-only override. That is the distinction doing the work, so all |
| + * three take the same accent and stay told apart by shape. */ |
| [data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight { |
| background-color: var(--rst-accent-soft); |
| color: var(--rst-text); |
| } |
| +/* A link inside a selected cell. Vendored as the browser's own `blue`, |
| + * which on the fill in dark is 2.4:1. */ |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight > a { |
| + color: var(--rst-accent); |
| +} |
| + |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-top, |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.selection-top, |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.copying-top { |
| + border-top-color: var(--rst-accent); |
| + box-shadow: none; |
| +} |
| + |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-left, |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.selection-left, |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.copying-left { |
| + border-left-color: var(--rst-accent); |
| + box-shadow: none; |
| +} |
| + |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-right, |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.selection-right, |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.copying-right { |
| + border-right-color: var(--rst-accent); |
| +} |
| + |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-bottom, |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.selection-bottom, |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.copying-bottom { |
| + border-bottom-color: var(--rst-accent); |
| +} |
| + |
| +/* The rows the fill handle is being dragged over — the preview of what |
| + * a release would fill, NOT the range a formula is pointing at. There is |
| + * no formula chrome in this app: grid.js sets `parseFormulas: false`, |
| + * because our engine computes and the client only paints. The library |
| + * adds these classes from updateCopySelection, off `selectedCorner`. |
| + * Vendored as rgba(0,0,0,0.05) — a darkening, which on a dark grid is |
| + * nothing at all. */ |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.selection { |
| + background-color: var(--rst-accent-soft); |
| + color: var(--rst-text); |
| +} |
| + |
| +/* The anchor — the one cell the keyboard is on. The library paints it |
| + * transparent on purpose so it reads out of the fill; our .highlight |
| + * rule was opaque and took that back, leaving a range with no visible |
| + * cursor in either scheme. |
| + * |
| + * It comes LAST on purpose. It ties .highlight and .selection at |
| + * (0,3,3), and the anchor is inside both while you drag the fill handle |
| + * — so on a tie it has to be the later rule or the cursor disappears at |
| + * exactly the moment you are aiming with it. */ |
| +[data-sheet-grid] .jss_worksheet > tbody > tr > td.highlight-selected { |
| + background-color: var(--rst-bg); |
| + color: var(--rst-text); |
| +} |
| + |
| +/* The fill handle at the corner of the range: a black knob ringed in |
| + * white, which in dark reads as a white dot with a dark centre — the |
| + * inverse of the thing it is meant to be. It is 1px plus its ring, so |
| + * it is the accent or it is invisible. */ |
| +[data-sheet-grid] .jss_corner { |
| + background-color: var(--rst-accent); |
| + border-color: var(--rst-bg); |
| +} |
| + |
| +/* ── The right-click menu ──────────────────────────────────────────── |
| + * |
| + * Not Jspreadsheet's: jsuites', which it binds to `contextmenu` whether |
| + * or not the host app configures one. So it exists here, it is reachable |
| + * on every cell, and vendored it is #fff with #555 text — a white card |
| + * dropped on a dark grid, and the last unthemed surface on this screen. |
| + * |
| + * It is `position: fixed` and mounted inside the grid, so it is scoped |
| + * the same way everything else here is. */ |
| +[data-sheet-grid] .jcontextmenu { |
| + background: var(--rst-surface); |
| + color: var(--rst-text); |
| + border-color: var(--rst-line-strong); |
| + box-shadow: var(--rst-shadow-pop); |
| +} |
| + |
| +[data-sheet-grid] .jcontextmenu > div a { |
| + color: var(--rst-text); |
| +} |
| + |
| +[data-sheet-grid] .jcontextmenu > div span { |
| + color: var(--rst-text-muted); |
| +} |
| + |
| +[data-sheet-grid] .jcontextmenu > div:hover { |
| + background: var(--rst-accent-soft); |
| + color: var(--rst-text); |
| +} |
| + |
| +[data-sheet-grid] .jcontextmenu .jcontextmenu-disabled a, |
| +[data-sheet-grid] .jcontextmenu .jcontextmenu-disabled::before { |
| + color: var(--rst-text-faint); |
| +} |
| + |
| +[data-sheet-grid] .jcontextmenu hr { |
| + border-color: var(--rst-line); |
| +} |
| + |
| /* The cell editor is an input the library drops into the td. Left |
| * alone it inherits the browser's own white. */ |
| [data-sheet-grid] .jss_worksheet td input, |