Three more issues raised by upstream review after the v1.7 fix push.
Each is encoded both in code and in spec so future drift can't
reintroduce them:
1. Required `storageKey` was the wrong fix for cross-page state bleed.
Forcing every callsite to pass `storageKey` made future
production-page migrations a build-breaking event for no real
safety benefit. Better engineering: keep the prop optional and
default to a key derived from `window.location.pathname` so each
route gets its own localStorage slot automatically. Pathname-
derived defaults are unique by construction; the cross-page bleed
risk only exists for hardcoded shared keys, which the doc warns
against. SSR-safe (falls back to a stable string when window is
unavailable). Pages can still pass an explicit `storageKey` to opt
out of pathname coupling (e.g. when sub-routes should share state).
2. `MaskedInput` `type` was overridable by callers, which defeated
the component's purpose for credentials. Two-layer fix:
- Compile-time: `MaskedInputProps` now extends
`Omit<InputHTMLAttributes, 'type'>` so callers literally cannot
pass `type`. The TS surface enforces the contract.
- Runtime: the `type` attribute on `<Input>` is now placed AFTER
the spread so even an `as`-cast bypass can't override it.
3. The reveal toggle was keyboard-inaccessible. Both `MaskedInput`
and `Field` had `tabIndex={-1}` on the eye-icon button, removing
it from the tab order. Keyboard-only users couldn't show or hide
the secret. Resolution:
- Drop `tabIndex={-1}` so the button joins the natural tab order.
- Add `aria-pressed={revealed}` so screen readers announce toggle
state.
- Add a focus-visible accent ring so the focused state is visible
without a mouse.
Decisions log: v1.8 entry records the rationale for each correction
and explicitly calls out that v1.7's "required storageKey" fix was
worse than the problem it tried to solve.
Validation: typecheck + lint + format + build clean. Tests 519/521
pass (2 pre-existing account-visual-groups failures on dev,
unrelated). Styleguide demos still pass storageKey explicitly so
behavior is unchanged for the only existing callsites.
18 KiB
Design System — Decisions Log
The 6 open questions from the brainstorm phase, resolved before implementation.
| # | Question | Decision | Rationale |
|---|---|---|---|
| 1 | Storybook vs in-app /_styleguide |
In-app /_styleguide |
Zero-config, lives in the repo, gated by import.meta.env.DEV. Storybook is a heavy second build pipeline for marginal benefit at this scale. |
| 2 | Archetype B name (Monitor vs Dashboard) | Monitor | "Dashboard" is the whole product. "Monitor" matches existing health/analytics framing and avoids overloading the term. |
| 3 | JsonPane editability scope | Read-only by default, opt-in editable prop |
Cliproxy is the only page that genuinely needs in-pane editing today. Opt-in keeps the API safe for codex/copilot/cursor (read-only). |
| 4 | Health terminal aesthetic | Keep as MonitorCard variant="terminal" |
Preserves the ccs health --watch feel. Implemented as opt-in variant, not a separate primitive. |
| 5 | i18n key namespace convention | Per-page namespaces (pages.<name>.*) + shared (common.*) |
Current i18n already uses ad-hoc per-page keys; this just formalizes it. Primitives use common.* so they're translation-stable across pages. |
| 6 | SectionRail activation | Scroll-spy via IntersectionObserver |
Preserves long-form config feel + shows all validation errors at once. Click-to-switch hides errors in inactive sections, which is worse UX for forms. |
v1.1 revision (2026-04-25) — identity-strip patterns
Phase 2 attempted to migrate home and cliproxy to a one-size-fits-all PageShell + PageHeader chrome. Both regressed on density:
- home had a single-row hero (logo + title + version + 4 inline stats) — splitting it into a stacked PageHeader + KpiRow doubled vertical footprint and lost scannability
- cliproxy had identity in the left rail — adding a top PageHeader duplicated branding and stole ~80px from the 3-pane body
Resolution: the design system is restructured around three identity-strip patterns extracted from the existing canonical references:
| # | Pattern | Reference | When |
|---|---|---|---|
| 7a | HeroBar (1-row dense) |
pages/home.tsx |
Dashboard pages with ≤4 hero stats |
| 7b | Rail-anchored identity (no top chrome) | pages/cliproxy.tsx |
Multi-entity Config pages where rail carries brand |
| 7c | PageHeader (current) |
pages/health.tsx |
Pages where description / status info is non-redundant |
Phase 2's home + cliproxy migrations are reverted. Health stays migrated (Monitor archetype + PageHeader works there). Future page migrations adapt to whichever pattern fits, NOT the other way around.
Why bottom-up: the existing references already proved their patterns work in production. The job of the design system is to formalize what works, not impose what should.
v1.2 revision (2026-04-25) — health redesign and bespoke pattern
After the v1.1 restructure, the health page received a separate, focused redesign per a dedicated handoff brief (plans/reports/handoff-260425-1417-health-page-redesign.md). The redesign went bespoke:
- New domain components:
HealthStatusRibbon,HealthPriorityCard,HealthPriorityList,HealthAuditSection - Severity-driven layout: priority surfaces (errors/warnings) at the top with prominent fix affordances, audit accordion below
- Dynamic colored background tied to overall status, glassmorphic accents
- No
PageShell, noPageHeader, noMonitorLayout— full custom shell
Resolution: add a fourth identity-strip pattern — §1d Bespoke — for pages whose content shape doesn't fit the three primary patterns. The v1.1 row's claim that "health stays migrated (Monitor archetype + PageHeader works there)" no longer holds; corrected here.
Side effects:
PageHeaderlost its canonical reference (was health). New reference TBD when another page adopts it.MonitorLayoutlost its canonical reference. Primitives remain available for future Monitor pages.Bespokeis explicitly an escape hatch — not a default. Code review enforces "you tried the three patterns first."
v1.8 revision (2026-04-26) — PR-Agent round 2: storageKey default, MaskedInput type-safety, keyboard-accessible reveal toggle
Three more substantive issues from upstream review on PR #1109 after the v1.7 fix push. Each is encoded in code + spec:
Required storageKey was the wrong fix. v1.7 made storageKey: string REQUIRED to prevent cross-page state bleed, but that turned every future production-page migration into a build-breaking event. Better engineering: keep the prop optional and default to a key derived from window.location.pathname so each route gets its own localStorage slot automatically. The cross-page bleed concern still holds for hardcoded shared keys, but a pathname-derived default is unique by construction. SSR-safe (falls back to a stable string when window is unavailable). Pages can still pass an explicit storageKey to opt out of pathname coupling (e.g. when sub-routes should share split state).
MaskedInput type was overridable by callers. The spread {...props} came AFTER the hardcoded type={revealed ? 'text' : 'password'}, so a caller passing type="text" would silently render a credential in plaintext — a real leakage risk that defeated the component's purpose. Two-layer fix:
- Compile-time:
MaskedInputPropsnow extendsOmit<InputHTMLAttributes, 'type'>so callers literally cannot passtype. Type system enforces the contract. - Runtime: the
typeattribute on<Input>is now placed AFTER the spread so even if the compile-time check is bypassed (e.g. viaas-cast), the component still wins. Belt-and-braces.
Reveal toggle was keyboard-inaccessible. Both MaskedInput and Field had tabIndex={-1} on the eye-icon button, which removed it from the tab order — keyboard-only users couldn't show or hide the secret. Resolution:
- Drop
tabIndex={-1}so the button joins the natural tab order. - Add
aria-pressed={revealed}so screen readers announce toggle state. - Add a focus-visible accent ring so the focused state is visible without a mouse.
These are accessibility regressions that should never have shipped; the new tests-of-record for §5g are: (a) type cannot be overridden, (b) the toggle is reachable via Tab and announces state.
v1.7 revision (2026-04-26) — PR-Agent feedback: enforce mins, require storageKey, broaden sensitive heuristic
Three substantive issues surfaced by upstream PR review on PR #1109. Each is encoded in code + spec:
Width floor — pixel claim was unenforceable. The previous spec wording said "form ≥ 360px / json ≥ 320px" but react-resizable-panels v3 only accepts percentage minSize. On a 1280px viewport this could let a user drag a pane down to ~250px — well below the documented floor. Resolution: bump minSize from 25 to 30, restate the floor as percentage (≥ 30% of body width after the rail), document the actual 300–360px range across realistic viewports, note the v3 API constraint, and leave the door open for a future onResize clamp if hard pixel floors become necessary.
storageKey was no longer optional. The previous default storageKey="ccs.config-layout" meant any <ConfigLayout> without an explicit key would share localStorage state with every other Config page — split ratios bleeding across unrelated pages. Resolution: make storageKey REQUIRED in the ConfigLayout props (no default). TypeScript now enforces explicit per-page keys. Pages MUST pass e.g. storageKey="config-layout.cliproxy" — this is checked at compile time, not at runtime.
Sensitive-field heuristic was too narrow. The previous regex AUTH_TOKEN|API_KEY|SECRET|PASSWORD|PRIVATE_KEY missed common secret names: ACCESS_TOKEN, REFRESH_TOKEN, BEARER_TOKEN, CLIENT_SECRET, CLIENT_ID, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, GCP/Azure/GitHub/OpenAI/Anthropic variants, JWT, OAUTH, CREDENTIAL, PAT, WEBHOOK_SECRET, HMAC_KEY, SIGNING_KEY, SSH_KEY. Resolution: extract the heuristic to src/lib/sensitive-label.ts (isSensitiveLabel(label)) and broaden the pattern to cover all of those; case-insensitive; tolerates _/- separators. The Field component imports the shared helper. Adding a new pattern means editing one regex; every consumer inherits.
The shared helper is the Single Source of Truth so future drift can't reintroduce per-component heuristics that disagree with each other.
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 controlsmax-w-[360px]— cap so the rail never dominates the body when an entity label is unusually long; per-itemtruncateinsideListPanehandles 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:
FormSectiongains a 2px Crail leading-edge stripe (before:bg-accent/30, brightens toaccent/70on hover) and a 1.5px accent dot prefixing each title — the "1-accent-dot rule"FormPaneheader gains a 1pxaccent/40top strip and afrom-card to-card/70gradient; body wrapper usesbg-muted/20so FormSections (bg-card/60) read as elevated cards rather than floating; footer usesbg-muted/40to anchor the primary save actionJsonPaneshell shifts tobg-muted/30so 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 (editablein accent tones,read onlyin neutral tones)- The inner
<pre>block inJsonViewsits inside abg-card/80shadow-innerwell — gives the code a subtle embossed feel rather than floating on a flat surface - New §5 Color & accent usage in
design-system.mdcodifies 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/*, andbg-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
jsonprop is omitted, form fills remaining width and no divider renders - Below
<1024pxthe 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:
- 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.tsxis the canonical Config reference §1c PageHeaderis explicitly disallowed above anyConfigLayout. It is reserved for Monitor pages with no left rail- 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 - 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)
- Phase 3 + Phase 4 page migrations that adopted
PageHeaderare now non-canonical until refactored to rail-anchored. PR #1105 is held; merging requires a follow-up commit that stripsPageHeaderfrom 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:
PageHeaderloses its remaining canonical references (Phase 3+4 pages) until a Monitor-without-rail page adopts it- The
/_styleguideIntro 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: · " line if changed.