Skip to content

feat(ui): finish the chrome glyph conversion (RIG-3739) - #1219

Open
rigel-mintaka wants to merge 5 commits into
mainfrom
compass-ux/3739-chrome-glyphs-t5c
Open

rigel-mintaka wants to merge 5 commits into
mainfrom
compass-ux/3739-chrome-glyphs-t5c

Conversation

@rigel-mintaka

@rigel-mintaka rigel-mintaka commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

This PR is part of a stack containing 5 PRs:

  1. main
  2. feat(ui): add the dot-matrix Glyph primitive and its four chrome bitmaps (RIG-3736) #1194
  3. feat(ui): derive the activity-bar avatar initial through one owner (RIG-3737) #1199
  4. feat(ui): split ActivityBarItem into its glyph and avatar arms (RIG-3738) #1202
  5. feat(ui): convert the chrome pictographs to dot-matrix glyphs (RIG-3739) #1211
  6. "feat(ui): finish the chrome glyph conversion (RIG-3739)" (this PR)

T5c, the last conversion slice. Four sites move to <Glyph> and one swaps a
character; only one new bitmap was needed, because most of these were the same
affordances already converted in T5a/T5b:

  • BacklogView section caret and LogPanel's minimize toggle both reuse
    disclosure. The toggle's two states are one glyph rotated 180deg rather than
    two bitmaps, so the pair cannot drift out of alignment.
  • UsageBar's branch mark reuses vcs.
  • LogPanel's stop mark is the one new glyph, a filled 7x7 square.

comms.ts channelGlyph is data, not JSX — it returns the marker string that
three render sites draw beside a channel name, alongside @ and #. Returning
a GlyphName would force all three to branch glyph-vs-character and put a
bitmap in the same alignment column as two characters, so the fix is a covered
character instead: U+2317 (absent from Space Mono) becomes & U+0026, the
Discord convention for a group DM and plain ASCII.

The six arrows in Bridge and SettingsView stay text. U+2192 is in the
Space Mono cmap, so converting them was a style call rather than a coverage fix,
and a bitmap arrow reads no better at this size — two of the six sit mid-
sentence, where it would break the type.

agent.png and backlog.png are recaptured. LogPanel mounts on the agent
surface with the log open by default, so the stop mark and the minimize toggle
both render there, and the backlog chevron renders on the backlog board. A first
recapture pass wrote nothing and I read that as "these sites are not captured" —
wrong. Playwright only rewrites a baseline whose diff exceeds tolerance, and the
0.001 ratio allows 921 px on a 1280x720 shot. Measured against the previous
baselines the deltas are 913 px (agent) and 1387 px (backlog): the backlog shot
was over budget and would have failed CI, and the agent shot sat at 99% of it,
one render shift from a confusing red. Both now depict the new bitmaps.

That leaves the Unifont pin blocked only by the six fixture strings in
stub-data.ts (, ▪▪▪▪), which belong to T8. Every rendered chrome
character now resolves in the brand face — measured by parsing the cmap, not by
eye.

Verified: 1112 pass / 0 fail; typecheck, stylelint, biome, and markdownlint
clean. The new bitmap is one orthogonally-connected component and its documented
grid matches its cells.

rigel-mintaka and others added 5 commits September 13, 2026 17:57
…aps (RIG-3736)

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
`<rect width="1" height="1">` 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 `<Glyph/>` 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 <matt@rigel.build>
…IG-3737)

`fleetItemForAgent` and `unreachableFleetItem` each computed the fleet tab's
letter with `(handle.at(0) ?? "?").toUpperCase()`. Both now call
`avatarInitial`, which implements D1 of the glyph-primitives record.

`.at(0)` reads a UTF-16 code unit, so an astral first character came back as
half a surrogate pair. The helper takes the first grapheme instead, then
NFKD-normalizes it and strips combining marks so an accented handle keeps its
letter (`Émile` -> `E`) rather than degrading.

The result is clamped to one printable ASCII character. That clamp is the
condition on which the e2e Unifont pin can retire: the avatar arm is the only
activity-bar text whose glyph is not drawn by us, so bounding it to ASCII
removes the last reason to ship a fallback face for this surface.

`?` remains only for scripts no Latin letter represents. Those handles do
collapse to one tab letter, which the tab's `title` and `aria-label` already
compensate for — both carry the full handle, and the icon span is
`aria-hidden`. An uppercase that expands (`ß` -> `SS`) keeps its first letter
for the same reason: the initial exists to tell agents apart.

`icon` keeps its name here; splitting the field into the glyph and avatar arms
is the next commit in this stack.

Ref: RIG-3737, RIG-3603. Design: docs/designs/ui/compass-glyph-primitives/design.md (DL-367).

Co-authored-by: Matt Wilkinson <matt@rigel.build>
…738)

`ActivityBarItem.icon: string` carried two unrelated things: a fixed chrome
symbol on the four static tabs, and a person's initial on the fleet tabs. The
name fit neither, and `agentId`/`unreachable` sat on the shared interface
though only fleet tabs ever have them.

The item now splits at the item, not the field: `GlyphTabItem` carries
`name: GlyphName` and renders `<Glyph/>`; `AvatarTabItem` carries `letter`
plus the `agentId` and `unreachable` that only it uses. A field-level union
was rejected — it keeps a field called `icon` whose value is sometimes a
person's initial.

The union, the constructors, the render branch and the CSS land together
because they must: the activity-bar loop reads both `tab.icon` and
`tab.agentId` off the un-narrowed item, so landing the type without the
render branch is a strict-mode error and leaves a red commit mid-stack.

Narrowing removed a non-null assertion in `AgentUnreachable` — its `agentId`
is now required by the type rather than asserted at the use site. `store.ts`
is unchanged: `rightTabGroups` holds a genuinely mixed list, so the union is
already the right type there.

The glyph box is pinned to a whole-pixel offset. An 11px box flex-centered in
the tab's 32px content box lands at 10.5px, which smears every 1px
`crispEdges` cell across two device pixels; whole-integer margins that fill
the axis exactly leave no free space for centering to halve. The SVG is also seated at the box's top edge: an inline-level
replaced box rides the text baseline, so without that it renders 2px below
the box the margins just placed. `state-dot.css` carries the same rule for
the same reason. Measured in Chromium at dpr 1 and 2: the glyph lands at
(11, 11) with no overflow; flex centering would put the box at 11.5px, and
without the seating rule the glyph sat at y=13.

`bridge-colheads.png` is recaptured here rather than with the rest of the
baselines, because this is the commit that repaints those pixels. Its clip is
computed from the column-head boxes and runs 1050px wide, so it overlaps the
activity bar; at 855x41 the 0.001 diff-pixel RATIO allows about 35 pixels and
the four glyphs change 99. The full-page captures contain the same changed
pixels but pass, their delta swamped by a budget proportional to mostly
unchanged area — so they are recaptured later, with the rest.

This surface had no tests. It now covers both render arms, the StateDot's
presence and absence, the constructors, and the margin arithmetic the
whole-pixel offset depends on.

Ref: RIG-3738, RIG-3603. Design: docs/designs/ui/compass-glyph-primitives/design.md (DL-367).

Co-authored-by: Matt Wilkinson <matt@rigel.build>
T5a and T5b of the glyph work: every rendered pictographic character in
`LeftSidebar`, `App`, `RightSidebar`, and `AgentView` becomes a `<Glyph>`
bitmap. 18 new semantic names join the four the activity bar already used, for
22 total. Names describe the role, never the character — `pin`/`pin-outline`
for `★`/`☆`, `disclosure` for the caret — so a later art change does not
falsify the name.

The two verdict-mark sites are the reason the record calls this out. `<Glyph>`
is `aria-hidden`, so a bare `✓`/`✗` that WAS the accessible name reads as
nothing once converted. Those wrappers now carry `role="img"` plus the verdict
word; `RightSidebar.prpane.test.tsx` covers all three verdicts and reddens if
either attribute is dropped (measured: 0 pass / 2 fail with the `aria-label`
removed, and again with the `role`).

`FILE_ICON` was a two-entry map of characters, one of which stays text (`·` is
in the brand face). Rather than widen its value type to `GlyphName | string`,
it becomes a `FileIcon` component switching on the closed `FileNode["kind"]`
union — no cast, no mixed map.

The audit table lands in `components.md` rather than the frozen design record,
which the ledger says is never edited past its `Status:` header. It records
every site's decorative-vs-name-bearing decision, and the two keeps (`·`, `−`)
with the reason each is safe: both are present in the Space Mono cmap, verified
by parsing the font rather than by eye.

The eight drifted baselines are recaptured here, not deferred: the commit that
repaints the pixels owns its baseline, so every commit in the stack stays green
and a bisect means something. Verified the drift is the glyph swap and nothing
else — in `bridge.png`, changed pixels fall only in the left sidebar (x 0..255)
and the right activity bar (x 1216..1279), with EXACTLY ZERO in the content area
between them, so no text reflowed.

Verified: 1111 pass / 0 fail (up from 1109); `compass-ui:typecheck`,
`stylelint`, `biome check`, and markdownlint all clean. Reviewed; the a11y of
all ~20 converted sites was audited against the JSX individually.
T5c, the last conversion slice. Four sites move to `<Glyph>` and one swaps a
character; only one new bitmap was needed, because most of these were the same
affordances already converted in T5a/T5b:

- `BacklogView` section caret and `LogPanel`'s minimize toggle both reuse
  `disclosure`. The toggle's two states are one glyph rotated 180deg rather than
  two bitmaps, so the pair cannot drift out of alignment.
- `UsageBar`'s branch mark reuses `vcs`.
- `LogPanel`'s stop mark is the one new glyph, a filled 7x7 square.

`comms.ts` `channelGlyph` is data, not JSX — it returns the marker string that
three render sites draw beside a channel name, alongside `@` and `#`. Returning
a `GlyphName` would force all three to branch glyph-vs-character and put a
bitmap in the same alignment column as two characters, so the fix is a covered
character instead: `⌗` U+2317 (absent from Space Mono) becomes `&` U+0026, the
Discord convention for a group DM and plain ASCII.

The six `→` arrows in `Bridge` and `SettingsView` stay text. U+2192 is in the
Space Mono cmap, so converting them was a style call rather than a coverage fix,
and a bitmap arrow reads no better at this size — two of the six sit mid-
sentence, where it would break the type.

`agent.png` and `backlog.png` are recaptured. `LogPanel` mounts on the agent
surface with the log open by default, so the stop mark and the minimize toggle
both render there, and the backlog chevron renders on the backlog board. A first
recapture pass wrote nothing and I read that as "these sites are not captured" —
wrong. Playwright only rewrites a baseline whose diff exceeds tolerance, and the
0.001 ratio allows 921 px on a 1280x720 shot. Measured against the previous
baselines the deltas are 913 px (agent) and 1387 px (backlog): the backlog shot
was over budget and would have failed CI, and the agent shot sat at 99% of it,
one render shift from a confusing red. Both now depict the new bitmaps.

That leaves the Unifont pin blocked only by the six fixture strings in
`stub-data.ts` (`➜`, `▪▪▪▪`), which belong to T8. Every rendered chrome
character now resolves in the brand face — measured by parsing the cmap, not by
eye.

Verified: 1112 pass / 0 fail; typecheck, stylelint, biome, and markdownlint
clean. The new bitmap is one orthogonally-connected component and its documented
grid matches its cells.
@linear-code

linear-code Bot commented Sep 13, 2026

Copy link
Copy Markdown

RIG-3739

@rigel-mintaka
rigel-mintaka added this pull request to stack #1200 September 13, 2026 22:06
@rigel-mintaka
rigel-mintaka marked this pull request as ready for review September 13, 2026 22:06
@github-actions

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-ux-3739-chrome-glyph-0p38.compass-eng-docs.pages.dev

Deployed from compass-ux/3739-chrome-glyphs-t5c at 0a88c56.

Changed pages:

Base automatically changed from compass-ux/3739-chrome-glyphs to main September 13, 2026 22:43
@trunk-io

trunk-io Bot commented Sep 13, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant