From 288e9ebe190a57b036b91dd29d442108f24794b1 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sat, 12 Sep 2026 18:08:04 -0400 Subject: [PATCH] feat(ui): add the dot-matrix Glyph primitive and its four chrome bitmaps (RIG-3736) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopts the DL-150 / DL-199 technique for UI chrome symbols: an 11x11 1-bit grid at one CSS px per cell, `shape-rendering="crispEdges"`, one `` per lit cell filled on `currentColor` so the consuming control's color flows through. `GLYPH_CELLS` is keyed on the exhaustive `GlyphName` union, so a name without a bitmap is a compile error rather than a silent runtime blank — the guard `BadgeGlyph`'s `GlyphKey` already provides. The four canonical grids are transcribed into `design/components.md` §Glyphs, which stays the source of truth for the geometry. The glyph is decorative: `aria-hidden`, no `role="img"` and no `aria-label`. It carries no meaning of its own, so a name-bearing label belongs on the consuming control. This is the deliberate difference from `BadgeGlyph`, whose glyph encodes status. The `vcs` glyph is one orthogonally-connected shape: cells that touch only at a corner read as detached specks at 11px under `crispEdges`, not as a line, so the branch is fused to the trunk rather than meeting it diagonally. `status` and `pr` are deliberately multi-part (four blocks, two arrows). No call-site adopts `` yet; `ActivityBarItem`, the render site, and the chrome audit follow in later commits of this stack. Ref: RIG-3736, RIG-3603. Design: docs/designs/ui/compass-glyph-primitives/design.md (DL-367). Co-authored-by: Matt Wilkinson --- apps/ui/src/components/Glyph.test.tsx | 45 +++++ apps/ui/src/components/Glyph.tsx | 230 ++++++++++++++++++++++++++ apps/ui/src/design/components.md | 87 ++++++++++ 3 files changed, 362 insertions(+) create mode 100644 apps/ui/src/components/Glyph.test.tsx create mode 100644 apps/ui/src/components/Glyph.tsx diff --git a/apps/ui/src/components/Glyph.test.tsx b/apps/ui/src/components/Glyph.test.tsx new file mode 100644 index 000000000..d4d2b69dd --- /dev/null +++ b/apps/ui/src/components/Glyph.test.tsx @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test"; +import { render } from "@solidjs/testing-library"; +import { GLYPH_NAMES, Glyph } from "./Glyph"; + +// The invariants named by the frozen record (compass-glyph-primitives +// §"The primitive"): every glyph name yields a non-empty cell list, +// and every cell lies within the 11×11 grid. We reach the geometry through the +// rendered SVG so the assertions bind the observable output, not the table. + +function renderedCells(root: Element): Array<[number, number]> { + const cells: Array<[number, number]> = []; + for (const rect of root.querySelectorAll("rect")) { + cells.push([ + Number(rect.getAttribute("x")), + Number(rect.getAttribute("y")), + ]); + } + return cells; +} + +describe("Glyph", () => { + test("is decorative — aria-hidden, no role or label", () => { + const { container } = render(() => ); + const svg = container.querySelector("svg"); + expect(svg?.getAttribute("aria-hidden")).toBe("true"); + expect(svg?.getAttribute("role")).toBeNull(); + expect(svg?.getAttribute("aria-label")).toBeNull(); + }); + + for (const name of GLYPH_NAMES) { + test(`${name} lights cells, all within the 11×11 grid`, () => { + const { container } = render(() => ); + const cells = renderedCells(container); + expect(cells.length).toBeGreaterThan(0); + for (const [x, y] of cells) { + expect(Number.isInteger(x)).toBe(true); + expect(Number.isInteger(y)).toBe(true); + expect(x).toBeGreaterThanOrEqual(0); + expect(x).toBeLessThanOrEqual(10); + expect(y).toBeGreaterThanOrEqual(0); + expect(y).toBeLessThanOrEqual(10); + } + }); + } +}); diff --git a/apps/ui/src/components/Glyph.tsx b/apps/ui/src/components/Glyph.tsx new file mode 100644 index 000000000..83ceea4e9 --- /dev/null +++ b/apps/ui/src/components/Glyph.tsx @@ -0,0 +1,230 @@ +import { type Component, For } from "solid-js"; + +/** A fixed chrome symbol drawn as an 11×11 1-bit pixel-art grid at one CSS px + * per cell (RIG-3603, design compass-glyph-primitives). Adopts the DL-150 / + * DL-199 technique as-is: one `` per lit cell, + * filled on `currentColor` so the consuming control's color flows through. + * + * The glyph is decorative — `aria-hidden`, no `role="img"` and no + * `aria-label`. It carries no meaning of its own, so a name-bearing label + * belongs on the consuming control (this is the deliberate difference from + * `BadgeGlyph`, whose glyph carries status meaning). */ + +export type GlyphName = "status" | "files" | "vcs" | "pr"; + +/** [x, y] of each lit cell (11×11, one CSS px per cell), transcribed from the + * frozen ASCII grids in `design/components.md` §Glyphs (`#` = lit). Keying on + * the exhaustive `GlyphName` union makes a name without a bitmap a compile + * error, not a silent runtime blank. */ +const GLYPH_CELLS: Record< + GlyphName, + ReadonlyArray +> = { + status: [ + [1, 1], + [2, 1], + [3, 1], + [4, 1], + [6, 1], + [7, 1], + [8, 1], + [9, 1], + [1, 2], + [2, 2], + [3, 2], + [4, 2], + [6, 2], + [7, 2], + [8, 2], + [9, 2], + [1, 3], + [2, 3], + [3, 3], + [4, 3], + [6, 3], + [7, 3], + [8, 3], + [9, 3], + [1, 4], + [2, 4], + [3, 4], + [4, 4], + [6, 4], + [7, 4], + [8, 4], + [9, 4], + [1, 6], + [2, 6], + [3, 6], + [4, 6], + [6, 6], + [7, 6], + [8, 6], + [9, 6], + [1, 7], + [2, 7], + [3, 7], + [4, 7], + [6, 7], + [7, 7], + [8, 7], + [9, 7], + [1, 8], + [2, 8], + [3, 8], + [4, 8], + [6, 8], + [7, 8], + [8, 8], + [9, 8], + [1, 9], + [2, 9], + [3, 9], + [4, 9], + [6, 9], + [7, 9], + [8, 9], + [9, 9], + ], + files: [ + [1, 2], + [2, 2], + [3, 2], + [4, 2], + [1, 3], + [2, 3], + [3, 3], + [4, 3], + [1, 4], + [2, 4], + [3, 4], + [4, 4], + [5, 4], + [6, 4], + [7, 4], + [8, 4], + [9, 4], + [1, 5], + [2, 5], + [3, 5], + [4, 5], + [5, 5], + [6, 5], + [7, 5], + [8, 5], + [9, 5], + [1, 6], + [2, 6], + [3, 6], + [4, 6], + [5, 6], + [6, 6], + [7, 6], + [8, 6], + [9, 6], + [1, 7], + [2, 7], + [3, 7], + [4, 7], + [5, 7], + [6, 7], + [7, 7], + [8, 7], + [9, 7], + [1, 8], + [2, 8], + [3, 8], + [4, 8], + [5, 8], + [6, 8], + [7, 8], + [8, 8], + [9, 8], + [1, 9], + [2, 9], + [3, 9], + [4, 9], + [5, 9], + [6, 9], + [7, 9], + [8, 9], + [9, 9], + ], + vcs: [ + [1, 1], + [2, 1], + [7, 1], + [8, 1], + [1, 2], + [2, 2], + [6, 2], + [7, 2], + [8, 2], + [1, 3], + [2, 3], + [5, 3], + [6, 3], + [1, 4], + [2, 4], + [3, 4], + [4, 4], + [5, 4], + [1, 5], + [2, 5], + [3, 5], + [1, 6], + [2, 6], + [1, 7], + [2, 7], + [1, 8], + [2, 8], + [1, 9], + [2, 9], + ], + pr: [ + [8, 1], + [1, 2], + [2, 2], + [3, 2], + [4, 2], + [5, 2], + [6, 2], + [7, 2], + [8, 2], + [9, 2], + [8, 3], + [2, 7], + [1, 8], + [2, 8], + [3, 8], + [4, 8], + [5, 8], + [6, 8], + [7, 8], + [8, 8], + [9, 8], + [2, 9], + ], +}; + +/** Every glyph name, derived from the table itself so callers that enumerate + * glyphs (the cell-validity test) pick up a new name automatically. */ +export const GLYPH_NAMES = Object.keys(GLYPH_CELLS) as readonly GlyphName[]; + +export const Glyph: Component<{ name: GlyphName }> = (props) => { + return ( + + ); +}; diff --git a/apps/ui/src/design/components.md b/apps/ui/src/design/components.md index 324e2f989..8e2eb1dd8 100644 --- a/apps/ui/src/design/components.md +++ b/apps/ui/src/design/components.md @@ -346,6 +346,93 @@ vs an 18px integer-multiple — is resolved: 9px is the intended shipped size. ......... ``` +## Glyphs + +- **Classes:** none of its own — the `` primitive renders a bare + inline SVG that the consuming control positions and colors. Adopted first by + the right-sidebar activity bar (`.r-tab .r-tab-icon[data-kind="glyph"]`). +- **Geometry:** an 11×11 1-bit bitmap grid, `shape-rendering="crispEdges"` (no + anti-aliasing); one `` per lit cell, filled on + `currentColor` so the consumer's color flows through. 11×11 (not the state + dot's 9×9) gives a true center cell and enough cells for pictographs. +- **Accessibility:** glyphs are decorative — `aria-hidden="true"`, no + `role="img"` and no `aria-label`. A name-bearing label belongs on the + consuming control, never on the glyph (this is the deliberate difference from + the axis badge, whose glyph carries status meaning). +- **Names:** the closed set is `status | files | vcs | pr` — the four static + activity-bar tabs. The set grows semantic names (never character names) as + the chrome audit converts further sites. + +### The four canonical glyph grids (11×11) + +`#` = lit cell, `.` = off; one CSS px per cell. Coordinates are `[x, y]` with +`x` = column (0..10), `y` = row (0..10), origin top-left. + +`status` — a status grid (replaces `▦`): four cells in a 2×2 block: + +```text +........... +.####.####. +.####.####. +.####.####. +.####.####. +........... +.####.####. +.####.####. +.####.####. +.####.####. +........... +``` + +`files` — a folder (replaces `🗀`): a tab over a wider body: + +```text +........... +........... +.####...... +.####...... +.#########. +.#########. +.#########. +.#########. +.#########. +.#########. +........... +``` + +`vcs` — a version-control branch (replaces `⎇`): a trunk with a branch +diverging to a top-right node: + +```text +........... +.##....##.. +.##...###.. +.##..##.... +.#####..... +.###....... +.##........ +.##........ +.##........ +.##........ +........... +``` + +`pr` — two opposing horizontal arrows (replaces `⇄`): right over left: + +```text +........... +........#.. +.#########. +........#.. +........... +........... +........... +..#........ +.#########. +..#........ +........... +``` + ## Tabs - **Class:** `.cx-tabs` · `data-orientation="h | v"`, with `.cx-tab` items