Skip to content

feat(frontend): design system foundation (M6.2) - #66

Merged
vgtray merged 7 commits into
mainfrom
feat/35-design-system
Apr 22, 2026
Merged

feat(frontend): design system foundation (M6.2)#66
vgtray merged 7 commits into
mainfrom
feat/35-design-system

Conversation

@vgtray

@vgtray vgtray commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Establishes the single design language for the control-room UI. Dark-only for v1 per M6.2 scope.

Stacked on top of #64 (M6.1 deps) — will retarget main automatically once #64 merges.

Tokens — design-system/tokens.css

All defined under --ds-* namespace so they never collide with the existing shadcn-style --background/--foreground from styles/dark.css:

  • Surface: --ds-bg-base, bg-surface, bg-elevated, border, border-strong
  • Text: --ds-fg-primary, fg-muted, fg-subtle
  • Accent: --ds-accent (#00d4ff), accent-hover, accent-glow
  • Status: --ds-status-{nominal,warning,critical}
  • Per-agent identity: --ds-agent-{sentinel,investigator,kb-builder,work-order,qa}
  • Radii, fonts, motion durations, easing
  • @media (prefers-reduced-motion: reduce) overrides motion durations to 0

Motion primitives — design-system/motion.ts

fadeInUp, streamToken, artifactReveal, anomalyPulse, handoffSweep, drawerSlide as typed framer-motion Variants. Shared easing cubic-bezier(0.16, 1, 0.3, 1).

Primitives (7)

Component Variants / Features
Button default / accent / ghost / danger · sm / md / lg · focus ring + disabled state
Card (+ Header/Title/Description) surface + elevated, padding none/sm/md/lg
Badge default / accent / nominal / warning / critical / agent (maps to --ds-agent-* per AgentId)
Drawer left / right / bottom · Escape to close · optional overlay · framer slide
Tabs (+ List/Trigger/Content) controlled or uncontrolled, ARIA roles
StatusDot nominal / warning / critical / unknown + optional pulse halo
KbdKey keyboard indicator (Inter + mono, --ds-radius-xs)

Icons — design-system/icons.tsx

Curated lucide-react re-export surface (Activity, AlertTriangle, Bot, Sparkles, Wrench, etc.).

Tailwind v4 bridge — styles/index.css

@theme inline block exposes every token as a Tailwind color/font utility (bg-ds-bg-surface, text-ds-fg-primary, font-mono, …) so both CSS vars and utility classes work interchangeably.

Dark mode + fonts — index.html

  • <html class="dark"> + color-scheme: dark
  • Preload Inter 400/500/600/700 + JetBrains Mono 400/500/600 from Google Fonts (display=swap)

Debug route /designsrc/pages/DesignPage.tsx

Renders every primitive, token palette, typography samples, and motion replays in one page. No Storybook, no lib dep.

Test plan

  • npm run typecheck — green
  • npm run build — green (CSS 29 kB / JS 418 kB gzip 133 kB)
  • npm run check (Biome lint + format) — green
  • Zero hex literal in primitive components — all colors via CSS vars
  • /design route renders every primitive in all states (manual smoke)
  • Motion respects prefers-reduced-motion (durations drop to 0)

Closes #35

vgtray added 2 commits April 22, 2026 17:57
*.tsbuildinfo is TypeScript's incremental build cache, machine-local
and different per build. Was accidentally tracked in an initial commit.
Untrack the two existing files and add the pattern to .gitignore so
future builds stop polluting git status.
Establish the single design language for the control-room UI. Dark-only
for v1.

- design-system/tokens.css: CSS vars for surface, text, accent,
  equipment status, per-agent identity colors, radii, typography,
  motion timing, with prefers-reduced-motion override.
- design-system/motion.ts: framer-motion variants — fadeInUp,
  streamToken, artifactReveal, anomalyPulse, handoffSweep, drawerSlide.
- Primitives: Button (default/accent/ghost/danger × sm/md/lg), Card
  (+ CardHeader/Title/Description), Badge (incl. per-agent variant
  mapping to --ds-agent-*), Drawer (left/right/bottom with Escape +
  overlay), Tabs (controlled + uncontrolled), StatusDot (nominal/
  warning/critical/unknown + optional pulse), KbdKey.
- icons.tsx: curated lucide-react re-export surface.
- styles/index.css: @theme inline bridge exposing tokens as Tailwind v4
  color utilities (bg-ds-bg-surface, text-ds-fg-primary, …).
- index.html: enable dark class on <html>, preload Inter + JetBrains
  Mono from Google Fonts.
- Add /design debug route rendering every primitive in each state,
  token palettes, typography samples, and motion replays.
Copilot AI review requested due to automatic review settings April 22, 2026 16:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Establishes a foundational, dark-only design system for the frontend control-room UI (tokens, primitives, motion helpers), plus a /design debug route to preview components and styles without introducing Storybook.

Changes:

  • Adds ARIA design-system CSS tokens and bridges them into Tailwind v4 utilities via @theme inline.
  • Introduces initial primitive components (Button/Card/Badge/Drawer/Tabs/StatusDot/KbdKey) and shared Framer Motion variants + curated Lucide icon re-exports.
  • Adds /design route and updates index.html for dark mode defaults and Google Fonts loading.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
frontend/src/styles/index.css Imports design tokens and maps them to Tailwind v4 theme variables.
frontend/src/pages/DesignPage.tsx New debug page rendering tokens/components/motion demos.
frontend/src/design-system/tokens.css Adds --ds-* token set (surface/text/accent/status/agents/fonts/motion).
frontend/src/design-system/motion.ts Adds shared Framer Motion Variants primitives.
frontend/src/design-system/index.ts Barrel exports for design-system primitives/types/icons/motion.
frontend/src/design-system/icons.tsx Curated lucide-react re-export surface.
frontend/src/design-system/Tabs.tsx Adds a custom tabs primitive with ARIA roles.
frontend/src/design-system/StatusDot.tsx Adds status indicator dot with optional pulse animation.
frontend/src/design-system/KbdKey.tsx Adds keyboard key indicator primitive.
frontend/src/design-system/Drawer.tsx Adds sliding drawer primitive (overlay optional, Escape to close).
frontend/src/design-system/Card.tsx Adds Card primitive + header/title/description subcomponents.
frontend/src/design-system/Button.tsx Adds Button primitive with variants/sizes/focus/disabled styling.
frontend/src/design-system/Badge.tsx Adds Badge primitive including per-agent styling.
frontend/src/App.tsx Registers /design route.
frontend/index.html Forces dark mode, sets color-scheme, and loads Inter + JetBrains Mono via Google Fonts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread frontend/src/App.tsx
Comment on lines +11 to 12
<Route path="/design" element={<DesignPage />} />
<Route
Comment on lines +103 to +108
if (active !== value) return null;
return (
<div
role="tabpanel"
id={`${idBase}-panel-${value}`}
aria-labelledby={`${idBase}-trigger-${value}`}
Comment on lines +3 to +46
const easeOut: Transition["ease"] = [0.16, 1, 0.3, 1];

export const fadeInUp: Variants = {
hidden: { opacity: 0, y: 8 },
visible: { opacity: 1, y: 0, transition: { duration: 0.22, ease: easeOut } },
};

export const streamToken: Variants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.08, ease: "linear" } },
};

export const artifactReveal: Variants = {
hidden: { opacity: 0, scale: 0.96, y: 6 },
visible: {
opacity: 1,
scale: 1,
y: 0,
transition: { duration: 0.28, ease: easeOut },
},
};

export const anomalyPulse: Variants = {
idle: { boxShadow: "0 0 0 0 rgba(239, 68, 68, 0.0)" },
pulse: {
boxShadow: ["0 0 0 0 rgba(239, 68, 68, 0.55)", "0 0 0 12px rgba(239, 68, 68, 0)"],
transition: { duration: 1.4, repeat: Infinity, ease: "easeOut" },
},
};

export const handoffSweep: Variants = {
hidden: { opacity: 0, x: -16 },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.32, ease: easeOut },
},
exit: { opacity: 0, x: 16, transition: { duration: 0.2 } },
};

export const drawerSlide: Variants = {
hidden: { x: "100%" },
visible: { x: 0, transition: { duration: 0.24, ease: easeOut } },
exit: { x: "100%", transition: { duration: 0.2, ease: easeOut } },
Comment on lines +28 to +37
<span
className={`inline-block relative align-middle ${className}`}
style={{
width: size,
height: size,
...style,
}}
role="status"
aria-label={`status: ${status}`}
{...rest}
Comment thread frontend/index.html
Comment on lines +8 to +13
<title>ARIA — Adaptive Runtime Intelligence</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet" />
Comment on lines +14 to +20
const variants: Record<Variant, string> = {
default:
"bg-[var(--ds-bg-elevated)] text-[var(--ds-fg-primary)] border border-[var(--ds-border)] hover:bg-[color-mix(in_oklab,var(--ds-bg-elevated),white_6%)] hover:border-[var(--ds-border-strong)]",
accent: "bg-[var(--ds-accent)] text-[var(--ds-accent-fg)] hover:bg-[var(--ds-accent-hover)]",
ghost: "bg-transparent text-[var(--ds-fg-muted)] hover:text-[var(--ds-fg-primary)] hover:bg-[var(--ds-bg-elevated)]",
danger: "bg-[var(--ds-status-critical)] text-white hover:bg-[color-mix(in_oklab,var(--ds-status-critical),white_10%)]",
};
Comment thread frontend/src/pages/DesignPage.tsx Outdated
Comment on lines +275 to +276
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
Comment on lines +63 to +90
export function TabsTrigger({
value,
className = "",
children,
}: {
value: string;
className?: string;
children: ReactNode;
}) {
const { value: active, setValue, idBase } = useTabs();
const selected = active === value;
return (
<button
type="button"
role="tab"
aria-selected={selected}
aria-controls={`${idBase}-panel-${value}`}
id={`${idBase}-trigger-${value}`}
onClick={() => setValue(value)}
className={`h-7 px-3 text-xs font-medium rounded-[var(--ds-radius-sm)] transition-colors duration-[var(--ds-motion-fast)] ${
selected
? "bg-[var(--ds-bg-surface)] text-[var(--ds-fg-primary)]"
: "text-[var(--ds-fg-muted)] hover:text-[var(--ds-fg-primary)]"
} ${className}`}
>
{children}
</button>
);
Comment on lines +49 to +55
<style>{`
@keyframes ds-status-pulse {
0% { transform: scale(1); opacity: 0.6; }
100% { transform: scale(2.4); opacity: 0; }
}
`}</style>
</span>
Comment on lines +5 to +86
export interface DrawerProps {
open: boolean;
onClose: () => void;
side?: "left" | "right" | "bottom";
width?: number | string;
height?: number | string;
className?: string;
children: ReactNode;
/**
* If true, an overlay is rendered that closes the drawer on click.
* Default true for bottom/left, false for right (chat drawer stays docked).
*/
overlay?: boolean;
}

export function Drawer({
open,
onClose,
side = "right",
width = 420,
height = "40vh",
className = "",
children,
overlay,
}: DrawerProps) {
const showOverlay = overlay ?? side !== "right";

useEffect(() => {
if (!open) return;
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
}
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, onClose]);

const position = {
left: "top-0 left-0 h-full",
right: "top-0 right-0 h-full",
bottom: "left-0 right-0 bottom-0",
}[side];

const slideDir = {
left: { hidden: { x: "-100%" }, visible: { x: 0 }, exit: { x: "-100%" } },
right: drawerSlide,
bottom: { hidden: { y: "100%" }, visible: { y: 0 }, exit: { y: "100%" } },
}[side];

const sizeStyle = side === "bottom" ? { height } : { width };

return (
<AnimatePresence>
{open && (
<>
{showOverlay && (
<motion.div
className="fixed inset-0 bg-black/40 z-40"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={onClose}
aria-hidden
/>
)}
<motion.aside
className={`fixed ${position} z-50 bg-[var(--ds-bg-surface)] border-[var(--ds-border)] shadow-2xl ${
side === "right"
? "border-l"
: side === "left"
? "border-r"
: "border-t"
} ${className}`}
style={sizeStyle}
variants={slideDir}
initial="hidden"
animate="visible"
exit="exit"
transition={{ duration: 0.24, ease: [0.16, 1, 0.3, 1] }}
role="dialog"
aria-modal={showOverlay ? true : undefined}
>
@vgtray
vgtray changed the base branch from feat/34-install-ui-deps to main April 22, 2026 16:19
vgtray added 5 commits April 22, 2026 18:34
…rain

Second-pass polish on the design system to shed the "first-draft AI" tells
before the app shell lands on top of it.

Tokens (tokens.css):
- Accent: #00d4ff (neon) → #3ab5c9 (desaturated cyan, SCADA feel).
  Hover creuses toward #5fd0e3 so the interaction is actually visible.
- Critical: #ef4444 → #d84545 (brick red, less "demo alert").
- Nominal / warning also pulled toward deeper earthy tones.
- Agent identities washed down so the 5 colors whisper instead of shout
  next to each other on a dense timeline.
- bg-surface / bg-elevated steps widened (#0d131d / #1a2435) so elevation
  reads clearly.
- Radii tightened by 1-2px across the board for a more technical feel.
- Type scale added (xs → 2xl) so downstream components can stop picking
  px literals.

Signature:
- Body fractalNoise overlay (~3.5% opacity, overlay blend-mode, pure SVG
  data-uri, zero lib) — subtle film grain that makes dark surfaces feel
  like matter, not flat CSS. Respects prefers-reduced-motion.
- New AriaMark component: minimal triangle "A" bisected by a telemetry
  pulse. Replaces the generic Sparkles icon in the design page header
  and becomes the reusable brand mark for the app shell topbar (M6.3).

Badge (Badge.tsx):
- rounded-full → rounded-[var(--ds-radius-xs)] (rectangles, not chat pills)
  so tags read as industrial status labels instead of social-media bubbles.
- New `tag` prop enables uppercase + mono + letter-spacing for SCADA-style
  status tags ("MONITORED", "ALARM", "RUNNING").

DesignPage:
- Header uses AriaMark (not Sparkles).
- Brand mark section shows the logo at 16/24/40/64.
- Badge section gains a row of tag-variant status labels.
Hybrid visual law for ARIA, synthesized from the modern-design skill
(editorial rigor) and the industrial-brutalist-ui skill (tactical
telemetry), adapted for a data-dense control-room app — not a landing.

12 sections:
 1. Direction artistique (Editorial Industrial Telemetry)
 2. Palette locked (dark substrate, steel cyan accent)
 3. Typography (Inter + JetBrains Mono, scale capped, tracking rules)
 4. Grid & spatial architecture (compartments, radius discipline)
 5. Six micro-pattern signatures (brackets, registration marks,
    hairlines, status rail, scoped scanlines, AriaMark)
 6. Motion language (narrow variant vocabulary, banned patterns)
 7. Iconography (stroke-width 1.5, no decorative sparkles)
 8. Component translations per existing primitive
 9. Anti-patterns list — armed for review (modern-design landing
    patterns rejected, brutalist absolute-zero-radius softened,
    global AI-slop guardrails)
10. Deps delta (nothing mandatory to install)
11. Rollout plan (what lands in this PR vs later milestones)
12. Validation checklist for Adam
Implements the "Immediately" rollout from docs/DESIGN_PLAN.md: the app
now has its visual language, not just a clean dark theme.

Icons (icons.tsx):
- All lucide re-exports wrapped to default stroke-width 1.5 (matches the
  1px hairline grid discipline). Callers can still override per-instance.

4 new primitives (DESIGN_PLAN §5, §8):
- SectionHeader — mono uppercase +0.08em tracking micro-label with
  optional [ brackets ], optional marker prefix, optional right-aligned
  metadata slot. The single most recognizable visual pattern across the
  app, applied everywhere a section/panel needs a name.
- MetaStrip — right-aligned registration-mark metadata strip rendering
  as `LABEL / value · LABEL / value`. Never legal text, always real data
  (site id, cell id, commit sha). Structural signal that "this is a
  real system".
- Hairline — 1px (or 2px) decorative divider, optional inline label
  (`─── control room ───`). Editorial rule between sections.
- StatusRail — 2px colored left-edge rail for cards / rows. Silent on
  nominal/idle, speaks on warning/critical (optional pulse). Vocabulary:
  status tones + agent tones + accent + idle.

Card: new `rail` prop composes StatusRail on the left edge. Card title
tightened to 16px (-0.01em tracking) to match the type scale in §3.

Tabs: selected trigger visual swapped from filled surface pill to a 2px
bottom rail in accent color. Triggers are now mono uppercase at
+0.08em tracking for SCADA feel. The list itself is a 1px bottom border
— Tabs *are* the compartment line.

DesignPage: full rewrite as a live application of the plan, not a
component dump. Uses real SectionHeaders with § markers, MetaStrip with
plausible REV/UNIT/BUILD values, Hairlines between zones, Cards with
nominal + critical status rails, Tabs in the new visual, typography
samples at the real scale stops. Serves double duty: primitive
showcase + reference implementation of the §5 signatures.

Zero new deps. Inter + JetBrains Mono from Google Fonts suffice for v1.
Ensures any future Claude agent session reading this repo picks up the
full project contract without re-deriving it from chat history:

- Team lanes (vgtray frontend / zestones backend), deadline, stack.
- Mandatory reading order before acting (ROADMAP → idea.md → relevant
  milestone issues → DESIGN_PLAN → live GitHub state → prior lesson
  on scope-creep).
- Workflow rules (branch naming, conventional commits English,
  no Co-Authored-By: Claude, issue = branch = PR, user merges).
- Docker volume quirk: deps changes require
  "docker compose exec frontend npm install" after pull.
- Board automation IDs (project v2 #28 field + option IDs) so chef
  can move issues In progress / In review / Done via GraphQL.
- Quality gates required before any PR.
- Design discipline: DESIGN_PLAN.md §9 anti-patterns enforced.
- Interface contract pointer (WS event schema frozen in M4.1).
- Hard no-goes: never touch backend lane, never merge, never force-push,
  never invent tokens, never add deps without §10 justification.

Drop-in for every agent that spawns in this repo.
@vgtray
vgtray merged commit e62944f into main Apr 22, 2026
9 checks passed
@vgtray
vgtray deleted the feat/35-design-system branch April 22, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

M6.2 — Design system foundation

2 participants