# 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. `` 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. `` 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., , , 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., — rendered before label }; secondaryAction?: { label: string; // e.g., "Add Phase" onClick: () => void; icon?: React.ReactNode; // e.g., }; } ``` When both `action` and `secondaryAction` are provided, the primary action renders as `` ### 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. `` 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. `` 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 `` 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. `` 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 `` 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. `` 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, `` 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. `` (via Sonner) Global notification toasts for asynchronous feedback. Rendered by the `` 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) ``` ### 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. `` 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. `` 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 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 = { '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 ``. The tooltip content shows `label` (if provided) followed by the key combination in `` 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 `` 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 pills Shortcut help overlay row: Next file j Previous file k Toggle resolve r Search ⌘ K ``` ### `` 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 `` element - `` 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 - `` 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 `
/` 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 `` 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.