diff --git a/apps/ui/e2e/__screens__/agent.png b/apps/ui/e2e/__screens__/agent.png index 8e1947d3..8b371c16 100644 Binary files a/apps/ui/e2e/__screens__/agent.png and b/apps/ui/e2e/__screens__/agent.png differ diff --git a/apps/ui/e2e/__screens__/backlog.png b/apps/ui/e2e/__screens__/backlog.png index c8280f98..3e9675e8 100644 Binary files a/apps/ui/e2e/__screens__/backlog.png and b/apps/ui/e2e/__screens__/backlog.png differ diff --git a/apps/ui/e2e/__screens__/bridge-empty.png b/apps/ui/e2e/__screens__/bridge-empty.png index c45e3438..0d784156 100644 Binary files a/apps/ui/e2e/__screens__/bridge-empty.png and b/apps/ui/e2e/__screens__/bridge-empty.png differ diff --git a/apps/ui/e2e/__screens__/bridge-prs.png b/apps/ui/e2e/__screens__/bridge-prs.png index 008c19e6..5c8219f3 100644 Binary files a/apps/ui/e2e/__screens__/bridge-prs.png and b/apps/ui/e2e/__screens__/bridge-prs.png differ diff --git a/apps/ui/e2e/__screens__/bridge.png b/apps/ui/e2e/__screens__/bridge.png index a87d7775..576aa910 100644 Binary files a/apps/ui/e2e/__screens__/bridge.png and b/apps/ui/e2e/__screens__/bridge.png differ diff --git a/apps/ui/e2e/__screens__/done.png b/apps/ui/e2e/__screens__/done.png index 6bcf9f99..95914494 100644 Binary files a/apps/ui/e2e/__screens__/done.png and b/apps/ui/e2e/__screens__/done.png differ diff --git a/apps/ui/e2e/__screens__/right-sidebar.png b/apps/ui/e2e/__screens__/right-sidebar.png index 06864655..01ff4f68 100644 Binary files a/apps/ui/e2e/__screens__/right-sidebar.png and b/apps/ui/e2e/__screens__/right-sidebar.png differ diff --git a/apps/ui/e2e/__screens__/settings.png b/apps/ui/e2e/__screens__/settings.png index d233ccfe..9cc90843 100644 Binary files a/apps/ui/e2e/__screens__/settings.png and b/apps/ui/e2e/__screens__/settings.png differ diff --git a/apps/ui/src/App.test.tsx b/apps/ui/src/App.test.tsx index 97348397..5d88e35d 100644 --- a/apps/ui/src/App.test.tsx +++ b/apps/ui/src/App.test.tsx @@ -272,14 +272,14 @@ describe("coaching tooltips (RIG-2530 T2)", () => { ); expect(left).not.toBeNull(); expect(right).not.toBeNull(); - // The visible content is a decorative block glyph; the accessible name - // must come from aria-label, never the glyph. + // The visible content is a decorative SVG (aria-hidden, no text); + // the accessible name must come from aria-label, never the glyph. expect(left?.getAttribute("aria-label")).toBe("Toggle left sidebar"); expect(right?.getAttribute("aria-label")).toBe("Toggle right sidebar"); - expect(left?.textContent?.trim()).not.toBe(""); - expect(left?.getAttribute("aria-label")).not.toBe( - left?.textContent?.trim(), - ); + const glyph = left?.querySelector("svg"); + expect(glyph).not.toBeNull(); + expect(glyph?.getAttribute("aria-hidden")).toBe("true"); + expect(left?.textContent?.trim()).toBe(""); }); test("both sidebar toggles are now live: their coached chords dispatch", async () => { diff --git a/apps/ui/src/App.tsx b/apps/ui/src/App.tsx index 972796f1..6a3a4c14 100644 --- a/apps/ui/src/App.tsx +++ b/apps/ui/src/App.tsx @@ -14,6 +14,7 @@ import { CoachTipContent, CoachTipTrigger, } from "./components/CoachTip"; +import { Glyph } from "./components/Glyph"; import { LeftSidebar } from "./components/LeftSidebar"; import { Palette } from "./components/Palette"; import { RightSidebar } from "./components/RightSidebar"; @@ -77,7 +78,7 @@ const App: Component = (props) => {
Compass ADE @@ -95,7 +96,7 @@ const App: Component = (props) => { aria-keyshortcuts={bridgeAria} > Bridge @@ -146,7 +147,7 @@ const App: Component = (props) => { )} onClick={() => store.toggleLeft()} > - ▐ + = (props) => { )} onClick={() => store.toggleRight()} > - ▌ + = ( aria-label="Split right" onClick={() => splitWith("row")} > - ⊞▏ +
@@ -265,7 +266,7 @@ export const AgentView: Component = () => { title={`Close ${tab.title}`} onClick={() => store.closeTab(tab.id)} > - ✕ + diff --git a/apps/ui/src/components/Glyph.tsx b/apps/ui/src/components/Glyph.tsx index 83ceea4e..60ffefdf 100644 --- a/apps/ui/src/components/Glyph.tsx +++ b/apps/ui/src/components/Glyph.tsx @@ -10,7 +10,29 @@ import { type Component, For } from "solid-js"; * belongs on the consuming control (this is the deliberate difference from * `BadgeGlyph`, whose glyph carries status meaning). */ -export type GlyphName = "status" | "files" | "vcs" | "pr"; +export type GlyphName = + | "status" + | "files" + | "vcs" + | "pr" + | "cross" + | "close" + | "neutral" + | "split-right" + | "split-down" + | "disclosure" + | "disclosure-open" + | "check" + | "role" + | "pin" + | "pin-outline" + | "subscribed" + | "unsubscribed" + | "list" + | "gear" + | "logo" + | "panel-right" + | "panel-left"; /** [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 @@ -205,6 +227,690 @@ const GLYPH_CELLS: Record< [9, 8], [2, 9], ], + cross: [ + [1, 1], + [2, 1], + [8, 1], + [9, 1], + [1, 2], + [2, 2], + [3, 2], + [7, 2], + [8, 2], + [9, 2], + [2, 3], + [3, 3], + [4, 3], + [6, 3], + [7, 3], + [8, 3], + [3, 4], + [4, 4], + [5, 4], + [6, 4], + [7, 4], + [4, 5], + [5, 5], + [6, 5], + [3, 6], + [4, 6], + [5, 6], + [6, 6], + [7, 6], + [2, 7], + [3, 7], + [4, 7], + [6, 7], + [7, 7], + [8, 7], + [1, 8], + [2, 8], + [3, 8], + [7, 8], + [8, 8], + [9, 8], + [1, 9], + [2, 9], + [8, 9], + [9, 9], + ], + close: [ + [1, 0], + [9, 0], + [1, 1], + [2, 1], + [8, 1], + [9, 1], + [2, 2], + [3, 2], + [7, 2], + [8, 2], + [3, 3], + [4, 3], + [6, 3], + [7, 3], + [4, 4], + [5, 4], + [6, 4], + [5, 5], + [4, 6], + [5, 6], + [6, 6], + [3, 7], + [4, 7], + [6, 7], + [7, 7], + [2, 8], + [3, 8], + [7, 8], + [8, 8], + [1, 9], + [2, 9], + [8, 9], + [9, 9], + [1, 10], + [9, 10], + ], + neutral: [ + [4, 3], + [5, 3], + [6, 3], + [4, 4], + [5, 4], + [6, 4], + [4, 5], + [5, 5], + [6, 5], + ], + "split-right": [ + [1, 1], + [2, 1], + [3, 1], + [4, 1], + [5, 1], + [6, 1], + [7, 1], + [8, 1], + [9, 1], + [1, 2], + [5, 2], + [9, 2], + [1, 3], + [5, 3], + [9, 3], + [1, 4], + [5, 4], + [9, 4], + [1, 5], + [5, 5], + [9, 5], + [1, 6], + [5, 6], + [9, 6], + [1, 7], + [5, 7], + [9, 7], + [1, 8], + [5, 8], + [9, 8], + [1, 9], + [2, 9], + [3, 9], + [4, 9], + [5, 9], + [6, 9], + [7, 9], + [8, 9], + [9, 9], + ], + "split-down": [ + [1, 1], + [2, 1], + [3, 1], + [4, 1], + [5, 1], + [6, 1], + [7, 1], + [8, 1], + [9, 1], + [1, 2], + [9, 2], + [1, 3], + [9, 3], + [1, 4], + [9, 4], + [1, 5], + [2, 5], + [3, 5], + [4, 5], + [5, 5], + [6, 5], + [7, 5], + [8, 5], + [9, 5], + [1, 6], + [9, 6], + [1, 7], + [9, 7], + [1, 8], + [9, 8], + [1, 9], + [2, 9], + [3, 9], + [4, 9], + [5, 9], + [6, 9], + [7, 9], + [8, 9], + [9, 9], + ], + disclosure: [ + [3, 1], + [3, 2], + [4, 2], + [3, 3], + [4, 3], + [5, 3], + [3, 4], + [4, 4], + [5, 4], + [6, 4], + [3, 5], + [4, 5], + [5, 5], + [6, 5], + [7, 5], + [3, 6], + [4, 6], + [5, 6], + [6, 6], + [3, 7], + [4, 7], + [5, 7], + [3, 8], + [4, 8], + [3, 9], + ], + "disclosure-open": [ + [1, 2], + [2, 2], + [3, 2], + [4, 2], + [5, 2], + [6, 2], + [7, 2], + [8, 2], + [9, 2], + [2, 3], + [3, 3], + [4, 3], + [5, 3], + [6, 3], + [7, 3], + [8, 3], + [3, 4], + [4, 4], + [5, 4], + [6, 4], + [7, 4], + [4, 5], + [5, 5], + [6, 5], + [5, 6], + ], + check: [ + [9, 2], + [8, 3], + [9, 3], + [7, 4], + [8, 4], + [1, 5], + [2, 5], + [6, 5], + [7, 5], + [2, 6], + [3, 6], + [5, 6], + [6, 6], + [3, 7], + [4, 7], + [5, 7], + [4, 8], + ], + role: [ + [5, 2], + [4, 3], + [5, 3], + [6, 3], + [3, 4], + [4, 4], + [5, 4], + [6, 4], + [7, 4], + [2, 5], + [3, 5], + [4, 5], + [5, 5], + [6, 5], + [7, 5], + [8, 5], + [3, 6], + [4, 6], + [5, 6], + [6, 6], + [7, 6], + [4, 7], + [5, 7], + [6, 7], + [5, 8], + ], + pin: [ + [3, 1], + [4, 1], + [5, 1], + [6, 1], + [7, 1], + [3, 2], + [4, 2], + [5, 2], + [6, 2], + [7, 2], + [3, 3], + [4, 3], + [5, 3], + [6, 3], + [7, 3], + [3, 4], + [4, 4], + [5, 4], + [6, 4], + [7, 4], + [3, 5], + [4, 5], + [5, 5], + [6, 5], + [7, 5], + [5, 6], + [5, 7], + [5, 8], + ], + "pin-outline": [ + [3, 1], + [4, 1], + [5, 1], + [6, 1], + [7, 1], + [3, 2], + [7, 2], + [3, 3], + [7, 3], + [3, 4], + [7, 4], + [3, 5], + [4, 5], + [5, 5], + [6, 5], + [7, 5], + [5, 6], + [5, 7], + [5, 8], + ], + subscribed: [ + [4, 1], + [5, 1], + [6, 1], + [3, 2], + [4, 2], + [5, 2], + [6, 2], + [7, 2], + [2, 3], + [3, 3], + [4, 3], + [5, 3], + [6, 3], + [7, 3], + [8, 3], + [2, 4], + [3, 4], + [4, 4], + [5, 4], + [6, 4], + [7, 4], + [8, 4], + [2, 5], + [3, 5], + [4, 5], + [5, 5], + [6, 5], + [7, 5], + [8, 5], + [2, 6], + [3, 6], + [4, 6], + [5, 6], + [6, 6], + [7, 6], + [8, 6], + [2, 7], + [3, 7], + [4, 7], + [5, 7], + [6, 7], + [7, 7], + [8, 7], + [3, 8], + [4, 8], + [5, 8], + [6, 8], + [7, 8], + [4, 9], + [5, 9], + [6, 9], + ], + unsubscribed: [ + [4, 1], + [5, 1], + [6, 1], + [3, 2], + [4, 2], + [5, 2], + [6, 2], + [7, 2], + [2, 3], + [3, 3], + [4, 3], + [5, 3], + [6, 3], + [7, 3], + [8, 3], + [2, 4], + [3, 4], + [7, 4], + [8, 4], + [2, 5], + [3, 5], + [7, 5], + [8, 5], + [2, 6], + [3, 6], + [7, 6], + [8, 6], + [2, 7], + [3, 7], + [4, 7], + [5, 7], + [6, 7], + [7, 7], + [8, 7], + [3, 8], + [4, 8], + [5, 8], + [6, 8], + [7, 8], + [4, 9], + [5, 9], + [6, 9], + ], + list: [ + [1, 1], + [2, 1], + [3, 1], + [4, 1], + [5, 1], + [6, 1], + [7, 1], + [8, 1], + [9, 1], + [1, 2], + [9, 2], + [1, 3], + [2, 3], + [3, 3], + [4, 3], + [5, 3], + [6, 3], + [7, 3], + [8, 3], + [9, 3], + [1, 4], + [9, 4], + [1, 5], + [2, 5], + [3, 5], + [4, 5], + [5, 5], + [6, 5], + [7, 5], + [8, 5], + [9, 5], + [1, 6], + [9, 6], + [1, 7], + [2, 7], + [3, 7], + [4, 7], + [5, 7], + [6, 7], + [7, 7], + [8, 7], + [9, 7], + [1, 8], + [9, 8], + [1, 9], + [2, 9], + [3, 9], + [4, 9], + [5, 9], + [6, 9], + [7, 9], + [8, 9], + [9, 9], + ], + gear: [ + [4, 1], + [5, 1], + [6, 1], + [4, 2], + [5, 2], + [6, 2], + [1, 3], + [2, 3], + [3, 3], + [4, 3], + [5, 3], + [6, 3], + [7, 3], + [8, 3], + [9, 3], + [1, 4], + [2, 4], + [3, 4], + [7, 4], + [8, 4], + [9, 4], + [1, 5], + [2, 5], + [3, 5], + [7, 5], + [8, 5], + [9, 5], + [1, 6], + [2, 6], + [3, 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], + [4, 8], + [5, 8], + [6, 8], + [4, 9], + [5, 9], + [6, 9], + ], + logo: [ + [5, 1], + [4, 2], + [5, 2], + [6, 2], + [3, 3], + [4, 3], + [6, 3], + [7, 3], + [2, 4], + [3, 4], + [7, 4], + [8, 4], + [1, 5], + [2, 5], + [8, 5], + [9, 5], + [2, 6], + [3, 6], + [7, 6], + [8, 6], + [3, 7], + [4, 7], + [6, 7], + [7, 7], + [4, 8], + [5, 8], + [6, 8], + [5, 9], + ], + "panel-right": [ + [1, 1], + [2, 1], + [3, 1], + [4, 1], + [5, 1], + [6, 1], + [7, 1], + [8, 1], + [9, 1], + [1, 2], + [5, 2], + [6, 2], + [7, 2], + [8, 2], + [9, 2], + [1, 3], + [5, 3], + [6, 3], + [7, 3], + [8, 3], + [9, 3], + [1, 4], + [5, 4], + [6, 4], + [7, 4], + [8, 4], + [9, 4], + [1, 5], + [5, 5], + [6, 5], + [7, 5], + [8, 5], + [9, 5], + [1, 6], + [5, 6], + [6, 6], + [7, 6], + [8, 6], + [9, 6], + [1, 7], + [5, 7], + [6, 7], + [7, 7], + [8, 7], + [9, 7], + [1, 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], + ], + "panel-left": [ + [1, 1], + [2, 1], + [3, 1], + [4, 1], + [5, 1], + [6, 1], + [7, 1], + [8, 1], + [9, 1], + [1, 2], + [2, 2], + [3, 2], + [4, 2], + [5, 2], + [9, 2], + [1, 3], + [2, 3], + [3, 3], + [4, 3], + [5, 3], + [9, 3], + [1, 4], + [2, 4], + [3, 4], + [4, 4], + [5, 4], + [9, 4], + [1, 5], + [2, 5], + [3, 5], + [4, 5], + [5, 5], + [9, 5], + [1, 6], + [2, 6], + [3, 6], + [4, 6], + [5, 6], + [9, 6], + [1, 7], + [2, 7], + [3, 7], + [4, 7], + [5, 7], + [9, 7], + [1, 8], + [2, 8], + [3, 8], + [4, 8], + [5, 8], + [9, 8], + [1, 9], + [2, 9], + [3, 9], + [4, 9], + [5, 9], + [6, 9], + [7, 9], + [8, 9], + [9, 9], + ], }; /** Every glyph name, derived from the table itself so callers that enumerate diff --git a/apps/ui/src/components/LeftSidebar.tsx b/apps/ui/src/components/LeftSidebar.tsx index 7b8dc48e..d360a697 100644 --- a/apps/ui/src/components/LeftSidebar.tsx +++ b/apps/ui/src/components/LeftSidebar.tsx @@ -18,6 +18,7 @@ import { detectPlatform } from "../keyboard/dispatch"; import { shortcutForAria } from "../keyboard/keymap"; import { type Agent, type AgentTreeNode, agentTree } from "../stub-data"; import { CoachTip, CoachTipContent, CoachTipTrigger } from "./CoachTip"; +import { Glyph } from "./Glyph"; import { RuntimeMarker } from "./RuntimeMarker"; import { StateDot } from "./StateDot"; @@ -51,7 +52,7 @@ const AgentLeaf: Component<{ agent: Agent; badge?: number }> = (props) => { {a().account.handle} - ◆ + @@ -83,7 +84,7 @@ const AgentLeaf: Component<{ agent: Agent; badge?: number }> = (props) => { : store.pinAgent(a().account.id) } > - {pinned() ? "★" : "☆"} + {pinned() ? : } ); @@ -107,7 +108,9 @@ const Branch: Component<{ node: AgentTreeNode }> = (props) => { aria-label={`${collapsed() ? "Expand" : "Collapse"} ${props.node.agent.account.handle}'s agents`} onClick={() => store.toggleAgent(agentId())} > - + + + = (props) => { title="Always subscribed — this subscription is implicit and can't be turned off." aria-label="Always subscribed" > - ◉ + } > @@ -232,7 +235,11 @@ const ChannelRow: Component<{ channel: Channel }> = (props) => { } aria-pressed={subscribed() ? "true" : "false"} > - {subscribed() ? "◉" : "○"} + {subscribed() ? ( + + ) : ( + + )} @@ -276,7 +283,9 @@ const BrowseChannels: Component<{ channels: Channel[] }> = (props) => { onClick={() => setOpen((o) => !o)} aria-expanded={open() ? "true" : "false"} > - + + + browse channels {props.channels.length} @@ -330,7 +339,9 @@ const ChannelsSection: Component = () => { onClick={() => store.toggleSection("channels")} aria-expanded={!collapsed() ? "true" : "false"} > - + + + Channels @@ -387,7 +398,9 @@ const AgentsSection: Component = () => { onClick={() => store.toggleSection("agents")} aria-expanded={!collapsed() ? "true" : "false"} > - + + + Agent workspaces @@ -449,7 +462,7 @@ export const LeftSidebar: Component = () => { aria-keyshortcuts={ariaChord("view.bridge")} > Bridge {inFlightCount()} @@ -465,7 +478,7 @@ export const LeftSidebar: Component = () => { aria-keyshortcuts={ariaChord("view.backlog")} > Backlog {backlogCount()} @@ -484,7 +497,7 @@ export const LeftSidebar: Component = () => { aria-keyshortcuts={ariaChord("view.done")} > Done @@ -499,7 +512,7 @@ export const LeftSidebar: Component = () => { aria-keyshortcuts={ariaChord("view.settings")} > Settings diff --git a/apps/ui/src/components/RightSidebar.prpane.test.tsx b/apps/ui/src/components/RightSidebar.prpane.test.tsx new file mode 100644 index 00000000..b67238d0 --- /dev/null +++ b/apps/ui/src/components/RightSidebar.prpane.test.tsx @@ -0,0 +1,93 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { render } from "@solidjs/testing-library"; +import { flush } from "solid-js"; +import { STUB_COMMS_STATE } from "../comms-stub"; +import { StoreContext } from "../context"; +import { type AppStore, createAppStore } from "../store"; +import { testQueryClient } from "../test-support"; +import { RightSidebar } from "./RightSidebar"; + +// Accessible-name regression for the PR-review verdict marks (RIG-3603 F1). +// The verdict mark WAS the only accessible name; once the glyph work replaced +// the bare ✓/✗/• with an `aria-hidden` ``, the name had to move to the +// wrapper `.rv` span (role="img" + aria-label). No test guarded that, so a +// future edit dropping either attribute would silently mute the verdict for +// screen-reader users. This drives the real exported RightSidebar the way a +// user reaches the pane — select an issue with bot reviews, activate the PR +// tab — since PrPane is module-private. +function mountRightSidebar(): { store: AppStore; container: HTMLElement } { + let store!: AppStore; + const { container } = render(() => { + store = createAppStore({ + initialComms: STUB_COMMS_STATE, + queryClient: testQueryClient(), + }); + return ( + + + + ); + }); + return { store, container }; +} + +// Open the PR pane over a given fixture issue, then return its rendered verdict +// marks keyed by chip word (`.rv` carries the word on BOTH `data-v` and its +// aria-label, so `data-v` is a name-independent handle to the mark). +function verdictMarks( + store: AppStore, + container: HTMLElement, + issueId: string, +): Map { + store.selectIssue(issueId); + store.setActiveRightTab("pr"); + flush(); + const marks = new Map(); + for (const el of container.querySelectorAll( + ".pr-reviews .review-chip .rv", + )) { + const chip = el.getAttribute("data-v"); + if (chip) marks.set(chip, el); + } + return marks; +} + +// The observable contract a screen reader consumes: the `.rv` wrapper names the +// verdict (role="img" + aria-label = the chip word), and the inner glyph is +// hidden so the name is not doubled. +function assertNamedMark(mark: HTMLElement, expectedWord: string): void { + expect(mark.getAttribute("role")).toBe("img"); + expect(mark.getAttribute("aria-label")).toBe(expectedWord); + const svg = mark.querySelector("svg"); + expect(svg).not.toBeNull(); + expect(svg?.getAttribute("aria-hidden")).toBe("true"); +} + +describe("RightSidebar PR pane — verdict mark accessible name", () => { + // selectIssue/pin state write through to the process-wide happy-dom + // localStorage; clear it around every case (the sibling suite's discipline). + beforeEach(() => globalThis.localStorage.clear()); + afterEach(() => globalThis.localStorage.clear()); + + // ws-1022 (RIG-1022 / PR #453) carries bot reviews greptile→approved, + // cubic→approved, CodeRabbit→commented — the latest-per-author collapse + // leaves both `approved` and `commented` chips on the pane. + test("approved and commented marks are named for a screen reader", () => { + const { store, container } = mountRightSidebar(); + const marks = verdictMarks(store, container, "ws-1022"); + expect(marks.has("approved")).toBe(true); + expect(marks.has("commented")).toBe(true); + assertNamedMark(marks.get("approved") as HTMLElement, "approved"); + assertNamedMark(marks.get("commented") as HTMLElement, "commented"); + }); + + // ws-1023 (RIG-1023 / PR #443) carries greptile→changes_requested and + // CodeRabbit→commented, so it is the reachable source of the `changes` chip + // (VERDICT_CHIP maps "changes_requested" → "changes"). + test("the changes mark is named for a screen reader", () => { + const { store, container } = mountRightSidebar(); + const marks = verdictMarks(store, container, "ws-1023"); + expect(marks.has("changes")).toBe(true); + assertNamedMark(marks.get("changes") as HTMLElement, "changes"); + }); +}); diff --git a/apps/ui/src/components/RightSidebar.tsx b/apps/ui/src/components/RightSidebar.tsx index 198c1075..6c3fb2a5 100644 --- a/apps/ui/src/components/RightSidebar.tsx +++ b/apps/ui/src/components/RightSidebar.tsx @@ -27,11 +27,21 @@ import { STUB_FILES, } from "../stub-data"; import { ChannelView } from "./ChannelView"; -import { Glyph } from "./Glyph"; +import { Glyph, type GlyphName } from "./Glyph"; import { RuntimeMarker } from "./RuntimeMarker"; import { StateDot } from "./StateDot"; -const FILE_ICON: Record = { dir: "▸", file: "·" }; +/** The explorer row icon. A dir gets the `disclosure` glyph; a file keeps `·`, + * which Space Mono covers (record D3), so the two-value `kind` splits cleanly + * without a mixed-type icon map. */ +const FileIcon: Component<{ kind: FileNode["kind"] }> = (props) => ( + + + + + · + +); const STATUS_MARK: Record = { modified: "M", added: "A", @@ -46,7 +56,9 @@ const FileRow: Component<{ node: FileNode; depth: number }> = (props) => ( class="file-row" style={{ "padding-left": `${props.depth * 12 + 6}px` }} > - {FILE_ICON[props.node.kind]} + + + {props.node.name} {(s) => ( @@ -220,6 +232,18 @@ const VERDICT_CHIP: Record = commented: "commented", }; +/** Each verdict's chrome glyph. The mark WAS the only name (record: bare ✓/✗ + * read as nothing once `aria-hidden`), so the `.rv` span carries the verdict + * word as its `aria-label`. */ +const VERDICT_GLYPH: Record< + PullRequest["reviews"][number]["verdict"], + GlyphName +> = { + approved: "check", + changes_requested: "cross", + commented: "neutral", +}; + /** The PR pane body: state badge, checks, bot reviews, thread progress. */ const PrPane: Component<{ pr: PullRequest }> = (props) => { const total = () => props.pr.threads.length; @@ -249,12 +273,13 @@ const PrPane: Component<{ pr: PullRequest }> = (props) => { {(r) => ( {r.author} - - {r.verdict === "approved" - ? "✓" - : r.verdict === "changes_requested" - ? "✗" - : "•"} + + )} @@ -325,7 +350,7 @@ const RepoBranchDropdown: Component = () => { fallback={
{repo().name}
@@ -343,11 +368,11 @@ const RepoBranchDropdown: Component = () => { }} > {repo().name} @@ -386,11 +411,11 @@ const RepoBranchDropdown: Component = () => { }} > {repo().currentBranch} 1}> diff --git a/apps/ui/src/design/components.md b/apps/ui/src/design/components.md index 8e2eb1dd..bf1828d6 100644 --- a/apps/ui/src/design/components.md +++ b/apps/ui/src/design/components.md @@ -433,6 +433,350 @@ diverging to a top-right node: ........... ``` +### T5b glyph grids (RightSidebar + AgentView, 11×11) + +`cross` — a bold ballot X (replaces `✗`, the changes-requested verdict mark): + +```text +........... +.##.....##. +.###...###. +..###.###.. +...#####... +....###.... +...#####... +..###.###.. +.###...###. +.##.....##. +........... +``` + +`close` — a thin X close mark (replaces `✕`, the pane/tab close affordance): + +```text +.#.......#. +.##.....##. +..##...##.. +...##.##... +....###.... +.....#..... +....###.... +...##.##... +..##...##.. +.##.....##. +.#.......#. +``` + +`neutral` — a centered 3×3 dot (replaces `•`, the commented verdict mark): + +```text +........... +........... +........... +....###.... +....###.... +....###.... +........... +........... +........... +........... +........... +``` + +`split-right` — a box divided by a vertical rule (replaces `⊞▏`, split-right): + +```text +........... +.#########. +.#...#...#. +.#...#...#. +.#...#...#. +.#...#...#. +.#...#...#. +.#...#...#. +.#...#...#. +.#########. +........... +``` + +`split-down` — a box divided by a horizontal rule (replaces `⊞▁`, split-down): + +```text +........... +.#########. +.#.......#. +.#.......#. +.#.......#. +.#########. +.#.......#. +.#.......#. +.#.......#. +.#########. +........... +``` + +### T5a glyph grids (LeftSidebar + App, 11×11) + +`disclosure` — a right-pointing triangle (replaces `▸`, the closed disclosure +caret; CSS rotates it 90° open): + +```text +........... +...#....... +...##...... +...###..... +...####.... +...#####... +...####.... +...###..... +...##...... +...#....... +........... +``` + +`disclosure-open` — a down-pointing triangle (replaces `▼`, the expanded +disclosure caret): + +```text +........... +........... +.#########. +..#######.. +...#####... +....###.... +.....#..... +........... +........... +........... +........... +``` + +`check` — a check tick (replaces `✓`, the Done view mark): + +```text +........... +........... +.........#. +........##. +.......##.. +.##...##... +..##.##.... +...###..... +....#...... +........... +........... +``` + +`role` — a filled diamond (replaces `◆`, the non-worker role pip): + +```text +........... +........... +.....#..... +....###.... +...#####... +..#######.. +...#####... +....###.... +.....#..... +........... +........... +``` + +`pin` — a filled push-pin (replaces `★`, the pinned agent state): + +```text +........... +...#####... +...#####... +...#####... +...#####... +...#####... +.....#..... +.....#..... +.....#..... +........... +........... +``` + +`pin-outline` — a hollow push-pin (replaces `☆`, the unpinned agent state): + +```text +........... +...#####... +...#...#... +...#...#... +...#...#... +...#####... +.....#..... +.....#..... +.....#..... +........... +........... +``` + +`subscribed` — a filled disc (replaces `◉`, subscribed / always-subscribed): + +```text +........... +....###.... +...#####... +..#######.. +..#######.. +..#######.. +..#######.. +..#######.. +...#####... +....###.... +........... +``` + +`unsubscribed` — a hollow ring (replaces `○`, joined-not-subscribed): + +```text +........... +....###.... +...#####... +..#######.. +..##...##.. +..##...##.. +..##...##.. +..#######.. +...#####... +....###.... +........... +``` + +`list` — a stacked-rows list (replaces `▤`, the Backlog view): + +```text +........... +.#########. +.#.......#. +.#########. +.#.......#. +.#########. +.#.......#. +.#########. +.#.......#. +.#########. +........... +``` + +`gear` — a settings cog (replaces `⚙`, the Settings view): + +```text +........... +....###.... +....###.... +.#########. +.###...###. +.###...###. +.###...###. +.#########. +....###.... +....###.... +........... +``` + +`logo` — the Compass diamond mark (replaces `◇`, the brand logo): + +```text +........... +.....#..... +....###.... +...##.##... +..##...##.. +.##.....##. +..##...##.. +...##.##... +....###.... +.....#..... +........... +``` + +`panel-left` — a pane frame with the left region filled (replaces `▌`, the +toggle-left-sidebar control): + +```text +........... +.#########. +.#####...#. +.#####...#. +.#####...#. +.#####...#. +.#####...#. +.#####...#. +.#####...#. +.#########. +........... +``` + +`panel-right` — a pane frame with the right region filled (replaces `▐`, the +toggle-right-sidebar control): + +```text +........... +.#########. +.#...#####. +.#...#####. +.#...#####. +.#...#####. +.#...#####. +.#...#####. +.#...#####. +.#########. +........... +``` + +### Chrome conversion audit (T5a + T5b) + +Every chrome site converted from a character to a ``, with the +accessibility decision each one forced. `` is always `aria-hidden`, so a +*name-bearing* site is one where the character WAS the accessible name and the +name had to be moved onto a wrapper or control; a *decorative* site already had +one from adjacent text or an `aria-label`. + +All sites below are **decorative** — the control or its neighbouring text +already carries the name — except the two marked *name-bearing*, where the +character WAS the name and it moved onto the wrapper. + +| Site | Was | Glyph | +| --- | --- | --- | +| `App` brand mark | `◇` | `logo` | +| `App` view tab | `▦` | `status` | +| `App` sidebar toggles | `▌` `▐` | `panel-left` `panel-right` | +| `LeftSidebar` role pip | `◆` | `role` | +| `LeftSidebar` pin toggle | `★` `☆` | `pin` `pin-outline` | +| `LeftSidebar` folder caret | `▼` | `disclosure-open` | +| `LeftSidebar` browse/ws carets | `▸` | `disclosure` | +| `LeftSidebar` subscribe toggle | `◉` `○` | `subscribed` `unsubscribed` | +| `LeftSidebar` views | `▦` `▤` `✓` `⚙` | `status` `list` `check` `gear` | +| `RightSidebar` file row | `▸` | `disclosure` | +| `RightSidebar` repo/branch | `🗀` `⎇` | `files` `vcs` | +| `RightSidebar` dropdown carets | `▾` | `disclosure-open` | +| `AgentView` pane split | `⊞▏` `⊞▁` | `split-right` `split-down` | +| `AgentView` pane/tab close | `✕` | `close` | + +Name-bearing — the glyph is hidden, so the wrapper carries `role="img"` plus an +`aria-label`: + +| Site | Was | Glyph | Announces | +| --- | --- | --- | --- | +| `LeftSidebar` always-subscribed | `◉` | `subscribed` | `Always subscribed` | +| `RightSidebar` verdicts | `✓` `✗` `•` | `check` `cross` `neutral` | verdict | + +Kept as text, covered by the brand face — not conversions: + +| Site | Char | Why | +| --- | --- | --- | +| `RightSidebar` file row | `·` U+00B7 | covered; a glyph would misalign | +| diff stats (3 sites) | `−` U+2212 | in the cmap; pairs with ASCII `+` | + +The two name-bearing rows are the hazard this table exists to catch: a bare +`✓`/`✗` verdict mark reads as nothing once the glyph is hidden, so those sites +carry the verdict word on the wrapper. `RightSidebar.prpane.test.tsx` pins that. + ## Tabs - **Class:** `.cx-tabs` · `data-orientation="h | v"`, with `.cx-tab` items