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
@@ -24,9 +24,11 @@ Noise that stays becomes invisible, and then real problems hide in it.
2424 reader would look in, link to it from elsewhere, and restructure when2525 appending would make a wall of text. Shipped roadmap items move to its2626 Shipped list as one line.27-- Diagrams are Mermaid code blocks, never ASCII art; screens and terminal28- output stay code blocks. Draw one where a flow, a state machine or a29- sequence would otherwise take paragraphs30- ([the docs site](docs/contributing/site.md#diagrams)).27+- Diagrams are Mermaid code blocks, never ASCII art. Draw one where a28+ flow, a state machine or a sequence would otherwise take paragraphs29+ ([the docs site](docs/contributing/site.md#diagrams)). 012's screen is30+ a picture, a still drawn from a golden screen, never text in a code31+ block ([Pictures of 012](docs/contributing/site.md#pictures-of-012));32+ code blocks are for what a reader types or a shell prints.3133 - No scratch files, debug tests, temporary samples or build outputs (`*.test`, binaries) in commits; `make lint` refuses compiled files and anything over 2 MB.3234 - Visual changes are reviewed in the golden gallery (`make screens`).@@ -68,7 +68,7 @@ e2e: $(GHOSTTY_STAMP)
6868 # Rewrite golden screens and build e2e/testdata/screens/gallery.html for6969 # visual review. Review the diff and the gallery before committing.7070 screens: $(GHOSTTY_STAMP)71- cd e2e && PKG_CONFIG_PATH=$(GHOSTTY_OUT)/share/pkgconfig go test -count=1 -run TestScreens ./... -update71+ cd e2e && PKG_CONFIG_PATH=$(GHOSTTY_OUT)/share/pkgconfig go test -count=1 -run 'TestScreens|TestStills' ./... -update7272 7373 # Stress: fetch real datasets into .deps/stress, run the benchmarks built7474 # with -tags stress, print a summary and record the run in@@ -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+159+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.@@ -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 andBinary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
Binary file.
@@ -15,6 +15,7 @@ require (
1515 github.com/xuri/efp v0.0.1 // indirect1616 github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect1717 golang.org/x/crypto v0.53.0 // indirect18+ golang.org/x/image v0.45.0 // indirect1819 golang.org/x/net v0.56.0 // indirect19- golang.org/x/text v0.38.0 // indirect20+ golang.org/x/text v0.41.0 // indirect2021 )@@ -24,9 +24,13 @@ golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
2424 golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=2525 golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=2626 golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=27+golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=28+golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=2729 golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=2830 golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=2931 golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=3032 golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=33+golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=34+golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=3135 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=3236 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=@@ -37,6 +37,7 @@ func TestMain(m *testing.M) {
3737 fmt.Fprintf(os.Stderr, "building 012: %v\n%s", err, out)3838 os.Exit(1)3939 }40+ addDocScreens()4041 code := m.Run()4142 os.RemoveAll(dir)4243 os.Exit(code)@@ -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+}@@ -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+}@@ -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+}@@ -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+}@@ -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+}@@ -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@@ -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@@ -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@@ -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@@ -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@@ -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@@ -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@@ -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@@ -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@@ -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@@ -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@@ -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@@ -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+'</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@@ -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+'</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@@ -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 {@@ -247,6 +247,18 @@ kbd {
247247 margin: 1rem 0;248248 }249249 250+/* Stills of 012 come in a dark and a light picture, marked as GitHub251+ marks them (#gh-dark-mode-only), and the site shows the one for its252+ color mode. Each draws its own window, so it has no border. */253+[data-theme='light'] .markdown img[src$='#gh-dark-mode-only'],254+[data-theme='dark'] .markdown img[src$='#gh-light-mode-only'] {255+ display: none;256+}257+258+.markdown img[src$='-mode-only'] {259+ border: 0;260+}261+250262 /* ---------- Navbar: the control panel's first line ---------- */251263 252264 .navbar {