diff --git a/apps/ui/src/components/Glyph.test.tsx b/apps/ui/src/components/Glyph.test.tsx
new file mode 100644
index 00000000..d4d2b69d
--- /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 00000000..83ceea4e
--- /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 324e2f98..8e2eb1dd 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