13 files reviewed with mission-control design lens. Key additions: - theme: extended indigo scale, 4-level surface hierarchy, 22 terminal tokens, transition/z-index/focus-visible token categories - All screens: keyboard shortcuts, loading/error/empty states hardened - 5 new shared components: StatusDot, SkeletonLoader, Toast, Badge, KeyboardShortcutHint - settings: expanded from 2 to 5 sub-pages (accounts, workspace, danger zone) - review-tab: 3-pane layout, inline comments, file nav, hunk controls - execution-tab: zoom, partial failure state, stale agent detection - dialogs: 2 bugs found (mutation locking, error placement) Total: 4,039 → 9,302 lines (+130% from review pass)
1321 lines
47 KiB
Markdown
1321 lines
47 KiB
Markdown
# Shared Components (v2)
|
|
|
|
### NEW — no v1 equivalent
|
|
|
|
Reusable component specifications used across multiple pages. Each component
|
|
includes: description, props/API, ASCII mockups of all states, usage locations,
|
|
and proposed source file path.
|
|
|
|
---
|
|
|
|
## 1. `<SaveIndicator>`
|
|
|
|
Inline status indicator for auto-save operations. Shows current save state
|
|
with icon + label, auto-hides on success.
|
|
|
|
### Props
|
|
|
|
```ts
|
|
interface SaveIndicatorProps {
|
|
status: 'idle' | 'saving' | 'saved' | 'error';
|
|
onRetry?: () => void;
|
|
/**
|
|
* When true, "saved" state displays a persistent timestamp ("Saved at 3:42 PM")
|
|
* instead of fading out after 2s. Use for high-stakes contexts like page content
|
|
* editing where the user needs ongoing confirmation that their work is safe.
|
|
* Default: false (fade-out behavior).
|
|
*/
|
|
persistent?: boolean;
|
|
}
|
|
```
|
|
|
|
### States
|
|
|
|
```
|
|
Idle: (hidden — renders nothing)
|
|
|
|
Saving: [spinner] Saving...
|
|
|
|
Saved: [v] Saved <- fades out after 2s, transitions to idle
|
|
|
|
Error: [!] Failed to save [retry]
|
|
```
|
|
|
|
### Detailed ASCII
|
|
|
|
**Saving**
|
|
```
|
|
+---------------------------+
|
|
| [spinner] Saving... |
|
|
+---------------------------+
|
|
```
|
|
- `text-muted-foreground text-sm`
|
|
- Spinner is a 14px animated SVG
|
|
|
|
**Saved (default: `persistent=false`)**
|
|
```
|
|
+---------------------------+
|
|
| [v] Saved |
|
|
+---------------------------+
|
|
```
|
|
- `text-green-600 text-sm`
|
|
- Checkmark icon, 14px
|
|
- Fades to `opacity-0` over 300ms after 2s delay, then sets status to `idle`
|
|
|
|
**Saved (persistent mode: `persistent=true`)**
|
|
```
|
|
+---------------------------+
|
|
| [v] Saved at 3:42 PM |
|
|
+---------------------------+
|
|
```
|
|
- Same styling as default saved state, but does NOT fade out
|
|
- Timestamp updates on each save, formatted via `Intl.DateTimeFormat` with `hour`/`minute`
|
|
- Remains visible until next `saving` or `error` transition
|
|
|
|
**Error**
|
|
```
|
|
+--------------------------------------+
|
|
| [!] Failed to save [retry] |
|
|
+--------------------------------------+
|
|
```
|
|
- `text-destructive text-sm`
|
|
- Alert icon, 14px
|
|
- `[retry]` is a text button: `text-destructive underline cursor-pointer`
|
|
- Does not auto-hide — persists until retry succeeds or user navigates away
|
|
|
|
### Usage
|
|
|
|
| Page | Location | Persistent? |
|
|
|------|----------|-------------|
|
|
| Content tab | Top-right of Tiptap editor toolbar (see content-tab.md) | Yes — page content is high-stakes |
|
|
| Plan tab | Top-right of phase description editor (see plan-tab.md) | No — phase descriptions are lightweight |
|
|
|
|
### Source
|
|
|
|
- `packages/web/src/components/SaveIndicator.tsx` (proposed)
|
|
|
|
---
|
|
|
|
## 2. `<EmptyState>`
|
|
|
|
Centered placeholder shown when a list or section has no items. Configurable
|
|
icon, messaging, and optional CTA button.
|
|
|
|
### Props
|
|
|
|
```ts
|
|
interface EmptyStateProps {
|
|
icon: React.ReactNode; // e.g., <Inbox />, <ListTodo />, <Users />
|
|
title: string; // e.g., "No initiatives yet"
|
|
description?: string; // e.g., "Create an initiative to get started."
|
|
action?: {
|
|
label: string; // e.g., "Plan with Agent"
|
|
onClick: () => void;
|
|
icon?: React.ReactNode; // e.g., <Sparkles /> — rendered before label
|
|
};
|
|
secondaryAction?: {
|
|
label: string; // e.g., "Add Phase"
|
|
onClick: () => void;
|
|
icon?: React.ReactNode; // e.g., <Plus />
|
|
};
|
|
}
|
|
```
|
|
|
|
When both `action` and `secondaryAction` are provided, the primary action
|
|
renders as `<Button variant="default">` and the secondary as
|
|
`<Button variant="outline">`, side by side with `gap-3`.
|
|
|
|
### Default (with action)
|
|
|
|
```
|
|
+-------------------------------------------+
|
|
| |
|
|
| [icon] |
|
|
| No phases yet |
|
|
| Add a phase to start planning. |
|
|
| |
|
|
| [ Add Phase ] |
|
|
| |
|
|
+-------------------------------------------+
|
|
```
|
|
|
|
### Without action
|
|
|
|
```
|
|
+-------------------------------------------+
|
|
| |
|
|
| [icon] |
|
|
| No conversations yet |
|
|
| Questions from agents appear here. |
|
|
| |
|
|
+-------------------------------------------+
|
|
```
|
|
|
|
### With primary + secondary action
|
|
|
|
```
|
|
+-------------------------------------------+
|
|
| |
|
|
| [icon] |
|
|
| No phases yet |
|
|
| Add a phase to start planning, |
|
|
| or let an agent plan for you. |
|
|
| |
|
|
| [sparkles Plan with Agent] [+ Add Phase]|
|
|
| |
|
|
+-------------------------------------------+
|
|
```
|
|
- Primary: `<Button variant="default">` (filled indigo)
|
|
- Secondary: `<Button variant="outline">` (border only)
|
|
- Container: `flex gap-3 justify-center`
|
|
|
|
### Without description
|
|
|
|
```
|
|
+-------------------------------------------+
|
|
| |
|
|
| [icon] |
|
|
| No agents running |
|
|
| |
|
|
+-------------------------------------------+
|
|
```
|
|
|
|
### Styling
|
|
|
|
- Container: `flex flex-col items-center justify-center py-16 text-center`
|
|
- Icon: `h-12 w-12 text-muted-foreground mb-4`
|
|
- Title: `text-lg font-medium text-foreground mb-1`
|
|
- Description: `text-sm text-muted-foreground mb-6`
|
|
- Action button: default shadcn `<Button>` variant
|
|
|
|
### Usage
|
|
|
|
| Page | Title | Primary Action | Secondary Action |
|
|
|------|-------|----------------|------------------|
|
|
| Initiatives list (no data) | "No initiatives yet" | "New Initiative" | -- |
|
|
| Initiatives list (no match) | "No matching initiatives" | -- (text link instead) | -- |
|
|
| Plan tab (no phases) | "No phases yet" | "Plan with Agent" | "Add Phase" |
|
|
| Execution tab (no tasks) | "No tasks to execute" | "Go to Plan Tab" | -- |
|
|
| Agents page (no agents) | "No agents running" | -- | -- |
|
|
| Inbox page (no conversations) | "No conversations yet" | -- | -- |
|
|
| Settings > Accounts (none) | "No accounts registered" | "Add Account" | -- |
|
|
|
|
### Source
|
|
|
|
- `packages/web/src/components/EmptyState.tsx` (proposed)
|
|
|
|
---
|
|
|
|
## 3. `<ErrorState>`
|
|
|
|
Centered error display with optional retry action. Used when a data fetch
|
|
fails or a section encounters an unrecoverable error.
|
|
|
|
### Props
|
|
|
|
```ts
|
|
interface ErrorStateProps {
|
|
message: string; // e.g., "Failed to load initiatives"
|
|
onRetry?: () => void; // If provided, shows Retry button
|
|
details?: string; // Technical error detail (stack trace, API response)
|
|
errorCode?: string; // e.g., "TRPC_TIMEOUT", "NETWORK_ERROR"
|
|
}
|
|
```
|
|
|
|
### With retry
|
|
|
|
```
|
|
+-------------------------------------------+
|
|
| |
|
|
| [AlertCircle] |
|
|
| Failed to load initiatives |
|
|
| |
|
|
| [ Retry ] |
|
|
| |
|
|
+-------------------------------------------+
|
|
```
|
|
|
|
### Without retry (navigation fallback)
|
|
|
|
```
|
|
+-------------------------------------------+
|
|
| |
|
|
| [AlertCircle] |
|
|
| Error loading initiative |
|
|
| Could not fetch initiative data. |
|
|
| |
|
|
| [ Back to Initiatives ] |
|
|
| |
|
|
+-------------------------------------------+
|
|
```
|
|
|
|
### With details + error code (expandable)
|
|
|
|
```
|
|
+-------------------------------------------+
|
|
| |
|
|
| [AlertCircle] |
|
|
| Failed to load initiatives |
|
|
| Error: TRPC_TIMEOUT |
|
|
| |
|
|
| [ Retry ] |
|
|
| |
|
|
| [v] Show details |
|
|
| +-----------------------------------+|
|
|
| | TRPCClientError: Request timed ||
|
|
| | out after 10000ms ||
|
|
| | at fetchQuery (trpc-client.ts: ||
|
|
| | 42:11) ||
|
|
| +-----------------------------------+|
|
|
| |
|
|
+-------------------------------------------+
|
|
```
|
|
- `Error: <code>` — `text-sm text-muted-foreground font-mono`, shown below the message when `errorCode` is provided
|
|
- `[v] Show details` — collapsible trigger, `text-xs text-muted-foreground cursor-pointer`
|
|
- Details panel: `bg-muted rounded-md p-3 font-mono text-xs text-muted-foreground max-h-32 overflow-y-auto`
|
|
- Details are collapsed by default — click to expand. Uses a `<details>/<summary>` element or state toggle.
|
|
- When `details` prop is absent, the collapsible section is not rendered at all.
|
|
|
|
Note: The "without retry" variant is not a prop configuration of `<ErrorState>`
|
|
itself. Pages that need navigation instead of retry render `<ErrorState>`
|
|
without `onRetry` and add their own navigation button below it.
|
|
|
|
### Styling
|
|
|
|
- Container: `flex flex-col items-center justify-center py-16 text-center`
|
|
- Icon: `h-12 w-12 text-destructive mb-4` (AlertCircle from lucide-react)
|
|
- Message: `text-lg font-medium text-foreground mb-4`
|
|
- Retry button: `<Button variant="outline">Retry</Button>`
|
|
|
|
### Usage
|
|
|
|
| Page | Message | Has Retry? |
|
|
|------|---------|------------|
|
|
| Initiatives list | "Failed to load initiatives" | Yes |
|
|
| Initiative detail | "Error loading initiative" | No (nav button) |
|
|
| Review tab | "Failed to load proposals" | Yes |
|
|
| Agents page | "Failed to load agents" | Yes |
|
|
| Settings page | "Failed to load settings" | Yes |
|
|
|
|
### Source
|
|
|
|
- `packages/web/src/components/ErrorState.tsx` (proposed)
|
|
|
|
---
|
|
|
|
## 4. `<CommandPalette>`
|
|
|
|
Global search overlay triggered by keyboard shortcut. Provides quick
|
|
navigation to initiatives, agents, tasks, and settings pages.
|
|
|
|
### Trigger
|
|
|
|
- `Cmd+K` (Mac) / `Ctrl+K` (Windows)
|
|
- Header button `[cmd-k]` (see app-layout.md)
|
|
|
|
### Props
|
|
|
|
```ts
|
|
interface CommandPaletteProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
}
|
|
```
|
|
|
|
Internally fetches data via tRPC queries (initiatives, agents, tasks).
|
|
|
|
### Default State (open, empty query)
|
|
|
|
```
|
|
+----------------------------------------------------------+
|
|
| [search] Search everything... [Esc] |
|
|
| --------------------------------------------------------|
|
|
| Recent |
|
|
| -> Auth System Overhaul |
|
|
| -> Agents |
|
|
| -> Settings |
|
|
+----------------------------------------------------------+
|
|
```
|
|
|
|
Shows recent navigation history when search is empty.
|
|
|
|
### With Search Results
|
|
|
|
```
|
|
+----------------------------------------------------------+
|
|
| [search] auth___ [Esc] |
|
|
| --------------------------------------------------------|
|
|
| Initiatives |
|
|
| Auth System Overhaul [ACTIVE] |
|
|
| --------------------------------------------------------|
|
|
| Tasks |
|
|
| Implement auth middleware [execute] 3/7 |
|
|
| Review auth token flow [verify] done |
|
|
| --------------------------------------------------------|
|
|
| Agents |
|
|
| blue-fox-7 (auth middleware) [RUNNING] |
|
|
+----------------------------------------------------------+
|
|
```
|
|
|
|
### No Results
|
|
|
|
```
|
|
+----------------------------------------------------------+
|
|
| [search] xyznonexistent___ [Esc] |
|
|
| --------------------------------------------------------|
|
|
| |
|
|
| No results for "xyznonexistent" |
|
|
| |
|
|
| Try a shorter query, or browse: |
|
|
| -> All Initiatives |
|
|
| -> All Agents |
|
|
| -> Settings |
|
|
| |
|
|
+----------------------------------------------------------+
|
|
```
|
|
|
|
- "Try a shorter query, or browse:" — `text-sm text-muted-foreground`
|
|
- Fallback navigation links are always the same three static items
|
|
- Clicking a fallback link navigates and closes the palette
|
|
|
|
### Keyboard Navigation
|
|
|
|
```
|
|
[search] auth___ [Esc]
|
|
---------------------------------------------------------
|
|
Initiatives
|
|
> Auth System Overhaul [ACTIVE] <- highlighted
|
|
---------------------------------------------------------
|
|
Tasks
|
|
Implement auth middleware [execute] 3/7
|
|
```
|
|
|
|
- `>` indicates the currently highlighted item
|
|
- `Arrow Up` / `Arrow Down` — move highlight
|
|
- `Enter` — navigate to highlighted item, close palette
|
|
- `Esc` — close palette
|
|
- Type to filter — results update as you type (debounced 100ms)
|
|
|
|
### Result Groups
|
|
|
|
| Group | Source | Display |
|
|
|-------|--------|---------|
|
|
| Initiatives | `listInitiatives` query | Name + status badge |
|
|
| Tasks | `listTasks` query (across initiatives) | Name + category + status |
|
|
| Agents | `listAgents` query | Name + task description + status badge |
|
|
| Pages | `Go to Initiatives`, `Go to Agents`, etc. | Static navigation items |
|
|
|
|
Groups are hidden when they have 0 matches. Max 5 items per group.
|
|
Maximum total visible results: 20 (across all groups combined).
|
|
|
|
### Recent Items
|
|
|
|
Recent navigation history is stored in `sessionStorage` key `cw-cmd-recent`
|
|
as an ordered array of `{ type, id, label, path }` objects (max 10 entries,
|
|
LRU eviction). Navigating to any entity via the palette pushes it to the
|
|
front. The "Recent" section only appears when the search query is empty.
|
|
|
|
```
|
|
Recent [clock icon per row]
|
|
-> Auth System Overhaul [initiative]
|
|
-> blue-fox-7 [agent]
|
|
-> Settings [page]
|
|
```
|
|
|
|
### Fuzzy Matching Behavior
|
|
|
|
- Uses substring match by default (case-insensitive)
|
|
- Query is split on whitespace: `"auth pkce"` matches "Implement **auth** **PKCE** flow"
|
|
- Each token must appear somewhere in the candidate string (AND logic)
|
|
- No typo tolerance — keep it simple. If fuzzy matching via a library like
|
|
`fuse.js` is added later, configure threshold conservatively (0.3) to avoid
|
|
noise in a mission-control context where precision matters.
|
|
|
|
### Result Ranking
|
|
|
|
Within each group, results are ordered by:
|
|
1. **Exact prefix match** — query matches the start of the name (highest)
|
|
2. **Substring position** — earlier match index ranks higher
|
|
3. **Recency** — more recently updated items break ties (`updatedAt` DESC)
|
|
|
|
### Section Keyboard Shortcuts
|
|
|
|
| Key | Action |
|
|
|-----|--------|
|
|
| `Arrow Up` / `Arrow Down` | Move highlight between items (wraps around) |
|
|
| `Enter` | Navigate to highlighted item, close palette |
|
|
| `Esc` | Close palette |
|
|
| `Backspace` on empty query | Close palette |
|
|
| `Cmd+K` / `Ctrl+K` while open | Close palette (toggle behavior) |
|
|
|
|
### Overlay Behavior
|
|
|
|
- Renders as a centered modal with backdrop (`bg-black/50`)
|
|
- Width: `max-w-lg` (512px)
|
|
- Position: `top-[20%]` of viewport
|
|
- Backdrop click closes the palette
|
|
- Focus is trapped inside the modal while open
|
|
- Search input auto-focuses on open
|
|
|
|
### Source
|
|
|
|
- `packages/web/src/components/CommandPalette.tsx` (proposed)
|
|
|
|
---
|
|
|
|
## 5. `<ThemeToggle>`
|
|
|
|
3-state segmented control for switching between light, system, and dark
|
|
color schemes. Persists selection to `localStorage`.
|
|
|
|
### Props
|
|
|
|
```ts
|
|
// No props — reads/writes theme from ThemeProvider context
|
|
// ThemeProvider wraps the app root
|
|
type Theme = 'light' | 'system' | 'dark';
|
|
```
|
|
|
|
### States
|
|
|
|
```
|
|
Light active: [*sun*] [monitor] [moon]
|
|
System active: [sun] [*monitor*] [moon]
|
|
Dark active: [sun] [monitor] [*moon*]
|
|
```
|
|
|
|
### Detailed ASCII
|
|
|
|
**Light mode selected**
|
|
```
|
|
+-----+----------+---------+
|
|
| sun | monitor | moon |
|
|
+-----+----------+---------+
|
|
^^^
|
|
filled bg, active text
|
|
```
|
|
|
|
**System mode selected (default)**
|
|
```
|
|
+-----+----------+---------+
|
|
| sun | monitor | moon |
|
|
+-----+----------+---------+
|
|
^^^^^^^^
|
|
filled bg, active text
|
|
```
|
|
|
|
**Dark mode selected**
|
|
```
|
|
+-----+----------+---------+
|
|
| sun | monitor | moon |
|
|
+-----+----------+---------+
|
|
^^^^^^
|
|
filled bg, active text
|
|
```
|
|
|
|
### Styling
|
|
|
|
- Container: `inline-flex rounded-lg bg-muted p-0.5 gap-0.5`
|
|
- Segment (inactive): `px-2 py-1 rounded-md text-muted-foreground hover:text-foreground`
|
|
- Segment (active): `px-2 py-1 rounded-md bg-background text-foreground shadow-sm`
|
|
- Icons: 16px lucide-react icons (`Sun`, `Monitor`, `Moon`)
|
|
- No text labels — icons only in the header (compact)
|
|
|
|
### Tooltip on hover
|
|
|
|
```
|
|
[sun] -> "Light"
|
|
[monitor] -> "System (Dark)" or "System (Light)" — reflects resolved preference
|
|
[moon] -> "Dark"
|
|
```
|
|
|
|
The System tooltip dynamically resolves the current `prefers-color-scheme`
|
|
media query to show the effective mode. This eliminates user confusion about
|
|
what "System" actually means on their machine. Read `isDark` from the
|
|
`ThemeProvider` context to determine the parenthetical label.
|
|
|
|
### Persistence
|
|
|
|
- Stored in `localStorage` key `cw-theme`
|
|
- Default: `system`
|
|
- On load: reads `localStorage`, applies `class="dark"` to `<html>` if
|
|
theme is `dark` or if theme is `system` and `prefers-color-scheme: dark`
|
|
|
|
### Usage
|
|
|
|
| Page | Location |
|
|
|------|----------|
|
|
| App layout (header) | Right cluster, between Cmd+K button and health dot |
|
|
|
|
See app-layout.md for header placement.
|
|
|
|
### Source
|
|
|
|
- `packages/web/src/components/ThemeToggle.tsx` (proposed)
|
|
- `packages/web/src/providers/ThemeProvider.tsx` (proposed)
|
|
|
|
---
|
|
|
|
## 6. `<StatusDot>`
|
|
|
|
Semantic status indicator rendered as a small colored circle. Used pervasively
|
|
across the UI for agents, tasks, health, conversations, and previews. This is
|
|
the single most repeated visual element in the app — it needs to be a proper
|
|
component, not ad-hoc `<span className="h-2 w-2 rounded-full bg-..." />` copy-pasted everywhere.
|
|
|
|
### Props
|
|
|
|
```ts
|
|
interface StatusDotProps {
|
|
/**
|
|
* Maps to a status token from the theme (status-active-dot, status-success-dot, etc.).
|
|
* This is the semantic status, not a color — the theme resolves the actual hue.
|
|
*/
|
|
status: 'active' | 'success' | 'warning' | 'error' | 'neutral' | 'urgent';
|
|
/**
|
|
* Dot diameter. Defaults to 'md'.
|
|
* - 'sm' (6px): inline with text, badges, compact lists
|
|
* - 'md' (8px): agent cards, task rows, sidebar items
|
|
* - 'lg' (10px): health indicator in header, large card headers
|
|
*/
|
|
size?: 'sm' | 'md' | 'lg';
|
|
/**
|
|
* When true, applies a pulsing opacity animation (0.4 -> 1.0, 2s cycle).
|
|
* Use for statuses that represent ongoing activity (running agent, building preview).
|
|
* Default: false.
|
|
*
|
|
* Animation uses opacity only — no scale — to avoid layout shifts in tight layouts
|
|
* like agent cards. The keyframes:
|
|
* 0% { opacity: 1 }
|
|
* 50% { opacity: 0.4 }
|
|
* 100% { opacity: 1 }
|
|
*/
|
|
pulse?: boolean;
|
|
/**
|
|
* Accessible label for screen readers. Required.
|
|
* Examples: "Running", "Crashed", "Connected", "Blocked"
|
|
*
|
|
* Rendered as `aria-label` on the element and `role="status"`.
|
|
* Status should never be communicated through color alone —
|
|
* this label is the accessible equivalent.
|
|
*/
|
|
label: string;
|
|
}
|
|
```
|
|
|
|
### Size Variants
|
|
|
|
```
|
|
sm (6px): * <- inline with text runs
|
|
md (8px): (*) <- default, agent cards
|
|
lg (10px): ( * ) <- header health dot
|
|
```
|
|
|
|
### Visual Rendering
|
|
|
|
```
|
|
+--------------------------------------------+
|
|
| Active (pulse): (*) ← opacity animation |
|
|
| Success (static): (v) ← solid green |
|
|
| Warning (static): (?) ← solid amber |
|
|
| Error (static): (!) ← solid red |
|
|
| Neutral (static): (-) ← solid grey |
|
|
| Urgent (pulse): (!) ← pulsing purple |
|
|
+--------------------------------------------+
|
|
```
|
|
|
|
### Color Mapping (from theme tokens)
|
|
|
|
| Status | Token | Typical Color | Default Pulse? |
|
|
|--------|-------|---------------|----------------|
|
|
| `active` | `status-active-dot` | Blue | Yes (running agents, building previews) |
|
|
| `success` | `status-success-dot` | Green | No |
|
|
| `warning` | `status-warning-dot` | Amber | No |
|
|
| `error` | `status-error-dot` | Red | No |
|
|
| `neutral` | `status-neutral-dot` | Grey | No |
|
|
| `urgent` | `status-urgent-dot` | Purple | Yes (blocked tasks, urgent inbox) |
|
|
|
|
Note: `pulse` is a prop, not automatic per status. The table above shows
|
|
conventional usage. A completed agent does not pulse; a health dot in green
|
|
state pulses briefly (3s) then stops — that logic lives in the consuming
|
|
component, not in `StatusDot` itself.
|
|
|
|
### Styling
|
|
|
|
```
|
|
.status-dot {
|
|
display: inline-block;
|
|
border-radius: 9999px; /* fully round */
|
|
flex-shrink: 0; /* never collapse in flex layouts */
|
|
background: hsl(var(--status-{status}-dot));
|
|
}
|
|
|
|
.status-dot--sm { width: 6px; height: 6px; }
|
|
.status-dot--md { width: 8px; height: 8px; }
|
|
.status-dot--lg { width: 10px; height: 10px; }
|
|
|
|
.status-dot--pulse {
|
|
animation: status-pulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes status-pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.4; }
|
|
}
|
|
```
|
|
|
|
### Usage
|
|
|
|
| Page | Context | Status | Size | Pulse? |
|
|
|------|---------|--------|------|--------|
|
|
| App header | Health indicator | success/warning/error | lg | Green: 3s then stop |
|
|
| Agents | Agent card status | active/warning/error/neutral | md | active: yes |
|
|
| Execution tab | Phase/task status | active/success/warning/neutral | md | active: yes |
|
|
| Inbox | Conversation priority | urgent/warning/neutral/success | md | urgent: yes |
|
|
| Dialogs | Task dependency status | varies | sm | No |
|
|
| Settings | Account health | success/warning | sm | No |
|
|
|
|
### Accessibility
|
|
|
|
- `role="status"` + `aria-label` on every instance
|
|
- Color is never the sole indicator — always paired with text label or icon in context
|
|
- Pulse animation respects `prefers-reduced-motion: reduce` (disable animation)
|
|
|
|
### Source
|
|
|
|
- `packages/web/src/components/StatusDot.tsx` (proposed)
|
|
|
|
---
|
|
|
|
## 7. `<SkeletonLoader>`
|
|
|
|
Shimmer placeholder blocks shown during data loading. Mirrors the anatomy of
|
|
the content it replaces to prevent layout shift. Used on every data-fetching
|
|
page.
|
|
|
|
### Props
|
|
|
|
```ts
|
|
interface SkeletonProps {
|
|
/**
|
|
* Shape variant:
|
|
* - 'line': horizontal bar (text placeholder). Default height 16px.
|
|
* - 'circle': avatar / status dot placeholder. Uses `size` for diameter.
|
|
* - 'rect': block placeholder (card, image area). Uses `width` x `height`.
|
|
*/
|
|
variant?: 'line' | 'circle' | 'rect';
|
|
/** Width. For 'line': defaults to '100%'. For 'circle': ignored (uses size). */
|
|
width?: string | number;
|
|
/** Height. For 'line': defaults to 16px. For 'circle': ignored (uses size). */
|
|
height?: string | number;
|
|
/** Diameter for 'circle' variant. Defaults to 32px. */
|
|
size?: number;
|
|
/** Additional className for width/margin overrides. */
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Composite skeleton that renders N skeleton cards matching a specific
|
|
* page's card anatomy. Use instead of manually assembling Skeleton primitives.
|
|
*/
|
|
interface SkeletonCardProps {
|
|
/** Number of skeleton cards to render. Default: 5. */
|
|
count?: number;
|
|
/** Card layout variant matching the page being loaded. */
|
|
layout: 'agent-card' | 'initiative-card' | 'conversation-card' | 'project-card' | 'account-card';
|
|
}
|
|
```
|
|
|
|
### Shimmer Animation
|
|
|
|
All skeleton elements share a single shimmer sweep: a translucent highlight
|
|
gradient that moves left-to-right on a 1.5s infinite loop. This uses a CSS
|
|
`background-position` animation on a linear gradient, not a pseudo-element
|
|
overlay, so it works on any shape.
|
|
|
|
```
|
|
Resting: ░░░░░░░░░░░░░░░░░░░░
|
|
Shimmer: ░░░░░▓▓▓░░░░░░░░░░░░ <- highlight sweeps right
|
|
Shimmer: ░░░░░░░░░░░░▓▓▓░░░░░
|
|
```
|
|
|
|
```css
|
|
.skeleton {
|
|
background: linear-gradient(
|
|
90deg,
|
|
hsl(var(--muted)) 25%,
|
|
hsl(var(--muted-foreground) / 0.08) 50%,
|
|
hsl(var(--muted)) 75%
|
|
);
|
|
background-size: 200% 100%;
|
|
animation: shimmer 1.5s ease-in-out infinite;
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
@keyframes shimmer {
|
|
0% { background-position: 200% 0; }
|
|
100% { background-position: -200% 0; }
|
|
}
|
|
```
|
|
|
|
Uses `--duration-glacial` (500ms) as the animation timing reference from
|
|
the theme spec, though the actual cycle is 1.5s (3x glacial) for a gentler feel.
|
|
|
|
### Composite Card Skeletons
|
|
|
|
**Agent card skeleton** (matches `agents.md` card anatomy):
|
|
```
|
|
+---------------------------+
|
|
| [.] ░░░░░░░░░░░░ | <- circle (8px) + line (60%)
|
|
| ░░░░░ . ░░░░░░ | <- line (40%) + dot + line (50%)
|
|
| ░░░░░░░░░░░░░ | <- line (70%)
|
|
| ░░░░ ░░░ ░░░ | <- line (30%) + gap + line (20%) + line (20%)
|
|
+---------------------------+
|
|
```
|
|
|
|
**Initiative card skeleton** (matches `initiatives-list.md` card anatomy):
|
|
```
|
|
+---------------------------+
|
|
| ░░░░░░░░░░░░░░░░ ░░ ░░ | <- line (65%) + badge + badge
|
|
| ░░░░░░░░░░ ░░░░░ | <- line (40%) + line (30%)
|
|
| ░░░░░░░░░░░░░░░░░░░░░░░ | <- rect (100% x 6px) progress bar
|
|
| ░░░░░░░░ ░░░░░ ░░░░ | <- line (35%) + line (25%) + line (20%)
|
|
+---------------------------+
|
|
```
|
|
|
|
**Conversation card skeleton** (matches `inbox.md` card anatomy):
|
|
```
|
|
+---------------------------+
|
|
| [.] ░░░░░░░░░░ ░░░ | <- circle (8px) + line (55%) + line (15%)
|
|
| ░░░░░░░░░░░░░░░░░░ | <- line (80%)
|
|
| ░░░░░░░░░░░░ | <- line (50%)
|
|
+---------------------------+
|
|
```
|
|
|
|
### Loading-to-Content Transition
|
|
|
|
- Skeletons fade out: `opacity-0` over 150ms (`duration-fast`)
|
|
- Content fades in: `opacity-100` over 150ms
|
|
- No layout jump: skeleton card dimensions must exactly match real card dimensions
|
|
- Container uses `min-h` to prevent collapse during transition
|
|
- On error: skeletons fade out, `<ErrorState>` fades in centered
|
|
|
|
### Accessibility
|
|
|
|
- `aria-hidden="true"` on all skeleton elements (they are decorative)
|
|
- `role="status"` + `aria-label="Loading"` on the skeleton container
|
|
- Respects `prefers-reduced-motion: reduce` — disable shimmer, show static grey blocks
|
|
|
|
### Usage
|
|
|
|
| Page | Card Layout | Count | Notes |
|
|
|------|-------------|-------|-------|
|
|
| Initiatives list | `initiative-card` | 5 | Matches card height to prevent jump |
|
|
| Agents | `agent-card` | 5 | Output panel shows empty state, not skeleton |
|
|
| Inbox | `conversation-card` | 6 | List panel only |
|
|
| Settings > Projects | `project-card` | 3 | |
|
|
| Settings > Accounts | `account-card` | 3 | |
|
|
| Initiative detail | Custom | 1 | Two-row header + tab bar + content area |
|
|
| Content tab | Custom | 1 | Sidebar tree + editor area |
|
|
|
|
### Source
|
|
|
|
- `packages/web/src/components/Skeleton.tsx` (proposed — primitives)
|
|
- `packages/web/src/components/SkeletonCard.tsx` (proposed — composite cards)
|
|
|
|
---
|
|
|
|
## 8. `<Toast>` (via Sonner)
|
|
|
|
Global notification toasts for asynchronous feedback. Rendered by the
|
|
`<Toaster>` provider at the app root. Individual toasts are fired imperatively
|
|
via `toast()`, `toast.success()`, `toast.error()`, etc.
|
|
|
|
This project uses [Sonner](https://sonner.emilkowal.dev/) (already a shadcn/ui
|
|
dependency). No custom toast component is needed — this spec documents the
|
|
configuration and usage conventions.
|
|
|
|
### Toaster Configuration
|
|
|
|
```tsx
|
|
// In app root layout (see app-layout.md)
|
|
<Toaster
|
|
position="bottom-right"
|
|
toastOptions={{
|
|
className: 'font-sans text-sm',
|
|
duration: 5000, // default auto-dismiss: 5s
|
|
}}
|
|
visibleToasts={3} // max 3 visible, older collapse to "+N more"
|
|
richColors // enable semantic color variants
|
|
closeButton // always show dismiss X
|
|
offset={16} // 16px from viewport edge
|
|
/>
|
|
```
|
|
|
|
### Toast Variants
|
|
|
|
```
|
|
Success: +---------------------------------------+
|
|
| [v] Account added [x] |
|
|
+---------------------------------------+
|
|
bg-status-success-bg, auto-dismiss 5s
|
|
|
|
Error: +---------------------------------------+
|
|
| [!] Failed to delete project [x] |
|
|
| [Retry] |
|
|
+---------------------------------------+
|
|
bg-status-error-bg, persistent (no auto-dismiss)
|
|
|
|
Info: +---------------------------------------+
|
|
| [i] Answer sent to blue-fox-7 [x] |
|
|
+---------------------------------------+
|
|
default bg, auto-dismiss 5s
|
|
|
|
Warning: +---------------------------------------+
|
|
| [!] Account X exhausted, [x] |
|
|
| switched to Y |
|
|
+---------------------------------------+
|
|
bg-status-warning-bg, auto-dismiss 5s
|
|
|
|
With action:+---------------------------------------+
|
|
| [!] blue-fox-7 crashed [x] |
|
|
| [View Agent] |
|
|
+---------------------------------------+
|
|
persistent, action navigates to /agents
|
|
```
|
|
|
|
### Duration Rules
|
|
|
|
| Category | Duration | Rationale |
|
|
|----------|----------|-----------|
|
|
| Success confirmations | 5s | User glances, moves on |
|
|
| Info / status changes | 5s | FYI, not blocking |
|
|
| Warnings (account switch, etc.) | 5s | Notable but not actionable |
|
|
| "All tasks complete" | 8s | Celebration moment, give it room |
|
|
| "Agent has a question" | 8s | Needs attention but not emergency |
|
|
| Agent crashed | Persistent | Action required — must be manually dismissed |
|
|
| Task needs approval | Persistent | Action required |
|
|
| Errors with retry | Persistent | User must acknowledge or retry |
|
|
|
|
### Critical Event Toasts (fired globally via EventBus)
|
|
|
|
These fire regardless of the current page (configured in app-layout.md):
|
|
|
|
| Event | Toast Call | Duration |
|
|
|-------|-----------|----------|
|
|
| Agent crashed | `toast.error("blue-fox-7 crashed", { action: { label: "View", onClick: navigate } })` | Persistent |
|
|
| Task needs approval | `toast("Task ready for approval", { action: { label: "Review", onClick: navigate } })` | Persistent |
|
|
| All tasks complete | `toast.success("Initiative complete — ready for review")` | 8s |
|
|
| Account exhausted | `toast.warning("Account X exhausted, switched to Y")` | 5s |
|
|
| Agent asking question | `toast("blue-fox-7 has a question", { action: { label: "Answer", onClick: navigate } })` | 8s |
|
|
|
|
### Stacking Behavior
|
|
|
|
```
|
|
+---+
|
|
(viewport bottom-right) | +N | <- collapsed count
|
|
+---+
|
|
+---------------------------+
|
|
| Toast 3 (oldest visible) |
|
|
+---------------------------+
|
|
+---------------------------+
|
|
| Toast 2 |
|
|
+---------------------------+
|
|
+---------------------------+
|
|
| Toast 1 (newest) |
|
|
+---------------------------+
|
|
```
|
|
|
|
- Max 3 visible; older toasts collapse into a "+N more" pill
|
|
- New toasts enter from the bottom with `--ease-spring` (slight overshoot)
|
|
- Dismissed toasts slide out to the right with `--ease-in`
|
|
- Z-index: `var(--z-toast)` (60) — above modals, below command palette
|
|
|
|
### Source
|
|
|
|
- `packages/web/src/components/ui/sonner.tsx` (shadcn/ui generated — configure here)
|
|
- Toaster mount point: `packages/web/src/App.tsx` or root layout
|
|
|
|
---
|
|
|
|
## 9. `<Badge>`
|
|
|
|
Inline status label rendered as a small colored pill. Used for entity status
|
|
(RUNNING, ACTIVE, BLOCKED), category labels (execute, verify), tab counts,
|
|
and filter chips.
|
|
|
|
### Props
|
|
|
|
```ts
|
|
interface BadgeProps {
|
|
/** Visual variant controlling color scheme. */
|
|
variant:
|
|
| 'active' // blue bg — running, in_progress, building
|
|
| 'success' // green bg — completed, connected, done
|
|
| 'warning' // amber bg — waiting, exhausted, pending_approval
|
|
| 'error' // red bg — crashed, failed, disconnected
|
|
| 'neutral' // grey bg — stopped, idle, pending, exited
|
|
| 'urgent' // purple bg — blocked
|
|
| 'outline' // border-only, no fill — provider labels, category tags
|
|
| 'secondary'; // muted fill — mode labels, secondary metadata
|
|
/** Badge text content. Rendered uppercase in status badges, as-is for categories. */
|
|
children: React.ReactNode;
|
|
/** Size variant. Default: 'sm'. */
|
|
size?: 'sm' | 'xs';
|
|
/** Optional leading icon (rendered at 12px for xs, 14px for sm). */
|
|
icon?: React.ReactNode;
|
|
/**
|
|
* When true, text is rendered in uppercase with letter-spacing.
|
|
* Use for status badges (RUNNING, ACTIVE). Default: false.
|
|
*/
|
|
uppercase?: boolean;
|
|
}
|
|
```
|
|
|
|
### Visual Variants
|
|
|
|
```
|
|
Status badges (uppercase=true):
|
|
[RUNNING] <- variant="active", blue bg
|
|
[ACTIVE] <- variant="active", blue bg
|
|
[DONE] <- variant="success", green bg
|
|
[WAITING] <- variant="warning", amber bg
|
|
[CRASHED] <- variant="error", red bg
|
|
[STOPPED] <- variant="neutral", grey bg
|
|
[BLOCKED] <- variant="urgent", purple bg
|
|
[QUEUED] <- variant="active", blue bg
|
|
|
|
Category badges (uppercase=false, variant="outline"):
|
|
[execute] <- outline, no fill
|
|
[verify] <- outline, no fill
|
|
[research] <- outline, no fill
|
|
|
|
Provider/mode badges (uppercase=false):
|
|
[claude] <- variant="outline"
|
|
[execute] <- variant="secondary"
|
|
|
|
Tab count badges (size="xs"):
|
|
[*3] <- variant="active", xs size (agents tab)
|
|
[2] <- variant="warning", xs size (inbox tab)
|
|
|
|
Filter chips (with dismiss):
|
|
[running x] <- variant="active" with trailing X button
|
|
```
|
|
|
|
### Size Variants
|
|
|
|
| Size | Font | Padding | Border Radius | Line Height |
|
|
|------|------|---------|---------------|-------------|
|
|
| `sm` (default) | `text-xs` (12px) | `px-2 py-0.5` | `rounded-sm` (2px) | 20px |
|
|
| `xs` | `text-[10px]` | `px-1.5 py-px` | `rounded-sm` (2px) | 16px |
|
|
|
|
### Color Mapping (uses status tokens from theme)
|
|
|
|
| Variant | Background | Text | Border |
|
|
|---------|-----------|------|--------|
|
|
| `active` | `bg-status-active-bg` | `text-status-active-fg` | `border-status-active-border` |
|
|
| `success` | `bg-status-success-bg` | `text-status-success-fg` | `border-status-success-border` |
|
|
| `warning` | `bg-status-warning-bg` | `text-status-warning-fg` | `border-status-warning-border` |
|
|
| `error` | `bg-status-error-bg` | `text-status-error-fg` | `border-status-error-border` |
|
|
| `neutral` | `bg-status-neutral-bg` | `text-status-neutral-fg` | `border-status-neutral-border` |
|
|
| `urgent` | `bg-status-urgent-bg` | `text-status-urgent-fg` | `border-status-urgent-border` |
|
|
| `outline` | `transparent` | `text-foreground` | `border border-border` |
|
|
| `secondary` | `bg-secondary` | `text-secondary-foreground` | none |
|
|
|
|
### Entity-to-Variant Mapping
|
|
|
|
Use this table when deciding which variant to assign. This is the canonical
|
|
source — do not invent new mappings without adding them here.
|
|
|
|
| Entity | State | Variant | Label |
|
|
|--------|-------|---------|-------|
|
|
| Agent | running | `active` | RUNNING |
|
|
| Agent | waiting_for_input | `warning` | WAITING |
|
|
| Agent | completed | `neutral` | COMPLETED |
|
|
| Agent | crashed | `error` | CRASHED |
|
|
| Agent | stopped | `neutral` | STOPPED |
|
|
| Task | in_progress | `active` | RUNNING |
|
|
| Task | completed | `success` | DONE |
|
|
| Task | pending | `neutral` | PENDING |
|
|
| Task | pending_approval | `warning` | APPROVAL |
|
|
| Task | blocked | `urgent` | BLOCKED |
|
|
| Task | queued | `active` | QUEUED |
|
|
| Task | failed | `error` | FAILED |
|
|
| Initiative | active | `active` | ACTIVE |
|
|
| Initiative | completed | `success` | COMPLETED |
|
|
| Initiative | archived | `neutral` | ARCHIVED |
|
|
| Initiative mode | review | `warning` | REVIEW |
|
|
| Initiative mode | planning | `secondary` | PLANNING |
|
|
| Preview | building | `active` | BUILDING |
|
|
| Preview | running | `success` | RUNNING |
|
|
| Preview | failed | `error` | FAILED |
|
|
| Preview | stopped | `neutral` | STOPPED |
|
|
| Account | active | `success` | -- (no badge) |
|
|
| Account | exhausted | `warning` | EXHAUSTED |
|
|
|
|
### Accessibility
|
|
|
|
- Badge text is readable by screen readers (no `aria-hidden`)
|
|
- Color is never the sole differentiator — the text label provides meaning
|
|
- Sufficient contrast ratios are ensured by the status token pairs (bg/fg)
|
|
|
|
### Source
|
|
|
|
- `packages/web/src/components/Badge.tsx` (proposed — or extend shadcn/ui `badge.tsx`)
|
|
|
|
---
|
|
|
|
## 10. `<KeyboardShortcutHint>`
|
|
|
|
Tooltip component that displays the keyboard shortcut for an action. Used in
|
|
button tooltips, command palette results, and the `?` shortcut help overlay.
|
|
|
|
### Props
|
|
|
|
```ts
|
|
interface KeyboardShortcutHintProps {
|
|
/** The key combination. Platform-aware: automatically swaps Cmd/Ctrl.
|
|
* Format: modifier keys joined with '+'. Examples: "Cmd+K", "Shift+N", "Esc", "1"
|
|
*/
|
|
keys: string;
|
|
/** Tooltip text shown before the shortcut. e.g., "Search" renders as "Search ⌘K" */
|
|
label?: string;
|
|
/**
|
|
* Rendering mode:
|
|
* - 'tooltip': renders inside a shadcn Tooltip (default). Wraps `children`.
|
|
* - 'inline': renders as inline <kbd> elements (for command palette results, help overlay).
|
|
*/
|
|
mode?: 'tooltip' | 'inline';
|
|
/** The element that triggers the tooltip. Only used in 'tooltip' mode. */
|
|
children?: React.ReactNode;
|
|
}
|
|
```
|
|
|
|
### Platform Detection
|
|
|
|
```ts
|
|
// Detect platform once at module level
|
|
const isMac = navigator.platform?.startsWith('Mac') ||
|
|
navigator.userAgent?.includes('Mac');
|
|
|
|
// Key symbol mapping
|
|
const KEY_SYMBOLS: Record<string, string> = {
|
|
'Cmd': isMac ? '⌘' : 'Ctrl',
|
|
'Ctrl': isMac ? '⌃' : 'Ctrl',
|
|
'Alt': isMac ? '⌥' : 'Alt',
|
|
'Shift': '⇧',
|
|
'Enter': '↵',
|
|
'Backspace': '⌫',
|
|
'Esc': 'Esc',
|
|
'Tab': '⇥',
|
|
'ArrowUp': '↑',
|
|
'ArrowDown': '↓',
|
|
'ArrowLeft': '←',
|
|
'ArrowRight':'→',
|
|
};
|
|
```
|
|
|
|
### Tooltip Mode (default)
|
|
|
|
Wraps a trigger element with a shadcn `<Tooltip>`. The tooltip content
|
|
shows `label` (if provided) followed by the key combination in `<kbd>` pills.
|
|
|
|
```
|
|
Hovering over the Cmd+K button:
|
|
|
|
+-----+
|
|
| ⌘K | <- trigger element (the button)
|
|
+-----+
|
|
|
|
|
+-------------+
|
|
| Search ⌘ K | <- tooltip: label + kbd pills
|
|
+-------------+
|
|
```
|
|
|
|
```
|
|
Hovering over a tab:
|
|
|
|
+----------+
|
|
| Content | <- trigger (tab)
|
|
+----------+
|
|
|
|
|
+-----------+
|
|
| Content 1 | <- tooltip: label + key
|
|
+-----------+
|
|
```
|
|
|
|
### Inline Mode
|
|
|
|
Renders `<kbd>` pills directly in the flow. Used in command palette results
|
|
and the keyboard shortcut help overlay.
|
|
|
|
```
|
|
Command palette result with shortcut:
|
|
|
|
> Go to Agents ⌘ 2
|
|
^^^
|
|
inline <kbd> pills
|
|
|
|
Shortcut help overlay row:
|
|
|
|
Next file j
|
|
Previous file k
|
|
Toggle resolve r
|
|
Search ⌘ K
|
|
```
|
|
|
|
### `<kbd>` Pill Styling
|
|
|
|
```
|
|
Individual key:
|
|
+---+
|
|
| K | <- single character key
|
|
+---+
|
|
|
|
+------+
|
|
| Ctrl | <- modifier key (text on non-Mac)
|
|
+------+
|
|
|
|
+---+
|
|
| ⌘ | <- modifier key (symbol on Mac)
|
|
+---+
|
|
```
|
|
|
|
- Each key segment is a separate `<kbd>` element
|
|
- `<kbd>` styling: `inline-flex items-center justify-center min-w-[20px] h-5
|
|
px-1.5 rounded-sm bg-muted border border-border text-[11px] font-mono
|
|
text-muted-foreground leading-none`
|
|
- Multiple keys separated by `gap-0.5` (2px)
|
|
- Modifier+key combinations: `⌘` `K` (two separate pills, no `+` separator)
|
|
|
|
### Usage
|
|
|
|
| Location | Mode | Keys | Label |
|
|
|----------|------|------|-------|
|
|
| Header Cmd+K button | tooltip | `Cmd+K` | "Search" |
|
|
| Tab tooltips (1-4) | tooltip | `1`, `2`, `3`, `4` | Tab name |
|
|
| Command palette results | inline | varies | -- |
|
|
| `?` shortcut help overlay | inline | all shortcuts | Action name |
|
|
| Inbox send button | inline | `Cmd+Enter` | -- |
|
|
| Plan tab add task | tooltip | `N` | "New task" |
|
|
|
|
### Accessibility
|
|
|
|
- `<kbd>` elements are semantic HTML — screen readers announce them as keyboard input
|
|
- Tooltips use `role="tooltip"` via shadcn Tooltip
|
|
- Platform-specific symbols (⌘, ⌃, ⌥) have appropriate `aria-label` fallbacks
|
|
(`aria-label="Command"`, `aria-label="Control"`, `aria-label="Option"`)
|
|
|
|
### Source
|
|
|
|
- `packages/web/src/components/KeyboardShortcutHint.tsx` (proposed)
|
|
|
|
---
|
|
|
|
## Cross-Reference Index
|
|
|
|
Quick lookup of which shared components are used on each page.
|
|
|
|
| Component | app-layout | initiatives-list | initiative-detail | content-tab | plan-tab | execution-tab | review-tab | agents | inbox | settings |
|
|
|-----------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|
| SaveIndicator | | | | x | x | | | | | |
|
|
| EmptyState | | x | | | x | x | | x | x | x |
|
|
| ErrorState | | x | x | | x | | x | x | | x |
|
|
| CommandPalette | x | | | | | | | | | |
|
|
| ThemeToggle | x | | | | | | | | | |
|
|
| StatusDot | x | | | | | x | | x | x | x |
|
|
| SkeletonLoader | | x | x | x | | | | x | x | x |
|
|
| Toast | x | | | | | x | | | x | x |
|
|
| Badge | | x | x | | | x | x | x | | x |
|
|
| KeyboardShortcutHint | x | x | x | | x | x | x | x | x | |
|
|
|
|
---
|
|
|
|
## Design Review Notes
|
|
|
|
### Existing components: what was already solid
|
|
|
|
1. **SaveIndicator** — the `persistent` prop with timestamp display is exactly right.
|
|
High-stakes contexts (page content editing) need ongoing confirmation; lightweight
|
|
contexts (phase descriptions) do not. The `Intl.DateTimeFormat` call is a good
|
|
detail. One nit: consider adding a `fadeDuration` prop (default 2000ms) for
|
|
contexts where the fade timing needs tuning. Not critical — the current hardcoded
|
|
2s is fine for now.
|
|
|
|
2. **EmptyState** — the dual-action pattern with primary/secondary buttons is well
|
|
spec'd. The button variant assignments (default vs outline) are correct. The
|
|
usage table covering 7 pages with specific copy is useful for implementation.
|
|
The "without description" variant prevents over-engineering simple empty states.
|
|
|
|
3. **ErrorState** — the expandable details section with `<details>/<summary>` is
|
|
the right call. Using native HTML disclosure is simpler than a state toggle and
|
|
works without JS. The `errorCode` displayed in `font-mono` is good for bug
|
|
reports. One gap: there is no "Copy error details" button for users filing
|
|
issues. Consider adding a clipboard-copy icon inside the details panel.
|
|
|
|
4. **CommandPalette** — this section is thorough. The fuzzy matching behavior
|
|
(whitespace-split AND-logic, no typo tolerance) is a good conservative default
|
|
for a precision-oriented tool. The result ranking (prefix > position > recency)
|
|
is sound. Recent items in `sessionStorage` with LRU eviction is correct — these
|
|
should not persist across sessions. The "no results" fallback with static
|
|
navigation links prevents dead ends.
|
|
|
|
5. **ThemeToggle** — the dynamic `"System (Dark)"` / `"System (Light)"` tooltip
|
|
resolving `prefers-color-scheme` is the right UX. Users who pick "System" and
|
|
see dark mode need to know it is working as intended, not broken. Reading `isDark`
|
|
from ThemeProvider context is the clean implementation path.
|
|
|
|
### Added components: rationale
|
|
|
|
6. **StatusDot** — this was the most glaring omission. The dot appears on every
|
|
data-driven page, in at least 6 different contexts, and was being described
|
|
ad-hoc in agents.md, app-layout.md, inbox.md, execution-tab.md, and dialogs.md.
|
|
Without a shared spec, each page would implement its own dot with slightly
|
|
different sizes, animations, and accessibility. The `pulse` prop is intentionally
|
|
not automatic per status — the consuming component decides (e.g., health dot
|
|
pulses for 3s then stops; agent dot pulses indefinitely). The `label` prop is
|
|
required, not optional, because status-by-color-alone fails WCAG.
|
|
|
|
7. **SkeletonLoader** — every wireframe describes its own skeleton layout but none
|
|
referenced a shared primitive. The composite `SkeletonCard` pattern (with named
|
|
layouts like `agent-card`, `initiative-card`) prevents skeleton/content dimension
|
|
mismatches that cause layout jumps. The shimmer animation is standardized at
|
|
1.5s with theme token references.
|
|
|
|
8. **Toast** — app-layout.md already described Sonner configuration and critical
|
|
event toasts, but scattered across two sections. Centralizing the duration rules,
|
|
stacking behavior, and variant usage in one place prevents inconsistency. The
|
|
duration table (5s default, 8s for attention, persistent for action-required)
|
|
is a policy decision that should live in a shared spec, not be reinvented per page.
|
|
|
|
9. **Badge** — the most polymorphic component in the system. It appears as status
|
|
badges (RUNNING), category labels (execute), tab counts (*3), provider labels
|
|
(claude), and filter chips. Without a canonical variant-to-color mapping, every
|
|
developer picks their own colors. The entity-to-variant table is the single
|
|
source of truth — if a status is not in that table, it does not get a badge.
|
|
|
|
10. **KeyboardShortcutHint** — a keyboard-first tool that does not show its shortcuts
|
|
is just a tool. The `<kbd>` pill styling with platform-aware symbols (⌘ vs Ctrl)
|
|
is table stakes. The dual mode (tooltip wrapping trigger elements vs inline in
|
|
command palette results) covers both discovery patterns. The `aria-label`
|
|
fallbacks on Mac symbols prevent screen readers from announcing "unknown character."
|
|
|
|
### Open questions for implementation
|
|
|
|
- **StatusDot `pulse` and `prefers-reduced-motion`**: The spec says disable animation,
|
|
but should it fall back to a static solid dot or a subtly different visual (e.g.,
|
|
a ring outline)? A static dot for a running agent looks identical to a completed
|
|
agent to users with reduced-motion preferences. Recommendation: use a double-ring
|
|
outline as the reduced-motion fallback for `pulse=true`.
|
|
|
|
- **Badge filter chips**: The spec mentions `[running x]` with a dismiss button but
|
|
does not fully define the dismiss interaction. This depends on the filter bar
|
|
implementation in initiatives-list.md and agents.md. Defer the dismissible badge
|
|
variant until those filter patterns are finalized.
|
|
|
|
- **SkeletonCard dimension matching**: The spec says skeleton dimensions must match
|
|
real card dimensions. In practice, this requires the skeleton and card to share
|
|
a CSS class or dimension constant. Consider extracting card height values into
|
|
CSS custom properties (e.g., `--card-height-agent: 112px`) referenced by both
|
|
the real card and its skeleton.
|