This repository was archived by the owner on Sep 17, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
docs: Claude Code UI docs + design-sync 21 -> 29, fully re-synced and verified in Claude Design #136
Merged
Merged
docs: Claude Code UI docs + design-sync 21 -> 29, fully re-synced and verified in Claude Design #136
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
af5fbde
docs: add Claude Code UI docs and extend design-sync to the app chrome
ginccc 9740f57
fix: remove double page padding, and correct the sidebar-token measur…
ginccc fd855d1
fix: error branches on every data-loading page, revive a dead token, …
ginccc fbfa1b1
Merge branch 'main' into docs/ui-handoff-and-design-sync
ginccc 3e14354
fix: address CodeRabbit review — prerequisite error paths, testids, i…
ginccc 762ae5c
Merge branch 'main' into docs/ui-handoff-and-design-sync
ginccc 25165fc
fix(design-sync): stub the operator drawer out of the bundle
ginccc cc38c8a
feat(design-sync): resync all 29 components, with real prop contracts
ginccc 7542185
docs(design-sync): critical-review fixes — contract comment, add-a-co…
ginccc 54ffdac
fix: address Copilot + CodeRabbit review — background-refetch gating,…
ginccc b20a88f
docs(eddi-screens): make the canonical error example follow its own g…
ginccc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| --- | ||
| name: eddi-data | ||
| description: EDDI Manager's data and plumbing conventions — routing, TanStack Query hooks, ApiClient, i18n propagation, MSW mocks and tests. Load when wiring a screen to real data or adding a route. | ||
| --- | ||
|
|
||
| # EDDI Manager data & plumbing | ||
|
|
||
| AGENTS.md is the authority; this is the working summary for building a screen. | ||
|
|
||
| ## Routing | ||
|
|
||
| React Router v7, declarative mode (no data router). Routes are declared in `src/app.tsx`; | ||
| pages render through `AppLayout`'s `<Outlet />`. Resource detail routes carry the version as | ||
| a query param: `/manage/channels/:id?version=2`. `src/__tests__/route-integrity.test.ts` | ||
| asserts every nav target resolves — add the route and the sidebar entry together. | ||
|
|
||
| ## Server state — TanStack Query v5 | ||
|
|
||
| One hook file per domain in `src/hooks/` (`use-channels.ts`, `use-agents.ts`, …). Pages | ||
| never call the API directly: | ||
|
|
||
| ```tsx | ||
| const { data: channels, isLoading, error, refetch } = useEnrichedChannelDescriptors(); | ||
| const deleteMutation = useDeleteChannel(); | ||
| await deleteMutation.mutateAsync({ id, version }); | ||
| ``` | ||
|
|
||
| Filtering/search is client-side `useMemo` over the query result when the list is small — | ||
| that is the existing pattern, not a server round-trip per keystroke. | ||
|
|
||
| ## API calls | ||
|
|
||
| Use `ApiClient` (`src/lib/api-client.ts`) for ordinary JSON — it injects the Keycloak auth | ||
| header. Base URL is always `window.location.origin`; never hardcode. Raw `fetch` is only | ||
| justified for SSE, binary/blob and `text/plain` bodies, and **must** spread | ||
| `api.getAuthHeader()` itself — a missing header is a 401 that only shows up once OIDC is on. | ||
| `secrets.ts` uses raw fetch for historical reasons; it is debt, not a pattern to copy. | ||
|
|
||
| ## UI state | ||
|
|
||
| Zustand for chat/debug stores; `useState`/`useCallback` everywhere else. No Redux. | ||
|
|
||
| ## i18n — blocking requirement | ||
|
|
||
| Every string: `t("namespace.key", "English fallback")`. Add the key to | ||
| `src/i18n/locales/en.json`, then propagate translations to all 10 other locales | ||
| (de, fr, es, ar, zh, th, ja, ko, pt, hi) **in the same commit**. Each editor gets its own | ||
| namespace (`llmEditor.*`, `rulesEditor.*`). Because Arabic ships, every layout must use | ||
| logical properties. | ||
|
|
||
| ## Tests | ||
|
|
||
| - Unit: Vitest + RTL in `src/pages/__tests__/`, `renderPage(type)` helper wraps | ||
| `MemoryRouter` + `QueryClient` + `ThemeProvider`. Assert on `data-testid`. | ||
| - Mocks: MSW handlers in `src/test/mocks/handlers.ts`. Specific GET handlers must be | ||
| registered **before** the generic `createResourceHandlers` block. Mock data should match | ||
| the backend Java model. | ||
| - E2E: Playwright in `e2e/` — including `rtl.spec.ts` and `theme.spec.ts`, so a new screen | ||
| must survive both Arabic and dark mode. | ||
|
|
||
| ## Gates before you call it done | ||
|
|
||
| ```bash | ||
| npm run test # Vitest | ||
| npm run build # includes tsc -b | ||
| ``` | ||
|
|
||
| Pre-commit runs `eslint --max-warnings 0` on staged files, then `npm run typecheck`. | ||
| Use `npm run typecheck` (`tsc -b`), never `tsc --noEmit` — `tsconfig.json` is a solution | ||
| file (`"files": []` plus two project references), so `--noEmit` resolves zero inputs and | ||
| exits 0 without checking anything. Never commit to `main`; branch first. Update | ||
| `HANDOFF.md` when a phase completes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,185 @@ | ||||||||||||||||||||||
| --- | ||||||||||||||||||||||
| name: eddi-screens | ||||||||||||||||||||||
| description: How an EDDI Manager page is actually built — the app shell, the list-page pattern, detail pages, the config-editor chrome, wizards, and loading/empty/error states. Load before creating or restructuring a page. | ||||||||||||||||||||||
| --- | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # EDDI Manager screen patterns | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Most pages live in `src/pages/` and render inside `AppLayout` (sidebar + top bar) via its | ||||||||||||||||||||||
| `<Outlet />`. The layout already supplies `p-6`, `max-w-screen-2xl`, scroll, and a | ||||||||||||||||||||||
| `@container/main` context — such a page renders its own content only, no shell, no width cap, | ||||||||||||||||||||||
| no page background. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| **Three surfaces deliberately render outside `AppLayout`** and own their full frame: | ||||||||||||||||||||||
| `landing-page.tsx` (`/welcome`), `agent-studio.tsx` (`/manage/studio/:agentId`, a full-screen | ||||||||||||||||||||||
| breakout), and everything under `src/pages/workforce/` (mounted on `WorkforceLayout`, a | ||||||||||||||||||||||
| standalone app with no Manager chrome). Check `src/app.tsx` before assuming the shell is | ||||||||||||||||||||||
| there — those pages *do* supply their own padding, and the rules below are about the | ||||||||||||||||||||||
| `AppLayout` ones. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Page skeleton | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Most pages are a single `space-y-6` column — 29 of the 40 files in `src/pages/`. | ||||||||||||||||||||||
| Reference: `src/pages/agents.tsx`. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ```tsx | ||||||||||||||||||||||
| <div className="space-y-6"> | ||||||||||||||||||||||
| {/* 1. Header: icon + title + subtitle, actions right */} | ||||||||||||||||||||||
| <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> | ||||||||||||||||||||||
| <div> | ||||||||||||||||||||||
| <h1 className="flex items-center gap-2 text-3xl font-bold text-foreground"> | ||||||||||||||||||||||
| <Bot className="h-8 w-8 text-primary" /> | ||||||||||||||||||||||
| {t("pages.agents.title", "Agents")} | ||||||||||||||||||||||
| </h1> | ||||||||||||||||||||||
| <p className="mt-1 text-muted-foreground"> | ||||||||||||||||||||||
| {t("pages.agents.subtitle", "Build and deploy conversational agents")} | ||||||||||||||||||||||
| </p> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| <div className="flex flex-wrap items-center gap-2"> | ||||||||||||||||||||||
| <Button variant="outline" data-testid="import-agent-btn">…</Button> | ||||||||||||||||||||||
| <Button onClick={() => setCreateOpen(true)} data-testid="create-agent-btn"> | ||||||||||||||||||||||
| <Plus className="h-4 w-4" /> | ||||||||||||||||||||||
| {t("createOrWizard.newAgent", "New Agent")} | ||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {/* 2. Optional guidance banner */} | ||||||||||||||||||||||
| {/* 3. Toolbar: search + ViewToggle */} | ||||||||||||||||||||||
| {/* 4. Content: loading → error → empty → card grid or table */} | ||||||||||||||||||||||
| {/* 5. Dialogs last */} | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| ``` | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Rules that hold across pages: | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| - **Do not add `p-6` to the page root.** `AppLayout`'s `<main>` already applies | ||||||||||||||||||||||
| `p-6` inside `@container/main mx-auto max-w-screen-2xl`, so a page that pads itself | ||||||||||||||||||||||
| renders at 48px. Every page currently complies; keep it that way. | ||||||||||||||||||||||
| - `h1` is `flex items-center gap-2 text-3xl font-bold text-foreground` with an | ||||||||||||||||||||||
| `h-8 w-8 text-primary` Lucide icon. **Heading size is genuinely mixed** — 22 `text-3xl` | ||||||||||||||||||||||
| against 18 `text-2xl` (plus 3 `text-xl` on dense detail headers). `text-3xl` is the one to | ||||||||||||||||||||||
| reach for on a new top-level page, but match the neighbouring screens rather than treating | ||||||||||||||||||||||
| either as absolute. | ||||||||||||||||||||||
| - The page's primary action is a `primary` Button top-right; secondary actions sit beside it | ||||||||||||||||||||||
| in a `flex flex-wrap items-center gap-2` group. Buttons already supply `gap-2` from `cva`. | ||||||||||||||||||||||
| - Toolbar: search on `flex-1`, `ViewToggle` after it. Persist the choice with | ||||||||||||||||||||||
| `getStoredViewMode(page)` / `setStoredViewMode(page, mode)` from | ||||||||||||||||||||||
| `src/components/shared/view-mode.ts` — all 6 pages with a `ViewToggle` do. | ||||||||||||||||||||||
| Where a result count is shown, it is a | ||||||||||||||||||||||
| `text-xs font-semibold uppercase tracking-wider text-muted-foreground` line above the grid. | ||||||||||||||||||||||
| - Inline guidance banners: `rounded-xl border border-primary/20 bg-primary/5 p-4`. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## The three content states, in this order | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Loading → error → empty → content, as guarded blocks (the `agents.tsx` form): | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ```tsx | ||||||||||||||||||||||
| // A failed *background* refetch keeps the last good data (see the gating rule | ||||||||||||||||||||||
| // below), so only a failed initial load has nothing left to show. | ||||||||||||||||||||||
| const loadFailed = isError && !data; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {isLoading && ( | ||||||||||||||||||||||
| <div className="cq-card-grid" data-testid="agents-loading"> | ||||||||||||||||||||||
| {Array.from({ length: 4 }).map((_, i) => ( | ||||||||||||||||||||||
| <div key={i} className="rounded-xl border border-border bg-card p-5 space-y-3"> | ||||||||||||||||||||||
| <Skeleton className="h-5 w-3/4" /> | ||||||||||||||||||||||
| <Skeleton className="h-4 w-1/2" /> | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| ))} | ||||||||||||||||||||||
| </div> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {loadFailed && ( | ||||||||||||||||||||||
| <ErrorState message={t("common.error", "Something went wrong")} | ||||||||||||||||||||||
| onRetry={() => refetch()} retryLabel={t("common.retry", "Retry")} /> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| {!isLoading && !loadFailed && items.length === 0 && ( | ||||||||||||||||||||||
| <EmptyState icon={Bot} | ||||||||||||||||||||||
| title={search ? t("common.noResults", "No results found") | ||||||||||||||||||||||
| : t("agents.empty", "No agents yet")} | ||||||||||||||||||||||
| description={!search ? t("agents.emptyDescription", "Use the wizard to create one.") : undefined} | ||||||||||||||||||||||
| actionLabel={!search ? t("agents.createAgent", "Create Agent") : undefined} | ||||||||||||||||||||||
| onAction={!search ? () => setCreateOpen(true) : undefined} /> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| ``` | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Use the `Skeleton` primitive inside a card-shaped wrapper, not a bare `animate-pulse` div. | ||||||||||||||||||||||
| `ErrorState` is in 19 pages, `EmptyState` in 9. Empty state distinguishes "no results for | ||||||||||||||||||||||
| this search" from "nothing exists yet", and only the latter offers the create action. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| **Every page that loads data has an error branch — keep it that way.** Falling through to | ||||||||||||||||||||||
| the empty state on a failed fetch is the recurring bug here: it tells the user "nothing | ||||||||||||||||||||||
| exists yet" or "data will appear automatically" when the truth is "we could not reach the | ||||||||||||||||||||||
| backend". Pick the shape by surface: | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| - `ErrorState` — replaces the container. For a page or panel with nothing else to show. | ||||||||||||||||||||||
| Carries `data-testid="error-state"` and `data-testid="error-state-retry"`; assert on those | ||||||||||||||||||||||
| rather than on the translated copy, which changes with the locale. | ||||||||||||||||||||||
| **Gate it on the data being absent** (`isError && !data`), not on `isError` alone: | ||||||||||||||||||||||
| TanStack Query keeps the last good result when a background refetch fails (and | ||||||||||||||||||||||
| `refetchOnWindowFocus` is on by default), so an ungated branch replaces a usable page | ||||||||||||||||||||||
| with an error over a focus blip. `isError` alone is only correct when there can be no | ||||||||||||||||||||||
| cached data. | ||||||||||||||||||||||
| - `RefetchErrorNotice` — a compact amber strip that keeps the last good data on screen. | ||||||||||||||||||||||
| For a *background* refetch failure on a polling page, or an inline control (a picker | ||||||||||||||||||||||
| inside a form) where a full error box would be out of scale. Pass an explicit `message` | ||||||||||||||||||||||
| when the initial load failed — its default wording says data is merely stale. | ||||||||||||||||||||||
| - A neutral "unknown" state — when the value drives a decision. `gdpr.tsx` renders a grey | ||||||||||||||||||||||
| "status unavailable" chip rather than its green "Processing Active" badge, and disables | ||||||||||||||||||||||
| the toggle, because reporting an unknown restriction state as a known-safe one is worse | ||||||||||||||||||||||
| than showing nothing. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Card grid vs table | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Card grid: `grid gap-4 sm:grid-cols-2 lg:grid-cols-3`. When the page can sit next to the | ||||||||||||||||||||||
| open chat drawer, prefer the container-query classes from `src/index.css` — `cq-card-grid` | ||||||||||||||||||||||
| (1→2→3→4) or `cq-stat-grid` (1→2→4) — which respond to the content area, not the viewport. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Table: wrap in `rounded-xl border border-border/50 overflow-hidden`, header row | ||||||||||||||||||||||
| `border-b bg-muted/50` with `text-start px-4 py-3 font-medium` cells, body rows | ||||||||||||||||||||||
| `border-b border-border/30 hover:bg-muted/30 cursor-pointer transition-colors`, whole row | ||||||||||||||||||||||
| navigates, IDs in `font-mono text-xs text-muted-foreground`, numeric/version columns | ||||||||||||||||||||||
| `text-end`. Give each row `data-testid={\`thing-row-\${id}\`}`. | ||||||||||||||||||||||
|
Comment on lines
+140
to
+144
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Use logical padding in the table example. Line [125] documents Proposed correction- header row `text-start px-4 py-3 font-medium`
+ header row `text-start ps-4 pe-4 py-3 font-medium`As per coding guidelines: Use logical properties and utilities for RTL support. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Detail pages | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| `BackLink` at the top, then the same header block (title + subtitle + actions), then | ||||||||||||||||||||||
| content in `Card`s. Deletes go through `AlertDialog` with `isPending` bound to the | ||||||||||||||||||||||
| mutation. Resource routes carry the version: `/manage/channels/:id?version=N`. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Config editors | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Never build editor chrome. `ConfigEditorLayout` | ||||||||||||||||||||||
| (`src/components/editors/config-editor-layout.tsx`) owns the Form↔JSON tabs, version | ||||||||||||||||||||||
| picker, compare, dirty indicator, Save / Discard / Save & Test, and the unsaved-changes | ||||||||||||||||||||||
| guard. A new editor supplies only the form body: | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ```tsx | ||||||||||||||||||||||
| <ConfigEditorLayout | ||||||||||||||||||||||
| typeName={t("rulesEditor.title", "Behavior Rules")} typeIcon={GitBranch} | ||||||||||||||||||||||
| resourceId={id} data={json} versions={versions} currentVersion={version} | ||||||||||||||||||||||
| onVersionChange={setVersion} onSave={save} onSaveAndDeploy={saveAndDeploy} | ||||||||||||||||||||||
| renderFormEditor={(parsed, onChange, readOnly) => ( | ||||||||||||||||||||||
| <RulesEditor data={parsed} onChange={onChange} readOnly={readOnly} /> | ||||||||||||||||||||||
| )} | ||||||||||||||||||||||
| /> | ||||||||||||||||||||||
| ``` | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Then register it in `EDITOR_MAP` (`src/components/editors/editor-registry.tsx`), add an MSW | ||||||||||||||||||||||
| handler, add i18n keys, add a test — the four steps in AGENTS.md §3. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Wizards | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| `agent-wizard.tsx` and `group-wizard.tsx` are the reference. Entry is | ||||||||||||||||||||||
| `CreateOrWizardDialog` (quick create vs. guided). Keep step state local; only commit on | ||||||||||||||||||||||
| finish. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ## Accessibility baked into the patterns | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Tab bars and toggles are `role="tablist"`/`radiogroup` with arrow-key handling and roving | ||||||||||||||||||||||
| `tabIndex` — copy the handler from `ConfigEditorLayout` or `ViewToggle` rather than writing | ||||||||||||||||||||||
| plain buttons. Icons that repeat a visible label get `aria-hidden="true"`; icon-only | ||||||||||||||||||||||
| controls get `aria-label` via `t()`. The global focus ring is already defined in | ||||||||||||||||||||||
| `src/index.css` — don't remove outlines. | ||||||||||||||||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| --- | ||
| name: eddi-ui | ||
| description: 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. | ||
| --- | ||
|
|
||
| # 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: | ||
|
|
||
| ```tsx | ||
| <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`): | ||
|
|
||
| ```tsx | ||
| <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} | ||
| onChange={(e) => 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.** |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.