the catalogue: hand-ruled listings with drawer-tab marks
commit 2f10e1d4cf993946ef7cdb083ccbd2a4f8f26666 · parent 65137bffd7
· browse this tree
Ximianth, 2026-10-26 23:20:57 UTC
Tree listings keep their tabular bones (scannability is the point) and gain catalogue character: rows separate under a soft sibling of the rule tile (same generator, quieter inks, new purpose seed -- existing rule bytes stay pinned), and directory names carry a small hand-drawn drawer-tab silhouette as the link's own background, so the mark is part of the tap target. The '..' row stays unmarked: it is navigation, not contents. Row hover switches to background-color so it layers under the rule instead of replacing it.
diff --git a/chrome.go b/chrome.go index f616807..27bf9b6 100644 --- a/chrome.go +++ b/chrome.go @@ -348,9 +348,17 @@ a.sha { } a.sha:hover { color: var(--accent); } -/* Listing tables. */ -article table.tree td, article table.tree th { border: none; padding: .15rem .6rem; } -article table.tree tbody tr:hover { background: var(--well); } +/* The catalogue: listings keep their tabular bones; rows separate with soft + hand-rules (set in sketchCSS) and directory links carry the drawer-tab mark. */ +article table.tree { border-collapse: collapse; } +article table.tree td, article table.tree th { border: none; padding: .3rem .6rem; } +article table.tree tbody tr:hover { background-color: var(--well); } +article td.dir > a { + background-repeat: no-repeat; + background-position: left center; + background-size: 24px 18px; + padding-left: 30px; +} article th { text-align: left; background: none; } /* Table links skip the underline: in a column of links it is pure noise; the accent color and the row hover carry the affordance. Hover restores it. */ @@ -399,6 +407,14 @@ ul.ledger li { background-repeat: repeat-x; background-size: auto 9px; } +/* Catalogue rows separate with the rule's soft sibling; the tab mark rides the link. */ +article table.tree tbody tr { + background-image: %s; + background-position: bottom left; + background-repeat: repeat-x; + background-size: auto 8px; +} +article td.dir > a { background-image: %s; } a.sha { border: 7px solid transparent; border-image: %s 8 / 7px round; @@ -428,7 +444,7 @@ nav.arc a.va:active { background-image: %s; } nav.arc a.vb:active { background-image: %s; } nav.arc a.vc:active { background-image: %s; } `, - u("flank"), u("ground"), u("plinth"), u("rule"), u("rule"), u("chip"), u("frame"), u("frame"), + u("flank"), u("ground"), u("plinth"), u("rule"), u("rule"), u("rulesoft"), u("tab"), u("chip"), u("frame"), u("frame"), u("circle-a"), u("circle-b"), u("circle-c"), u("circle-b"), u("circle-c"), u("circle-a"), u("circle-c"), u("circle-a"), u("circle-b")) diff --git a/pages.go b/pages.go index 47f42b6..8df2df4 100644 --- a/pages.go +++ b/pages.go @@ -319,10 +319,13 @@ func (s *server) renderReadme(rp *repo, ref, sha string) (template.HTML, readmeI // ---- tree listing ---- +// The catalogue: listings keep their tabular bones, with light hand-ruled row +// separators and a drawer-tab mark ahead of directory names (the mark is the +// link's own background, so it is part of the tap target). var treeTmpl = template.Must(template.New("tree").Parse(`<h1>{{.Title}}</h1> <table class="tree"> <tbody> -{{range .Rows}}<tr><td class="name">{{if .Href}}<a href="{{.Href}}">{{.Name}}</a>{{else}}{{.Name}}{{end}}</td><td class="size">{{.Size}}</td></tr> +{{range .Rows}}<tr><td class="name{{if .Dir}} dir{{end}}">{{if .Href}}<a href="{{.Href}}">{{.Name}}</a>{{else}}{{.Name}}{{end}}</td><td class="size">{{.Size}}</td></tr> {{end}}</tbody> </table> `)) @@ -331,6 +334,7 @@ type treeRow struct { Name string Href string // empty renders unlinked (submodules) Size string // blob size in bytes; empty for non-blobs + Dir bool // directories wear the drawer-tab mark; the ".." row does not (it is nav, not contents) } type treeData struct { @@ -365,6 +369,7 @@ func (s *server) serveTreeListing(w http.ResponseWriter, rp *repo, ref, sha, pth case "tree": row.Name += "/" row.Href = treeHref(rp.name, ref, full) + "/" + row.Dir = true case "blob": row.Href = treeHref(rp.name, ref, full) row.Size = humanSize(e.size) diff --git a/sketch.go b/sketch.go index 443bf4d..502022b 100644 --- a/sketch.go +++ b/sketch.go @@ -135,11 +135,13 @@ func sketchAssets(seed string) map[string][]byte { m := map[string][]byte{} for _, p := range sketchPalettes { m["frame-"+p.scheme+".svg"] = frameSVG(seed, p) - m["rule-"+p.scheme+".svg"] = ruleSVG(seed, p) + m["rule-"+p.scheme+".svg"] = ruleSVG(seed, "rule", p, 0.7, 0.25) + m["rulesoft-"+p.scheme+".svg"] = ruleSVG(seed, "rulesoft", p, 0.4, 0.15) m["flank-"+p.scheme+".svg"] = flankSVG(seed, p) m["ground-"+p.scheme+".svg"] = groundSVG(seed, p) m["plinth-"+p.scheme+".svg"] = plinthSVG(seed, p) m["chip-"+p.scheme+".svg"] = chipSVG(seed, p) + m["tab-"+p.scheme+".svg"] = tabSVG(seed, p) for _, v := range []string{"a", "b", "c"} { m["circle-"+v+"-"+p.scheme+".svg"] = circleSVG(seed+"/"+v, p) } @@ -188,11 +190,11 @@ func frameSVG(seed string, p sketchPalette) []byte { return []byte(sb.String()) } -// ruleSVG is a wide, thin tile holding one hand-ruled horizontal line, -// for border-images on nav and heading underlines. Displacement is zero at both -// tile ends so repeat tiles cleanly. -func ruleSVG(seed string, p sketchPalette) []byte { - rng := newSketchRand(seed + "/rule/" + p.scheme) +// ruleSVG is a wide, thin tile holding one hand-ruled horizontal line, for nav and +// heading underlines (full-strength opacities) and row separators (the soft ones). +// Displacement is zero at both tile ends so repeat tiles cleanly. +func ruleSVG(seed, purpose string, p sketchPalette, opacity, ghostOpacity float64) []byte { + rng := newSketchRand(seed + "/" + purpose + "/" + p.scheme) var sb strings.Builder svgOpen(&sb, 480, 12) anchorRow := func(amp float64) []pt { @@ -205,8 +207,8 @@ func ruleSVG(seed string, p sketchPalette) []byte { var d1, d2 strings.Builder wobblePath(&d1, rng, anchorRow(1.1), 1.1) wobblePath(&d2, rng, anchorRow(1.5), 1.5) - svgPath(&sb, d1.String(), p.ink, 1.8, 0.7) - svgPath(&sb, d2.String(), p.ink, 1.3, 0.25) + svgPath(&sb, d1.String(), p.ink, 1.8, opacity) + svgPath(&sb, d2.String(), p.ink, 1.3, ghostOpacity) sb.WriteString("</svg>\n") return []byte(sb.String()) } @@ -365,3 +367,26 @@ func chipSVG(seed string, p sketchPalette) []byte { sb.WriteString("</svg>\n") return []byte(sb.String()) } + +// tabSVG is the catalogue's drawer-tab mark: a small folder silhouette standing +// ahead of directory names in tree listings, wordless and self-explanatory. +func tabSVG(seed string, p sketchPalette) []byte { + rng := newSketchRand(seed + "/tab/" + p.scheme) + var sb strings.Builder + svgOpen(&sb, 24, 18) + outline := func(d *strings.Builder, amp, over float64) { + roughLine(d, rng, pt{2, 15}, pt{2, 4}, amp, over) + roughLine(d, rng, pt{2, 4}, pt{9, 4}, amp, over) + roughLine(d, rng, pt{9, 4}, pt{11.5, 7}, amp, over) + roughLine(d, rng, pt{11.5, 7}, pt{22, 7}, amp, over) + roughLine(d, rng, pt{22, 7}, pt{22, 15}, amp, over) + roughLine(d, rng, pt{22, 15}, pt{2, 15}, amp, over) + } + var d1, d2 strings.Builder + outline(&d1, 0.6, 1.2) + outline(&d2, 0.9, 1.5) + svgPath(&sb, d1.String(), p.ink, 1.3, 0.7) + svgPath(&sb, d2.String(), p.ink, 1.0, 0.26) + sb.WriteString("</svg>\n") + return []byte(sb.String()) +} diff --git a/testdata/pages.md b/testdata/pages.md index 3b749bb..cc90aca 100644 --- a/testdata/pages.md +++ b/testdata/pages.md @@ -151,9 +151,9 @@ Content-Type: text/html; charset=utf-8 <main><article><h1>fixturama</h1> <table class="tree"> <tbody> -<tr><td class="name"><a href="/fixturama/tree/main/docs/">docs/</a></td><td class="size"></td></tr> -<tr><td class="name"><a href="/fixturama/tree/main/src/">src/</a></td><td class="size"></td></tr> -<tr><td class="name"><a href="/fixturama/tree/main/vendor/">vendor/</a></td><td class="size"></td></tr> +<tr><td class="name dir"><a href="/fixturama/tree/main/docs/">docs/</a></td><td class="size"></td></tr> +<tr><td class="name dir"><a href="/fixturama/tree/main/src/">src/</a></td><td class="size"></td></tr> +<tr><td class="name dir"><a href="/fixturama/tree/main/vendor/">vendor/</a></td><td class="size"></td></tr> <tr><td class="name"><a href="/fixturama/tree/main/.gitignore">.gitignore</a></td><td class="size">7 B</td></tr> <tr><td class="name"><a href="/fixturama/tree/main/README.md">README.md</a></td><td class="size">49 B</td></tr> </tbody> diff --git a/testdata/sketch.md b/testdata/sketch.md index 8ee5cdf..6f4dada 100644 --- a/testdata/sketch.md +++ b/testdata/sketch.md @@ -66,6 +66,26 @@ Regenerate with `go test -run TestSketchSnapshots -testmark.regen` and review th </svg> ``` +### rulesoft-light.svg + +[testmark]:# (rulesoft-light.svg) +```svg +<svg xmlns="http://www.w3.org/2000/svg" width="480" height="12" viewBox="0 0 480 12"> +<path d="M0.0 6.0C30.9 5.7 61.9 5.9 92.8 6.0C124.1 6.0 155.4 6.6 186.7 6.4C218.8 6.1 250.9 7.1 283.0 5.9C316.6 4.9 350.3 4.9 383.9 5.8C416.0 6.0 448.0 7.0 480.0 6.0" fill="none" stroke="#24292f" stroke-width="1.8" stroke-opacity="0.40" stroke-linecap="round"/> +<path d="M0.0 6.0C32.9 5.9 65.8 5.5 98.6 4.6C129.5 4.2 160.3 3.9 191.1 5.2C221.1 5.4 251.2 4.2 281.2 5.1C316.2 4.9 351.2 7.0 386.3 6.9C417.5 6.2 448.8 5.9 480.0 6.0" fill="none" stroke="#24292f" stroke-width="1.3" stroke-opacity="0.15" stroke-linecap="round"/> +</svg> +``` + +### tab-light.svg + +[testmark]:# (tab-light.svg) +```svg +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 18"> +<path d="M2.1 15.5C1.7 11.5 2.1 7.6 2.1 3.6M0.7 4.2C3.7 4.3 6.7 4.3 9.7 3.9M8.1 3.1C9.2 4.9 10.9 6.1 12.2 7.7M10.8 7.0C14.6 7.3 18.5 6.7 22.3 7.0M22.2 6.0C21.6 9.2 22.0 12.5 22.0 15.7M23.0 15.3C15.8 15.5 8.6 15.3 1.3 14.8" fill="none" stroke="#24292f" stroke-width="1.3" stroke-opacity="0.70" stroke-linecap="round"/> +<path d="M1.6 15.4C1.5 11.3 1.4 7.2 2.4 3.2M1.5 3.6C4.3 3.3 7.0 4.4 9.8 3.6M7.9 2.9C8.4 4.8 10.4 5.3 11.3 6.9M10.3 7.1C14.5 7.1 18.7 6.6 22.9 7.2M22.3 5.5C22.7 9.2 21.5 12.7 21.7 16.4M22.4 14.9C15.4 15.4 8.4 15.3 1.3 15.1" fill="none" stroke="#24292f" stroke-width="1.0" stroke-opacity="0.26" stroke-linecap="round"/> +</svg> +``` + ### circle-a-light.svg [testmark]:# (circle-a-light.svg) @@ -157,6 +177,26 @@ Regenerate with `go test -run TestSketchSnapshots -testmark.regen` and review th </svg> ``` +### rulesoft-dark.svg + +[testmark]:# (rulesoft-dark.svg) +```svg +<svg xmlns="http://www.w3.org/2000/svg" width="480" height="12" viewBox="0 0 480 12"> +<path d="M0.0 6.0C33.9 6.6 67.9 6.1 101.8 6.2C131.3 6.6 160.9 6.5 190.4 6.2C223.6 6.0 256.8 6.2 290.1 6.5C320.2 5.8 350.2 6.6 380.3 6.6C413.6 6.2 446.8 6.7 480.0 6.0" fill="none" stroke="#e6edf3" stroke-width="1.8" stroke-opacity="0.40" stroke-linecap="round"/> +<path d="M0.0 6.0C33.8 7.0 67.6 5.2 101.4 5.9C130.9 5.1 160.4 4.2 190.0 5.0C220.9 5.0 251.9 4.4 282.8 5.4C314.0 6.2 345.2 4.7 376.3 5.5C410.9 5.4 445.4 5.9 480.0 6.0" fill="none" stroke="#e6edf3" stroke-width="1.3" stroke-opacity="0.15" stroke-linecap="round"/> +</svg> +``` + +### tab-dark.svg + +[testmark]:# (tab-dark.svg) +```svg +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 18"> +<path d="M2.1 16.3C1.8 12.0 1.3 7.7 1.7 3.3M0.7 4.2C3.8 3.8 6.9 4.8 10.0 4.2M8.2 3.2C9.8 4.2 10.6 6.0 12.0 7.3M11.3 6.8C15.0 6.5 18.6 6.9 22.3 7.0M22.2 7.0C22.7 9.9 21.9 12.8 22.1 15.7M22.3 15.1C15.2 14.8 8.1 14.5 0.9 14.7" fill="none" stroke="#e6edf3" stroke-width="1.3" stroke-opacity="0.70" stroke-linecap="round"/> +<path d="M2.3 15.9C1.9 11.4 1.8 6.9 1.9 2.3M1.3 4.4C3.9 3.5 6.7 4.6 9.2 3.6M7.9 3.4C8.8 4.9 10.1 6.0 11.5 7.0M11.0 7.1C15.0 6.5 19.0 7.4 23.0 6.6M22.1 6.2C21.9 9.5 22.7 12.8 21.9 16.0M22.4 14.8C15.1 15.5 7.8 15.5 0.5 15.2" fill="none" stroke="#e6edf3" stroke-width="1.0" stroke-opacity="0.26" stroke-linecap="round"/> +</svg> +``` + ### circle-a-dark.svg [testmark]:# (circle-a-dark.svg)