FN-6531: polish Compound Engineering dashboard UI

Align the bundled Compound Engineering plugin with Fusion dashboard spacing, radius, and control conventions.

- Apply shared dashboard card, button, icon button, and input classes to CE panels and controls.
- Replace ad-hoc CE layout spacing and radius values with dashboard design tokens across desktop and mobile surfaces.
- Document the plugin's theme consistency expectations and add tests that guard representative spacing, radius, and textarea token usage.
- Add a patch changeset for the published Fusion package.

Files changed:
 .changeset/fn-6531-compound-engineering-ui.md      |   5 +
 docs/plugins/compound-engineering.md               |   5 +
 .../src/dashboard/CeFlow.tsx                       |  10 +-
 .../src/dashboard/CompoundEngineeringView.css      | 203 ++++++++++++++-------
 .../src/dashboard/CompoundEngineeringView.tsx      |  11 +-
 .../src/dashboard/__tests__/theme-tokens.test.ts   |  46 +++++
 6 files changed, 215 insertions(+), 65 deletions(-)

Fusion-Task-Id: FN-6531

Fusion-Task-Lineage: be99bbaa-0842-4598-b0a1-4f41816b032b
This commit is contained in:
gsxdsm
2026-06-17 03:36:14 -07:00
parent 4bfb6b46ad
commit 89171e0f16
6 changed files with 215 additions and 65 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Polish the bundled Compound Engineering dashboard view so its spacing, radii, and controls align with Fusion dashboard design tokens and shared component classes.

View File

@@ -25,6 +25,11 @@ needed. If the plugin is uninstalled, the workflow is hidden again.
The Compound Engineering view is registered as a primary plugin destination The Compound Engineering view is registered as a primary plugin destination
(`viewId: "compound-engineering"`). (`viewId: "compound-engineering"`).
It follows dashboard UI conventions: the view's panels, controls, responsive
layout, spacing, and radii use the shared `--space-*` / `--radius-*` design
tokens and shared button/card/input classes so the plugin remains visually
consistent across light, dark, desktop, and mobile surfaces.
It provides: It provides:
- An **artifact hub** that discovers CE artifacts from conventional locations - An **artifact hub** that discovers CE artifacts from conventional locations
(`STRATEGY.md`, `docs/ideation/`, `docs/brainstorms/`, plan docs, `docs/work/`, (`STRATEGY.md`, `docs/ideation/`, `docs/brainstorms/`, plan docs, `docs/work/`,

View File

@@ -42,6 +42,10 @@ export interface CeFlowProps {
// ── Transcript parsing ─────────────────────────────────────────────────────── // ── Transcript parsing ───────────────────────────────────────────────────────
/**
* FNXC:CompoundEngineeringUI 2026-06-17-00:52:
* CE flow markup keeps its rendered text and test ids stable while adding shared .input/.btn/.btn-icon hooks so dashboard theme spacing and focus conventions style interactive controls consistently.
*/
const BOTTOM_FOLLOW_THRESHOLD_PX = 50; const BOTTOM_FOLLOW_THRESHOLD_PX = 50;
type DisplayItem = type DisplayItem =
@@ -284,6 +288,7 @@ function RichQuestion({
}} }}
> >
<textarea <textarea
className="input"
data-testid="ce-flow-text-input" data-testid="ce-flow-text-input"
aria-label={question.question} aria-label={question.question}
value={text} value={text}
@@ -355,6 +360,7 @@ function RichQuestion({
<li key={opt.id}> <li key={opt.id}>
<label className="ce-flow-checkbox"> <label className="ce-flow-checkbox">
<input <input
className="ce-flow-checkbox-input"
type="checkbox" type="checkbox"
data-option={opt.id} data-option={opt.id}
checked={checked} checked={checked}
@@ -418,6 +424,7 @@ function DegradedQuestion({
}} }}
> >
<textarea <textarea
className="input"
data-testid="ce-flow-degraded-input" data-testid="ce-flow-degraded-input"
aria-label={question.question} aria-label={question.question}
value={text} value={text}
@@ -483,6 +490,7 @@ function QuestionPanel({
<div className="ce-flow-guidance-row"> <div className="ce-flow-guidance-row">
<textarea <textarea
id="ce-flow-guidance-input" id="ce-flow-guidance-input"
className="input"
data-testid="ce-flow-guidance-input" data-testid="ce-flow-guidance-input"
value={guidance} value={guidance}
disabled={disabled} disabled={disabled}
@@ -542,7 +550,7 @@ export function CeFlow(props: CeFlowProps) {
{onCancel && cancellable ? ( {onCancel && cancellable ? (
<button <button
type="button" type="button"
className="btn-icon ce-flow-cancel" className="btn btn-icon ce-flow-cancel"
data-testid="ce-flow-cancel" data-testid="ce-flow-cancel"
onClick={onCancel} onClick={onCancel}
disabled={Boolean(busy)} disabled={Boolean(busy)}

View File

@@ -1,14 +1,19 @@
/*
FNXC:CompoundEngineeringUI 2026-06-17-00:44:
The Compound Engineering dashboard view must follow the dashboard design scale so plugin spacing, radii, and controls read as native Fusion UI in dark, light, desktop, and mobile contexts. Use existing --space-* and --radius-* tokens instead of ad-hoc rem/px layout literals, and lean on shared .card/.btn/.btn-icon/.input conventions where markup already supplies those classes.
*/
.ce-view { .ce-view {
display: flex; display: flex;
color: var(--text); color: var(--text);
flex: 1 1 auto; flex: 1 1 auto;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: var(--space-lg);
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 1rem 1.25rem; padding: var(--space-lg) calc(var(--space-lg) + var(--space-xs));
box-sizing: border-box; box-sizing: border-box;
overflow: auto; overflow: auto;
} }
@@ -17,7 +22,8 @@
display: flex; display: flex;
align-items: baseline; align-items: baseline;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: var(--space-lg);
flex-wrap: wrap;
} }
.ce-view-header h2 { .ce-view-header h2 {
@@ -31,7 +37,7 @@
.ce-loading, .ce-loading,
.ce-view-error { .ce-view-error {
padding: 0.75rem 1rem; padding: var(--space-md) var(--space-lg);
font-size: 0.85rem; font-size: 0.85rem;
} }
@@ -41,10 +47,10 @@
.ce-empty { .ce-empty {
max-width: 36rem; max-width: 36rem;
padding: 1.5rem; padding: var(--space-xl);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.75rem; gap: var(--space-md);
align-items: flex-start; align-items: flex-start;
} }
@@ -59,17 +65,17 @@
.ce-groups { .ce-groups {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); grid-template-columns: repeat(auto-fill, minmax(calc(var(--space-2xl) * 9), 1fr));
gap: 1rem; gap: var(--space-lg);
} }
.ce-group { .ce-group {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: var(--radius-md);
padding: 0.75rem 1rem; padding: var(--space-md) var(--space-lg);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: var(--space-sm);
} }
.ce-group[data-empty="true"] { .ce-group[data-empty="true"] {
@@ -104,14 +110,14 @@
padding: 0; padding: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.35rem; gap: calc(var(--space-sm) - var(--space-xs) / 2);
} }
.ce-artifact { .ce-artifact {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 0.5rem; gap: var(--space-sm);
font-size: 0.85rem; font-size: 0.85rem;
} }
@@ -127,7 +133,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
color: inherit; color: inherit;
padding: 0.25rem 0; padding: var(--space-xs) 0;
flex: 1; flex: 1;
} }
@@ -160,7 +166,7 @@
.ce-artifact-error { .ce-artifact-error {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 0.15rem; gap: calc(var(--space-xs) / 2);
} }
.ce-view[data-mobile="true"] { .ce-view[data-mobile="true"] {
@@ -172,14 +178,38 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.ce-view[data-mobile="true"] .ce-view-header,
.ce-view[data-mobile="true"] .ce-flow-header,
.ce-view[data-mobile="true"] .ce-flow-guidance-row {
align-items: stretch;
flex-direction: column;
}
.ce-view[data-mobile="true"] .ce-view-start,
.ce-view[data-mobile="true"] .ce-flow-close,
.ce-view[data-mobile="true"] .ce-flow-cancel {
margin-left: 0;
}
.ce-view[data-mobile="true"] .ce-flow-turn {
max-width: 100%;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.ce-view { .ce-view {
width: 100%; width: 100%;
padding: var(--space-sm); padding: var(--space-sm);
} }
.ce-view-header,
.ce-flow-header,
.ce-flow-guidance-row {
align-items: stretch;
flex-direction: column;
}
.ce-session-row { .ce-session-row {
align-items: center; align-items: stretch;
flex-direction: row; flex-direction: row;
} }
@@ -187,6 +217,11 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.ce-flow-turn {
max-width: 100%;
}
.ce-view-start,
.ce-session-cancel, .ce-session-cancel,
.ce-session-discard, .ce-session-discard,
.ce-flow-cancel, .ce-flow-cancel,
@@ -197,21 +232,21 @@
/* --- Stage launcher (U6) --- */ /* --- Stage launcher (U6) --- */
.ce-launcher { .ce-launcher {
margin: 0.75rem 0; margin: var(--space-md) 0;
} }
.ce-launcher-list { .ce-launcher-list {
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(calc(var(--space-2xl) * 4.5), 1fr));
gap: 0.5rem; gap: var(--space-sm);
} }
.ce-launcher-tile { .ce-launcher-tile {
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: var(--space-sm);
} }
.ce-launcher-icon { .ce-launcher-icon {
flex: none; flex: none;
@@ -222,18 +257,18 @@
/* --- CeFlow interactive renderer (U6) --- */ /* --- CeFlow interactive renderer (U6) --- */
.ce-flow { .ce-flow {
margin: 0.75rem 0; margin: var(--space-md) 0;
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
flex-direction: column; flex-direction: column;
gap: 0.6rem; gap: calc(var(--space-sm) + var(--space-xs) / 2);
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
} }
.ce-flow-header { .ce-flow-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: var(--space-sm);
} }
.ce-flow-header h3 { .ce-flow-header h3 {
margin: 0; margin: 0;
@@ -258,7 +293,7 @@
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
flex-direction: column; flex-direction: column;
gap: 0.35rem; gap: calc(var(--space-sm) - var(--space-xs) / 2);
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
} }
@@ -278,10 +313,15 @@
font-style: italic; font-style: italic;
color: var(--text-muted); color: var(--text-muted);
} }
.ce-flow-question-panel,
.ce-flow-question { .ce-flow-question {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.4rem; gap: var(--space-sm);
}
.ce-flow-question {
gap: var(--space-xs);
} }
.ce-flow-question-text { .ce-flow-question-text {
font-weight: 600; font-weight: 600;
@@ -295,7 +335,7 @@
.ce-flow-text { .ce-flow-text {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.4rem; gap: var(--space-xs);
} }
.ce-flow-text textarea, .ce-flow-text textarea,
.ce-flow-guidance-row textarea { .ce-flow-guidance-row textarea {
@@ -322,7 +362,7 @@
} }
.ce-flow-confirm { .ce-flow-confirm {
display: flex; display: flex;
gap: 0.5rem; gap: var(--space-sm);
} }
.ce-flow-options { .ce-flow-options {
list-style: none; list-style: none;
@@ -330,7 +370,7 @@
padding: 0; padding: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.35rem; gap: calc(var(--space-sm) - var(--space-xs) / 2);
} }
.ce-flow-options ul { .ce-flow-options ul {
list-style: none; list-style: none;
@@ -338,7 +378,7 @@
padding: 0; padding: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.3rem; gap: calc(var(--space-sm) - var(--space-xs) / 2);
} }
.ce-flow-option { .ce-flow-option {
width: 100%; width: 100%;
@@ -353,8 +393,27 @@
.ce-flow-checkbox { .ce-flow-checkbox {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.4rem; gap: var(--space-sm);
padding: var(--space-xs) var(--space-sm);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface);
cursor: pointer; cursor: pointer;
transition: border-color var(--transition-fast), background var(--transition-fast), box-shadow var(--transition-fast);
}
.ce-flow-checkbox:hover {
background: var(--card-hover);
border-color: var(--text-muted);
}
.ce-flow-checkbox:has(input:focus-visible) {
border-color: var(--todo);
box-shadow: var(--focus-ring);
}
.ce-flow-checkbox-input {
width: var(--space-lg);
height: var(--space-lg);
margin: 0;
accent-color: var(--todo);
} }
.ce-flow-error { .ce-flow-error {
color: var(--color-error); color: var(--color-error);
@@ -364,12 +423,12 @@
/* Degraded chat fallback (R8/AE1) — must read as visibly distinct. */ /* Degraded chat fallback (R8/AE1) — must read as visibly distinct. */
.ce-flow-degraded { .ce-flow-degraded {
border: 1px dashed var(--color-warning); border: 1px dashed var(--color-warning);
border-radius: 6px; border-radius: var(--radius-sm);
padding: 0.6rem; padding: calc(var(--space-sm) + var(--space-xs) / 2);
background: color-mix(in srgb, var(--color-warning) 8%, transparent); background: color-mix(in srgb, var(--color-warning) 8%, transparent);
} }
.ce-flow-degraded-banner { .ce-flow-degraded-banner {
margin: 0 0 0.4rem; margin: 0 0 var(--space-xs);
font-size: 0.78rem; font-size: 0.78rem;
font-weight: 600; font-weight: 600;
color: var(--color-warning); color: var(--color-warning);
@@ -377,13 +436,13 @@
.ce-flow-degraded-options { .ce-flow-degraded-options {
font-size: 0.78rem; font-size: 0.78rem;
color: var(--text-muted); color: var(--text-muted);
margin: 0 0 0.4rem; margin: 0 0 var(--space-xs);
padding-left: 1.1rem; padding-left: calc(var(--space-lg) + var(--space-xs) / 2);
} }
/* Sessions panel — manage/switch across multiple concurrent CE sessions. */ /* Sessions panel — manage/switch across multiple concurrent CE sessions. */
.ce-sessions { .ce-sessions {
margin-bottom: 0.8rem; margin-bottom: var(--space-md);
} }
.ce-sessions-list { .ce-sessions-list {
list-style: none; list-style: none;
@@ -391,12 +450,12 @@
padding: 0; padding: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.3rem; gap: calc(var(--space-sm) - var(--space-xs) / 2);
} }
.ce-session-row { .ce-session-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: var(--space-sm);
} }
.ce-session-row.is-active .ce-session-open { .ce-session-row.is-active .ce-session-open {
border-color: var(--todo); border-color: var(--todo);
@@ -407,14 +466,19 @@
min-width: 0; min-width: 0;
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 0.6rem; justify-content: flex-start;
gap: calc(var(--space-sm) + var(--space-xs) / 2);
text-align: left; text-align: left;
padding: 0.4rem 0.6rem; padding: var(--space-sm) var(--space-md);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: var(--radius-md);
background: transparent; background: var(--surface);
cursor: pointer; cursor: pointer;
} }
.ce-session-open:hover:not(:disabled) {
background: var(--card-hover);
border-color: var(--text-muted);
}
.ce-session-open:disabled { .ce-session-open:disabled {
cursor: default; cursor: default;
opacity: 0.6; opacity: 0.6;
@@ -489,22 +553,22 @@
/* ── Q&A transcript bubbles ────────────────────────────────────────────── */ /* ── Q&A transcript bubbles ────────────────────────────────────────────── */
.ce-flow-transcript { .ce-flow-transcript {
list-style: none; list-style: none;
margin: 0 0 0.8rem; margin: 0 0 var(--space-md);
padding: 0; padding: 0;
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
flex-direction: column; flex-direction: column;
gap: 0.45rem; gap: calc(var(--space-sm) - var(--space-xs) / 4);
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto;
} }
.ce-flow-turn { .ce-flow-turn {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.15rem; gap: calc(var(--space-xs) / 2);
max-width: 85%; max-width: 85%;
padding: 0.45rem 0.65rem; padding: calc(var(--space-sm) - var(--space-xs) / 4) calc(var(--space-sm) + var(--space-xs) / 2);
border-radius: 10px; border-radius: var(--radius-lg);
background: color-mix(in srgb, var(--border) 30%, transparent); background: color-mix(in srgb, var(--border) 30%, transparent);
} }
.ce-flow-turn-user { .ce-flow-turn-user {
@@ -531,7 +595,7 @@
font-style: italic; font-style: italic;
color: var(--text-muted); color: var(--text-muted);
border-top: 1px dashed color-mix(in srgb, var(--border) 60%, transparent); border-top: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
padding-top: 0.25rem; padding-top: var(--space-xs);
} }
.ce-flow-turn-done { .ce-flow-turn-done {
align-self: center; align-self: center;
@@ -556,11 +620,11 @@
.ce-flow-activity { .ce-flow-activity {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.25rem; gap: var(--space-xs);
margin: 0.3rem 0 0; margin: calc(var(--space-sm) - var(--space-xs) / 2) 0 0;
padding: 0.5rem 0.6rem; padding: var(--space-sm) calc(var(--space-sm) + var(--space-xs) / 2);
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent); border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
border-radius: 8px; border-radius: var(--radius-md);
background: color-mix(in srgb, var(--border) 12%, transparent); background: color-mix(in srgb, var(--border) 12%, transparent);
max-height: 16rem; max-height: 16rem;
overflow-y: auto; overflow-y: auto;
@@ -592,19 +656,19 @@
/* ── Live working pane ──────────────────────────────────────────────────── */ /* ── Live working pane ──────────────────────────────────────────────────── */
.ce-flow-working { .ce-flow-working {
margin: 0.4rem 0; margin: var(--space-xs) 0;
} }
.ce-flow-working-label { .ce-flow-working-label {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.45rem; gap: calc(var(--space-sm) - var(--space-xs) / 4);
margin: 0 0 0.3rem; margin: 0 0 calc(var(--space-sm) - var(--space-xs) / 2);
font-size: 0.82rem; font-size: 0.82rem;
color: var(--text-muted); color: var(--text-muted);
} }
.ce-flow-pulse { .ce-flow-pulse {
width: 8px; width: var(--space-sm);
height: 8px; height: var(--space-sm);
border-radius: 50%; border-radius: 50%;
background: var(--todo); background: var(--todo);
animation: ce-pulse 1.2s ease-in-out infinite; animation: ce-pulse 1.2s ease-in-out infinite;
@@ -616,22 +680,39 @@
/* ── Steering / guidance channel ────────────────────────────────────────── */ /* ── Steering / guidance channel ────────────────────────────────────────── */
.ce-flow-guidance { .ce-flow-guidance {
margin-top: 0.5rem; margin-top: var(--space-sm);
padding-top: 0.5rem; padding-top: var(--space-sm);
border-top: 1px dashed color-mix(in srgb, var(--border) 70%, transparent); border-top: 1px dashed color-mix(in srgb, var(--border) 70%, transparent);
} }
.ce-flow-guidance-label { .ce-flow-guidance-label {
display: block; display: block;
font-size: 0.74rem; font-size: 0.74rem;
color: var(--text-dim); color: var(--text-dim);
margin-bottom: 0.3rem; margin-bottom: calc(var(--space-sm) - var(--space-xs) / 2);
} }
.ce-flow-guidance-row { .ce-flow-guidance-row {
display: flex; display: flex;
gap: 0.4rem; gap: var(--space-xs);
align-items: flex-end; align-items: flex-end;
} }
.ce-flow-guidance-row textarea { .ce-flow-guidance-row textarea {
flex: 1; flex: 1;
resize: vertical; resize: vertical;
} }
.ce-flow-recover,
.ce-flow-complete {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--space-sm);
padding: var(--space-md);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface);
}
.ce-flow-recover p,
.ce-flow-complete p {
margin: 0;
}

View File

@@ -14,6 +14,11 @@ import type { CeSession, CeSessionStatus } from "../session/session-store.js";
const CE_PLUGIN_ID = "fusion-plugin-compound-engineering"; const CE_PLUGIN_ID = "fusion-plugin-compound-engineering";
/**
* FNXC:CompoundEngineeringUI 2026-06-17-00:52:
* The dashboard surface keeps CE-specific data-testid values and semantics intact while adding shared Fusion classes to panels and controls so plugin layout inherits the system button/card rhythm.
*/
/** Resolve a lucide icon name (from the registry) to a component, with fallback. */ /** Resolve a lucide icon name (from the registry) to a component, with fallback. */
function resolveIcon(name: string): LucideIcon { function resolveIcon(name: string): LucideIcon {
const icons = LucideIcons as unknown as Record<string, LucideIcon>; const icons = LucideIcons as unknown as Record<string, LucideIcon>;
@@ -105,7 +110,7 @@ function SessionsPanel({
> >
<button <button
type="button" type="button"
className="ce-session-open" className="btn ce-session-open"
data-testid="ce-session-open" data-testid="ce-session-open"
disabled={disabled} disabled={disabled}
onClick={() => onOpen(s)} onClick={() => onOpen(s)}
@@ -129,7 +134,7 @@ function SessionsPanel({
) : ( ) : (
<button <button
type="button" type="button"
className="btn-icon ce-session-cancel" className="btn btn-icon ce-session-cancel"
data-testid="ce-session-cancel" data-testid="ce-session-cancel"
disabled={disabled} disabled={disabled}
onClick={() => onCancel(s)} onClick={() => onCancel(s)}
@@ -234,7 +239,7 @@ function StageGroup({
}) { }) {
const empty = group.entries.length === 0; const empty = group.entries.length === 0;
return ( return (
<section className="ce-group" data-testid="ce-group" data-stage={group.stage} data-empty={empty ? "true" : "false"}> <section className="ce-group card" data-testid="ce-group" data-stage={group.stage} data-empty={empty ? "true" : "false"}>
<header className="ce-group-header"> <header className="ce-group-header">
<h3>{group.label}</h3> <h3>{group.label}</h3>
<span className="ce-group-count">{group.entries.length}</span> <span className="ce-group-count">{group.entries.length}</span>

View File

@@ -38,6 +38,20 @@ function expectTextareaThemeTokens(selector: string, surfaceName: string) {
expect(block, `expected ${surfaceName} not to rely on a transparent background`).not.toMatch(/background:\s*transparent\s*;/); expect(block, `expected ${surfaceName} not to rely on a transparent background`).not.toMatch(/background:\s*transparent\s*;/);
} }
function declarationValues(blocks: string[], property: string): string[] {
const escaped = property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pattern = new RegExp(`${escaped}\\s*:\\s*([^;]+);`, "g");
return blocks.flatMap((block) => Array.from(block.matchAll(pattern), (match) => match[1].trim()));
}
function expectTokenizedDeclaration(selector: string, property: string, tokenPattern: RegExp) {
const blocks = [...selectorBlocks(selector), ...selectorGroupBlocks(selector)];
expect(blocks, `expected block for ${selector}`).not.toHaveLength(0);
const values = declarationValues(blocks, property);
expect(values, `expected ${selector} to declare ${property}`).not.toHaveLength(0);
expect(values.some((value) => tokenPattern.test(value)), `expected ${selector} ${property} to use a design token`).toBe(true);
}
describe("CompoundEngineeringView theme tokens", () => { describe("CompoundEngineeringView theme tokens", () => {
it("does not use hardcoded legacy color fallbacks", () => { it("does not use hardcoded legacy color fallbacks", () => {
const forbiddenPatterns = [ const forbiddenPatterns = [
@@ -88,6 +102,38 @@ describe("CompoundEngineeringView theme tokens", () => {
expect(viewBlock).toMatch(/color:\s*var\(--text\)\s*;/); expect(viewBlock).toMatch(/color:\s*var\(--text\)\s*;/);
}); });
it("uses dashboard spacing and radius tokens for representative layout surfaces", () => {
const spacingToken = /^(?:0|var\(--space-[^)]+\)|calc\([^;]*var\(--space-[^)]+\)[^;]*\))(?:\s+(?:0|var\(--space-[^)]+\)|calc\([^;]*var\(--space-[^)]+\)[^;]*\)))*$/;
const radiusToken = /^(?:var\(--radius(?:-[^)]+)?\)|50%)$/;
const tokenizedDeclarations = [
[".ce-view", "gap", spacingToken],
[".ce-view", "padding", spacingToken],
[".ce-group", "gap", spacingToken],
[".ce-group", "padding", spacingToken],
[".ce-group", "border-radius", radiusToken],
[".ce-launcher-list", "gap", spacingToken],
[".ce-sessions-list", "gap", spacingToken],
[".ce-flow", "gap", spacingToken],
[".ce-flow-turn", "gap", spacingToken],
[".ce-flow-turn", "padding", spacingToken],
[".ce-flow-turn", "border-radius", radiusToken],
[".ce-session-open", "gap", spacingToken],
[".ce-session-open", "padding", spacingToken],
[".ce-session-open", "border-radius", radiusToken],
] as const;
for (const [selector, property, pattern] of tokenizedDeclarations) {
expectTokenizedDeclaration(selector, property, pattern);
}
});
it("does not use hardcoded border-radius lengths", () => {
expect(css, "expected border-radius to use radius tokens or 50% only").not.toMatch(
/border-radius\s*:\s*(?!50%\s*;)[^;]*(?:px|rem)\s*;/,
);
});
it("themes every CE free-text textarea with dashboard input tokens", () => { it("themes every CE free-text textarea with dashboard input tokens", () => {
const textareaSurfaces = [ const textareaSurfaces = [
{ {