EDDI Manager UI
Black & gold admin dashboard. React 19 + Tailwind CSS v4 + CSS-variable tokens.
Style with Tailwind utilities that read tokens. Never hand-write CSS, never invent a
class system, never use raw hex.
What already exists — import it
src/components/ui/ — primitives
| Component |
Notes |
Button |
variant: primary (default) · secondary · destructive · outline · ghost · link. size: sm · md (default) · lg · icon. asChild for link buttons. Lucide icon as a child auto-sizes to 16px. |
Badge |
variant: default (gold) · secondary · success · warning · destructive · outline. Pill, text-xs font-semibold. |
Card |
Compose Card > CardHeader (CardTitle, CardDescription) + CardContent + CardFooter. rounded-xl border bg-card shadow-sm; header is p-5 pb-0, content p-5. |
Input |
h-10 rounded-lg, gold focus ring. Plain InputHTMLAttributes. |
Skeleton |
Loading placeholder. |
AccessibleDialog |
Focus-trapped modal. |
AlertDialog |
Confirm/destructive prompt: open, onOpenChange, title, description, onConfirm, confirmLabel, cancelLabel, isPending, variant (destructive | warning), plus children for extra controls between description and buttons (e.g. a "permanently delete" checkbox). Use for every delete. |
UnsavedChangesDialog |
Discard-changes confirm. Pair with useUnsavedChangesGuard. |
DropdownMenu |
Radix wrapper. |
ErrorBoundary |
Wrap risky subtrees. |
StreamBadge |
Live/streaming indicator. |
src/components/shared/ — app-level
| Component |
Use it for |
EmptyState |
icon (Lucide), title, description?, actionLabel?, onAction?. Dashed-border box, py-16. |
ErrorState |
message, onRetry?, retryLabel?. Destructive-tinted box. |
ViewToggle |
Card ↔ list switch. Already keyboard-accessible (arrow keys, radiogroup). |
BackLink |
Detail-page back nav. Takes only to / label — no className. |
AgentPicker, SecretKeyPicker |
Async pickers, already wired to react-query. |
ResourceTypeBadge |
type slug → per-type color chip (rules amber, apicalls green, llm pink, …). |
ActionBadge |
Diff actions: CREATE · UPDATE · SKIP · CONFLICT. |
InfiniteScrollSentinel |
Intersection-observer load-more trigger. |
CommandPalette |
Global Ctrl+K. Opens via its store, not props. |
CreateOrWizardDialog |
"Quick create or launch the wizard" fork. |
ModeSwitcher, RefetchErrorNotice |
Mode switch; background-refetch failure notice. |
UpdateCheckCard |
"Is a newer EDDI released?" panel for the dashboard. No props. Opt-in — issues no request until the operator asks. |
src/components/layout/ — the shell
| Component |
Notes |
AppLayout |
Owns the whole frame: sidebar, top bar, drawers, onboarding, and <main> with p-6, max-w-screen-2xl and an @container/main context. A page renders its body only. |
Sidebar |
collapsed / onToggle. Four collapsible nav sections (persisted to eddi-sidebar-sections), Manager/Workforce switch, approvals count badge, external links, help menu, version footer. Uses the sidebar-* tokens, not the page tokens. |
TopBar |
onMenuClick / sidebarVisible. Breadcrumb, command-palette trigger, theme and chat controls. |
PlatformStatus |
Backend connectivity pill with a click-to-expand popover (instance, latency, last checked). |
PageLoader |
Route-level skeleton. Use it for lazy-route fallbacks, not for in-page loading. |
MockDataBanner |
Demo-mode strip; self-hides unless MSW is active. |
UpdateBanner |
New-release strip, mounted by AppLayout. No props; self-hides unless the opt-in check is on and an update is available. |
ThemeProvider |
Light/dark; toggles the dark class. Wrap tests and previews in it. |
Tokens
Declared in @theme in src/index.css; .dark overrides them. Use the semantic name.
| Class suffix |
Light |
Meaning |
primary / primary-foreground |
#f59e0b / #0c0a09 |
brand gold + text on gold |
background / foreground |
#fafaf9 / #1c1917 |
page + body text |
card / card-foreground |
#ffffff / #1c1917 |
card surface |
secondary / secondary-foreground |
#f5f5f4 / #1c1917 |
muted surface, hover fills |
muted / muted-foreground |
#f5f5f4 / #78716c |
subtle surface, secondary text |
border, input |
#e7e5e4 |
hairlines, field borders |
destructive / destructive-foreground |
#dc2626 / #fff |
danger |
accent |
#fbbf24 |
brighter gold |
sidebar / sidebar-foreground |
#ffffff / #44403c |
sidebar surface + text |
sidebar-border |
#e7e5e4 |
sidebar hairlines |
sidebar-accent / -foreground |
#b45309 / #ffffff |
active nav; dark mode restores bright gold #f59e0b on #0c0a09 |
Other constants: --radius: 0.5rem (rounded-lg fields/buttons, rounded-xl cards and
containers, rounded-full badges), --font-sans is Noto Sans Variable with per-script
fallbacks — do not set another font.
Status colors outside the token set (emerald for success/deploy, amber for dirty state,
blue for update) exist in a few places. Reuse the existing pattern rather than inventing
a new palette: text-emerald-600 dark:text-emerald-400, bg-amber-100 … dark:bg-amber-900/30.
Examples
Card with status:
<Card className="max-w-md">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>{agent.name}</CardTitle>
<Badge variant="success">{t("agents.deployed", "Deployed")}</Badge>
</div>
<CardDescription>{agent.description}</CardDescription>
</CardHeader>
<CardContent className="text-sm text-muted-foreground">…</CardContent>
<CardFooter className="gap-2">
<Button size="sm">{t("common.open", "Open")}</Button>
<Button size="sm" variant="outline">{t("common.configure", "Configure")}</Button>
</CardFooter>
</Card>
Search field (note ps-9 and start-3, not pl-9/left-3):
<div className="relative flex-1 max-w-sm">
<Search className="absolute start-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input className="ps-9" placeholder={t("x.search", "Search…")} value={q}
=> setQ(e.target.value)} data-testid="x-search" />
</div>
Do not
- Do not add a shadcn/ui component because it "looks the same". If the primitive is
missing, write it into
src/components/ui/ in the house style (cva + cn(), tokens,
forwardRef, displayName) so it is reusable and syncable.
- Do not use raw hex or Tailwind's default palette for brand color —
bg-amber-500 is
not bg-primary. (Third-party brand marks like Slack's #4A154B are the exception.)
- Do not restyle a variant at the call site. Add the variant to the
cva config.
- Do not use directional spacing (
pl-, pr-, ml-, mr-, left-, right-,
text-left, text-right).
- Do not write a
.css file for component styling. src/index.css holds tokens, base
styles and a few genuinely global patterns (spotlight, container-query grids) — that is
the only place raw CSS belongs.
- Do not hardcode a font-family, shadow scale, or radius outside the tokens.
- Do not mix component and utility exports in one file —
react-refresh/only-export-components
fails the build.
- Do not ship an untranslated string.
1---2name: eddi-ui3description: EDDI Manager's UI vocabulary — the 25 existing components, their variant props, the brand tokens, and the styling rules. Load before writing or editing any component that renders.4---56# EDDI Manager UI78Black & gold admin dashboard. React 19 + Tailwind CSS v4 + CSS-variable tokens.9Style with Tailwind utilities that read tokens. Never hand-write CSS, never invent a10class system, never use raw hex.1112## What already exists — import it1314### `src/components/ui/` — primitives1516| Component | Notes |17|---|---|18| `Button` | `variant`: primary (default) · secondary · destructive · outline · ghost · link. `size`: sm · md (default) · lg · icon. `asChild` for link buttons. Lucide icon as a child auto-sizes to 16px. |19| `Badge` | `variant`: default (gold) · secondary · success · warning · destructive · outline. Pill, `text-xs font-semibold`. |20| `Card` | Compose `Card` > `CardHeader` (`CardTitle`, `CardDescription`) + `CardContent` + `CardFooter`. `rounded-xl border bg-card shadow-sm`; header is `p-5 pb-0`, content `p-5`. |21| `Input` | `h-10 rounded-lg`, gold focus ring. Plain `InputHTMLAttributes`. |22| `Skeleton` | Loading placeholder. |23| `AccessibleDialog` | Focus-trapped modal. |24| `AlertDialog` | Confirm/destructive prompt: `open`, `onOpenChange`, `title`, `description`, `onConfirm`, `confirmLabel`, `cancelLabel`, `isPending`, `variant` (`destructive` \| `warning`), plus `children` for extra controls between description and buttons (e.g. a "permanently delete" checkbox). Use for every delete. |25| `UnsavedChangesDialog` | Discard-changes confirm. Pair with `useUnsavedChangesGuard`. |26| `DropdownMenu` | Radix wrapper. |27| `ErrorBoundary` | Wrap risky subtrees. |28| `StreamBadge` | Live/streaming indicator. |2930### `src/components/shared/` — app-level3132| Component | Use it for |33|---|---|34| `EmptyState` | `icon` (Lucide), `title`, `description?`, `actionLabel?`, `onAction?`. Dashed-border box, `py-16`. |35| `ErrorState` | `message`, `onRetry?`, `retryLabel?`. Destructive-tinted box. |36| `ViewToggle` | Card ↔ list switch. Already keyboard-accessible (arrow keys, radiogroup). |37| `BackLink` | Detail-page back nav. Takes only `to` / `label` — no `className`. |38| `AgentPicker`, `SecretKeyPicker` | Async pickers, already wired to react-query. |39| `ResourceTypeBadge` | `type` slug → per-type color chip (rules amber, apicalls green, llm pink, …). |40| `ActionBadge` | Diff actions: CREATE · UPDATE · SKIP · CONFLICT. |41| `InfiniteScrollSentinel` | Intersection-observer load-more trigger. |42| `CommandPalette` | Global Ctrl+K. Opens via its store, not props. |43| `CreateOrWizardDialog` | "Quick create or launch the wizard" fork. |44| `ModeSwitcher`, `RefetchErrorNotice` | Mode switch; background-refetch failure notice. |45| `UpdateCheckCard` | "Is a newer EDDI released?" panel for the dashboard. No props. Opt-in — issues no request until the operator asks. |4647### `src/components/layout/` — the shell4849| Component | Notes |50|---|---|51| `AppLayout` | Owns the whole frame: sidebar, top bar, drawers, onboarding, and `<main>` with `p-6`, `max-w-screen-2xl` and an `@container/main` context. A page renders its body only. |52| `Sidebar` | `collapsed` / `onToggle`. Four collapsible nav sections (persisted to `eddi-sidebar-sections`), Manager/Workforce switch, approvals count badge, external links, help menu, version footer. Uses the `sidebar-*` tokens, not the page tokens. |53| `TopBar` | `onMenuClick` / `sidebarVisible`. Breadcrumb, command-palette trigger, theme and chat controls. |54| `PlatformStatus` | Backend connectivity pill with a click-to-expand popover (instance, latency, last checked). |55| `PageLoader` | Route-level skeleton. Use it for lazy-route fallbacks, not for in-page loading. |56| `MockDataBanner` | Demo-mode strip; self-hides unless MSW is active. |57| `UpdateBanner` | New-release strip, mounted by `AppLayout`. No props; self-hides unless the opt-in check is on and an update is available. |58| `ThemeProvider` | Light/dark; toggles the `dark` class. Wrap tests and previews in it. |5960## Tokens6162Declared in `@theme` in `src/index.css`; `.dark` overrides them. Use the semantic name.6364| Class suffix | Light | Meaning |65|---|---|---|66| `primary` / `primary-foreground` | `#f59e0b` / `#0c0a09` | brand gold + text on gold |67| `background` / `foreground` | `#fafaf9` / `#1c1917` | page + body text |68| `card` / `card-foreground` | `#ffffff` / `#1c1917` | card surface |69| `secondary` / `secondary-foreground` | `#f5f5f4` / `#1c1917` | muted surface, hover fills |70| `muted` / `muted-foreground` | `#f5f5f4` / `#78716c` | subtle surface, secondary text |71| `border`, `input` | `#e7e5e4` | hairlines, field borders |72| `destructive` / `destructive-foreground` | `#dc2626` / `#fff` | danger |73| `accent` | `#fbbf24` | brighter gold |74| `sidebar` / `sidebar-foreground` | `#ffffff` / `#44403c` | sidebar surface + text |75| `sidebar-border` | `#e7e5e4` | sidebar hairlines |76| `sidebar-accent` / `-foreground` | `#b45309` / `#ffffff` | active nav; dark mode restores bright gold `#f59e0b` on `#0c0a09` |7778Other constants: `--radius: 0.5rem` (`rounded-lg` fields/buttons, `rounded-xl` cards and79containers, `rounded-full` badges), `--font-sans` is Noto Sans Variable with per-script80fallbacks — do not set another font.8182Status colors outside the token set (emerald for success/deploy, amber for dirty state,83blue for update) exist in a few places. Reuse the existing pattern rather than inventing84a new palette: `text-emerald-600 dark:text-emerald-400`, `bg-amber-100 … dark:bg-amber-900/30`.8586## Examples8788Card with status:8990```tsx91<Card className="max-w-md">92 <CardHeader>93 <div className="flex items-center justify-between">94 <CardTitle>{agent.name}</CardTitle>95 <Badge variant="success">{t("agents.deployed", "Deployed")}</Badge>96 </div>97 <CardDescription>{agent.description}</CardDescription>98 </CardHeader>99 <CardContent className="text-sm text-muted-foreground">…</CardContent>100 <CardFooter className="gap-2">101 <Button size="sm">{t("common.open", "Open")}</Button>102 <Button size="sm" variant="outline">{t("common.configure", "Configure")}</Button>103 </CardFooter>104</Card>105```106107Search field (note `ps-9` and `start-3`, not `pl-9`/`left-3`):108109```tsx110<div className="relative flex-1 max-w-sm">111 <Search className="absolute start-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />112 <Input className="ps-9" placeholder={t("x.search", "Search…")} value={q}113 onChange={(e) => setQ(e.target.value)} data-testid="x-search" />114</div>115```116117## Do not118119- **Do not add a shadcn/ui component** because it "looks the same". If the primitive is120 missing, write it into `src/components/ui/` in the house style (`cva` + `cn()`, tokens,121 `forwardRef`, `displayName`) so it is reusable and syncable.122- **Do not use raw hex or Tailwind's default palette for brand color** — `bg-amber-500` is123 not `bg-primary`. (Third-party brand marks like Slack's `#4A154B` are the exception.)124- **Do not restyle a variant at the call site.** Add the variant to the `cva` config.125- **Do not use directional spacing** (`pl-`, `pr-`, `ml-`, `mr-`, `left-`, `right-`,126 `text-left`, `text-right`).127- **Do not write a `.css` file** for component styling. `src/index.css` holds tokens, base128 styles and a few genuinely global patterns (spotlight, container-query grids) — that is129 the only place raw CSS belongs.130- **Do not hardcode a font-family, shadow scale, or radius** outside the tokens.131- **Do not mix component and utility exports in one file** — `react-refresh/only-export-components`132 fails the build.133- **Do not ship an untranslated string.**