Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions apps/ui/src/components/Glyph.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, test } from "bun:test";
import { render } from "@solidjs/testing-library";
import { GLYPH_NAMES, Glyph } from "./Glyph";

// The <Glyph/> invariants named by the frozen record (compass-glyph-primitives
// §"The <Glyph/> 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(() => <Glyph name="status" />);
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(() => <Glyph name={name} />);
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);
}
});
}
});
230 changes: 230 additions & 0 deletions apps/ui/src/components/Glyph.tsx
Original file line number Diff line number Diff line change
@@ -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 `<rect width="1" height="1">` 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<readonly [number, number]>
> = {
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 (
<svg
viewBox="0 0 11 11"
width="11"
height="11"
shape-rendering="crispEdges"
aria-hidden="true"
>
<For each={GLYPH_CELLS[props.name]}>
{([x, y]) => (
<rect x={x} y={y} width="1" height="1" fill="currentColor" />
)}
</For>
</svg>
);
};
87 changes: 87 additions & 0 deletions apps/ui/src/design/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,93 @@ vs an 18px integer-multiple — is resolved: 9px is the intended shipped size.
.........
```

## Glyphs

- **Classes:** none of its own — the `<Glyph name>` 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 `<rect width="1" height="1">` 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
Expand Down
Loading