Skip to content
Open
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
Binary file modified apps/ui/e2e/__screens__/agent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/backlog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/bridge-colheads.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/bridge-empty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/bridge-prs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/bridge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/done.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/right-sidebar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified apps/ui/e2e/__screens__/settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions apps/ui/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Glyph> 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 () => {
Expand Down
9 changes: 5 additions & 4 deletions apps/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -77,7 +78,7 @@ const App: Component<RouteSectionProps> = (props) => {
<header class="topbar">
<div class="brand">
<span class="logo" aria-hidden="true">
<Glyph name="logo" />
</span>
<span class="title">Compass</span>
<span class="subtitle">ADE</span>
Expand All @@ -95,7 +96,7 @@ const App: Component<RouteSectionProps> = (props) => {
aria-keyshortcuts={bridgeAria}
>
<span class="tab-glyph" aria-hidden="true">
<Glyph name="status" />
</span>
Bridge
</CoachTipTrigger>
Expand Down Expand Up @@ -146,7 +147,7 @@ const App: Component<RouteSectionProps> = (props) => {
)}
onClick={() => store.toggleLeft()}
>
<Glyph name="panel-left" />
</CoachTipTrigger>
<CoachTipContent
label="Toggle left sidebar"
Expand All @@ -165,7 +166,7 @@ const App: Component<RouteSectionProps> = (props) => {
)}
onClick={() => store.toggleRight()}
>
<Glyph name="panel-right" />
</CoachTipTrigger>
<CoachTipContent
label="Toggle right sidebar"
Expand Down
77 changes: 74 additions & 3 deletions apps/ui/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@
.folder-caret {
color: var(--cx-text-faint);
font-size: 9px;
width: 10px;
width: 11px;
display: inline-flex;
justify-content: center;
/* stylelint-disable-next-line declaration-property-value-disallowed-list -- TODO(motion migration, D9/foundation-T8): raw 0.12s grandfathered by the RIG-2034 cutover lane, which never scoped the motion axis */
Expand Down Expand Up @@ -1586,6 +1586,15 @@
font-family: var(--cx-font-ui);
}

/* Seat the inline branch glyph (state-dot.css §seating). */
.usage-git-glyph {
display: inline-flex;
}

.usage-git-glyph > svg {
display: block;
}

/* ── Shared helpers ─────────────────────────────────────────────────────── */

.muted {
Expand Down Expand Up @@ -1640,11 +1649,17 @@

.backlog-chevron {
color: var(--cx-text-faint);
font-size: 10px;
display: inline-flex;
/* stylelint-disable-next-line declaration-property-value-disallowed-list -- TODO(motion migration, D9/foundation-T8): raw 0.12s/ease grandfathered by the RIG-2034 cutover lane, which never scoped the motion axis */
transition: transform 0.12s ease;
}

/* Seat the inline glyph SVG (state-dot.css §seating): an inline replaced box
* otherwise rides the text baseline and overflows its box. */
.backlog-chevron > svg {
display: block;
}

.backlog-chevron.open {
transform: rotate(90deg);
}
Expand Down Expand Up @@ -2261,7 +2276,32 @@
opacity: 0.55;
}

.r-tab .r-tab-icon {
/* The glyph arm is an 11px 1-bit SVG box; the avatar arm is a 15px mono
* letter. They split so the glyph box carries NO font metrics (the SVG is the
* content) and the letter keeps its type. */
.r-tab .r-tab-icon[data-kind="glyph"] {
display: block;
width: 11px;
height: 11px;
/* D2: an odd 11px box centered in the tab's 32px content box (34px −
* 2×1px border, box-sizing: border-box) lands at (32 − 11) / 2 = 10.5px —
* a half pixel that smears every 1px crispEdges cell across two device
* pixels. Whole-integer margins that fill the content box exactly
* (10 + 11 + 11 = 32 per axis) leave zero free space for centering to
* split, so the box's offset is its whole-pixel margin (10px). */
margin: 10px 11px 11px 10px;
}

/* Seat the SVG at the box's top edge. An inline-level replaced box rides the
* parent's text baseline, so the inherited line-height pushes the glyph 2px
* down and out of its box — the whole-pixel margin above would then describe
* the span, not the pixels. state-dot.css carries the same rule for the same
* reason. */
.r-tab .r-tab-icon[data-kind="glyph"] > svg {
display: block;
}

.r-tab .r-tab-icon[data-kind="avatar"] {
font-size: 15px;
line-height: 1;
}
Expand Down Expand Up @@ -3460,6 +3500,17 @@
cursor: default;
}

/* The Stop control's halt-mark glyph, seated inline beside the "stop" label. */
.obs-stop-glyph {
display: inline-flex;
vertical-align: middle;
color: currentColor;
}

.obs-stop-glyph > svg {
display: block;
}

/* A REFUSED stop, said beside the control rather than left in the console —
* same weight and colour as the ask block's refusal (.ask-error). */
.obs-error {
Expand All @@ -3482,6 +3533,26 @@
color: var(--cx-text);
}

/* The minimize/expand toggle's caret glyph. `disclosure` points right; the open
* panel keeps that (the old `⟩`), and the minimized rail rotates it 180° to
* point left (the old `⟨`, the expand affordance). Both states are one glyph so
* the pair can never misalign the way the old character pair could. */
.obs-min-glyph {
display: inline-flex;
color: currentColor;
transform: rotate(180deg);
/* stylelint-disable-next-line declaration-property-value-disallowed-list -- TODO(motion migration, D9/foundation-T8): raw 0.12s/ease grandfathered by the RIG-2034 cutover lane, which never scoped the motion axis */
transition: transform 0.12s ease;
}

.obs-min-glyph.open {
transform: rotate(0deg);
}

.obs-min-glyph > svg {
display: block;
}

.log-panel .obs-body {
flex: 1;
min-height: 0;
Expand Down
8 changes: 5 additions & 3 deletions apps/ui/src/comms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@ export function isDm(channel: Channel): boolean {
}

/** The glyph before a channel name, by kind (Discord-style: # for a channel,
* @ for a DM, a cluster glyph for a group DM). One home so the rail row and
* the channel header never drift. */
* @ for a DM, & for a group DM — the "cluster" marker). One home so the rail
* row and the channel header never drift. All three are ASCII, covered by the
* brand face; this stays DATA (a string in the marker column), not a `<Glyph>`
* — a bitmap for one kind only would mix glyph and character in one column. */
export function channelGlyph(kind: Channel["kind"]): string {
switch (kind) {
case "dm":
return "@";
case "group_dm":
return "";
return "&";
default:
return "#";
}
Expand Down
9 changes: 5 additions & 4 deletions apps/ui/src/components/AgentView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "../store";
import type { Agent, Terminal } from "../stub-data";
import { ChannelView } from "./ChannelView";
import { Glyph } from "./Glyph";
import { LogPanel } from "./LogPanel";
import { RuntimeMarker } from "./RuntimeMarker";
import { StateDot } from "./StateDot";
Expand Down Expand Up @@ -86,7 +87,7 @@ const PaneView: Component<{ pane: Pane; agent: Agent; focused: boolean }> = (
aria-label="Split right"
onClick={() => splitWith("row")}
>
⊞▏
<Glyph name="split-right" />
</button>
<button
type="button"
Expand All @@ -95,7 +96,7 @@ const PaneView: Component<{ pane: Pane; agent: Agent; focused: boolean }> = (
aria-label="Split down"
onClick={() => splitWith("column")}
>
⊞▁
<Glyph name="split-down" />
</button>
<Show when={props.pane.kind !== "chat"}>
<button
Expand All @@ -105,7 +106,7 @@ const PaneView: Component<{ pane: Pane; agent: Agent; focused: boolean }> = (
aria-label="Close pane"
onClick={() => store.closePane(props.pane.id)}
>
<Glyph name="close" />
</button>
</Show>
</div>
Expand Down Expand Up @@ -265,7 +266,7 @@ export const AgentView: Component = () => {
title={`Close ${tab.title}`}
onClick={() => store.closeTab(tab.id)}
>
<Glyph name="close" />
</button>
</Show>
</div>
Expand Down
5 changes: 4 additions & 1 deletion apps/ui/src/components/BacklogView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type Component, createSignal, For, Show } from "solid-js";
import { isMultiForge, issueKey } from "../board-render";
import { useStore } from "../context";
import type { Issue } from "../stub-data";
import { Glyph } from "./Glyph";

/** A single Linear-style issue row: id · title · priority · state · tracker.
* Clicking the row selects the issue (staying on the view). Not the board
Expand Down Expand Up @@ -63,7 +64,9 @@ const BacklogSection: Component<{
aria-controls={contentId}
onClick={() => setOpen(!open())}
>
<span class={["backlog-chevron", { open: open() }]}>▸</span>
<span class={["backlog-chevron", { open: open() }]} aria-hidden="true">
<Glyph name="disclosure" />
</span>
<span class="backlog-section-title">{props.title}</span>
<span class="backlog-count">{props.rows.length}</span>
</button>
Expand Down
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);
}
});
}
});
Loading
Loading