cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Stills: golden screens drawn as PNGs for the docs, dark and light

Dami committed f5693f2
docScreens in e2e/stills_test.go records states at a size that reads on
a page; make screens draws each golden into docs/media in the reference
palettes, text in Go Mono and box drawing, blocks, braille and symbols
drawn as a terminal draws them. Each PNG carries the hash of its golden,
and make e2e fails on a stale one. doccheck knows stills, and wants a
page to show both color modes; the site shows the one for its mode.
commit
f5693f2341f4aca25d730e3ffa612f3cf1b08a7a
parent
4cdc742e4327bb56d4790a14d681e9fb4c658257
author
Dami <dami.etoile@gmail.com>
kind
feat risk score 2.0 of 3, Jev's estimate
40 files changed, +1528 -35
MCLAUDE.md10+-
MMakefile2+-
Mdocs/contributing/site.md55++++++-
Mdocs/contributing/testing.md12+-
Adocs/media/evaluate-dark.pngbin
Adocs/media/evaluate-light.pngbin
Adocs/media/layout-dark.pngbin
Adocs/media/layout-light.pngbin
Adocs/media/rules-bars-dark.pngbin
Adocs/media/rules-bars-light.pngbin
Adocs/media/rules-panel-dark.pngbin
Adocs/media/rules-panel-light.pngbin
Adocs/media/table-dark.pngbin
Adocs/media/table-light.pngbin
Adocs/media/trace-view-dark.pngbin
Adocs/media/trace-view-light.pngbin
Me2e/go.mod3+-
Me2e/go.sum4+
Me2e/harness_test.go1+
Ae2e/stillboxes_test.go281++++++++++++++++++++++++++++++
Ae2e/stillcells_test.go245++++++++++++++++++++++++++
Ae2e/stilldraw_test.go229++++++++++++++++++++++++
Ae2e/stills_test.go154++++++++++++++++
Ae2e/stillshapes_test.go231+++++++++++++++++++++++++
Ae2e/testdata/screens/docs-evaluate-light.html12+
Ae2e/testdata/screens/docs-evaluate.html12+
Ae2e/testdata/screens/docs-layout-light.html28+++
Ae2e/testdata/screens/docs-layout.html28+++
Ae2e/testdata/screens/docs-palette-light.html18++
Ae2e/testdata/screens/docs-palette.html18++
Ae2e/testdata/screens/docs-rules-bars-light.html12+
Ae2e/testdata/screens/docs-rules-bars.html12+
Ae2e/testdata/screens/docs-rules-panel-light.html12+
Ae2e/testdata/screens/docs-rules-panel.html12+
Ae2e/testdata/screens/docs-table-light.html13+
Ae2e/testdata/screens/docs-table.html13+
Ae2e/testdata/screens/docs-trace-view-light.html14+
Ae2e/testdata/screens/docs-trace-view.html14+
Mscripts/doccheck/main.go106+++++++++--
Mwebsite/src/css/custom.css12+
M CLAUDE.md +6 -4
M Makefile +1 -1
M docs/contributing/site.md +52 -3
@@ -75,7 +75,9 @@ the site's first commit for every page there has been.
7575 and the sidebar is generated from the tree: `_category_.json` for7676 folders, `title` and `sidebar_position` for pages7777 ([Docs](testing.md#docs) has the rules the tree follows).78-- Images in `docs/media/` are bundled from the pages that show them.78+- Images in `docs/media/` are bundled from the pages that show them;79+ an image marked `#gh-dark-mode-only` or `#gh-light-mode-only` shows in80+ that color mode only ([Pictures of 012](#pictures-of-012)).7981 - Prism has no nushell grammar, so `nu` code blocks use a small one in8082 `website/src/prism/nushell.ts`, loaded by the swizzled8183 `website/src/theme/prism-include-languages.ts`.
@@ -105,8 +107,8 @@ are IBM Plex Mono, body text IBM Plex Sans, both bundled with the site.
105107 106108 Diagrams are Mermaid, in a `mermaid` code block, never ASCII art: the107109 same source is drawn on the site and on GitHub. A code block of text108-stays for what is text on a terminal: screens, mockups of them and109-command output.110+stays for text a reader types or a shell prints; 012's own screen is a111+picture ([Pictures of 012](#pictures-of-012)).110112 111113 - Draw one where a flow, a state machine or a sequence would otherwise112114 take paragraphs: a flowchart for data moving through the code, a state
@@ -121,3 +123,50 @@ command output.
121123 (Docusaurus's component, swizzled) draws with Mermaid's `base` theme122124 and variables read from the palette in `custom.css` for the color123125 mode shown, so diagrams don't set their own styles.126+127+## Pictures of 012128+129+A page shows 012 as a picture: a still of the screen in the site's130+color mode, or a recording. A screen typed out in a code block reads as131+a test fixture, loses the colors that carry meaning, and wraps on a132+narrow page, so `scripts/doclint` fails a code block that draws one133+(box-drawing lines with the menu bar or the notebook's toolbar, or the134+grid's column headers).135+136+A still is a [golden screen](testing.md#golden-screens) drawn as a PNG.137+Each is an entry of `docScreens` in `e2e/stills_test.go`: its name,138+the screen whose setup it records, and a terminal size that reads on a139+page (80 columns, unless the state needs more). `make screens` records140+it on a dark and a light terminal and draws both into `docs/media`:141+142+```go143+{name: "evaluate", from: "evaluate", cols: 80, rows: 12},144+// docs/media/evaluate-dark.png and docs/media/evaluate-light.png145+```146+147+The drawing is Go (`e2e/stilldraw_test.go`): the golden's cells in the148+[reference palettes](testing.md#golden-screens), text in Go Mono, and149+box drawing, blocks, braille and the chrome's symbols drawn as a150+terminal draws them, so frames join and every machine draws the same151+picture. Each PNG records which golden it was drawn from, and152+`make e2e` fails when a golden has changed since.153+154+A page shows both pictures together, marked as GitHub marks an image for155+one color mode; `custom.css` shows the site's the same way:156+157+```md158+![Evaluate formula on B8, two steps in: the next part underlined](../media/evaluate-dark.png#gh-dark-mode-only)159+![Evaluate formula on B8, two steps in: the next part underlined](../media/evaluate-light.png#gh-light-mode-only)160+```161+162+- The alt text says what the picture shows, the state and what to look163+ at, the same words on both. Write a range in it as `B3 to B5`: the site164+ reads `:B5` in text as a directive and drops it.165+- One still where the state is the point: what a feature looks like166+ before a page explains it, or a state words describe badly (a box,167+ marks in the grid). The prose keeps what the picture can't: keys,168+ limits, why.169+- A flow of several steps is a recording, a VHS tape in `demos/`170+ ([Demo recordings](testing.md#demo-recordings)).171+- `scripts/doccheck` fails on a still no page shows, or one shown172+ without its other color mode.
M docs/contributing/testing.md +8 -4
@@ -85,7 +85,10 @@ with light-terminal variants. The states are listed in
8585 `e2e/screenlist_test.go`, with the fixtures they share in8686 `e2e/screensetup_test.go`. `make screens` rewrites them and builds8787 `e2e/testdata/screens/gallery.html` with dark and light reference palettes.88-Every visual change is reviewed there before it's committed.88+Every visual change is reviewed there before it's committed. It also89+draws the docs' stills, the screens `docScreens` lists, into90+`docs/media` ([Pictures of 012](site.md#pictures-of-012)); `make e2e`91+fails on one not drawn from its golden as it is.8992 9093 ## The excelize oracle9194
@@ -203,11 +206,12 @@ and a line in the folder's README. `make site` builds them into the
203206 [docs site](site.md) and fails on any link or anchor it can't resolve.204207 205208 `make lint` runs `scripts/doclint`, which flags wording that narrates206-history (see [CLAUDE.md](../../CLAUDE.md)), and `scripts/doccheck`, which209+history (see [CLAUDE.md](../../CLAUDE.md)) and code blocks that draw210+012's screen ([Pictures of 012](site.md#pictures-of-012)), and `scripts/doccheck`, which207211 checks the tree above, that relative links and anchors resolve, that208212 every `docs/...md` path Go code names exists, that every file in209-`docs/media` is shown by a doc and made by a tape, that every tape210-records something a doc shows, and that a page the site has published213+`docs/media` is shown by a doc and made by a tape or drawn from a golden214+screen, that every tape and still is shown by a doc, and that a page the site has published211215 isn't gone without a redirect ([The docs site](site.md#old-addresses-keep-working)).212216 Unit tests check what the docs say about213217 the code: menu paths lead to menu items, command ids and keys exist and
A docs/media/evaluate-dark.png +0 -0

Binary file.

A docs/media/evaluate-light.png +0 -0

Binary file.

A docs/media/layout-dark.png +0 -0

Binary file.

A docs/media/layout-light.png +0 -0

Binary file.

A docs/media/rules-bars-dark.png +0 -0

Binary file.

A docs/media/rules-bars-light.png +0 -0

Binary file.

A docs/media/rules-panel-dark.png +0 -0

Binary file.

A docs/media/rules-panel-light.png +0 -0

Binary file.

A docs/media/table-dark.png +0 -0

Binary file.

A docs/media/table-light.png +0 -0

Binary file.

A docs/media/trace-view-dark.png +0 -0

Binary file.

A docs/media/trace-view-light.png +0 -0

Binary file.

M e2e/go.mod +2 -1
M e2e/go.sum +4 -0
M e2e/harness_test.go +1 -0
A e2e/stillboxes_test.go +281 -0
@@ -0,0 +1,281 @@
1+package e2e2+3+import (4+ "image"5+ "image/color"6+ "math"7+)8+9+// Box drawing and block elements, drawn to the cell's edges as10+// terminals draw them, so a frame's lines meet across cells.11+12+// boxArms gives each box-drawing character the weight of its four arms,13+// up, right, down and left: 0 none, 1 light, 2 heavy, 3 double.14+var boxArms = map[rune]string{15+ '─': "0101", '━': "0202", '│': "1010", '┃': "2020",16+ '╴': "0001", '╵': "1000", '╶': "0100", '╷': "0010",17+ '╸': "0002", '╹': "2000", '╺': "0200", '╻': "0020",18+ '╼': "0201", '╽': "1020", '╾': "0102", '╿': "2010",19+ '╭': "0110", '╮': "0011", '╯': "1001", '╰': "1100",20+}21+22+// boxRuns are the characters U+250C to U+254B and U+2550 to U+256C, in23+// order, as their arms.24+const (25+ boxRun1 = "0110 0210 0120 0220 0011 0012 0021 0022 1100 1200 2100 2200 1001 1002 2001 2002 " +26+ "1110 1210 2110 1120 2120 2210 1220 2220 1011 1012 2011 1021 2021 2012 1022 2022 " +27+ "0111 0112 0211 0212 0121 0122 0221 0222 1101 1102 1201 1202 2101 2102 2201 2202 " +28+ "1111 1112 1211 1212 2111 1121 2121 2112 2211 1122 1221 2212 1222 2122 2221 2222"29+ boxRun2 = "0303 3030 0310 0130 0330 0013 0031 0033 1300 3100 3300 1003 3001 3003 " +30+ "1310 3130 3330 1013 3031 3033 0313 0131 0333 1303 3101 3303 1313 3131 3333"31+)32+33+// boxDashes are the dashed lines: vertical or not, heavy or not, and34+// how many dashes a cell.35+var boxDashes = map[rune]struct {36+ vertical, heavy bool37+ n int38+}{39+ '┄': {false, false, 3}, '┅': {false, true, 3}, '┆': {true, false, 3}, '┇': {true, true, 3},40+ '┈': {false, false, 4}, '┉': {false, true, 4}, '┊': {true, false, 4}, '┋': {true, true, 4},41+ '╌': {false, false, 2}, '╍': {false, true, 2}, '╎': {true, false, 2}, '╏': {true, true, 2},42+}43+44+func init() {45+ for i, arms := range splitFields(boxRun1) {46+ boxArms[rune(0x250c+i)] = arms47+ }48+ for i, arms := range splitFields(boxRun2) {49+ boxArms[rune(0x2550+i)] = arms50+ }51+}52+53+func splitFields(s string) []string {54+ var out []string55+ for i := 0; i+4 <= len(s); i += 5 {56+ out = append(out, s[i:i+4])57+ }58+ return out59+}60+61+// boxCell is one cell being drawn: its rectangle, center and the widths62+// of its lines.63+type boxCell struct {64+ img *image.NRGBA65+ cell image.Rectangle66+ c color.RGBA67+ mx, my float32 // the center68+ t, d float32 // a light line's width; a double line's offset from center69+ w [4]int // arms' weights70+}71+72+// boxDrawing draws U+2500 to U+257F; false for one it doesn't know.73+func boxDrawing(img *image.NRGBA, r rune, cell image.Rectangle, c color.RGBA) bool {74+ t := float32(lineWidth(cell.Dx()))75+ b := boxCell{img: img, cell: cell, c: c, t: t, d: t * 2,76+ mx: float32(cell.Min.X) + float32(cell.Dx())/2, my: float32(cell.Min.Y) + float32(cell.Dy())/2}77+ if dash, ok := boxDashes[r]; ok {78+ b.dashes(dash.vertical, dash.heavy, dash.n)79+ return true80+ }81+ if r >= '╱' && r <= '╳' {82+ b.diagonal(r)83+ return true84+ }85+ arms, ok := boxArms[r]86+ if !ok {87+ return false88+ }89+ for i := range b.w {90+ b.w[i] = int(arms[i] - '0')91+ }92+ if r >= '╭' && r <= '╰' {93+ b.rounded(r)94+ return true95+ }96+ for i := range b.w {97+ b.arm(i)98+ }99+ return true100+}101+102+func (b *boxCell) thickness(weight int) float32 {103+ if weight == 2 {104+ return 2.5 * b.t105+ }106+ return b.t107+}108+109+// rect fills the part of arm i from s0 to its cell edge, s measured from110+// the center outward, and q across it (down or right positive).111+func (b *boxCell) rect(i int, s0, q0, q1 float32) {112+ half := float32(b.cell.Dx()) / 2113+ if i%2 == 0 {114+ half = float32(b.cell.Dy()) / 2115+ }116+ var x0, y0, x1, y1 float32117+ switch i {118+ case 0:119+ x0, x1, y0, y1 = b.mx+q0, b.mx+q1, b.my-half, b.my-s0120+ case 1:121+ x0, x1, y0, y1 = b.mx+s0, b.mx+half, b.my+q0, b.my+q1122+ case 2:123+ x0, x1, y0, y1 = b.mx+q0, b.mx+q1, b.my+s0, b.my+half124+ default:125+ x0, x1, y0, y1 = b.mx-half, b.mx-s0, b.my+q0, b.my+q1126+ }127+ rd := func(v float32) int { return int(math.Round(float64(v))) }128+ fill(b.img, image.Rect(rd(x0), rd(y0), rd(x1), rd(y1)), b.c)129+}130+131+// arm draws arm i, joined to the lines across it.132+func (b *boxCell) arm(i int) {133+ k := b.w[i]134+ if k == 0 {135+ return136+ }137+ perpA, perpB, opp := b.w[(i+1)%4], b.w[(i+3)%4], b.w[(i+2)%4]138+ if k != 3 {139+ th := b.thickness(k)140+ s0 := -th / 2141+ switch perp := max(perpA, perpB); {142+ case perp == 3 && opp == 0 && min(perpA, perpB) == 0:143+ s0 = -b.d - b.t/2 // a corner: reach the far line144+ case perp == 3 && opp == 0:145+ s0 = b.d - b.t/2 // a tee: stop at the near line146+ case perp == 3:147+ s0 = -b.d - b.t/2148+ case perp > 0:149+ s0 = -b.thickness(perp) / 2150+ }151+ b.rect(i, s0, -th/2, th/2)152+ return153+ }154+ for _, o := range []float32{-1, 1} {155+ b.rect(i, b.doubleStart(i, o), o*b.d-b.t/2, o*b.d+b.t/2)156+ }157+}158+159+// doubleStart is where the line of double arm i on side o starts: at160+// the inner line of a double arm on its side, the outer line of one on161+// the other side, or the line across it.162+func (b *boxCell) doubleStart(i int, o float32) float32 {163+ side, other := 2, 0 // for a horizontal arm, o > 0 is down164+ if i%2 == 0 {165+ side, other = 1, 3166+ }167+ if o < 0 {168+ side, other = other, side169+ }170+ switch {171+ case b.w[side] == 3:172+ return b.d - b.t/2173+ case b.w[other] == 3:174+ return -b.d - b.t/2175+ case b.w[side] > 0 || b.w[other] > 0:176+ return -b.thickness(max(b.w[side], b.w[other])) / 2177+ }178+ return -b.t / 2179+}180+181+// rounded draws ╭ ╮ ╯ ╰: a quarter circle between two arms.182+func (b *boxCell) rounded(r rune) {183+ w, h := float32(b.cell.Dx()), float32(b.cell.Dy())184+ cx, cy, rad := w/2, h/2, w/2185+ var a0, a1, ox, oy float32186+ switch r {187+ case '╭':188+ ox, oy, a0, a1 = rad, rad, 1.5*math.Pi, math.Pi189+ case '╮':190+ ox, oy, a0, a1 = -rad, rad, -math.Pi/2, 0191+ case '╯':192+ ox, oy, a0, a1 = -rad, -rad, math.Pi/2, 0193+ default:194+ ox, oy, a0, a1 = rad, -rad, math.Pi/2, math.Pi195+ }196+ fillShape(b.img, b.cell, b.c, func(s *shape) {197+ pts := arc(cx+ox, cy+oy, rad, a0, a1, 16)198+ if oy > 0 {199+ pts = append(pts, pt{cx, h + 1})200+ } else {201+ pts = append(pts, pt{cx, -1})202+ }203+ s.stroke(pts, b.t)204+ })205+}206+207+func (b *boxCell) diagonal(r rune) {208+ w, h := float32(b.cell.Dx()), float32(b.cell.Dy())209+ fillShape(b.img, b.cell, b.c, func(s *shape) {210+ if r != '╲' {211+ s.seg(pt{0, h}, pt{w, 0}, b.t)212+ }213+ if r != '╱' {214+ s.seg(pt{0, 0}, pt{w, h}, b.t)215+ }216+ })217+}218+219+// dashes draws a dashed line of n dashes a cell.220+func (b *boxCell) dashes(vertical, heavy bool, n int) {221+ th := b.thickness(1)222+ if heavy {223+ th = b.thickness(2)224+ }225+ length, i := float32(b.cell.Dx()), 1226+ if vertical {227+ length, i = float32(b.cell.Dy()), 2228+ }229+ step := length / float32(n)230+ for k := range n {231+ s0 := -length/2 + step*float32(k) + step/4232+ b.segment(i, s0, s0+step/2, th)233+ }234+}235+236+// segment fills a stretch of the line along arm i's axis from s0 to s1,237+// measured from the center.238+func (b *boxCell) segment(i int, s0, s1, th float32) {239+ rd := func(v float32) int { return int(math.Round(float64(v))) }240+ if i == 2 {241+ fill(b.img, image.Rect(rd(b.mx-th/2), rd(b.my+s0), rd(b.mx+th/2), rd(b.my+s1)), b.c)242+ return243+ }244+ fill(b.img, image.Rect(rd(b.mx+s0), rd(b.my-th/2), rd(b.mx+s1), rd(b.my+th/2)), b.c)245+}246+247+// quadrants are U+2596 to U+259F as the quarters they fill: 1 upper248+// left, 2 upper right, 4 lower left, 8 lower right.249+var quadrants = [10]int{4, 8, 1, 1 | 4 | 8, 1 | 8, 1 | 2 | 4, 1 | 2 | 8, 2, 2 | 4, 2 | 4 | 8}250+251+// blockElement draws U+2580 to U+259F.252+func blockElement(img *image.NRGBA, r rune, cell image.Rectangle, c color.RGBA) bool {253+ x0, y0, w, h := cell.Min.X, cell.Min.Y, cell.Dx(), cell.Dy()254+ ex := func(n int) int { return x0 + (w*n+4)/8 } // n eighths across255+ ey := func(n int) int { return y0 + (h*n+4)/8 } // n eighths down256+ box := func(xa, ya, xb, yb int) { fill(img, image.Rect(xa, ya, xb, yb), c) }257+ switch {258+ case r == '▀':259+ box(x0, y0, cell.Max.X, ey(4))260+ case r >= '▁' && r <= '█':261+ box(x0, ey(8-int(r-'▀')), cell.Max.X, cell.Max.Y)262+ case r >= '▉' && r <= '▏':263+ box(x0, y0, ex(8-int(r-'█')), cell.Max.Y)264+ case r == '▐':265+ box(ex(4), y0, cell.Max.X, cell.Max.Y)266+ case r >= '░' && r <= '▓':267+ fill(img, cell, color.NRGBA{c.R, c.G, c.B, uint8(64 * (r - '░' + 1))})268+ case r == '▔':269+ box(x0, y0, cell.Max.X, ey(1))270+ case r == '▕':271+ box(ex(7), y0, cell.Max.X, cell.Max.Y)272+ default:273+ q := quadrants[r-'▖']274+ for bit, qr := range [4][4]int{{x0, y0, ex(4), ey(4)}, {ex(4), y0, cell.Max.X, ey(4)}, {x0, ey(4), ex(4), cell.Max.Y}, {ex(4), ey(4), cell.Max.X, cell.Max.Y}} {275+ if q&(1<<bit) != 0 {276+ box(qr[0], qr[1], qr[2], qr[3])277+ }278+ }279+ }280+ return true281+}
A e2e/stillcells_test.go +245 -0
@@ -0,0 +1,245 @@
1+package e2e2+3+import (4+ "fmt"5+ "html"6+ "image/color"7+ "strconv"8+ "strings"9+10+ "golang.org/x/text/width"11+)12+13+// A golden screen read back into cells, for drawing it as a still. The14+// HTML is what renderHTML writes: one line of text per row, runs of15+// cells with the same style in a span, colors as palette variables.16+17+// ink is a color as the golden names it: the terminal's default18+// foreground or background, a palette entry or an RGB value.19+type ink struct {20+ kind inkKind21+ palette int22+ rgb color.RGBA23+}24+25+type inkKind uint826+27+const (28+ inkNone inkKind = iota // the default: fg or bg as the slot says29+ inkFg30+ inkBg31+ inkPalette32+ inkRGB33+)34+35+// underline styles, as the golden's text-decoration names them.36+const (37+ ulNone = iota38+ ulSingle39+ ulDouble40+ ulWavy41+ ulDotted42+ ulDashed43+)44+45+// stillCell is one cell of the screen.46+type stillCell struct {47+ r rune // 0 for the second half of a wide character48+ fg, bg, ulInk ink49+ bold, italic, faint bool50+ underline int51+ strike, wide, filled bool52+}53+54+// parseScreen reads a golden's HTML into rows of cols cells.55+func parseScreen(src string, cols, rows int) ([][]stillCell, error) {56+ src = strings.TrimPrefix(src, `<pre class="screen">`)57+ src = strings.TrimSuffix(src, "</pre>")58+ grid := make([][]stillCell, rows)59+ for y := range grid {60+ grid[y] = make([]stillCell, cols)61+ }62+ for y, line := range strings.Split(src, "\n") {63+ if y >= rows {64+ return nil, fmt.Errorf("more than %d rows", rows)65+ }66+ if err := parseLine(grid[y], line); err != nil {67+ return nil, fmt.Errorf("row %d: %w", y, err)68+ }69+ }70+ return grid, nil71+}72+73+// parseLine fills row from one line of the golden.74+func parseLine(row []stillCell, line string) error {75+ x := 076+ var st stillCell77+ for line != "" {78+ switch {79+ case strings.HasPrefix(line, `<span style="`):80+ end := strings.Index(line, `">`)81+ if end < 0 {82+ return fmt.Errorf("unclosed span")83+ }84+ st = styleOf(line[len(`<span style="`):end])85+ line = line[end+2:]86+ case strings.HasPrefix(line, "</span>"):87+ st = stillCell{}88+ line = line[len("</span>"):]89+ default:90+ end := strings.IndexByte(line, '<')91+ if end < 0 {92+ end = len(line)93+ }94+ var err error95+ if x, err = putText(row, x, html.UnescapeString(line[:end]), st); err != nil {96+ return err97+ }98+ line = line[end:]99+ }100+ }101+ return nil102+}103+104+// putText writes text into row from column x in style st and returns105+// the column after it.106+func putText(row []stillCell, x int, text string, st stillCell) (int, error) {107+ for _, r := range text {108+ w := 1109+ if k := width.LookupRune(r).Kind(); k == width.EastAsianWide || k == width.EastAsianFullwidth {110+ w = 2111+ }112+ if x+w > len(row) {113+ return x, fmt.Errorf("wider than %d columns", len(row))114+ }115+ c := st116+ c.r, c.wide, c.filled = r, w == 2, true117+ row[x] = c118+ if w == 2 {119+ row[x+1] = st120+ row[x+1].filled = true121+ }122+ x += w123+ }124+ return x, nil125+}126+127+// styleOf reads a span's CSS.128+func styleOf(css string) stillCell {129+ var c stillCell130+ for _, decl := range strings.Split(css, ";") {131+ prop, val, _ := strings.Cut(decl, ":")132+ switch prop {133+ case "color":134+ c.fg = inkOf(val)135+ case "background":136+ c.bg = inkOf(val)137+ case "font-weight":138+ c.bold = val == "bold"139+ case "font-style":140+ c.italic = val == "italic"141+ case "opacity":142+ c.faint = true143+ case "text-decoration":144+ decorate(&c, val)145+ }146+ }147+ return c148+}149+150+// decorate reads a text-decoration: underline with its style and color,151+// or line-through.152+func decorate(c *stillCell, val string) {153+ f := strings.Fields(val)154+ if len(f) == 0 {155+ return156+ }157+ if f[0] == "line-through" {158+ c.strike = true159+ return160+ }161+ c.underline = ulSingle162+ for _, w := range f[1:] {163+ switch w {164+ case "double":165+ c.underline = ulDouble166+ case "wavy":167+ c.underline = ulWavy168+ case "dotted":169+ c.underline = ulDotted170+ case "dashed":171+ c.underline = ulDashed172+ default:173+ c.ulInk = inkOf(w)174+ }175+ }176+}177+178+// inkOf reads a CSS color as the goldens write them.179+func inkOf(v string) ink {180+ switch {181+ case v == "var(--fg)":182+ return ink{kind: inkFg}183+ case v == "var(--bg)":184+ return ink{kind: inkBg}185+ case strings.HasPrefix(v, "var(--vt-palette-"):186+ n, _ := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(v, "var(--vt-palette-"), ")"))187+ return ink{kind: inkPalette, palette: n}188+ case strings.HasPrefix(v, "#") && len(v) == 7:189+ n, _ := strconv.ParseUint(v[1:], 16, 32)190+ return ink{kind: inkRGB, rgb: rgba(uint32(n))}191+ }192+ return ink{}193+}194+195+func rgba(v uint32) color.RGBA {196+ return color.RGBA{R: uint8(v >> 16), G: uint8(v >> 8), B: uint8(v), A: 0xff}197+}198+199+// stillPalette is a terminal's colors: the reference palettes the200+// gallery shows.201+type stillPalette struct {202+ ansi [16]uint32203+ fg, bg uint32204+}205+206+var (207+ darkStill = stillPalette{darkANSI, 0xc5c8c6, 0x1d1f21}208+ lightStill = stillPalette{lightANSI, 0x1d1f21, 0xfafafa}209+)210+211+// color resolves an ink; def is what inkNone means in its slot.212+func (p stillPalette) color(k ink, def inkKind) color.RGBA {213+ if k.kind == inkNone {214+ k.kind = def215+ }216+ switch k.kind {217+ case inkFg:218+ return rgba(p.fg)219+ case inkPalette:220+ return rgba(p.xterm(k.palette))221+ case inkRGB:222+ return k.rgb223+ }224+ return rgba(p.bg)225+}226+227+// xterm is palette entry n: the 16 ANSI colors, then xterm's 6x6x6 cube228+// and gray ramp.229+func (p stillPalette) xterm(n int) uint32 {230+ switch {231+ case n < 16:232+ return p.ansi[n]233+ case n < 232:234+ n -= 16235+ level := func(v int) uint32 {236+ if v == 0 {237+ return 0238+ }239+ return uint32(55 + 40*v)240+ }241+ return level(n/36)<<16 | level(n/6%6)<<8 | level(n%6)242+ }243+ g := uint32(8 + 10*(n-232))244+ return g<<16 | g<<8 | g245+}
A e2e/stilldraw_test.go +229 -0
@@ -0,0 +1,229 @@
1+package e2e2+3+import (4+ "fmt"5+ "image"6+ "image/color"7+ "image/draw"8+ "math"9+10+ "golang.org/x/image/font"11+ "golang.org/x/image/font/gofont/gomono"12+ "golang.org/x/image/font/gofont/gomonobold"13+ "golang.org/x/image/font/gofont/gomonobolditalic"14+ "golang.org/x/image/font/gofont/gomonoitalic"15+ "golang.org/x/image/font/opentype"16+ "golang.org/x/image/math/fixed"17+ "golang.org/x/image/vector"18+)19+20+// Drawing a screen's cells as a picture of a terminal: text in Go Mono21+// (bundled with golang.org/x/image, so every machine draws the same22+// letters), and box drawing, blocks, braille and the symbols 012 draws23+// its chrome with drawn as shapes, as terminals draw them, so lines join24+// across cells.25+26+const (27+ stillFontSize = 26 // pixels; stills are drawn at twice the size they show28+ stillPad = 28 // around the screen, inside the window29+ stillRadius = 18 // the window's corners30+)31+32+// stillFaces are Go Mono's four styles at the still's size.33+type stillFaces struct {34+ regular, bold, italic, boldItalic font.Face35+ cw, ch, ascent int36+}37+38+func loadFaces() (*stillFaces, error) {39+ var fs stillFaces40+ for _, f := range []struct {41+ ttf []byte42+ dst *font.Face43+ }{{gomono.TTF, &fs.regular}, {gomonobold.TTF, &fs.bold}, {gomonoitalic.TTF, &fs.italic}, {gomonobolditalic.TTF, &fs.boldItalic}} {44+ parsed, err := opentype.Parse(f.ttf)45+ if err != nil {46+ return nil, err47+ }48+ face, err := opentype.NewFace(parsed, &opentype.FaceOptions{Size: stillFontSize, DPI: 72, Hinting: font.HintingNone})49+ if err != nil {50+ return nil, err51+ }52+ *f.dst = face53+ }54+ adv, _ := fs.regular.GlyphAdvance('0')55+ m := fs.regular.Metrics()56+ fs.cw = adv.Round()57+ fs.ch = int(math.Round(stillFontSize * 1.3))58+ fs.ascent = (fs.ch-(m.Ascent+m.Descent).Round())/2 + m.Ascent.Round()59+ return &fs, nil60+}61+62+func (fs *stillFaces) face(c stillCell) font.Face {63+ switch {64+ case c.bold && c.italic:65+ return fs.boldItalic66+ case c.bold:67+ return fs.bold68+ case c.italic:69+ return fs.italic70+ }71+ return fs.regular72+}73+74+// drawStill draws grid as a terminal window in palette p. It returns the75+// runes it had no way to draw.76+func drawStill(grid [][]stillCell, p stillPalette, fs *stillFaces) (*image.NRGBA, []rune) {77+ cols, rows := len(grid[0]), len(grid)78+ w, h := cols*fs.cw+2*stillPad, rows*fs.ch+2*stillPad79+ img := image.NewNRGBA(image.Rect(0, 0, w, h))80+ window(img, p)81+ var missing []rune82+ for y, row := range grid {83+ for x := range row {84+ origin := image.Pt(stillPad+x*fs.cw, stillPad+y*fs.ch)85+ if !drawCell(img, row, x, origin, p, fs) {86+ missing = append(missing, row[x].r)87+ }88+ }89+ }90+ return img, missing91+}92+93+// window fills a rounded rectangle with the terminal's background, with94+// a hairline border so the window reads on a page of the same color.95+func window(img *image.NRGBA, p stillPalette) {96+ b := img.Bounds()97+ w, h := float32(b.Dx()), float32(b.Dy())98+ border := blend(rgba(p.bg), rgba(p.fg), 0.18)99+ fillShape(img, b, border, func(s *shape) { s.roundRect(0, 0, w, h, stillRadius) })100+ fillShape(img, b, rgba(p.bg), func(s *shape) { s.roundRect(2, 2, w-2, h-2, stillRadius-2) })101+}102+103+// drawCell draws cell x of row at origin; false when its rune has no104+// glyph.105+func drawCell(img *image.NRGBA, row []stillCell, x int, origin image.Point, p stillPalette, fs *stillFaces) bool {106+ c := row[x]107+ span := 1108+ if c.wide {109+ span = 2110+ }111+ r := image.Rect(origin.X, origin.Y, origin.X+span*fs.cw, origin.Y+fs.ch)112+ if c.r == 0 && c.filled {113+ return true // the second half of a wide character, drawn with the first114+ }115+ bg := p.color(c.bg, inkBg)116+ if c.bg.kind != inkNone {117+ draw.Draw(img, image.Rect(origin.X, origin.Y, origin.X+fs.cw, origin.Y+fs.ch), image.NewUniform(bg), image.Point{}, draw.Src)118+ }119+ fg := p.color(c.fg, inkFg)120+ if c.faint {121+ fg = blend(bg, fg, 0.6)122+ }123+ ok := true124+ if c.r != 0 && c.r != ' ' && c.r != ' ' {125+ ok = drawGlyph(img, c, r, fg, fs)126+ }127+ lines(img, c, r, fg, p, fs)128+ return ok129+}130+131+// drawGlyph draws c's rune: as a shape when it's one terminals draw132+// themselves, else from the font.133+func drawGlyph(img *image.NRGBA, c stillCell, r image.Rectangle, fg color.RGBA, fs *stillFaces) bool {134+ if drawn := drawSpecial(img, c.r, r, fg, fs); drawn {135+ return true136+ }137+ face := fs.face(c)138+ if _, ok := face.GlyphAdvance(c.r); !ok {139+ return false140+ }141+ adv, _ := face.GlyphAdvance(c.r)142+ d := font.Drawer{Dst: img, Src: image.NewUniform(fg), Face: face}143+ d.Dot = fixed.P(r.Min.X, r.Min.Y+fs.ascent)144+ d.Dot.X += (fixed.I(r.Dx()) - adv) / 2145+ d.DrawString(string(c.r))146+ return true147+}148+149+// lines draws c's underline and strikethrough.150+func lines(img *image.NRGBA, c stillCell, r image.Rectangle, fg color.RGBA, p stillPalette, fs *stillFaces) {151+ t := lineWidth(fs.cw)152+ if c.strike {153+ y := r.Min.Y + fs.ascent - stillFontSize*3/10154+ fill(img, image.Rect(r.Min.X, y, r.Max.X, y+t), fg)155+ }156+ if c.underline == ulNone {157+ return158+ }159+ col := fg160+ if c.ulInk.kind != inkNone {161+ col = p.color(c.ulInk, inkFg)162+ }163+ y := r.Min.Y + fs.ascent + t*2164+ switch c.underline {165+ case ulDouble:166+ fill(img, image.Rect(r.Min.X, y-t, r.Max.X, y), col)167+ fill(img, image.Rect(r.Min.X, y+t, r.Max.X, y+2*t), col)168+ case ulWavy:169+ wavy(img, r, y, t, col)170+ case ulDotted:171+ for x := r.Min.X; x < r.Max.X; x += 2 * t {172+ fill(img, image.Rect(x, y, x+t, y+t), col)173+ }174+ case ulDashed:175+ for x := r.Min.X; x < r.Max.X; x += r.Dx() / 2 {176+ fill(img, image.Rect(x, y, x+r.Dx()*3/10, y+t), col)177+ }178+ default:179+ fill(img, image.Rect(r.Min.X, y, r.Max.X, y+t), col)180+ }181+}182+183+// wavy draws a curly underline: one wave a cell, in phase across cells.184+func wavy(img *image.NRGBA, r image.Rectangle, y, t int, col color.RGBA) {185+ amp := float32(t) * 1.2186+ fillShape(img, image.Rect(r.Min.X, y-2*t-2, r.Max.X, y+2*t+2), col, func(s *shape) {187+ var pts [][2]float32188+ w := float32(r.Dx())189+ for i := 0; i <= 12; i++ {190+ fx := w * float32(i) / 12191+ pts = append(pts, [2]float32{fx, float32(2*t+2) + amp*float32(math.Sin(2*math.Pi*float64(i)/12))})192+ }193+ s.stroke(pts, float32(t))194+ })195+}196+197+// lineWidth is a light line's width for cells cw wide.198+func lineWidth(cw int) int { return max(1, (cw+4)/8) }199+200+func fill(img *image.NRGBA, r image.Rectangle, c color.Color) {201+ draw.Draw(img, r, image.NewUniform(c), image.Point{}, draw.Over)202+}203+204+// blend mixes a toward b by t.205+func blend(a, b color.RGBA, t float64) color.RGBA {206+ mix := func(x, y uint8) uint8 { return uint8(math.Round(float64(x)*(1-t) + float64(y)*t)) }207+ return color.RGBA{mix(a.R, b.R), mix(a.G, b.G), mix(a.B, b.B), 0xff}208+}209+210+// fillShape rasterizes the shape build makes, in r's coordinates, and211+// fills it with c.212+func fillShape(img *image.NRGBA, r image.Rectangle, c color.Color, build func(*shape)) {213+ s := &shape{z: vector.NewRasterizer(r.Dx(), r.Dy())}214+ s.z.DrawOp = draw.Over215+ build(s)216+ s.z.Draw(img, r, image.NewUniform(c), image.Point{})217+}218+219+func runeList(rs []rune) string {220+ seen := map[rune]bool{}221+ var out string222+ for _, r := range rs {223+ if !seen[r] {224+ seen[r] = true225+ out += fmt.Sprintf(" %q", r)226+ }227+ }228+ return out229+}
A e2e/stills_test.go +154 -0
@@ -0,0 +1,154 @@
1+package e2e2+3+import (4+ "bytes"5+ "crypto/sha256"6+ "encoding/binary"7+ "encoding/hex"8+ "fmt"9+ "hash/crc32"10+ "image"11+ "image/png"12+ "os"13+ "path/filepath"14+ "strings"15+ "testing"16+)17+18+// Stills are pictures of 012 for the docs, drawn from golden screens:19+// each docScreen is recorded again from another screen's setup, on a20+// terminal the size a page shows well, dark and light, and drawn in its21+// reference palette to docs/media/<name>-dark.png and <name>-light.png.22+// `make screens` records and draws them; `make e2e` fails when a23+// still wasn't drawn from its golden as it is.24+type docScreen struct {25+ name, from string // docs/media/<name>-*.png; the screen whose setup it records26+ cols, rows uint1627+}28+29+var docScreens = []docScreen{30+ {name: "trace-view", from: "trace-view", cols: 80, rows: 14},31+ {name: "evaluate", from: "evaluate", cols: 80, rows: 12},32+ {name: "rules-bars", from: "rules-bars", cols: 80, rows: 12},33+ {name: "rules-panel", from: "rules-panel", cols: 100, rows: 12},34+ {name: "table", from: "table", cols: 80, rows: 13},35+ {name: "layout", from: "layout", cols: 80, rows: 28},36+ {name: "palette", from: "palette", cols: 80, rows: 18},37+}38+39+// stillsVersion changes when the drawing does, so every still is40+// drawn again.41+const stillsVersion = "1"42+43+// addDocScreens adds the docs' screens, docs-<name> and44+// docs-<name>-light, to the golden screens once every screen they're45+// recorded from is listed.46+func addDocScreens() {47+ byName := map[string]screen{}48+ for _, sc := range screens {49+ byName[sc.name] = sc50+ }51+ for _, d := range docScreens {52+ from, ok := byName[d.from]53+ if !ok {54+ panic("docScreens: no golden screen " + d.from)55+ }56+ sc := from57+ sc.name, sc.opts.cols, sc.opts.rows = "docs-"+d.name, d.cols, d.rows58+ light := sc59+ light.name += "-light"60+ light.opts.light = true61+ screens = append(screens, sc, light)62+ }63+}64+65+func TestStills(t *testing.T) {66+ faces, err := loadFaces()67+ if err != nil {68+ t.Fatal(err)69+ }70+ for _, d := range docScreens {71+ for _, v := range []struct {72+ suffix string73+ p stillPalette74+ }{{"dark", darkStill}, {"light", lightStill}} {75+ screen := "docs-" + d.name76+ if v.suffix == "light" {77+ screen += "-light"78+ }79+ t.Run(d.name+"-"+v.suffix, func(t *testing.T) {80+ path := filepath.Join("..", "docs", "media", d.name+"-"+v.suffix+".png")81+ checkStill(t, screen, int(d.cols), int(d.rows), v.p, faces, path)82+ })83+ }84+ }85+}86+87+// checkStill draws the still of screen into path under -update, and88+// otherwise checks the one there was drawn from the golden as it is.89+func checkStill(t *testing.T, screen string, cols, rows int, p stillPalette, fs *stillFaces, path string) {90+ src, err := os.ReadFile(filepath.Join("testdata", "screens", screen+".html"))91+ if err != nil {92+ t.Fatal(err)93+ }94+ sum := sha256.Sum256([]byte(fmt.Sprintf("%s\n%v\n%s", stillsVersion, p, src)))95+ want := hex.EncodeToString(sum[:])96+ if !*update {97+ if got := pngText(path, "012-golden"); got != want {98+ t.Errorf("%s is not drawn from the golden %s as it is; run make screens", path, screen)99+ }100+ return101+ }102+ grid, err := parseScreen(string(src), cols, rows)103+ if err != nil {104+ t.Fatalf("%s: %v", screen, err)105+ }106+ img, missing := drawStill(grid, p, fs)107+ if len(missing) > 0 {108+ t.Errorf("%s: no glyph for%s", screen, runeList(missing))109+ }110+ if err := writePNG(path, img, "012-golden", want); err != nil {111+ t.Fatal(err)112+ }113+}114+115+// writePNG writes img with a text chunk key=value after its header, so116+// the check can tell which golden it was drawn from without drawing it117+// again (drawing isn't bit for bit the same on every CPU).118+func writePNG(path string, img image.Image, key, value string) error {119+ var buf bytes.Buffer120+ enc := png.Encoder{CompressionLevel: png.BestCompression}121+ if err := enc.Encode(&buf, img); err != nil {122+ return err123+ }124+ b := buf.Bytes()125+ const header = 8 + 8 + 13 + 4 // signature, then IHDR's length, type, data and CRC126+ data := []byte(key + "\x00" + value)127+ chunk := binary.BigEndian.AppendUint32(nil, uint32(len(data)))128+ chunk = append(chunk, "tEXt"...)129+ chunk = append(chunk, data...)130+ chunk = binary.BigEndian.AppendUint32(chunk, crc32.ChecksumIEEE(chunk[4:]))131+ out := append(append(append([]byte{}, b[:header]...), chunk...), b[header:]...)132+ return os.WriteFile(path, out, 0o644)133+}134+135+// pngText is the value of the PNG's text chunk named key, or "".136+func pngText(path, key string) string {137+ b, err := os.ReadFile(path)138+ if err != nil || len(b) < 8 {139+ return ""140+ }141+ for b = b[8:]; len(b) >= 12; {142+ n := int(binary.BigEndian.Uint32(b))143+ if len(b) < 12+n {144+ return ""145+ }146+ if string(b[4:8]) == "tEXt" {147+ if k, v, ok := strings.Cut(string(b[8:8+n]), "\x00"); ok && k == key {148+ return v149+ }150+ }151+ b = b[12+n:]152+ }153+ return ""154+}
A e2e/stillshapes_test.go +231 -0
@@ -0,0 +1,231 @@
1+package e2e2+3+import (4+ "image"5+ "image/color"6+ "math"7+8+ "golang.org/x/image/vector"9+)10+11+// shape builds a filled outline for the rasterizer. Every polygon is12+// wound the same way, so overlapping parts add up rather than cancel,13+// except holes, wound the other way.14+type shape struct {15+ z *vector.Rasterizer16+}17+18+type pt = [2]float3219+20+// poly adds a closed polygon; hole winds it the other way.21+func (s *shape) poly(pts []pt, hole bool) {22+ var area float3223+ for i, p := range pts {24+ q := pts[(i+1)%len(pts)]25+ area += p[0]*q[1] - q[0]*p[1]26+ }27+ if (area < 0) != hole {28+ for i, j := 0, len(pts)-1; i < j; i, j = i+1, j-1 {29+ pts[i], pts[j] = pts[j], pts[i]30+ }31+ }32+ s.z.MoveTo(pts[0][0], pts[0][1])33+ for _, p := range pts[1:] {34+ s.z.LineTo(p[0], p[1])35+ }36+ s.z.ClosePath()37+}38+39+// arc is n+1 points along a circle's arc from a0 to a1 (radians, y down).40+func arc(cx, cy, r, a0, a1 float32, n int) []pt {41+ pts := make([]pt, 0, n+1)42+ for i := 0; i <= n; i++ {43+ a := float64(a0 + (a1-a0)*float32(i)/float32(n))44+ pts = append(pts, pt{cx + r*float32(math.Cos(a)), cy + r*float32(math.Sin(a))})45+ }46+ return pts47+}48+49+func (s *shape) disk(cx, cy, r float32) {50+ pts := arc(cx, cy, r, 0, 2*math.Pi, 32)51+ s.poly(pts[:32], false)52+}53+54+// ring is a circle's outline t wide, centered on radius r.55+func (s *shape) ring(cx, cy, r, t float32) {56+ outer := arc(cx, cy, r+t/2, 0, 2*math.Pi, 32)57+ inner := arc(cx, cy, r-t/2, 0, 2*math.Pi, 32)58+ s.poly(outer[:32], false)59+ s.poly(inner[:32], true)60+}61+62+// pie is the slice of a disk from a0 to a1.63+func (s *shape) pie(cx, cy, r, a0, a1 float32) {64+ s.poly(append([]pt{{cx, cy}}, arc(cx, cy, r, a0, a1, 24)...), false)65+}66+67+// seg is a straight line t wide from a to b.68+func (s *shape) seg(a, b pt, t float32) {69+ dx, dy := b[0]-a[0], b[1]-a[1]70+ l := float32(math.Hypot(float64(dx), float64(dy)))71+ if l == 0 {72+ return73+ }74+ nx, ny := -dy/l*t/2, dx/l*t/275+ s.poly([]pt{{a[0] + nx, a[1] + ny}, {b[0] + nx, b[1] + ny}, {b[0] - nx, b[1] - ny}, {a[0] - nx, a[1] - ny}}, false)76+}77+78+// stroke is a line through pts, t wide, with round joins and ends.79+func (s *shape) stroke(pts []pt, t float32) {80+ for i := range pts {81+ if i > 0 {82+ s.seg(pts[i-1], pts[i], t)83+ }84+ s.disk(pts[i][0], pts[i][1], t/2)85+ }86+}87+88+// roundRect is a rectangle with corners of radius r.89+func (s *shape) roundRect(x0, y0, x1, y1, r float32) {90+ var pts []pt91+ pts = append(pts, arc(x1-r, y0+r, r, -math.Pi/2, 0, 8)...)92+ pts = append(pts, arc(x1-r, y1-r, r, 0, math.Pi/2, 8)...)93+ pts = append(pts, arc(x0+r, y1-r, r, math.Pi/2, math.Pi, 8)...)94+ pts = append(pts, arc(x0+r, y0+r, r, math.Pi, 3*math.Pi/2, 8)...)95+ s.poly(pts, false)96+}97+98+// symbol draws in a square the cell's width, centered on the cell:99+// (u, v) from 0 to 1 across it.100+type symbol struct {101+ *shape102+ x0, y0, side float32103+}104+105+func (y symbol) p(u, v float32) pt { return pt{y.x0 + u*y.side, y.y0 + v*y.side} }106+107+func (y symbol) tri(a, b, c pt) {108+ y.poly([]pt{y.p(a[0], a[1]), y.p(b[0], b[1]), y.p(c[0], c[1])}, false)109+}110+111+func (y symbol) line(t float32, uv ...float32) {112+ var pts []pt113+ for i := 0; i+1 < len(uv); i += 2 {114+ pts = append(pts, y.p(uv[i], uv[i+1]))115+ }116+ y.stroke(pts, t*y.side)117+}118+119+func (y symbol) box(u0, v0, u1, v1, t float32) {120+ y.line(t, u0, v0, u1, v0, u1, v1, u0, v1, u0, v0)121+}122+123+func (y symbol) rect(u0, v0, u1, v1 float32) {124+ y.poly([]pt{y.p(u0, v0), y.p(u1, v0), y.p(u1, v1), y.p(u0, v1)}, false)125+}126+127+// symbols are the shapes 012 draws its chrome with that fonts draw at128+// odd sizes or not at all: triangles, circles, checks and the notebook129+// toolbar's icons.130+var symbols = map[rune]func(y symbol){131+ '▶': func(y symbol) { y.tri(pt{.2, .12}, pt{.92, .5}, pt{.2, .88}) },132+ '▸': func(y symbol) { y.tri(pt{.3, .28}, pt{.76, .5}, pt{.3, .72}) },133+ '◀': func(y symbol) { y.tri(pt{.8, .12}, pt{.08, .5}, pt{.8, .88}) },134+ '◂': func(y symbol) { y.tri(pt{.7, .28}, pt{.24, .5}, pt{.7, .72}) },135+ '▼': func(y symbol) { y.tri(pt{.08, .22}, pt{.92, .22}, pt{.5, .86}) },136+ '▾': func(y symbol) { y.tri(pt{.22, .34}, pt{.78, .34}, pt{.5, .7}) },137+ '▲': func(y symbol) { y.tri(pt{.08, .8}, pt{.92, .8}, pt{.5, .16}) },138+ '▴': func(y symbol) { y.tri(pt{.22, .68}, pt{.78, .68}, pt{.5, .32}) },139+ '■': func(y symbol) { y.rect(.14, .14, .86, .86) },140+ '●': func(y symbol) { y.disk(y.p(.5, .5)[0], y.p(.5, .5)[1], .4*y.side) },141+ '○': func(y symbol) { circle(y, 0, 0) },142+ '◔': func(y symbol) { circle(y, -math.Pi/2, 0) },143+ '◑': func(y symbol) { circle(y, -math.Pi/2, math.Pi/2) },144+ '◕': func(y symbol) { circle(y, -math.Pi/2, math.Pi) },145+ '✓': func(y symbol) { y.line(.12, .14, .54, .4, .8, .88, .2) },146+ '✗': func(y symbol) { y.line(.12, .2, .2, .8, .8); y.line(.12, .8, .2, .2, .8) },147+ '❯': func(y symbol) { y.line(.16, .3, .16, .72, .5, .3, .84) },148+ '⧉': func(y symbol) { y.box(.08, .08, .64, .64, .08); y.box(.36, .36, .92, .92, .08) },149+ '⎘': func(y symbol) { y.box(.16, .16, .84, .94, .08); y.rect(.34, .04, .66, .26) },150+ '↻': reload,151+ '↵': func(y symbol) {152+ y.line(.09, .84, .12, .84, .62, .3, .62)153+ y.tri(pt{.06, .62}, pt{.36, .38}, pt{.36, .86})154+ },155+ '✂': func(y symbol) {156+ y.ring(y.p(.22, .26)[0], y.p(.22, .26)[1], .13*y.side, .08*y.side)157+ y.ring(y.p(.22, .74)[0], y.p(.22, .74)[1], .13*y.side, .08*y.side)158+ y.line(.09, .32, .34, .94, .7)159+ y.line(.09, .32, .66, .94, .3)160+ },161+}162+163+// circle is an outline with the slice from a0 to a1 filled.164+func circle(y symbol, a0, a1 float32) {165+ c := y.p(.5, .5)166+ y.ring(c[0], c[1], .36*y.side, .09*y.side)167+ if a1 > a0 {168+ y.pie(c[0], c[1], .38*y.side, a0, a1)169+ }170+}171+172+// reload is ↻: most of a circle, clockwise, with the arrowhead at its173+// end.174+func reload(y symbol) {175+ c := y.p(.5, .52)176+ r := .32 * y.side177+ a0, a1 := float32(-math.Pi/5), float32(1.4*math.Pi)178+ y.stroke(arc(c[0], c[1], r, a0, a1, 24), .09*y.side)179+ end := pt{c[0] + r*float32(math.Cos(float64(a1))), c[1] + r*float32(math.Sin(float64(a1)))}180+ dir := pt{-float32(math.Sin(float64(a1))), float32(math.Cos(float64(a1)))}181+ n := pt{-dir[1], dir[0]}182+ h := .22 * y.side183+ y.poly([]pt{184+ {end[0] + dir[0]*h, end[1] + dir[1]*h},185+ {end[0] + n[0]*h*.8, end[1] + n[1]*h*.8},186+ {end[0] - n[0]*h*.8, end[1] - n[1]*h*.8},187+ }, false)188+}189+190+// drawSpecial draws r as terminals draw it rather than from the font:191+// box drawing, blocks and braille fill the cell and join their192+// neighbors, and symbols are drawn at one size. False when r is text.193+func drawSpecial(img *image.NRGBA, r rune, cell image.Rectangle, c color.RGBA, fs *stillFaces) bool {194+ switch {195+ case r >= 0x2500 && r <= 0x257f:196+ return boxDrawing(img, r, cell, c)197+ case r >= 0x2580 && r <= 0x259f:198+ return blockElement(img, r, cell, c)199+ case r >= 0x2800 && r <= 0x28ff:200+ braille(img, r, cell, c)201+ return true202+ }203+ draw, ok := symbols[r]204+ if !ok {205+ return false206+ }207+ // A symbol is as big as a capital letter, centered on one, and may208+ // reach a little into the cells beside it, as fonts draw them.209+ side := float32(stillFontSize) * .9210+ area := cell.Inset(-cell.Dx() / 4)211+ mid := float32(fs.ascent+cell.Dx()/4) - float32(stillFontSize)*.33212+ fillShape(img, area, c, func(s *shape) {213+ draw(symbol{shape: s, x0: (float32(area.Dx()) - side) / 2, y0: mid - side/2, side: side})214+ })215+ return true216+}217+218+// braille draws the dots of a braille pattern, two columns of four.219+func braille(img *image.NRGBA, r rune, cell image.Rectangle, c color.RGBA) {220+ bits := int(r - 0x2800)221+ // dots 1-3 and 7 down the left, 4-6 and 8 down the right222+ pos := [8][2]int{{0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {0, 3}, {1, 3}}223+ w, h := float32(cell.Dx()), float32(cell.Dy())224+ fillShape(img, cell, c, func(s *shape) {225+ for i, p := range pos {226+ if bits&(1<<i) != 0 {227+ s.disk(w*(.27+.46*float32(p[0])), h*(.14+.24*float32(p[1])), w*.14)228+ }229+ }230+ })231+}
A e2e/testdata/screens/docs-evaluate-light.html +12 -0
@@ -0,0 +1,12 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">EVAL</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">Evaluate</span> <span style="font-weight:bold">B8</span> <span style="color:var(--vt-palette-8)">──────────────────────────────────────────────────────────────┐</span>3+ <span style="color:var(--vt-palette-8)">│</span> =<span style="color:var(--vt-palette-4);font-weight:bold;font-style:italic;text-decoration:underline">719.466666666667</span><span style="font-weight:bold;text-decoration:underline">*</span><span style="color:var(--vt-palette-4);font-weight:bold;font-style:italic;text-decoration:underline">4</span> <span style="color:var(--vt-palette-8)">│</span>4+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">├────────────────────────────────────────────────────────────────────────────┤</span>5+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0)">Next</span> <span style="font-weight:bold;text-decoration:underline">AVERAGE(Expenses)*C2</span> <span style="color:var(--vt-palette-0)">=</span> <span style="color:var(--vt-palette-4);font-style:italic">2877.86666666667</span> <span style="color:var(--vt-palette-8)">│</span>6+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">2</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">3</span> <span style="color:var(--vt-palette-8)">─┘</span>7+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">5</span><span style="background:var(--vt-palette-7)"> </span> Transit 968+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">6</span><span style="background:var(--vt-palette-7)"> </span> Savings r <span style="color:var(--vt-palette-1);text-decoration:underline wavy var(--vt-palette-1)">#DIV/0!</span>9+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">7</span><span style="background:var(--vt-palette-7)"> </span> Total 2158.410+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">8</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">2877.8667</span><span style="background:var(--vt-palette-6)"> </span>11+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">9</span><span style="background:var(--vt-palette-7)"> </span>12+The underlined part is computed next <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Enter</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">evaluate</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Esc</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">close</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-evaluate.html +12 -0
@@ -0,0 +1,12 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">EVAL</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">Evaluate</span> <span style="font-weight:bold">B8</span> <span style="color:var(--vt-palette-8)">──────────────────────────────────────────────────────────────┐</span>3+ <span style="color:var(--vt-palette-8)">│</span> =<span style="color:var(--vt-palette-6);font-weight:bold;font-style:italic;text-decoration:underline">719.466666666667</span><span style="font-weight:bold;text-decoration:underline">*</span><span style="color:var(--vt-palette-6);font-weight:bold;font-style:italic;text-decoration:underline">4</span> <span style="color:var(--vt-palette-8)">│</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">├────────────────────────────────────────────────────────────────────────────┤</span>5+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-8)">Next</span> <span style="font-weight:bold;text-decoration:underline">AVERAGE(Expenses)*C2</span> <span style="color:var(--vt-palette-8)">=</span> <span style="color:var(--vt-palette-6);font-style:italic">2877.86666666667</span> <span style="color:var(--vt-palette-8)">│</span>6+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">2</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">3</span> <span style="color:var(--vt-palette-8)">─┘</span>7+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span> Transit 968+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> Savings r <span style="color:var(--vt-palette-1);text-decoration:underline wavy var(--vt-palette-1)">#DIV/0!</span>9+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">7</span><span style="background:var(--vt-palette-8)"> </span> Total 2158.410+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">8</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">2877.8667</span><span style="background:var(--vt-palette-6)"> </span>11+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>12+The underlined part is computed next <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Enter</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">evaluate</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">close</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-layout-light.html +28 -0
@@ -0,0 +1,28 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">C4</span><span style="background:var(--vt-palette-7)"> </span> Tram 28 and the castle, before the crowds3+4+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B</span><span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">C</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">D</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">E</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">G</span><span style="background:var(--vt-palette-7)"> </span>5+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span> Trip to Lisbon6+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0)">┏━━━━━━━━━┯━━━━━━━━━┯━━━━━━━━━┓</span>7+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0)">┃</span>Day <span style="color:var(--vt-palette-0)">│</span>Cost <span style="color:var(--vt-palette-0)">│</span>Notes <span style="color:var(--vt-palette-0)">┃</span>8+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0)">┣━════════╪═════════╪════════━┫</span>9+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0)">┃</span> <span style="color:var(--vt-palette-0)">│</span> <span style="color:var(--vt-palette-0)">│</span>Train, <span style="color:var(--vt-palette-0)">┃</span>10+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0)">┃</span> <span style="color:var(--vt-palette-0)">│</span> <span style="color:var(--vt-palette-0)">│</span>book the <span style="color:var(--vt-palette-0)">┃</span>11+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0)">┃</span> <span style="color:var(--vt-palette-0)">│</span> <span style="color:var(--vt-palette-0)">│</span>early <span style="color:var(--vt-palette-0)">┃</span>12+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">3</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0)">┃</span>Fri <span style="color:var(--vt-palette-0)">│</span> 120 <span style="color:var(--vt-palette-0)">│</span>fare <span style="color:var(--vt-palette-0)">┃</span>13+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0)">┠─────────┼─────────┼─────────┨</span>14+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0)">┃</span> <span style="color:var(--vt-palette-0)">│</span> <span style="color:var(--vt-palette-0)">│</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Tram</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">28</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0)">┃</span>15+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0)">┃</span> <span style="color:var(--vt-palette-0)">│</span> <span style="color:var(--vt-palette-0)">│</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">and</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">the</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0)">┃</span>16+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0)">┃</span> <span style="color:var(--vt-palette-0)">│</span> <span style="color:var(--vt-palette-0)">│</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">castle,</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0)">┃</span>17+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0)">┃</span> <span style="color:var(--vt-palette-0)">│</span> <span style="color:var(--vt-palette-0)">│</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">before</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0)">┃</span>18+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0)">┃</span> <span style="color:var(--vt-palette-0)">│</span> <span style="color:var(--vt-palette-0)">│</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">the</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0)">┃</span>19+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">4</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0)">┃</span>Sat <span style="color:var(--vt-palette-0)">│</span> 45 <span style="color:var(--vt-palette-0)">│</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">crowds</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0)">┃</span>20+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0)">┠─────────┼─────────┼─────────┨</span>21+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">5</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0)">┃</span>Sun <span style="color:var(--vt-palette-0)">│</span> 60 <span style="color:var(--vt-palette-0)">│</span>Fly home <span style="color:var(--vt-palette-0)">┃</span>22+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0)">┠─────────┼─────────┼─────────┨</span>23+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0)">┃</span> <span style="color:var(--vt-palette-0)">│</span> <span style="color:var(--vt-palette-0)">│</span> <span style="color:var(--vt-palette-0)">┃</span>24+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">6</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0)">┃</span>Total <span style="color:var(--vt-palette-0)">│</span> 225 <span style="color:var(--vt-palette-0)">│</span> <span style="color:var(--vt-palette-0)">┃</span>25+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0)">┗━━━━━━━━━┷━━━━━━━━━┷━━━━━━━━━┛</span>26+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">7</span><span style="background:var(--vt-palette-7)"> </span>27+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">8</span><span style="background:var(--vt-palette-7)"> </span>28+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-0)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-0)">modified</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">search</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+/</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">shortcuts</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-layout.html +28 -0
@@ -0,0 +1,28 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C4</span><span style="background:var(--vt-palette-8)"> </span> Tram 28 and the castle, before the crowds3+4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">C</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span> Trip to Lisbon6+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-7)">┏━━━━━━━━━┯━━━━━━━━━┯━━━━━━━━━┓</span>7+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-7)">┃</span>Day <span style="color:var(--vt-palette-7)">│</span>Cost <span style="color:var(--vt-palette-7)">│</span>Notes <span style="color:var(--vt-palette-7)">┃</span>8+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-7)">┣━════════╪═════════╪════════━┫</span>9+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-7)">┃</span> <span style="color:var(--vt-palette-7)">│</span> <span style="color:var(--vt-palette-7)">│</span>Train, <span style="color:var(--vt-palette-7)">┃</span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-7)">┃</span> <span style="color:var(--vt-palette-7)">│</span> <span style="color:var(--vt-palette-7)">│</span>book the <span style="color:var(--vt-palette-7)">┃</span>11+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-7)">┃</span> <span style="color:var(--vt-palette-7)">│</span> <span style="color:var(--vt-palette-7)">│</span>early <span style="color:var(--vt-palette-7)">┃</span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-7)">┃</span>Fri <span style="color:var(--vt-palette-7)">│</span> 120 <span style="color:var(--vt-palette-7)">│</span>fare <span style="color:var(--vt-palette-7)">┃</span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-7)">┠─────────┼─────────┼─────────┨</span>14+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-7)">┃</span> <span style="color:var(--vt-palette-7)">│</span> <span style="color:var(--vt-palette-7)">│</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Tram</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">28</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-7)">┃</span>15+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-7)">┃</span> <span style="color:var(--vt-palette-7)">│</span> <span style="color:var(--vt-palette-7)">│</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">and</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">the</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-7)">┃</span>16+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-7)">┃</span> <span style="color:var(--vt-palette-7)">│</span> <span style="color:var(--vt-palette-7)">│</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">castle,</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-7)">┃</span>17+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-7)">┃</span> <span style="color:var(--vt-palette-7)">│</span> <span style="color:var(--vt-palette-7)">│</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">before</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-7)">┃</span>18+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-7)">┃</span> <span style="color:var(--vt-palette-7)">│</span> <span style="color:var(--vt-palette-7)">│</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">the</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-7)">┃</span>19+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">4</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-7)">┃</span>Sat <span style="color:var(--vt-palette-7)">│</span> 45 <span style="color:var(--vt-palette-7)">│</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">crowds</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-7)">┃</span>20+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-7)">┠─────────┼─────────┼─────────┨</span>21+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-7)">┃</span>Sun <span style="color:var(--vt-palette-7)">│</span> 60 <span style="color:var(--vt-palette-7)">│</span>Fly home <span style="color:var(--vt-palette-7)">┃</span>22+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-7)">┠─────────┼─────────┼─────────┨</span>23+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-7)">┃</span> <span style="color:var(--vt-palette-7)">│</span> <span style="color:var(--vt-palette-7)">│</span> <span style="color:var(--vt-palette-7)">┃</span>24+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-7)">┃</span>Total <span style="color:var(--vt-palette-7)">│</span> 225 <span style="color:var(--vt-palette-7)">│</span> <span style="color:var(--vt-palette-7)">┃</span>25+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-7)">┗━━━━━━━━━┷━━━━━━━━━┷━━━━━━━━━┛</span>26+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">7</span><span style="background:var(--vt-palette-8)"> </span>27+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span>28+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-8)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-8)">modified</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">search</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+/</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">shortcuts</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-palette-light.html +18 -0
@@ -0,0 +1,18 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">MENU</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">Search</span> <span style="font-weight:bold">the</span> <span style="font-weight:bold">menus</span> <span style="color:var(--vt-palette-8)">─────────────────────────────────────────────────────────┐</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">›</span> <span style="color:var(--vt-palette-0)">Type</span> <span style="color:var(--vt-palette-0)">a</span> <span style="color:var(--vt-palette-0)">command,</span> <span style="color:var(--vt-palette-0)">e.g.</span> <span style="color:var(--vt-palette-0)">save</span> <span style="color:var(--vt-palette-0)">or</span> <span style="color:var(--vt-palette-0)">width</span> <span style="color:var(--vt-palette-8)">│</span>4+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">├────────────────────────────────────────────────────────────────────────────┤</span>5+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">New</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">File</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>6+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Open <span style="color:var(--vt-palette-0)">File</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+O</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span>7+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Import <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-8)">│</span>8+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Save <span style="color:var(--vt-palette-0)">File</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+S</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-8)">│</span>9+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Save as <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-8)">│</span>10+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as CSV <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-0)">›</span> <span style="color:var(--vt-palette-0)">Download</span> <span style="color:var(--vt-palette-8)">│</span>11+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as TSV <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-0)">›</span> <span style="color:var(--vt-palette-0)">Download</span> <span style="color:var(--vt-palette-8)">│</span>12+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as XLSX <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-0)">›</span> <span style="color:var(--vt-palette-0)">Download</span> <span style="color:var(--vt-palette-8)">│</span>13+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as SQLite <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-0)">›</span> <span style="color:var(--vt-palette-0)">Download</span> <span style="color:var(--vt-palette-8)">│</span>14+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as JSON <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-0)">›</span> <span style="color:var(--vt-palette-0)">Download</span> <span style="color:var(--vt-palette-8)">│</span>15+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as NUON <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-0)">›</span> <span style="color:var(--vt-palette-0)">Download</span> <span style="color:var(--vt-palette-8)">│</span>16+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">│</span> Decimal arithmetic <span style="color:var(--vt-palette-0)">File</span> <span style="color:var(--vt-palette-0)">›</span> <span style="color:var(--vt-palette-0)">Settings</span> <span style="color:var(--vt-palette-8)">│</span>17+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">225</span> <span style="color:var(--vt-palette-0)">of</span> <span style="color:var(--vt-palette-0)">225</span> <span style="color:var(--vt-palette-8)">─┘</span>18+Start a new, empty sheet <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Up/Down</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">move</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Enter</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">run</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Esc</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">close</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-palette.html +18 -0
@@ -0,0 +1,18 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">MENU</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">Search</span> <span style="font-weight:bold">the</span> <span style="font-weight:bold">menus</span> <span style="color:var(--vt-palette-8)">─────────────────────────────────────────────────────────┐</span>3+ <span style="color:var(--vt-palette-8)">│</span> <span style="font-weight:bold">›</span> <span style="color:var(--vt-palette-8)">Type</span> <span style="color:var(--vt-palette-8)">a</span> <span style="color:var(--vt-palette-8)">command,</span> <span style="color:var(--vt-palette-8)">e.g.</span> <span style="color:var(--vt-palette-8)">save</span> <span style="color:var(--vt-palette-8)">or</span> <span style="color:var(--vt-palette-8)">width</span> <span style="color:var(--vt-palette-8)">│</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">├────────────────────────────────────────────────────────────────────────────┤</span>5+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">New</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">File</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>6+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> Open <span style="color:var(--vt-palette-8)">File</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+O</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>7+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> Import <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">│</span>8+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> Save <span style="color:var(--vt-palette-8)">File</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+S</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">│</span>9+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> Save as <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">│</span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as CSV <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Download</span> <span style="color:var(--vt-palette-8)">│</span>11+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as TSV <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Download</span> <span style="color:var(--vt-palette-8)">│</span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as XLSX <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Download</span> <span style="color:var(--vt-palette-8)">│</span>13+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as SQLite <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Download</span> <span style="color:var(--vt-palette-8)">│</span>14+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as JSON <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Download</span> <span style="color:var(--vt-palette-8)">│</span>15+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> Download as NUON <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Download</span> <span style="color:var(--vt-palette-8)">│</span>16+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">│</span> Decimal arithmetic <span style="color:var(--vt-palette-8)">File</span> <span style="color:var(--vt-palette-8)">›</span> <span style="color:var(--vt-palette-8)">Settings</span> <span style="color:var(--vt-palette-8)">│</span>17+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-8)">└───────────────────────────────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">225</span> <span style="color:var(--vt-palette-8)">of</span> <span style="color:var(--vt-palette-8)">225</span> <span style="color:var(--vt-palette-8)">─┘</span>18+Start a new, empty sheet <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Up/Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">move</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Enter</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">run</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">close</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-rules-bars-light.html +12 -0
@@ -0,0 +1,12 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">E3</span><span style="background:var(--vt-palette-7)"> </span> Art3+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Alt+Down</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">choose</span> <span style="color:var(--vt-palette-0)">from</span> <span style="color:var(--vt-palette-0)">the</span> <span style="color:var(--vt-palette-0)">list</span>4+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">C</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">D</span><span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">E</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">G</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">H</span><span style="background:var(--vt-palette-7)"> </span>5+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span> <span style="font-weight:bold">Name</span> <span style="font-weight:bold">Points</span> <span style="font-weight:bold">Change</span> <span style="font-weight:bold">Grade</span> <span style="font-weight:bold">Track</span> <span style="font-weight:bold">Passed</span>6+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span> Ada <span style="color:var(--vt-palette-4)">██████</span><span style="color:var(--bg);background:var(--vt-palette-4)">9</span>2 <span style="color:var(--vt-palette-2)">↑</span> 12 <span style="color:var(--vt-palette-3)">◕</span> <span style="color:var(--vt-palette-7)">▐</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Math</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">▾</span><span style="color:var(--vt-palette-7)">▌</span> [✓]7+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">3</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-5);font-style:italic">Bo</span> <span style="color:var(--vt-palette-4)">███▉</span> 48 <span style="color:var(--vt-palette-1)">↓</span> -8 <span style="color:var(--vt-palette-3)">◔</span> <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Art</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">▾</span> [ ]8+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">4</span><span style="background:var(--vt-palette-7)"> </span> Cy <span style="color:var(--vt-palette-4)">██████</span>75 <span style="color:var(--vt-palette-3)">→</span> 3 <span style="color:var(--vt-palette-3)">◑</span> <span style="color:var(--vt-palette-7)">▐</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Math</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">▾</span><span style="color:var(--vt-palette-7)">▌</span> [✓]9+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">5</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-5);font-style:italic">Bo</span> <span style="color:var(--vt-palette-4)">██▍</span> 30 <span style="color:var(--vt-palette-1)">↓</span> -2 <span style="color:var(--vt-palette-1)">○</span> <span style="color:var(--vt-palette-0)">▾</span> [ ]10+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">6</span><span style="background:var(--vt-palette-7)"> </span> Dee <span style="color:var(--vt-palette-4)">█████</span><span style="color:var(--bg);background:var(--vt-palette-4)">100</span> <span style="color:var(--vt-palette-2)">↑</span> 15 <span style="color:var(--vt-palette-2)">●</span> <span style="color:var(--vt-palette-7)">▐</span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Music</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">▾</span><span style="color:var(--vt-palette-7)">▌</span> [✓]11+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">7</span><span style="background:var(--vt-palette-7)"> </span>12+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-0)">+</span> <span style="color:var(--vt-palette-8)">│</span> scores.012 <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">search</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+/</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">shortcuts</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F10</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">menu</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-rules-bars.html +12 -0
@@ -0,0 +1,12 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E3</span><span style="background:var(--vt-palette-8)"> </span> Art3+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Alt+Down</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">choose</span> <span style="color:var(--vt-palette-8)">from</span> <span style="color:var(--vt-palette-8)">the</span> <span style="color:var(--vt-palette-8)">list</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">E</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">H</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span> <span style="font-weight:bold">Name</span> <span style="font-weight:bold">Points</span> <span style="font-weight:bold">Change</span> <span style="font-weight:bold">Grade</span> <span style="font-weight:bold">Track</span> <span style="font-weight:bold">Passed</span>6+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span> Ada <span style="color:var(--vt-palette-4)">██████</span><span style="color:var(--bg);background:var(--vt-palette-4)">9</span>2 <span style="color:var(--vt-palette-2)">↑</span> 12 <span style="color:var(--vt-palette-3)">◕</span> <span style="color:var(--vt-palette-8)">▐</span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Math</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">▾</span><span style="color:var(--vt-palette-8)">▌</span> [✓]7+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">3</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-5);font-style:italic">Bo</span> <span style="color:var(--vt-palette-4)">███▉</span> 48 <span style="color:var(--vt-palette-1)">↓</span> -8 <span style="color:var(--vt-palette-3)">◔</span> <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Art</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">▾</span> [ ]8+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="background:var(--vt-palette-8)"> </span> Cy <span style="color:var(--vt-palette-4)">██████</span>75 <span style="color:var(--vt-palette-3)">→</span> 3 <span style="color:var(--vt-palette-3)">◑</span> <span style="color:var(--vt-palette-8)">▐</span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Math</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">▾</span><span style="color:var(--vt-palette-8)">▌</span> [✓]9+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-5);font-style:italic">Bo</span> <span style="color:var(--vt-palette-4)">██▍</span> 30 <span style="color:var(--vt-palette-1)">↓</span> -2 <span style="color:var(--vt-palette-1)">○</span> <span style="color:var(--vt-palette-8)">▾</span> [ ]10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> Dee <span style="color:var(--vt-palette-4)">█████</span><span style="color:var(--bg);background:var(--vt-palette-4)">100</span> <span style="color:var(--vt-palette-2)">↑</span> 15 <span style="color:var(--vt-palette-2)">●</span> <span style="color:var(--vt-palette-8)">▐</span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Music</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">▾</span><span style="color:var(--vt-palette-8)">▌</span> [✓]11+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">7</span><span style="background:var(--vt-palette-8)"> </span>12+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-8)">+</span> <span style="color:var(--vt-palette-8)">│</span> scores.012 <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">search</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+/</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">shortcuts</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F10</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">menu</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-rules-panel-light.html +12 -0
@@ -0,0 +1,12 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">RULES</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A1</span><span style="background:var(--vt-palette-7)"> </span> Task3+4+<span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">A</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">C</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">D</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">E</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">G</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">H</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">I</span><span style="background:var(--vt-palette-7)"> </span>5+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">1</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Task</span><span style="background:var(--vt-palette-6)"> </span> <span style="font-weight:bold">Owner</span> <span style="font-weight:bold">Hours</span> <span style="font-weight:bold">Done</span> <span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">Conditional</span> <span style="font-weight:bold">format</span> <span style="font-weight:bold">rules</span> <span style="color:var(--vt-palette-8)">───────────────────┐</span>6+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-2);text-decoration:line-through">Write</span><span style="text-decoration:line-through"> </span><span style="color:var(--vt-palette-2);text-decoration:line-through">spec</span> Ann <span style="color:var(--vt-palette-0)">▾</span><span style="background:#b58900"> </span><span style="color:#1d1f21;background:#b58900">12</span><span style="background:#b58900"> </span> [✓] <span style="color:var(--vt-palette-8)">│</span> + Add rule <span style="color:var(--vt-palette-8)">│</span>7+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">3</span><span style="background:var(--vt-palette-7)"> </span> Build UI Bo <span style="color:var(--vt-palette-0)">▾</span><span style="background:#c82829"> </span><span style="color:#ffffff;background:#c82829">40</span><span style="background:#c82829"> </span> [ ] <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-2);text-decoration:line-through">123</span> A2:A6 Custom formula is =$D2 <span style="color:var(--vt-palette-8)">│</span>8+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">4</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-2);text-decoration:line-through">Tests</span> Cy <span style="color:var(--vt-palette-0)">▾</span><span style="background:#bf5915"> </span><span style="color:#ffffff;background:#bf5915">26</span><span style="background:#bf5915"> </span> [✓] <span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="background:#718c00"> </span><span style="background:#9e8a00"> </span><span style="background:#bc680e"> </span><span style="background:#c82829"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">C2:C6</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Color</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">scale</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>9+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">5</span><span style="background:var(--vt-palette-7)"> </span> Docs <span style="text-decoration:underline dotted var(--vt-palette-3)">Dee</span> <span style="color:var(--vt-palette-0)">▾</span><span style="background:#888b00"> </span><span style="color:#1d1f21;background:#888b00">6</span><span style="background:#888b00"> </span> [ ] <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-15);background:var(--vt-palette-5);font-weight:bold">123</span><span style="background:var(--vt-palette-5)"> </span> C7 Greater than 80 <span style="color:var(--vt-palette-8)">│</span>10+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">6</span><span style="background:var(--vt-palette-7)"> </span> Release Ann <span style="color:var(--vt-palette-0)">▾</span><span style="background:#718c00"> </span><span style="color:var(--vt-palette-0);background:#718c00">3</span><span style="background:#718c00"> </span> [ ] <span style="color:var(--vt-palette-8)">└────────────────────────────────────</span> <span style="color:var(--vt-palette-0)">3</span> <span style="color:var(--vt-palette-0)">rules</span> <span style="color:var(--vt-palette-8)">─┘</span>11+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">7</span><span style="background:var(--vt-palette-7)"> </span> Total <span style="background:var(--vt-palette-5)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-5);font-weight:bold">87</span><span style="background:var(--vt-palette-5)"> </span>12+<span style="color:var(--vt-palette-0)">The</span> <span style="color:var(--vt-palette-0)">first</span> <span style="color:var(--vt-palette-0)">rule</span> <span style="color:var(--vt-palette-0)">that</span> <span style="color:var(--vt-palette-0)">matches</span> <span style="color:var(--vt-palette-0)">a</span> <span style="color:var(--vt-palette-0)">cell</span> <span style="color:var(--vt-palette-0)">wins</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Enter</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">edit</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Del</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">remove</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Shift+↑/↓</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">move</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Esc</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">close</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-rules-panel.html +12 -0
@@ -0,0 +1,12 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">RULES</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A1</span><span style="background:var(--vt-palette-8)"> </span> Task3+4+<span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">A</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">H</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">I</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">1</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Task</span><span style="background:var(--vt-palette-6)"> </span> <span style="font-weight:bold">Owner</span> <span style="font-weight:bold">Hours</span> <span style="font-weight:bold">Done</span> <span style="color:var(--vt-palette-8)">┌─</span> <span style="font-weight:bold">Conditional</span> <span style="font-weight:bold">format</span> <span style="font-weight:bold">rules</span> <span style="color:var(--vt-palette-8)">───────────────────┐</span>6+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-2);text-decoration:line-through">Write</span><span style="text-decoration:line-through"> </span><span style="color:var(--vt-palette-2);text-decoration:line-through">spec</span> Ann <span style="color:var(--vt-palette-8)">▾</span><span style="background:#f0c674"> </span><span style="color:#1d1f21;background:#f0c674">12</span><span style="background:#f0c674"> </span> [✓] <span style="color:var(--vt-palette-8)">│</span> + Add rule <span style="color:var(--vt-palette-8)">│</span>7+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span> Build UI Bo <span style="color:var(--vt-palette-8)">▾</span><span style="background:#cc6666"> </span><span style="color:#000000;background:#cc6666">40</span><span style="background:#cc6666"> </span> [ ] <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-2);text-decoration:line-through">123</span> A2:A6 Custom formula is =$D2 <span style="color:var(--vt-palette-8)">│</span>8+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-2);text-decoration:line-through">Tests</span> Cy <span style="color:var(--vt-palette-8)">▾</span><span style="background:#de966d"> </span><span style="color:#1d1f21;background:#de966d">26</span><span style="background:#de966d"> </span> [✓] <span style="color:var(--vt-palette-8)">│</span><span style="background:var(--vt-palette-6)"> </span><span style="background:#b5bd68"> </span><span style="background:#dcc370"> </span><span style="background:#e4a56f"> </span><span style="background:#cc6666"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">C2:C6</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">Color</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">scale</span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-8)">│</span>9+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span> Docs <span style="text-decoration:underline dotted var(--vt-palette-3)">Dee</span> <span style="color:var(--vt-palette-8)">▾</span><span style="background:#c9c06c"> </span><span style="color:#1d1f21;background:#c9c06c">6</span><span style="background:#c9c06c"> </span> [ ] <span style="color:var(--vt-palette-8)">│</span> <span style="color:var(--vt-palette-0);background:var(--vt-palette-5);font-weight:bold">123</span><span style="background:var(--vt-palette-5)"> </span> C7 Greater than 80 <span style="color:var(--vt-palette-8)">│</span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> Release Ann <span style="color:var(--vt-palette-8)">▾</span><span style="background:#b5bd68"> </span><span style="color:#1d1f21;background:#b5bd68">3</span><span style="background:#b5bd68"> </span> [ ] <span style="color:var(--vt-palette-8)">└────────────────────────────────────</span> <span style="color:var(--vt-palette-8)">3</span> <span style="color:var(--vt-palette-8)">rules</span> <span style="color:var(--vt-palette-8)">─┘</span>11+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">7</span><span style="background:var(--vt-palette-8)"> </span> Total <span style="background:var(--vt-palette-5)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-5);font-weight:bold">87</span><span style="background:var(--vt-palette-5)"> </span>12+<span style="color:var(--vt-palette-8)">The</span> <span style="color:var(--vt-palette-8)">first</span> <span style="color:var(--vt-palette-8)">rule</span> <span style="color:var(--vt-palette-8)">that</span> <span style="color:var(--vt-palette-8)">matches</span> <span style="color:var(--vt-palette-8)">a</span> <span style="color:var(--vt-palette-8)">cell</span> <span style="color:var(--vt-palette-8)">wins</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Enter</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">edit</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Del</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">remove</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Shift+↑/↓</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">move</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Esc</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">close</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-table-light.html +13 -0
@@ -0,0 +1,13 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">D3</span><span style="background:var(--vt-palette-7)"> </span> 2803+<span style="color:var(--vt-palette-0)">Table</span> <span style="color:var(--vt-palette-0)">Sales,</span> <span style="color:var(--vt-palette-0)">column</span> <span style="font-weight:bold">Amount</span><span style="color:var(--vt-palette-0)">:</span> <span style="color:var(--vt-palette-0)">Sales[Amount]</span> <span style="color:var(--vt-palette-0)">in</span> <span style="color:var(--vt-palette-0)">formulas</span>4+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">C</span><span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">D</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">E</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">G</span><span style="background:var(--vt-palette-7)"> </span>5+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">Region</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">Month</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">Units</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-4);font-weight:bold;text-decoration:underline">Amount</span><span style="text-decoration:underline"> </span> Total6+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span> North Jan 12 480 26807+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">3</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">South</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Jan</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">7</span><span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">280</span><span style="background:var(--vt-palette-6)"> </span> Per unit8+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">4</span><span style="background:var(--vt-palette-7)"> </span> East Feb 20 800 409+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">5</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">North</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Feb</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">9</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">360</span><span style="background:var(--vt-palette-7)"> </span>10+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">6</span><span style="background:var(--vt-palette-7)"> </span> West Mar 15 60011+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">7</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">South</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Mar</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">4</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">160</span><span style="background:var(--vt-palette-7)"> </span>12+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">8</span><span style="background:var(--vt-palette-7)"> </span>13+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-0)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-0)">modified</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">search</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+/</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">shortcuts</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-table.html +13 -0
@@ -0,0 +1,13 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D3</span><span style="background:var(--vt-palette-8)"> </span> 2803+<span style="color:var(--vt-palette-8)">Table</span> <span style="color:var(--vt-palette-8)">Sales,</span> <span style="color:var(--vt-palette-8)">column</span> <span style="font-weight:bold">Amount</span><span style="color:var(--vt-palette-8)">:</span> <span style="color:var(--vt-palette-8)">Sales[Amount]</span> <span style="color:var(--vt-palette-8)">in</span> <span style="color:var(--vt-palette-8)">formulas</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">D</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">Region</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">Month</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">Units</span><span style="text-decoration:underline"> </span><span style="color:var(--vt-palette-6);font-weight:bold;text-decoration:underline">Amount</span><span style="text-decoration:underline"> </span> Total6+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span> North Jan 12 480 26807+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">3</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">South</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Jan</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">7</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">280</span><span style="background:var(--vt-palette-6)"> </span> Per unit8+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="background:var(--vt-palette-8)"> </span> East Feb 20 800 409+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">North</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Feb</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">360</span><span style="background:var(--vt-palette-8)"> </span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> West Mar 15 60011+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">7</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">South</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Mar</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">160</span><span style="background:var(--vt-palette-8)"> </span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span>13+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-8)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-8)">modified</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">search</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+/</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">shortcuts</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-trace-view-light.html +14 -0
@@ -0,0 +1,14 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">B7</span><span style="background:var(--vt-palette-7)"> </span> =SUM(B3:B5)3+<span style="color:var(--vt-palette-0)">B7</span> <span style="color:var(--vt-palette-0)">reads</span> Expenses<span style="color:var(--vt-palette-0)">;</span> <span style="color:var(--vt-palette-0)">read</span> <span style="color:var(--vt-palette-0)">by</span> C7, B30↓ <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Alt+&#39;</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">list</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Alt+;</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">stop</span>4+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">A</span><span style="background:var(--vt-palette-7)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">B</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">C</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">D</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">E</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">F</span><span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">G</span><span style="background:var(--vt-palette-7)"> </span>5+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">1</span><span style="background:var(--vt-palette-7)"> </span> Household budget 20266+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">2</span><span style="background:var(--vt-palette-7)"> </span> 47+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">3</span><span style="background:var(--vt-palette-7)"> </span> Rent <span style="background:var(--vt-palette-2)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-2)">1450</span><span style="background:var(--vt-palette-2)"> </span>8+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">4</span><span style="background:var(--vt-palette-7)"> </span> Groceries<span style="background:var(--vt-palette-2)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-2)">612.4</span><span style="background:var(--vt-palette-2)"> </span>9+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">5</span><span style="background:var(--vt-palette-7)"> </span> Transit <span style="background:var(--vt-palette-2)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-2)">96</span><span style="background:var(--vt-palette-2)"> </span>10+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">6</span><span style="background:var(--vt-palette-7)"> </span> Savings r <span style="color:var(--vt-palette-1);text-decoration:underline wavy var(--vt-palette-1)">#DIV/0!</span>11+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">7</span><span style="background:var(--vt-palette-6)"> </span> Total <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">2158.4</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-5)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-5);font-weight:bold">539.6</span><span style="background:var(--vt-palette-5)"> </span>12+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">8</span><span style="background:var(--vt-palette-7)"> </span> 2877.866713+<span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">9</span><span style="background:var(--vt-palette-7)"> </span>14+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-0)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-0)">modified</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+K</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">search</span> <span style="background:var(--vt-palette-7)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-7)">Ctrl+/</span><span style="background:var(--vt-palette-7)"> </span> <span style="color:var(--vt-palette-0)">shortcuts</span></pre>\ No newline at end of file
A e2e/testdata/screens/docs-trace-view.html +14 -0
@@ -0,0 +1,14 @@
1+<pre class="screen"> <span style="text-decoration:underline">F</span>ile <span style="text-decoration:underline">E</span>dit <span style="text-decoration:underline">V</span>iew <span style="text-decoration:underline">I</span>nsert F<span style="text-decoration:underline">o</span>rmat <span style="text-decoration:underline">D</span>ata <span style="text-decoration:underline">H</span>elp <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">READY</span><span style="background:var(--vt-palette-6)"> </span>2+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">B7</span><span style="background:var(--vt-palette-8)"> </span> =SUM(B3:B5)3+<span style="color:var(--vt-palette-8)">B7</span> <span style="color:var(--vt-palette-8)">reads</span> Expenses<span style="color:var(--vt-palette-8)">;</span> <span style="color:var(--vt-palette-8)">read</span> <span style="color:var(--vt-palette-8)">by</span> C7, B30↓ <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Alt+&#39;</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">list</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Alt+;</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">stop</span>4+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">A</span><span style="background:var(--vt-palette-8)"> </span><span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">B</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">C</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">D</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">E</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">F</span><span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">G</span><span style="background:var(--vt-palette-8)"> </span>5+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">1</span><span style="background:var(--vt-palette-8)"> </span> Household budget 20266+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">2</span><span style="background:var(--vt-palette-8)"> </span> 47+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">3</span><span style="background:var(--vt-palette-8)"> </span> Rent <span style="background:var(--vt-palette-2)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-2)">1450</span><span style="background:var(--vt-palette-2)"> </span>8+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">4</span><span style="background:var(--vt-palette-8)"> </span> Groceries<span style="background:var(--vt-palette-2)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-2)">612.4</span><span style="background:var(--vt-palette-2)"> </span>9+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">5</span><span style="background:var(--vt-palette-8)"> </span> Transit <span style="background:var(--vt-palette-2)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-2)">96</span><span style="background:var(--vt-palette-2)"> </span>10+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">6</span><span style="background:var(--vt-palette-8)"> </span> Savings r <span style="color:var(--vt-palette-1);text-decoration:underline wavy var(--vt-palette-1)">#DIV/0!</span>11+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">7</span><span style="background:var(--vt-palette-6)"> </span> Total <span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6)">2158.4</span><span style="background:var(--vt-palette-6)"> </span><span style="background:var(--vt-palette-5)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-5);font-weight:bold">539.6</span><span style="background:var(--vt-palette-5)"> </span>12+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">8</span><span style="background:var(--vt-palette-8)"> </span> 2877.866713+<span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">9</span><span style="background:var(--vt-palette-8)"> </span>14+<span style="background:var(--vt-palette-6)"> </span><span style="color:var(--vt-palette-0);background:var(--vt-palette-6);font-weight:bold">Sheet1</span><span style="background:var(--vt-palette-6)"> </span> <span style="color:var(--vt-palette-8)">+</span> <span style="color:var(--vt-palette-8)">│</span> untitled <span style="color:var(--vt-palette-8)">modified</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+K</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">search</span> <span style="background:var(--vt-palette-8)"> </span><span style="color:var(--vt-palette-15);background:var(--vt-palette-8)">Ctrl+/</span><span style="background:var(--vt-palette-8)"> </span> <span style="color:var(--vt-palette-8)">shortcuts</span></pre>\ No newline at end of file
M scripts/doccheck/main.go +84 -22
@@ -8,8 +8,10 @@
88 // page front matter with a title and a sidebar_position (tree.go);99 // - every docs/...md path Go code names exists;1010 // - every file in docs/media is shown by some doc, comes from a VHS tape11-// in demos/ (its Output or a Screenshot), and no GIF is over maxGIF;12-// - every tape in demos/ records something a doc shows;11+// in demos/ (its Output or a Screenshot) or is a still of a golden12+// screen (e2e/stills_test.go), and no GIF is over maxGIF;13+// - every tape in demos/ records something a doc shows, and every14+// still is shown, its dark and light pictures together;1315 // - every docs page the site has published is still a page, or has a1416 // redirect from its address in website/redirects.json (redirects.go).1517 //
@@ -180,8 +182,8 @@ func checkLinks(docs map[string]*doc) []string {
180182 out = append(out, where+": no such file")181183 continue182184 }183- if anchor == "" {184- continue185+ if anchor == "" || modeOnly[anchor] {186+ continue // the color mode a still shows in, checked in checkMedia185187 }186188 td, ok := docs[target]187189 if !ok {
@@ -197,22 +199,60 @@ func checkLinks(docs map[string]*doc) []string {
197199 var (198200 tapeOutput = regexp.MustCompile(`(?m)^Output\s+"?out/([^"\s]+)"?`)199201 tapeScreenshot = regexp.MustCompile(`(?m)^Screenshot\s+"?out/stills/([^"\s]+)"?`)202+ // docScreen is an entry of e2e's docScreens, a still drawn from a203+ // golden screen as <name>-dark.png and <name>-light.png.204+ docScreen = regexp.MustCompile(`(?m)^\s*\{name: "([^"]+)", from:`)200205 )201206 202-// checkMedia reports media no doc shows, media no tape makes, GIFs over203-// maxGIF and tapes whose recordings no doc shows.207+// stillsSource lists the stills of golden screens.208+const stillsSource = "e2e/stills_test.go"209+210+// modeOnly are the fragments GitHub and the site show an image by: only211+// in dark mode, or only in light.212+var modeOnly = map[string]bool{"gh-dark-mode-only": true, "gh-light-mode-only": true}213+214+// checkMedia reports media no doc shows, media nothing makes, GIFs over215+// maxGIF, tapes and stills no doc shows, and stills shown without their216+// other color mode.204217 func checkMedia(docs map[string]*doc) []string {205- shown := map[string]bool{}218+ // shown maps a media path to the fragment each doc shows it with.219+ shown := map[string]map[string]string{}206220 for _, dc := range docs {207221 for _, l := range dc.links {208- file, _, _ := strings.Cut(l.target, "#")222+ file, frag, _ := strings.Cut(l.target, "#")209223 if file != "" && !strings.Contains(file, "://") {210- shown[path.Clean(path.Join(path.Dir(dc.path), file))] = true224+ p := path.Clean(path.Join(path.Dir(dc.path), file))225+ if shown[p] == nil {226+ shown[p] = map[string]string{}227+ }228+ shown[p][dc.path] = frag211229 }212230 }213231 }232+ madeBy := map[string]string{} // media file name -> the tape or stills list233+ out := tapeMedia(shown, madeBy)234+ out = append(out, stillMedia(shown, madeBy)...)235+ media, _ := filepath.Glob("docs/media/*")236+ for _, m := range media {237+ m = filepath.ToSlash(m)238+ name := path.Base(m)239+ if shown[m] == nil {240+ out = append(out, m+": no doc shows it")241+ }242+ if madeBy[name] == "" {243+ out = append(out, m+": nothing makes it: no tape in demos/ (Output or Screenshot), nor docScreens in "+stillsSource)244+ }245+ if fi, err := os.Stat(m); err == nil && path.Ext(m) == ".gif" && fi.Size() > maxGIF {246+ out = append(out, fmt.Sprintf("%s: %d KB, over %d KB", m, fi.Size()>>10, maxGIF>>10))247+ }248+ }249+ return out250+}251+252+// tapeMedia records what each tape makes in madeBy and reports tapes253+// whose recordings no doc shows.254+func tapeMedia(shown map[string]map[string]string, madeBy map[string]string) []string {214255 tapes, _ := filepath.Glob("demos/*.tape")215- madeBy := map[string]string{} // media file name -> tape216256 var out []string217257 for _, t := range tapes {218258 src, err := os.ReadFile(t)
@@ -227,27 +267,49 @@ func checkMedia(docs map[string]*doc) []string {
227267 madeBy[m[1]] = t228268 }229269 }230- if !slices.ContainsFunc(made, func(name string) bool { return shown["docs/media/"+name] }) {270+ if !slices.ContainsFunc(made, func(name string) bool { return shown["docs/media/"+name] != nil }) {231271 out = append(out, t+": no doc shows its recording or stills (docs/media/"+strings.Join(made, ", docs/media/")+")")232272 }233273 }234- media, _ := filepath.Glob("docs/media/*")235- for _, m := range media {236- m = filepath.ToSlash(m)237- name := path.Base(m)238- if !shown[m] {239- out = append(out, m+": no doc shows it")240- }241- if madeBy[name] == "" {242- out = append(out, m+": no tape in demos/ makes it (Output or Screenshot)")274+ return out275+}276+277+// stillMedia records the stills of golden screens in madeBy, and reports278+// one no doc shows, or one shown without its other color mode: a doc279+// shows <name>-dark.png#gh-dark-mode-only and280+// <name>-light.png#gh-light-mode-only together.281+func stillMedia(shown map[string]map[string]string, madeBy map[string]string) []string {282+ src, err := os.ReadFile(stillsSource)283+ if err != nil {284+ return []string{stillsSource + ": " + err.Error()}285+ }286+ var out []string287+ for _, m := range docScreen.FindAllStringSubmatch(string(src), -1) {288+ dark, light := "docs/media/"+m[1]+"-dark.png", "docs/media/"+m[1]+"-light.png"289+ madeBy[path.Base(dark)], madeBy[path.Base(light)] = stillsSource, stillsSource290+ if shown[dark] == nil && shown[light] == nil {291+ out = append(out, stillsSource+": no doc shows the still "+m[1])243292 }244- if fi, err := os.Stat(m); err == nil && path.Ext(m) == ".gif" && fi.Size() > maxGIF {245- out = append(out, fmt.Sprintf("%s: %d KB, over %d KB", m, fi.Size()>>10, maxGIF>>10))293+ for _, d := range sortedKeys(mergeKeys(shown[dark], shown[light])) {294+ if shown[dark][d] != "gh-dark-mode-only" || shown[light][d] != "gh-light-mode-only" {295+ out = append(out, d+": shows the still "+m[1]+" without both "+path.Base(dark)+"#gh-dark-mode-only and "+path.Base(light)+"#gh-light-mode-only")296+ }246297 }247298 }248299 return out249300 }250301 302+func mergeKeys(a, b map[string]string) map[string]bool {303+ out := map[string]bool{}304+ for k := range a {305+ out[k] = true306+ }307+ for k := range b {308+ out[k] = true309+ }310+ return out311+}312+251313 func sortedKeys[V any](m map[string]V) []string {252314 keys := make([]string, 0, len(m))253315 for k := range m {
M website/src/css/custom.css +12 -0