feat(ui/design-system): codify rail-anchored layout invariants and palette

Adds a page-level design system spec for CCS dashboard Config pages,
without migrating any production page yet. Pure-spec PR: docs +
primitive treatments + DEV-only /_styleguide route. Production page
adoption is intentionally left for follow-up work, one page at a time.

Spec — `ui/docs/design-system.md`:

- §0 Layout invariants (NEVER VIOLATE): two-column shell, no second
  horizontal strip below the global topbar, sibling-pane top alignment,
  cliproxy as canonical reference, user-resizable form/json split, and
  content-fit unified rail (`w-fit min-w-[240px] max-w-[360px]`)
- §1 identity-strip patterns: HeroBar (home), rail-anchored (cliproxy),
  PageHeader (Monitor-only), Bespoke (health)
- §2 body archetypes: Config (rail + form + optional json) and Monitor
- §4 anti-patterns with rejected code samples (PageHeader-over-
  ConfigLayout, tab-bar offsetting json pane, redundant description
  band, blank vertical band)
- §5 color & accent rules using the existing Pampas/Crail palette only:
  1-accent-dot rule, sibling-pane differentiation, status pill spec,
  three-tier intensity ladder, sensitive-field treatment per §5g

Decisions log — `ui/docs/design-decisions.md`:

- v1.6: content-fit rail (replaces fixed 260px)
- v1.5: color & accent treatment for body panes
- v1.4: resizable form/json split
- v1.3: layout invariants and rail-anchored default for Config

Primitives — `ui/src/components/config-layout/`:

- ConfigLayout: rail rendered as content-fit `<aside>` within the
  unified envelope; form↔json split via react-resizable-panels (already
  in deps) with autoSaveId-driven per-page localStorage persistence
- FormPane: 1px Crail top strip on header + soft from-card gradient +
  inset bottom shadow for scroll-depth cue; body wash bg-muted/20 so
  FormSections read as elevated cards; footer bg-muted/40 anchors save
- FormSection: 2px Crail leading-edge stripe with three-tier intensity
  (default 30% / hover 70% / focus-within solid + ring + shadow) + 1.5px
  accent dot prefixing each title
- JsonPane: bg-muted/30 shell visually recesses the read-only pane vs
  the active form pane (bg-card); accent dot + status pill in header;
  inner <pre> sits inside an inset-shadow well; content stays plain
  (a dedicated JSON viewer with its own color coding will replace it
  later — pages MUST NOT add inline JSON tints in the meantime)
- ListPane: selected row uses three-tier intensity ladder (no stripe at
  rest, accent/30 stripe on hover, solid Crail stripe + bg-accent/10
  row tint + accent-tinted icon and count badge on selection)

Sensitive input primitive — `ui/src/components/ui/masked-input.tsx`:

Upgraded to match §5g: lock glyph prefixing label, "sensitive" accent
pill on the label row, accent focus ring (the only place the default
ring is overridden), accent-tinted reveal toggle on hover. Existing
consumers (settings-dialog, websearch, friendly-ui-section, env-editor-
section) inherit the treatment without code changes.

Shared Field — `ui/src/components/forms/field.tsx`:

Tiny self-documenting Field component referenced by /_styleguide demos.
Auto-detects sensitive labels (AUTH_TOKEN | API_KEY | SECRET | PASSWORD
| PRIVATE_KEY) and applies §5g treatment. Production pages can adopt
this as a follow-up; no production code currently consumes it.

Live styleguide — `ui/src/pages/_styleguide.tsx`:

DEV-only route (gated by import.meta.env.DEV in App.tsx) showcasing
every primitive in isolation plus composed Config + Monitor archetype
demos. Intro callout surfaces the §0 invariants. §1b/§2a/§2b demos are
canonical rail-anchored examples authors can copy. Demo content uses
realistic Anthropic-style env field sets so the visual density mirrors
the cliproxy reference.

What this PR does NOT do:

- No production page rewrites — `pages/{api,accounts,codex,copilot,
  cursor,droid,shared,updates,analytics,claude-extension,cliproxy-ai-
  providers}` are unchanged
- No test selector updates — the existing test suite passes (excepting
  the 2 pre-existing account-visual-groups failures already on dev)
- No HTML preview reports — the canonical visual reference is the live
  /_styleguide page
- No new dependencies — `react-resizable-panels` was already declared

Validation: typecheck + lint + format + build clean. Test suite green
(519/521 — 2 pre-existing dev failures unrelated to this PR).
This commit is contained in:
Tam Nhu Tran committed 2026-04-26 13:30:03 -04:00
1 parent fa34ee82a1
commit 53030891c6
10 files changed
+902 -151

No files matched your search

+93
View File
@@ -52,6 +52,99 @@ After the v1.1 restructure, the `health` page received a separate, focused redes
---
## v1.6 revision (2026-04-26) — content-fit rail (unified envelope, not fixed width)
Live review of the rail-anchored pattern on the API Profiles page surfaced a regression: at the previously-mandated fixed `260px` rail width, the rail header "API Profiles" wrapped onto two lines, the description band wrapped, and the action buttons crowded each other — i.e. the rail was overflowing its own content even though the system mandated that exact width.
**Decision:** the left rail is **unified across pages but content-fit**, not fixed-width. Same primitives, same style treatment (§5), same vertical structure (header → search → list → footer) — but the *width* adapts to its header content within a unified envelope:
- `w-fit` — natural width grows to the largest atomic header element (title + buttons stay on one row)
- `min-w-[240px]` — floor so a sparse rail (e.g. 1-section SectionRail with no badges) doesn't squeeze controls
- `max-w-[360px]` — cap so the rail never dominates the body when an entity label is unusually long; per-item `truncate` inside `ListPane` handles labels beyond that
Pages MUST NOT override this envelope. If a page legitimately needs a wider rail, that is a system-wide envelope change (one PR raises the cap for everyone — uniformity is the point).
**Why not user-resizable?** The form↔json split *is* user-resizable (§0e) because users have different reading-vs-editing preferences mid-task. The rail isn't, because rail content is page-author-controlled and should be sized by the page author at design time. Letting users drag the rail width adds drag affordances inside an identity strip (visual noise) without solving a real recurring need.
**Encoded as §0a** in `design-system.md` (mandatory layout invariant; the prior "fixed minimum width" wording in §0e was rewritten to reference the new envelope). `ConfigLayout` ships the envelope as `<aside className="w-fit min-w-[240px] max-w-[360px] …">` so every Config page inherits the behaviour for free — pages MUST NOT roll their own rail width.
---
## v1.5 revision (2026-04-26) — color & accent treatment for body panes
User feedback during v1.4 styleguide review: the form and JSON panes both rendered as flat white surfaces with no visual hierarchy or accent presence — the panes were technically correct but visually inert and indistinguishable from one another.
**Decision:** apply a structured accent treatment using only the existing Pampas/Crail palette tokens — no new hues introduced. The treatment is encoded into the primitives (`FormPane`, `FormSection`, `JsonPane`) so every Config page inherits the look automatically; pages MUST NOT roll their own bg/border/accent overrides.
**Concrete changes:**
- `FormSection` gains a 2px Crail leading-edge stripe (`before:bg-accent/30`, brightens to `accent/70` on hover) and a 1.5px accent dot prefixing each title — the "1-accent-dot rule"
- `FormPane` header gains a 1px `accent/40` top strip and a `from-card to-card/70` gradient; body wrapper uses `bg-muted/20` so FormSections (`bg-card/60`) read as elevated cards rather than floating; footer uses `bg-muted/40` to anchor the primary save action
- `JsonPane` shell shifts to `bg-muted/30` so the right pane is visually recessed compared to the form pane (active editing surface) → recessed reading surface; header gets the same 1px Crail strip + accent dot + a status pill (`editable` in accent tones, `read only` in neutral tones)
- The inner `<pre>` block in `JsonView` sits inside a `bg-card/80` `shadow-inner` well — gives the code a subtle embossed feel rather than floating on a flat surface
- New **§5 Color & accent usage** in `design-system.md` codifies the rules: token table, the 1-accent-dot rule, sibling-pane differentiation, status pill spec, error/destructive scope, and a 6-point checklist for new panes
**Why it matters:**
- The user is not a designer and asked for guardrails they can follow. §5's checklist + token table is meant to be copy-pastable so future page authors don't need taste — they follow the rules
- Keeping the palette intact (Pampas + Crail only) means brand consistency is preserved while every pane gains depth
- The accent dot/stripe pattern is recognizable: any Config page will now read as "CCS-shaped" at a glance, even before the user reads a single label
**Constraints:**
- No raw Tailwind colors (`bg-blue-*`, `text-emerald-*`, etc.) anywhere except the existing health-priority bespoke page (§1d) and severity-driven semantics
- All elevation done via `bg-card`, `bg-muted/*`, and `bg-card/*` opacity steps — never via shadow alone
- Hover states change opacity of an existing token, never hue
---
## v1.4 revision (2026-04-26) — resizable form / json split
User feedback during v1.3 styleguide review: a single fixed form/json ratio cannot serve both editing (wide form) and reading (wide json) — env blocks like the GLM provider example demand a wide form to enter long values, while debugging effective configuration demands a wide json pane.
**Decision:** the **form (middle) and json (right) panes of `ConfigLayout` are user-resizable** via a draggable horizontal divider. Left rail width stays fixed (rail owns identity and doesn't compete for body width). User's chosen ratio persists in `localStorage` per-page (`ccs.config-layout.<page>.split`).
**Constraints:**
- Minimum widths enforced (form ≥ 360px, json ≥ 320px) — neither pane collapses to unreadable
- Default split: form `flex-1`, json `~38–42%` of remaining width
- When `json` prop is omitted, form fills remaining width and no divider renders
- Below `<1024px` the layout collapses to tabs (existing behaviour); divider is irrelevant
- Resize handle is keyboard-accessible (arrow keys move 16px increments, Home/End jump to min/max)
**Encoded as §0e** in `design-system.md` (mandatory layout invariant) plus a rule update in §2a Config archetype. Implementation lives in `ConfigLayout` so every Config page inherits the behaviour for free — pages MUST NOT roll their own resize logic.
**Why an invariant, not a feature flag:** the canonical reference (`cliproxy`) and every migrated provider page show env blocks where one pane is consistently more interesting than the other depending on the task. A fixed ratio is wrong by construction; making the split adjustable is closer to "how IDEs already work" and removes a recurring papercut without expanding the design surface.
---
## v1.3 revision (2026-04-26) — layout invariants and rail-anchored default for Config
Phase 3 + Phase 4 (PR #1105) migrated 12 Config pages to the §1c `PageHeader` pattern stacked above `ConfigLayout`. Live review of the API Profiles page exposed the regression:
- The global topbar (ClaudeKit / Sponsor / Connected / locale / theme) already occupies one horizontal strip
- Adding `PageHeader` ("API Profiles" + description) below it creates a **second strip** that costs ~80px of vertical real estate
- The form pane's tab bar (`Environment / Info & Usage`) pushes the json pane down by another ~40px, so the right column starts well below the left
- Net effect: a large blank L-shaped band wraps the body, the canonical `cliproxy` "no top chrome, body fills viewport" feel is lost, and identity is duplicated between PageHeader and the rail
**Resolution:**
1. New mandatory **§0 Layout invariants** in `design-system.md` — two-column shell, full viewport height, no second horizontal strip, sibling panes share one top edge, `cliproxy.tsx` is the canonical Config reference
2. `§1c PageHeader` is **explicitly disallowed** above any `ConfigLayout`. It is reserved for Monitor pages with no left rail
3. Decision table updated: **all** Config pages (single-entity AND multi-entity) use `§1b Rail-anchored`. The previous row "Single-entity Config OR Monitor → PageHeader" is removed
4. New **§4 Anti-patterns** section documents the four concrete failure modes with rejected code samples (PageHeader-over-ConfigLayout, tab-bar offsetting json pane, redundant description band, blank band above columns)
5. Phase 3 + Phase 4 page migrations that adopted `PageHeader` are now **non-canonical** until refactored to rail-anchored. PR #1105 is held; merging requires a follow-up commit that strips `PageHeader` from every migrated Config page and folds identity into the rail
**Why:** the canonical reference (`cliproxy`) already proved rail-anchored works for dense provider configs. Phase 3 forgot the bottom-up principle from v1.1 ("the design system is restructured around three identity-strip patterns extracted from the existing canonical references") and re-imposed top-down chrome on pages whose left rail already carries identity. v1.3 reasserts the principle and encodes the layout invariants so the rule cannot drift again silently.
**Side effects:**
- `PageHeader` loses its remaining canonical references (Phase 3+4 pages) until a Monitor-without-rail page adopts it
- The `/_styleguide` Intro is updated to surface §0 invariants prominently
- Phase 3 + Phase 4 retrospective report should be filed in `plans/reports/` documenting which pages need rail-anchor refactor and the LOC delta
---
## How to revisit
If a decision turns out wrong in practice, update this doc and bump the affected primitive — don't silently drift. Each row above should be appended with a "Revised: <date> · <reason>" line if changed.
+210 -12
View File
@@ -8,6 +8,49 @@ Some pages legitimately need a bespoke design (the redesigned `health` page is t
---
## 0. Layout invariants (NEVER VIOLATE)
These rules precede archetype choice. When an archetype seems to require violating them, the archetype is wrong, not the rule. Code review rejects PRs that break §0.
### 0a. Two-column shell, full viewport height
Every Config page is a strict two-column shell:
- **Left column** is the unified identity pane (rail / list). It owns page identity (brand, primary CTA, entity selector, status footer).
- **Left column width is content-fit, not fixed.** The rail is unified in *style* and *primitives* across every Config page, but the actual width adapts to its header content (`w-fit min-w-[240px] max-w-[360px]`). The rail must never overflow or wrap its title — if the rail can't fit "API Profiles" + a "+ New" button on one line at 240px, it grows up to 360px. Beyond that, list-item labels truncate per-item. Pages MUST NOT override this envelope; if a page legitimately needs a wider rail, raise it as a system-wide envelope change (one PR adjusts the cap for everyone).
- **Right column** is the main content (form, or form + json). It fills the remaining width.
- Both columns share the **same top edge**, flush against the global topbar.
- Together they fill `100vh` minus the global topbar — no scrolled-up blank band above either pane, no "first page-height is just header".
### 0b. No horizontal strip below the global topbar
The only horizontal strip allowed at the top of the viewport is the **global topbar** (logo, ClaudeKit / Sponsor badges, connection status, locale, theme).
You MUST NOT stack a second strip below it — no `PageHeader`, no breadcrumb row, no description band, no KPI ribbon — when the body is a `ConfigLayout`. Identity for Config pages lives **inside the left rail**, where it costs zero vertical real estate.
### 0c. Pane top alignment
Within a `ConfigLayout`, all panes (left rail, form, json) share the **same top edge**. A tab bar inside the form pane MUST NOT push the json pane down — they are sibling columns, not parent/child. Tabs belong **inside** the form pane's own scroll area.
### 0d. The cliproxy page is the canonical Config reference
When a Config page disagrees with `pages/cliproxy.tsx` on layout shape, the page is wrong. Anything that produces a blank band above either column, a duplicated identity strip, or a top offset between sibling panes is a §0 violation regardless of which §1 pattern was claimed.
### 0e. Form and JSON panes are user-resizable
The **middle (form) and right (json) panes of `ConfigLayout` MUST be horizontally resizable** by the user via a draggable divider:
- The left rail is **unified across pages but content-fit** — same envelope, same primitives, but the actual width adapts to its header content. Constraints: `min-w-[240px]` (floor — header controls never squeeze into a wrap) and `max-w-[360px]` (cap — rail can't dominate the body even when an entity label is unusually long; per-item `truncate` inside `ListPane` handles labels beyond that). The rail is NOT user-resizable — only the form↔json split is.
- The form ↔ json divider is the **only adjustable split**. Users frequently want a wide form for entering env values OR a wide json pane for reading effective config — never both at the design-time defaults.
- **Default ratio: form ~45% / json ~55%** of the body width remaining after the rail. The json pane is **slightly larger by default** because the canonical cliproxy reference shows users spend more time reading effective configuration than editing one field at a time. Persist the user's chosen ratio per-page via `react-resizable-panels` `autoSaveId` (resolves to a `localStorage` key).
- Each pane has an enforced **minimum width** (form ≥ 360px, json ≥ 320px) so neither collapses to unreadable.
- When the json pane is omitted (`json={undefined}`), the form expands to fill the remaining width — no divider rendered.
- Below the `<1024px` breakpoint the layout collapses to tabs (Browse / Configure / JSON) — resizing is irrelevant in tab mode.
This satisfies the recurring need to widen one pane to inspect a long env block or read raw configuration without losing the rail-anchored shell.
---
## 1. Identity-strip patterns (pick one per page)
Three patterns cover every page in the dashboard. The choice depends on what your page already has.
@@ -61,7 +104,7 @@ Page identity (brand + page-level CTA + status) lives **inside the left rail**.
- The left rail's own header section (in-place markup, no extracted primitive yet — keep it bespoke until a 2nd page adopts the pattern)
- Recommended order in the rail: brand strip → primary CTA → entity list → status widget → footer summary
### 1c. `PageHeader` — title-row chrome
### 1c. `PageHeader` — title-row chrome (Monitor-only)
**Canonical reference:** none yet (was `health.tsx` until its bespoke redesign — see §1d).
@@ -74,10 +117,12 @@ Page identity (brand + page-level CTA + status) lives **inside the left rail**.
Traditional title row with description and trailing actions.
**Use it when:**
- The page does NOT fit either canonical hero
**MUST NOT use:** above any `ConfigLayout`. See §0b. This includes single-entity Config, multi-entity Config, and any list/form/json layout. Use **§1b Rail-anchored** instead — the rail owns identity, the body fills the viewport.
**Use it when (Monitor only):**
- The body archetype is **Monitor** (KPI row + grid) and the page has **no left rail**
- The description carries genuinely non-redundant context (last refresh, page hierarchy, filter state, version)
- Body archetype below benefits from a clear identity strip
- A `HeroBar` (§1a) does not fit because the page has more than 4 hero numbers or no clean inline-stat shape
**API:** `<PageHeader title description status actions />` — title + description on left, status badges + action buttons on right.
@@ -99,11 +144,14 @@ When a page's content shape demands its own hierarchy (priority-driven sections,
| Page shape | Identity strip |
|------------|---------------|
| Dashboard / overview with ≤4 hero stats | **HeroBar** (home pattern) |
| Single-entity Config (rail + form + optional json) | **Rail-anchored** (cliproxy pattern, no top chrome) |
| Multi-entity Config (3-pane: list/form/json) | **Rail-anchored** (cliproxy pattern, no top chrome) |
| Single-entity Config OR Monitor with a real hero viz | **PageHeader** + body archetype |
| Monitor with a hero viz **and no left rail** | **PageHeader** + Monitor body |
| Severity / priority-driven page with custom hierarchy | **Bespoke** (health pattern) |
| Wizard / login / dialog | None — bespoke shell |
> **Rule of thumb:** if your page has a left rail at all, it uses §1b Rail-anchored. `PageHeader` is reserved for the narrow case of a Monitor without a rail.
---
## 2. Body archetypes
@@ -133,7 +181,8 @@ Left rail = `ListPane` (multi-entity) or `SectionRail` (single-entity, with `Int
**Rules:**
- Save action lives **only** in `FormPane` footer
- `<1024px`: collapses to tabs (Browse | Configure | JSON)
- Form ↔ json split is **user-resizable** with persisted ratio (see §0e). Left rail width is fixed.
- `<1024px`: collapses to tabs (Browse | Configure | JSON) — divider is hidden in tab mode
- `JsonPane` is read-only by default; opt-in `editable` for cliproxy-style inline editing
### 2b. Monitor — KPI row + 12-col grid
@@ -166,11 +215,37 @@ Left rail = `ListPane` (multi-entity) or `SectionRail` (single-entity, with `Int
## 3. Composing a new page
**Config page (default for any provider / profile / account / api management page):**
```tsx
// Example: a new dashboard-style page
<PageShell>
<HeroBar … /> {/* or PageHeader, or rail-anchored identity */}
<MonitorLayout … /> {/* or ConfigLayout */}
<ConfigLayout
left={<ListPane … />} // rail owns identity (§1b) — brand, CTA, list, status
form={<FormPane …/>} // tabs (if any) live INSIDE this pane's scroll area
json={<JsonPane …/>} // optional; same top edge as form (§0c)
/>
</PageShell>
```
No `PageHeader`. No description band. The rail header carries the brand and section name; the rail footer carries status / counts.
**Dashboard / home page (no rail):**
```tsx
<PageShell>
<HeroBar … /> // §1a — one row, ≤4 inline stats
<MonitorLayout … /> // optional grid below
</PageShell>
```
**Monitor page without a rail:**
```tsx
<PageShell>
<PageHeader title description actions /> {/* §1c — only valid here */}
<MonitorLayout kpis={<KpiRow … />}>
<MonitorGrid>…</MonitorGrid>
</MonitorLayout>
</PageShell>
```
@@ -178,7 +253,130 @@ Target LOC for a new page: **~80** for typical config, **~120** for monitor with
---
## 4. When NOT to use either archetype
## 4. Anti-patterns (REJECT IN REVIEW)
### 4a. `PageHeader` stacked above `ConfigLayout`
❌ DO NOT:
```tsx
<PageShell>
<PageHeader title="API Profiles" description="Premium APIs, local runtimes, custom endpoints" />
<ConfigLayout left={…} form={…} json={…} />
</PageShell>
```
The PageHeader steals ~80px from the body, duplicates identity the rail already carries, and pushes the form + json panes below the fold. Use rail-anchored identity (§1b) — move the title into the rail header and remove the description band entirely. See §0b.
### 4b. Tab bar offsetting sibling panes
❌ DO NOT place a tab bar (e.g. `Environment / Info & Usage`) at the top of the form pane such that the json pane on the right starts ~40–60px lower. Form and json are **siblings** sharing one top edge (§0c). Tabs belong **inside** the form pane's scroll area, not above it as a separate row.
### 4c. Description bands that repeat the rail
❌ DO NOT add a description below the title that simply restates what the left rail's items already convey ("Premium APIs, local runtimes, custom endpoints" when the rail already lists those entities). If the rail shows it, the band is noise.
### 4d. Blank vertical band above either column
❌ DO NOT introduce padding, spacing, or a wrapper that produces a >24px blank band between the global topbar and the top of either column. The two columns of a `ConfigLayout` are flush against the global topbar (§0a). If a wrapper requires that band, the wrapper is wrong.
---
## 5. Color & accent usage
The CCS palette is **Pampas (warm cream) + Crail (terracotta orange)**, defined in `src/index.css` as CSS variables. **Never introduce new hues.** Every visual decision routes through these tokens:
| Token | Role | When to use |
|-------|------|-------------|
| `--background` (Pampas) | Page canvas | Outermost shell only |
| `--card` | Elevated surface | FormPane shell, JSON header bar, cards |
| `--card/60`, `--card/80` | Soft elevation | FormSection bg, gradient header tails |
| `--muted/20`, `--muted/30`, `--muted/40` | Pane wash, footer anchor, JSON shell | Differentiating sibling panes without a hard border |
| `--accent` (Crail) | Identity / focus | Section dot, top-edge 1px strip, primary CTA, status pill on key chrome |
| `--accent/30`, `--accent/40` | Whisper accent | Vertical stripe on FormSection edge, top-edge strip on header bars |
| `--accent/10` | Tint background | Status pill bg ("editable", "sensitive", "connected") |
| `--muted-foreground` | Secondary text | Field labels, descriptions, hint copy |
| `--destructive` | Error / danger | Form errors, "delete account" buttons, anti-pattern callouts |
| `--ring` | Focus outlines | NEVER override per-component — let `focus-visible:ring-*` use the token |
### 5a. The 1-accent-dot rule
Each FormSection gets **exactly one accent dot** (1.5px circle, `bg-accent`) prefixing its title, and **one 2px Crail stripe** on the leading edge. These are the only places `--accent` saturates inside the body.
The header bars (FormPane, JsonPane) carry a 1px `accent/40` strip at the top edge — a quiet tie-back to the Save button at the bottom.
If a section needs more attention (e.g. a connected status), use an **outline pill** with `bg-accent/10` + `text-accent` + `border-accent/30`, never a filled `bg-accent` block in the body.
### 5b. Differentiating sibling panes
Form pane (`bg-card`) and JSON pane (`bg-muted/30`) MUST be visually distinct without a hard border line:
- **Form pane** = elevated card surface, active editing zone
- **JSON pane** = recessed muted surface, read-only inspection zone
Inside the JSON pane, the `<pre>` block uses `bg-card/80` with `shadow-inner` so the code sits in a subtle embossed well — the user can tell at a glance which side is "yours to edit" and which is "computed for you to read".
### 5c. Status pills
Use small uppercase pills (10px font, `tracking-wider`, `rounded`, 1px border) for status. Two tones:
- **Active / accent**: `border-accent/40 bg-accent/10 text-accent` — for `editable`, `connected`, `sensitive`, `default`
- **Neutral / muted**: `border-border bg-muted/60 text-muted-foreground` — for `read only`, `unset`, `disabled`
Do not use `bg-emerald-*` / `bg-amber-*` / `bg-blue-*` for status unless the semantic IS the color (e.g. health severity tiers in §1d Bespoke). Default to the palette tokens.
### 5d. Errors and danger
`--destructive` is the **only** red in the system. Use it for:
- Inline form validation errors (`text-destructive` for messages, `border-destructive/40` on bad inputs)
- Anti-pattern callouts in docs (the `§0 invariants` callout in `/_styleguide` uses it)
- Truly destructive button actions ("Delete account", "Reset config")
Never tint a description band, an info banner, or a hover state with `--destructive`.
### 5e. Quick checklist for a new pane
- [ ] FormSections use `bg-card/60` with the leading accent stripe + accent dot before each title
- [ ] FormPane header has a 1px `accent/40` top strip and a `from-card to-card/70` gradient
- [ ] FormPane footer uses `bg-muted/40` to anchor the primary save action
- [ ] JsonPane shell uses `bg-muted/30`; header uses `bg-card/80` with the accent dot + status pill
- [ ] No raw `text-blue-500`, `bg-green-100`, etc. — only palette tokens
- [ ] Hover states change opacity of existing tokens, not hue (e.g. `before:bg-accent/30 hover:before:bg-accent/70`)
### 5f. Interactive states (the three-tier intensity ladder)
Every interactive element uses the same intensity ladder for accent presence:
| State | Accent intensity | Example |
|-------|------------------|---------|
| Default | `accent/30` | FormSection leading stripe at rest |
| Hover / scan | `accent/70` | FormSection stripe on mouse-over (peripheral cue) |
| Focus / active | `accent` solid + soft ring | FormSection on `focus-within`, ListPane selected row |
`FormSection` implements all three: stripe goes `30 → 70 → 100`, plus a soft `ring-accent/20` and shadow lift on focus-within. The user always knows which group "owns" their cursor.
`ListPane` selected row uses the same ladder: 3px Crail stripe on the leading edge + `bg-accent/10` row tint + accent-tinted icon and badge. Hover gives a `accent/30` stripe preview without the row tint. This visually links the selected entity to the FormSection treatment on the right.
Sticky headers (`FormPane.header`) include a soft inset bottom shadow so when the body scrolls under, depth is preserved without a hard divider line.
### 5g. Sensitive fields
Fields whose label matches `AUTH_TOKEN|API_KEY|SECRET|PASSWORD|PRIVATE_KEY` are **automatically** rendered as sensitive. The treatment:
- Lock glyph (`lucide-react/Lock`) prefixing the label, tinted `accent/70`
- "sensitive" status pill on the right of the label row (accent tones per §5c)
- `<input type="password">` by default with a reveal/hide eye-toggle on the right edge
- Focus ring uses `ring-accent/40` + `border-accent/50` (vs. neutral `ring-ring` for ordinary fields) — the only place a default focus ring is overridden
Pages MUST NOT roll their own sensitive-field UI. If the heuristic doesn't match a label, pass `sensitive` explicitly.
### 5h. Raw configuration content
JSON content inside `JsonPane` renders **plain** — no inline syntax highlighting from this design system. A dedicated JSON viewer with its own color coding will replace the `<pre>` in a follow-up; until then the **chrome** of the pane (shell, header strip, accent dot, status pill, embossed code well) carries the §5 treatment, and the **content** stays uncolored. Pages MUST NOT add inline JSON tints in the meantime.
---
## 6. When NOT to use either archetype
These remain bespoke and are out of scope:
- `/login` — minimal centered shell
@@ -187,6 +385,6 @@ These remain bespoke and are out of scope:
---
## 5. Decisions
## 7. Decisions
See [`design-decisions.md`](./design-decisions.md) for the resolved open questions and the v1.1 revision rationale.
See [`design-decisions.md`](./design-decisions.md) for the resolved open questions and the v1.1 / v1.2 / v1.3 / v1.4 / v1.5 / v1.6 revision rationale.