# UI Shell Rework — Wave 1 (Foundation) Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Stand up a Tailwind + flyon-ui build pipeline in `services/web-ui` and define every CSS primitive the rework needs, without changing any user-visible page yet. End state: `docker compose up -d --build web-ui` succeeds, `app.[hash].css` is served, no existing page is broken. **Architecture:** Add a Node build stage to the web-ui Dockerfile that runs `tailwindcss --minify` once at image-build time. Tailwind config defines a custom flyon-ui theme that maps the existing oklch palette into flyon-ui's color slots, preserving brand hue 266. CSS primitives are split one-per-file under `src/css/components/` so each file holds one responsibility and stays small. Runtime image stays nginx-static. **Tech Stack:** Tailwind CSS 3, flyon-ui plugin, PostCSS, nginx (runtime), Docker multi-stage build. No JavaScript runtime added — build-only Node. **Reference spec:** `docs/superpowers/specs/2026-05-21-ui-shell-rework-design.md` --- ## File structure **Files this wave creates or modifies:** ``` services/web-ui/ ├── Dockerfile (MODIFY: add Node build stage) ├── package.json (CREATE: tailwind/flyon-ui deps) ├── tailwind.config.js (CREATE: theme port + flyon-ui plugin) ├── postcss.config.js (CREATE: tailwindcss + autoprefixer) ├── .gitignore (MODIFY: ignore node_modules + dist) ├── nginx.conf (MODIFY: long-cache /dist/*) ├── src/ │ └── css/ │ ├── app.css (CREATE: Tailwind directives + @layer + imports) │ └── components/ │ ├── tokens.css (CREATE: :root oklch token definitions) │ ├── motion.css (CREATE: shimmer/stutter keyframes + reduced-motion) │ ├── sidebar.css (CREATE: .wd-sidebar, .wd-nav-item, IN DEV pill) │ ├── topbar.css (CREATE: .wd-topbar, .wd-breadcrumb, .wd-search) │ ├── button.css (CREATE: .wd-btn variants × sizes) │ ├── form-controls.css (CREATE: .wd-input, .wd-label, .wd-hint, .wd-toggle) │ ├── field-group.css (CREATE: tabbed-section primitive) │ ├── slide-panel.css (CREATE: locked-layout slide-panel) │ ├── card-asset.css (CREATE: asset card family) │ ├── card-operational.css (CREATE: operational card family) │ ├── list-row.css (CREATE: inline list row) │ ├── empty-state.css (CREATE: empty-state pattern) │ ├── badge.css (CREATE: status pills + signal badges) │ └── toast.css (CREATE: toast with top-strip) ├── public/ │ ├── dist/ (CREATE: gitignored, build output target) │ └── fonts/ (CREATE: self-hosted woff2) │ ├── inter-400.woff2 │ ├── inter-500.woff2 │ ├── inter-600.woff2 │ ├── jetbrains-mono-400.woff2 │ └── jetbrains-mono-600.woff2 └── (existing HTML files untouched in wave 1) ``` **Files this wave does NOT touch:** every `services/web-ui/public/*.html`. Wave 2+ migrates them. --- ## Tasks ### Task 1: Scaffold Node build deps **Files:** - Create: `services/web-ui/package.json` - Modify: `services/web-ui/.gitignore` - [ ] **Step 1: Create package.json** Write `services/web-ui/package.json`: ```json { "name": "wild-dragon-web-ui-build", "version": "0.1.0", "private": true, "description": "Build-time-only deps for the Wild Dragon web-ui Tailwind/flyon-ui pipeline. Not shipped at runtime.", "scripts": { "build:css": "tailwindcss -i ./src/css/app.css -o ./public/dist/app.css --minify" }, "devDependencies": { "tailwindcss": "^3.4.0", "postcss": "^8.4.35", "autoprefixer": "^10.4.17", "flyonui": "^1.0.0" } } ``` - [ ] **Step 2: Update .gitignore** Append to `services/web-ui/.gitignore` (create if missing): ```gitignore node_modules/ public/dist/ ``` - [ ] **Step 3: Commit** ```bash cd /opt/wild-dragon HOME=/root git add services/web-ui/package.json services/web-ui/.gitignore HOME=/root git commit -m "web-ui: scaffold tailwind+flyon-ui build deps" ``` --- ### Task 2: PostCSS config **Files:** - Create: `services/web-ui/postcss.config.js` - [ ] **Step 1: Write postcss.config.js** ```js module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, }; ``` - [ ] **Step 2: Commit** ```bash HOME=/root git add services/web-ui/postcss.config.js HOME=/root git commit -m "web-ui: add postcss config for tailwind+autoprefixer" ``` --- ### Task 3: Tailwind config with custom flyon-ui theme **Files:** - Create: `services/web-ui/tailwind.config.js` Maps the existing oklch palette into flyon-ui's color slots. Brand hue 266 preserved. Wave-1 deliberately keeps the theme tight — only colors, fonts, and spacing scale. Additional theme keys (animation curves, radii, etc.) live in component CSS files via `@layer`. - [ ] **Step 1: Write tailwind.config.js** ```js /** @type {import('tailwindcss').Config} */ module.exports = { content: [ './public/**/*.html', './src/**/*.{css,js}', ], darkMode: 'class', theme: { extend: { colors: { // ── Surfaces (5-step depth, tinted toward brand hue 266) ── 'bg-deep': 'oklch(8% 0.011 266)', 'bg-base': 'oklch(11% 0.010 266)', 'bg-panel': 'oklch(15% 0.013 266)', 'bg-surface': 'oklch(19% 0.014 266)', 'bg-raised': 'oklch(24% 0.015 266)', 'bg-hover': 'oklch(28% 0.015 266)', // ── Accent (brand) ── accent: { DEFAULT: 'oklch(45% 0.20 266)', subtle: 'oklch(55% 0.20 266 / 0.12)', border: 'oklch(55% 0.20 266 / 0.36)', }, // ── Text ── 'text-primary': 'oklch(94% 0.008 266)', 'text-secondary': 'oklch(72% 0.014 266)', // tertiary bumped from 52% to 56% per a11y section in the spec 'text-tertiary': 'oklch(56% 0.012 266)', 'text-disabled': 'oklch(38% 0.010 266)', // ── Borders ── 'border-faint': 'oklch(22% 0.013 266)', 'border-default': 'oklch(28% 0.015 266)', 'border-strong': 'oklch(38% 0.018 266)', // ── Status / semantic signal ── 'signal-good': 'oklch(70% 0.18 148)', 'signal-bad': 'oklch(64% 0.22 25)', 'signal-warn': 'oklch(80% 0.16 90)', 'signal-idle': 'oklch(58% 0.012 266)', 'signal-good-bg': 'oklch(70% 0.18 148 / 0.12)', 'signal-bad-bg': 'oklch(64% 0.22 25 / 0.12)', 'signal-warn-bg': 'oklch(80% 0.16 90 / 0.12)', }, fontFamily: { sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'system-ui', 'sans-serif'], mono: ['"JetBrains Mono"', 'ui-monospace', '"SF Mono"', 'Consolas', 'monospace'], }, fontSize: { // tighter scale per density target 'xs': ['11px', { lineHeight: '1.4' }], 'sm': ['13px', { lineHeight: '1.5' }], 'base': ['14px', { lineHeight: '1.5' }], 'md': ['16px', { lineHeight: '1.4' }], 'lg': ['18px', { lineHeight: '1.4' }], 'xl': ['22px', { lineHeight: '1.3' }], '2xl': ['28px', { lineHeight: '1.2' }], '3xl': ['40px', { lineHeight: '1.1' }], 'tc': ['64px', { lineHeight: '1' }], // tally-grade timecode }, borderRadius: { 'sm': '4px', 'md': '6px', 'lg': '8px', }, transitionTimingFunction: { 'out-quart': 'cubic-bezier(0.25, 1, 0.5, 1)', 'out-expo': 'cubic-bezier(0.16, 1, 0.3, 1)', }, transitionDuration: { '120': '120ms', '180': '180ms', '240': '240ms', }, zIndex: { 'dropdown': '40', 'overlay': '80', 'panel': '90', 'toast': '200', }, }, }, plugins: [ require('flyonui'), ], }; ``` - [ ] **Step 2: Commit** ```bash HOME=/root git add services/web-ui/tailwind.config.js HOME=/root git commit -m "web-ui: tailwind config with oklch theme port (brand hue 266 preserved)" ``` --- ### Task 4: Create design-token CSS **Files:** - Create: `services/web-ui/src/css/components/tokens.css` Tailwind exposes the colors as utility classes, but we also keep CSS custom properties so non-Tailwind callers (the existing JS that sets `el.style.color = 'var(--accent)'`) still work. - [ ] **Step 1: Write tokens.css** ```css /* tokens.css ─ oklch design tokens * Mirrors tailwind.config.js so legacy callers that read * var(--accent), var(--bg-panel), etc. keep working during the * page-by-page migration. After all pages migrate to Tailwind * utility classes (wave 4 complete), this file shrinks to * just the few tokens that are referenced from inline styles. */ :root { /* Surfaces */ --bg-deep: oklch(8% 0.011 266); --bg-base: oklch(11% 0.010 266); --bg-panel: oklch(15% 0.013 266); --bg-surface: oklch(19% 0.014 266); --bg-raised: oklch(24% 0.015 266); --bg-hover: oklch(28% 0.015 266); /* Accent */ --accent: oklch(45% 0.20 266); --accent-subtle: oklch(55% 0.20 266 / 0.12); --accent-border: oklch(55% 0.20 266 / 0.36); /* Text — tertiary bumped to 56% for AA contrast on bg-panel */ --text-primary: oklch(94% 0.008 266); --text-secondary: oklch(72% 0.014 266); --text-tertiary: oklch(56% 0.012 266); --text-disabled: oklch(38% 0.010 266); /* Borders */ --border-faint: oklch(22% 0.013 266); --border: oklch(28% 0.015 266); --border-strong: oklch(38% 0.018 266); /* Semantic signal */ --signal-good: oklch(70% 0.18 148); --signal-bad: oklch(64% 0.22 25); --signal-warn: oklch(80% 0.16 90); --signal-idle: oklch(58% 0.012 266); --signal-good-bg: oklch(70% 0.18 148 / 0.12); --signal-bad-bg: oklch(64% 0.22 25 / 0.12); --signal-warn-bg: oklch(80% 0.16 90 / 0.12); /* Spacing (mirrors Tailwind 4pt scale) */ --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 20px; --sp-6: 24px; --sp-8: 32px; --sp-12: 48px; /* Type */ --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Consolas, monospace; /* Layout */ --sidebar-w: 200px; --topbar-h: 48px; /* Z */ --z-dropdown: 40; --z-overlay: 80; --z-panel: 90; --z-toast: 200; } ``` - [ ] **Step 2: Commit** ```bash HOME=/root git add services/web-ui/src/css/components/tokens.css HOME=/root git commit -m "web-ui: oklch design tokens (mirror tailwind theme for legacy callers)" ``` --- ### Task 5: Create motion CSS (shimmer + stutter + reduced-motion) **Files:** - Create: `services/web-ui/src/css/components/motion.css` - [ ] **Step 1: Write motion.css** ```css /* motion.css ─ keyframes + global reduced-motion override. * Keep keyframes here, not scattered across primitive files, * so a single import disables them under prefers-reduced-motion. */ /* Skeleton shimmer — gradient sweep, NOT opacity pulse */ @keyframes wd-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } /* "LIVE" tally-light stutter — bright 0.9s / dim 0.3s / bright 0.9s */ @keyframes wd-stutter { 0%, 42% { opacity: 1; } 42.01%, 56% { opacity: 0.55; } 56.01%, 100% { opacity: 1; } } /* Signal-strip sweep for recording cards */ @keyframes wd-signal-sweep { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } .wd-shimmer { background: linear-gradient( 90deg, var(--bg-surface) 0%, var(--bg-hover) 50%, var(--bg-surface) 100% ); background-size: 200% 100%; animation: wd-shimmer 1.8s ease-in-out infinite; } .wd-stutter { animation: wd-stutter 2.1s ease-in-out infinite; } .wd-sweep { animation: wd-signal-sweep 1.8s ease-in-out infinite; } @media (prefers-reduced-motion: reduce) { .wd-shimmer, .wd-stutter, .wd-sweep { animation: none; } *, *::before, *::after { transition-duration: 0ms !important; animation-duration: 0ms !important; } } ``` - [ ] **Step 2: Commit** ```bash HOME=/root git add services/web-ui/src/css/components/motion.css HOME=/root git commit -m "web-ui: motion primitives (shimmer/stutter/sweep + reduced-motion)" ``` --- ### Task 6: Create sidebar CSS primitive **Files:** - Create: `services/web-ui/src/css/components/sidebar.css` Matches design section 2: 200px wide, 28px rows, leading 4px accent dot for active (NOT a side-stripe). - [ ] **Step 1: Write sidebar.css** ```css /* sidebar.css ─ flat app navigation rail (200px, 28px rows) */ .wd-sidebar { width: var(--sidebar-w); flex-shrink: 0; display: flex; flex-direction: column; background: var(--bg-panel); border-right: 1px solid var(--border); overflow-y: auto; overflow-x: hidden; } .wd-sidebar-header { height: var(--topbar-h); display: flex; align-items: center; gap: 8px; padding: 0 12px; border-bottom: 1px solid var(--border-faint); flex-shrink: 0; } .wd-sidebar-logo { width: 18px; height: 18px; } .wd-sidebar-brand { font: 600 13px/1 var(--font); letter-spacing: -0.01em; color: var(--text-primary); } .wd-sidebar-nav { display: flex; flex-direction: column; padding: 8px 0; gap: 1px; } .wd-sidebar-section { font: 600 10px/1 var(--font); letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-tertiary); padding: 16px 12px 4px; } .wd-nav-item { position: relative; display: flex; align-items: center; gap: 8px; height: 28px; padding: 0 8px 0 16px; /* extra left padding for the accent-dot indent */ font: 500 13px/1 var(--font); color: var(--text-secondary); text-decoration: none; transition: background-color 120ms cubic-bezier(0.25, 1, 0.5, 1), color 120ms cubic-bezier(0.25, 1, 0.5, 1); } .wd-nav-item:hover { background: var(--bg-hover); color: var(--text-primary); } .wd-nav-item svg { width: 14px; height: 14px; flex-shrink: 0; color: var(--text-tertiary); } .wd-nav-item:hover svg { color: var(--text-secondary); } .wd-nav-item.is-active { background: var(--bg-surface); color: var(--text-primary); } .wd-nav-item.is-active svg { color: var(--text-primary); } /* 4px leading accent dot — NOT a side-stripe border (banned). * Vertically centered, 8px tall, 4px from the left edge. */ .wd-nav-item.is-active::before { content: ''; position: absolute; left: 4px; top: 50%; transform: translateY(-50%); width: 4px; height: 8px; border-radius: 2px; background: var(--accent); } /* IN DEV pill (auth-guard.js injects this) */ .wd-nav-item .nav-dev-badge { margin-left: auto; font: 700 9px/1 var(--font); letter-spacing: 0.12em; padding: 2px 5px; border-radius: 3px; background: oklch(28% 0.14 80 / 0.45); color: oklch(85% 0.16 85); border: 1px solid oklch(50% 0.16 80 / 0.55); text-transform: uppercase; flex-shrink: 0; } /* Footer user widget */ .wd-sidebar-footer { margin-top: auto; padding: 12px; border-top: 1px solid var(--border-faint); } .wd-sidebar-user { display: flex; align-items: center; gap: 8px; } .wd-sidebar-user-avatar { width: 28px; height: 28px; border-radius: 50%; background: var(--bg-surface); display: flex; align-items: center; justify-content: center; font: 600 11px/1 var(--font); color: var(--text-secondary); flex-shrink: 0; } .wd-sidebar-user-info { flex: 1; min-width: 0; } .wd-sidebar-user-name { font: 500 12px/1.3 var(--font); color: var(--text-primary); } .wd-sidebar-user-role { font: 500 10px/1.3 var(--font); color: var(--text-tertiary); letter-spacing: 0.06em; text-transform: uppercase; } .wd-sidebar-user-logout { opacity: 0; transition: opacity 120ms cubic-bezier(0.25, 1, 0.5, 1); } .wd-sidebar-user:hover .wd-sidebar-user-logout { opacity: 1; } ``` - [ ] **Step 2: Commit** ```bash HOME=/root git add services/web-ui/src/css/components/sidebar.css HOME=/root git commit -m "web-ui: sidebar primitive (200px, 28px rows, leading accent dot)" ``` --- ### Task 7: Create topbar CSS primitive **Files:** - Create: `services/web-ui/src/css/components/topbar.css` - [ ] **Step 1: Write topbar.css** ```css /* topbar.css ─ sticky 48px topbar with breadcrumb + scoped search */ .wd-topbar { position: sticky; top: 0; z-index: 30; height: var(--topbar-h); display: flex; align-items: center; padding: 0 12px 0 16px; gap: 16px; background: var(--bg-panel); border-bottom: 1px solid var(--border-faint); flex-shrink: 0; } .wd-topbar-left { flex: 0 1 auto; min-width: 0; } .wd-topbar-center { flex: 1 1 360px; display: flex; justify-content: center; } .wd-topbar-right { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; margin-left: auto; } /* Breadcrumb */ .wd-breadcrumb { display: flex; align-items: center; gap: 0; font: 500 13px/1 var(--font); } .wd-breadcrumb-crumb { color: var(--text-secondary); } .wd-breadcrumb-crumb:last-child { color: var(--text-primary); font-weight: 600; } .wd-breadcrumb-sep { width: 10px; height: 10px; margin: 0 8px; color: var(--text-tertiary); flex-shrink: 0; } /* Page-scoped search */ .wd-topbar-search { width: 100%; max-width: 360px; height: 28px; display: flex; align-items: center; gap: 6px; padding: 0 10px; background: var(--bg-deep); border: 1px solid var(--border-faint); border-radius: 4px; } .wd-topbar-search:focus-within { border-color: var(--accent-border); outline: 2px solid var(--accent-subtle); outline-offset: 1px; } .wd-topbar-search svg { width: 12px; height: 12px; color: var(--text-tertiary); flex-shrink: 0; } .wd-topbar-search input { flex: 1; background: transparent; border: 0; outline: 0; font: 400 13px/1 var(--font-mono); color: var(--text-primary); } .wd-topbar-search input::placeholder { color: var(--text-tertiary); font: 400 13px/1 var(--font-mono); } /* Vertical divider between secondary actions and primary CTA */ .wd-topbar-divider { width: 1px; height: 20px; background: var(--border-faint); margin: 0 4px; } ``` - [ ] **Step 2: Commit** ```bash HOME=/root git add services/web-ui/src/css/components/topbar.css HOME=/root git commit -m "web-ui: topbar primitive (breadcrumb + scoped search + sticky)" ``` --- ### Task 8: Create button CSS primitive **Files:** - Create: `services/web-ui/src/css/components/button.css` - [ ] **Step 1: Write button.css** ```css /* button.css ─ sm/md/lg × primary/secondary/ghost/danger */ .wd-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; border-radius: 4px; border: 1px solid transparent; font: 500 13px/1 var(--font); cursor: pointer; user-select: none; transition: background-color 120ms cubic-bezier(0.25, 1, 0.5, 1), border-color 120ms cubic-bezier(0.25, 1, 0.5, 1), color 120ms cubic-bezier(0.25, 1, 0.5, 1); } .wd-btn:focus-visible { outline: 2px solid var(--accent-subtle); outline-offset: 1px; } .wd-btn:active:not(:disabled) { opacity: 0.85; transition: opacity 60ms; } .wd-btn:disabled { opacity: 0.4; cursor: not-allowed; } .wd-btn > svg { width: 12px; height: 12px; flex-shrink: 0; } /* Sizes */ .wd-btn--sm { height: 28px; padding: 0 10px; font-size: 12px; } .wd-btn--md { height: 32px; padding: 0 12px; font-size: 13px; } .wd-btn--lg { height: 36px; padding: 0 14px; font-size: 13px; } /* Variants */ .wd-btn--primary { background: var(--accent); color: oklch(98% 0.005 266); } .wd-btn--primary:hover:not(:disabled) { background: oklch(52% 0.20 266); } .wd-btn--secondary { background: var(--bg-surface); color: var(--text-primary); border-color: var(--border); } .wd-btn--secondary:hover:not(:disabled) { background: var(--bg-hover); border-color: var(--border-strong); } .wd-btn--ghost { background: transparent; color: var(--text-secondary); } .wd-btn--ghost:hover:not(:disabled) { background: var(--bg-hover); color: var(--text-primary); } .wd-btn--danger { background: var(--signal-bad); color: oklch(98% 0.005 25); } .wd-btn--danger:hover:not(:disabled) { background: oklch(68% 0.22 25); } /* Icon-only (28px square) */ .wd-btn--icon { width: 28px; padding: 0; } ``` - [ ] **Step 2: Commit** ```bash HOME=/root git add services/web-ui/src/css/components/button.css HOME=/root git commit -m "web-ui: button primitive (sm/md/lg x primary/secondary/ghost/danger)" ``` --- ### Task 9: Create form-controls CSS primitive **Files:** - Create: `services/web-ui/src/css/components/form-controls.css` - [ ] **Step 1: Write form-controls.css** ```css /* form-controls.css ─ label/input/select/textarea/hint/toggle */ .wd-label { display: block; font: 600 11px/1 var(--font); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-tertiary); margin-bottom: 4px; } .wd-input, .wd-select, .wd-textarea { width: 100%; height: 32px; padding: 0 10px; background: var(--bg-deep); border: 1px solid var(--border); border-radius: 4px; font: 400 13px/1 var(--font); color: var(--text-primary); transition: border-color 120ms cubic-bezier(0.25, 1, 0.5, 1); } .wd-textarea { height: auto; min-height: 80px; padding: 8px 10px; line-height: 1.5; resize: vertical; } .wd-input:focus, .wd-select:focus, .wd-textarea:focus { border-color: var(--accent-border); outline: 2px solid var(--accent-subtle); outline-offset: 1px; } .wd-input:disabled, .wd-select:disabled, .wd-textarea:disabled { opacity: 0.5; cursor: not-allowed; } .wd-input::placeholder, .wd-textarea::placeholder { color: var(--text-tertiary); } .wd-hint { font: 400 11px/1.5 var(--font); color: var(--text-tertiary); margin-top: 4px; } .wd-hint code { font-family: var(--font-mono); font-size: 11px; background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; } .wd-form-group { display: flex; flex-direction: column; } .wd-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } .wd-form-row--3 { grid-template-columns: 1fr 1fr 1fr; } /* Toggle switch — 34x18 */ .wd-toggle { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; } .wd-toggle input { position: absolute; opacity: 0; width: 0; height: 0; } .wd-toggle-track { position: relative; width: 34px; height: 18px; background: var(--bg-hover); border: 1px solid var(--border); border-radius: 999px; transition: background 200ms cubic-bezier(0.25, 1, 0.5, 1); } .wd-toggle-track::after { content: ''; position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: var(--text-secondary); transition: transform 200ms cubic-bezier(0.25, 1, 0.5, 1), background 200ms cubic-bezier(0.25, 1, 0.5, 1); } .wd-toggle input:checked ~ .wd-toggle-track { background: var(--accent); border-color: var(--accent); } .wd-toggle input:checked ~ .wd-toggle-track::after { transform: translateX(16px); background: oklch(98% 0.005 266); } .wd-toggle-label { font: 400 13px/1 var(--font); color: var(--text-secondary); } ``` - [ ] **Step 2: Commit** ```bash HOME=/root git add services/web-ui/src/css/components/form-controls.css HOME=/root git commit -m "web-ui: form controls primitive (label/input/select/textarea/hint/toggle)" ``` --- ### Task 10: Create field-group CSS primitive (tabbed sections) **Files:** - Create: `services/web-ui/src/css/components/field-group.css` Generalization of the codec-block pattern from the recent recorders rewrite. - [ ] **Step 1: Write field-group.css** ```css /* field-group.css ─ titled section with optional inner tabs. * Generalized from the codec-block pattern in recorders.html. */ .wd-field-group { border: 1px solid var(--border-faint); border-radius: 6px; background: var(--bg-panel); overflow: hidden; } .wd-field-group-header { display: flex; align-items: center; justify-content: space-between; height: 36px; padding: 0 14px; background: var(--bg-surface); border-bottom: 1px solid var(--border-faint); } .wd-field-group-title { font: 600 11px/1 var(--font); letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-secondary); } .wd-tabs { display: flex; height: 32px; background: var(--bg-deep); border-bottom: 1px solid var(--border-faint); } .wd-tab { flex: 1; background: transparent; border: 0; border-bottom: 2px solid transparent; font: 500 11px/1 var(--font); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-tertiary); cursor: pointer; transition: color 120ms cubic-bezier(0.25, 1, 0.5, 1), border-color 120ms cubic-bezier(0.25, 1, 0.5, 1); } .wd-tab:hover { color: var(--text-secondary); } .wd-tab.is-active { color: var(--accent); border-bottom-color: var(--accent); } .wd-tab-panel { display: none; padding: 14px; } .wd-tab-panel.is-active { display: block; } ``` - [ ] **Step 2: Commit** ```bash HOME=/root git add services/web-ui/src/css/components/field-group.css HOME=/root git commit -m "web-ui: field-group primitive (tabbed section, codec-block generalized)" ``` --- ### Task 11: Create slide-panel CSS primitive **Files:** - Create: `services/web-ui/src/css/components/slide-panel.css` Codifies the layout fix from the codec-clipping bug as the primitive. - [ ] **Step 1: Write slide-panel.css** ```css /* slide-panel.css ─ right-sliding 460px panel with locked flex layout. * The min-height:0 on body is the flex-overflow footgun fix from * the codec-clipping bug. Don't remove it. */ .wd-slide-overlay { position: fixed; inset: 0; z-index: var(--z-overlay); background: oklch(8% 0.010 266 / 0.65); opacity: 0; pointer-events: none; transition: opacity 240ms cubic-bezier(0.25, 1, 0.5, 1); } .wd-slide-overlay.is-open { opacity: 1; pointer-events: all; } .wd-slide-panel { position: fixed; top: 0; right: 0; bottom: 0; width: 460px; height: 100vh; max-height: 100vh; z-index: var(--z-panel); background: var(--bg-panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; transform: translateX(100%); transition: transform 240ms cubic-bezier(0.16, 1, 0.3, 1); } .wd-slide-panel.is-open { transform: translateX(0); } .wd-slide-panel-header { display: flex; align-items: center; justify-content: space-between; height: 52px; padding: 0 18px; border-bottom: 1px solid var(--border-faint); flex-shrink: 0; } .wd-slide-panel-title { font: 600 14px/1 var(--font); letter-spacing: -0.005em; color: var(--text-primary); } .wd-slide-panel-body { flex: 1; min-height: 0; /* critical for flex overflow */ overflow-y: auto; padding: 18px; display: flex; flex-direction: column; gap: 16px; } .wd-slide-panel-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 14px 18px; background: var(--bg-deep); border-top: 1px solid var(--border-faint); flex-shrink: 0; } ``` - [ ] **Step 2: Commit** ```bash HOME=/root git add services/web-ui/src/css/components/slide-panel.css HOME=/root git commit -m "web-ui: slide-panel primitive (locked flex, codec-clipping bug codified)" ``` --- ### Task 12: Create asset card CSS primitive **Files:** - Create: `services/web-ui/src/css/components/card-asset.css` - [ ] **Step 1: Write card-asset.css** ```css /* card-asset.css ─ 16:9 thumbnail + metadata + role pill. * For Library, Project asset grids, recorder cards' video portion. */ .wd-card-asset { display: flex; flex-direction: column; background: var(--bg-panel); border: 1px solid var(--border-faint); border-radius: 6px; overflow: hidden; transition: border-color 120ms cubic-bezier(0.25, 1, 0.5, 1); } .wd-card-asset:hover { border-color: var(--border); } .wd-card-asset-thumb { position: relative; width: 100%; aspect-ratio: 16 / 9; background: oklch(0% 0 0); overflow: hidden; } .wd-card-asset-thumb img, .wd-card-asset-thumb video { width: 100%; height: 100%; object-fit: cover; display: block; transition: filter 120ms cubic-bezier(0.25, 1, 0.5, 1); } .wd-card-asset:hover .wd-card-asset-thumb img, .wd-card-asset:hover .wd-card-asset-thumb video { filter: brightness(1.04); } .wd-card-asset-chip { position: absolute; font: 500 10px/1 var(--font-mono); color: var(--text-primary); background: oklch(8% 0.010 266 / 0.7); border: 1px solid var(--border-faint); border-radius: 3px; padding: 3px 6px; letter-spacing: 0.02em; } .wd-card-asset-chip--duration { bottom: 8px; right: 8px; } .wd-card-asset-chip--comments { bottom: 8px; left: 8px; display: inline-flex; align-items: center; gap: 4px; } .wd-card-asset-chip--version { top: 8px; right: 8px; font-weight: 600; } .wd-card-asset-checkbox { position: absolute; top: 8px; left: 8px; opacity: 0; transition: opacity 120ms cubic-bezier(0.25, 1, 0.5, 1); } .wd-card-asset:hover .wd-card-asset-checkbox, .wd-card-asset.is-selected .wd-card-asset-checkbox, .wd-card-asset-grid.has-selection .wd-card-asset-checkbox { opacity: 1; } .wd-card-asset-meta { padding: 8px; display: flex; flex-direction: column; gap: 2px; min-width: 0; } .wd-card-asset-name { font: 500 13px/1.3 var(--font); color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .wd-card-asset-sub { font: 400 11px/1.3 var(--font); color: var(--text-tertiary); font-variant-numeric: tabular-nums; } .wd-card-asset-role { display: block; width: 100%; padding: 6px 8px; border: 1px solid var(--border-faint); border-top: 0; background: var(--bg-deep); font: 600 10px/1 var(--font); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-secondary); cursor: pointer; } .wd-card-asset-role--unset { border-style: dashed; color: var(--text-tertiary); } /* Grid container */ .wd-card-asset-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; } ``` - [ ] **Step 2: Commit** ```bash HOME=/root git add services/web-ui/src/css/components/card-asset.css HOME=/root git commit -m "web-ui: asset card primitive (16:9 thumb + role pill, replay-style)" ``` --- ### Task 13: Create operational card CSS primitive **Files:** - Create: `services/web-ui/src/css/components/card-operational.css` - [ ] **Step 1: Write card-operational.css** ```css /* card-operational.css ─ header + content + footer, for recorders/cluster/jobs. * Different SHAPE than asset cards on purpose (impeccable ban: identical card grids). */ .wd-card-op { display: flex; flex-direction: column; gap: 10px; padding: 14px; background: var(--bg-panel); border: 1px solid var(--border-faint); border-radius: 6px; transition: border-color 120ms cubic-bezier(0.25, 1, 0.5, 1); } .wd-card-op:hover { border-color: var(--border); } .wd-card-op.is-active { border-color: var(--accent-border); } .wd-card-op-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .wd-card-op-name { font: 600 14px/1.2 var(--font); color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .wd-card-op-content { display: flex; flex-direction: column; gap: 8px; font: 400 13px/1.4 var(--font); color: var(--text-secondary); } .wd-card-op-footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding-top: 8px; border-top: 1px solid var(--border-faint); } .wd-card-op-meta { font: 400 11px/1 var(--font-mono); color: var(--text-tertiary); letter-spacing: 0.02em; } .wd-card-op-actions { display: flex; gap: 6px; } /* Mini bars (CPU/mem on cluster cards) */ .wd-mini-bar { width: 100%; height: 4px; background: var(--bg-deep); border-radius: 2px; overflow: hidden; } .wd-mini-bar-fill { height: 100%; background: var(--accent); transition: width 240ms cubic-bezier(0.16, 1, 0.3, 1); } .wd-mini-bar-fill--warn { background: var(--signal-warn); } .wd-mini-bar-fill--bad { background: var(--signal-bad); } /* Signal strip (recorder live indicator) */ .wd-signal-strip { position: relative; height: 4px; background: var(--bg-deep); border-radius: 2px; overflow: hidden; } .wd-signal-strip-fill { position: absolute; inset: 0; width: 100%; background: linear-gradient(90deg, var(--accent), oklch(70% 0.18 266)); } /* Grid container */ .wd-card-op-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 14px; } ``` - [ ] **Step 2: Commit** ```bash HOME=/root git add services/web-ui/src/css/components/card-operational.css HOME=/root git commit -m "web-ui: operational card primitive (header/content/footer, recorder/cluster/jobs)" ``` --- ### Task 14: Create list-row + empty-state primitives **Files:** - Create: `services/web-ui/src/css/components/list-row.css` - Create: `services/web-ui/src/css/components/empty-state.css` Two small files batched into one task to keep granularity bite-sized but avoid trivial single-rule commits. - [ ] **Step 1: Write list-row.css** ```css /* list-row.css ─ table-like row, NOT a card. For containers/users/tokens. */ .wd-list { display: flex; flex-direction: column; } .wd-list-row { position: relative; display: flex; align-items: center; gap: 16px; height: 44px; padding: 0 16px; border-bottom: 1px solid var(--border-faint); transition: background-color 120ms cubic-bezier(0.25, 1, 0.5, 1); } .wd-list-row:hover { background: var(--bg-hover); } .wd-list-row.is-selected { background: var(--bg-surface); } .wd-list-row.is-selected::before { content: ''; position: absolute; left: 4px; top: 50%; transform: translateY(-50%); width: 4px; height: 8px; border-radius: 2px; background: var(--accent); } .wd-list-cell { font: 400 13px/1 var(--font); color: var(--text-primary); } .wd-list-cell--name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; } .wd-list-cell--meta { color: var(--text-tertiary); font-family: var(--font-mono); font-size: 12px; } .wd-list-cell--actions { margin-left: auto; display: flex; gap: 6px; } .wd-list-header { display: flex; align-items: center; gap: 16px; height: 36px; padding: 0 16px; border-bottom: 1px solid var(--border); font: 600 11px/1 var(--font); letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-tertiary); } ``` - [ ] **Step 2: Write empty-state.css** ```css /* empty-state.css ─ centered icon + title + body + CTA, no card chrome */ .wd-empty { min-height: 360px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; text-align: center; padding: 48px 24px; } .wd-empty-icon { width: 28px; height: 28px; color: var(--text-tertiary); } .wd-empty-title { font: 600 14px/1.3 var(--font); color: var(--text-primary); } .wd-empty-body { font: 400 13px/1.5 var(--font); color: var(--text-secondary); max-width: 360px; } .wd-empty-actions { display: flex; gap: 8px; margin-top: 4px; } ``` - [ ] **Step 3: Commit** ```bash HOME=/root git add services/web-ui/src/css/components/list-row.css services/web-ui/src/css/components/empty-state.css HOME=/root git commit -m "web-ui: list-row + empty-state primitives" ``` --- ### Task 15: Create badge + toast primitives **Files:** - Create: `services/web-ui/src/css/components/badge.css` - Create: `services/web-ui/src/css/components/toast.css` - [ ] **Step 1: Write badge.css** ```css /* badge.css ─ status pills + signal badges */ .wd-badge { display: inline-flex; align-items: center; gap: 4px; height: 18px; padding: 0 6px; border-radius: 3px; font: 600 10px/1 var(--font); letter-spacing: 0.08em; text-transform: uppercase; } .wd-badge--good { background: var(--signal-good-bg); color: var(--signal-good); } .wd-badge--bad { background: var(--signal-bad-bg); color: var(--signal-bad); } .wd-badge--warn { background: var(--signal-warn-bg); color: var(--signal-warn); } .wd-badge--idle { background: var(--bg-surface); color: var(--text-tertiary); } .wd-badge--info { background: var(--accent-subtle); color: var(--accent); } /* Status dot (4px circle, leading indicator on cards/rows) */ .wd-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; } .wd-dot--good { background: var(--signal-good); } .wd-dot--bad { background: var(--signal-bad); } .wd-dot--warn { background: var(--signal-warn); } .wd-dot--idle { background: var(--signal-idle); } ``` - [ ] **Step 2: Write toast.css** ```css /* toast.css ─ bottom-right, top-strip variant. * 4px top strip = allowed (not a side-stripe). */ .wd-toast-container { position: fixed; bottom: 16px; right: 16px; z-index: var(--z-toast); display: flex; flex-direction: column; gap: 8px; max-width: 320px; } .wd-toast { position: relative; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; padding: 12px 14px 12px 14px; display: flex; flex-direction: column; gap: 4px; box-shadow: 0 12px 32px -16px oklch(0% 0 0 / 0.7); } .wd-toast::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; border-radius: 4px 4px 0 0; } .wd-toast--success { border-color: var(--signal-good); } .wd-toast--success::before { background: var(--signal-good); } .wd-toast--error { border-color: var(--signal-bad); } .wd-toast--error::before { background: var(--signal-bad); } .wd-toast--warning { border-color: var(--signal-warn); } .wd-toast--warning::before { background: var(--signal-warn); } .wd-toast--info { border-color: var(--accent); } .wd-toast--info::before { background: var(--accent); } .wd-toast-title { font: 600 13px/1.3 var(--font); color: var(--text-primary); margin-top: 4px; } .wd-toast-body { font: 400 12px/1.5 var(--font); color: var(--text-secondary); } ``` - [ ] **Step 3: Commit** ```bash HOME=/root git add services/web-ui/src/css/components/badge.css services/web-ui/src/css/components/toast.css HOME=/root git commit -m "web-ui: badge + toast primitives" ``` --- ### Task 16: Create entry CSS that imports everything **Files:** - Create: `services/web-ui/src/css/app.css` - [ ] **Step 1: Write app.css** ```css /* app.css ─ Tailwind directives + primitive imports. * * Order matters: * 1. tokens (CSS custom properties for legacy callers) * 2. tailwind base (CSS reset) * 3. tailwind components * 4. our primitives (use tokens + can be overridden by utilities) * 5. tailwind utilities (highest specificity, last word) * 6. motion (animations + reduced-motion override) */ @import "./components/tokens.css"; @tailwind base; @tailwind components; /* Primitives — each one ~50-200 lines, one responsibility per file */ @import "./components/sidebar.css"; @import "./components/topbar.css"; @import "./components/button.css"; @import "./components/form-controls.css"; @import "./components/field-group.css"; @import "./components/slide-panel.css"; @import "./components/card-asset.css"; @import "./components/card-operational.css"; @import "./components/list-row.css"; @import "./components/empty-state.css"; @import "./components/badge.css"; @import "./components/toast.css"; @tailwind utilities; @import "./components/motion.css"; /* Global base — self-hosted fonts */ @font-face { font-family: 'Inter'; src: url('/fonts/inter-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; } @font-face { font-family: 'Inter'; src: url('/fonts/inter-500.woff2') format('woff2'); font-weight: 500; font-style: normal; font-display: swap; } @font-face { font-family: 'Inter'; src: url('/fonts/inter-600.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; } @font-face { font-family: 'JetBrains Mono'; src: url('/fonts/jetbrains-mono-400.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; } @font-face { font-family: 'JetBrains Mono'; src: url('/fonts/jetbrains-mono-600.woff2') format('woff2'); font-weight: 600; font-style: normal; font-display: swap; } html { font-family: var(--font); background: var(--bg-base); color: var(--text-primary); -webkit-font-smoothing: antialiased; } ``` - [ ] **Step 2: Commit** ```bash HOME=/root git add services/web-ui/src/css/app.css HOME=/root git commit -m "web-ui: app.css entry — tailwind directives + primitive imports" ``` --- ### Task 17: Add self-hosted font files **Files:** - Create: `services/web-ui/public/fonts/inter-400.woff2` - Create: `services/web-ui/public/fonts/inter-500.woff2` - Create: `services/web-ui/public/fonts/inter-600.woff2` - Create: `services/web-ui/public/fonts/jetbrains-mono-400.woff2` - Create: `services/web-ui/public/fonts/jetbrains-mono-600.woff2` Download from Google Fonts API (woff2 only, latin subset). These are binary files committed to the repo. - [ ] **Step 1: Download font files via MeshCentral on zampp1** Run on zampp1: ```bash cd /opt/wild-dragon/services/web-ui/public mkdir -p fonts cd fonts # Inter for WEIGHT in 400 500 600; do curl -sL -o "inter-${WEIGHT}.woff2" \ "https://github.com/rsms/inter/raw/master/docs/font-files/Inter-$(case $WEIGHT in 400) echo Regular;; 500) echo Medium;; 600) echo SemiBold;; esac).woff2" done # JetBrains Mono for WEIGHT in 400 600; do curl -sL -o "jetbrains-mono-${WEIGHT}.woff2" \ "https://github.com/JetBrains/JetBrainsMono/raw/master/fonts/webfonts/JetBrainsMono-$(case $WEIGHT in 400) echo Regular;; 600) echo SemiBold;; esac).woff2" done ls -la ``` Expected: 5 .woff2 files, each 50-200KB. - [ ] **Step 2: Verify each file is a valid woff2** ```bash for f in *.woff2; do echo "$f: $(file "$f" | grep -c 'Web Open Font Format')" done ``` Expected: each line ends in `: 1`. - [ ] **Step 3: Commit binary files** ```bash cd /opt/wild-dragon HOME=/root git add services/web-ui/public/fonts/*.woff2 HOME=/root git commit -m "web-ui: self-host Inter + JetBrains Mono woff2 fonts" ``` --- ### Task 18: Modify Dockerfile to add Node build stage **Files:** - Modify: `services/web-ui/Dockerfile` - [ ] **Step 1: Read existing Dockerfile to see current structure** Run on zampp1: ```bash cat /opt/wild-dragon/services/web-ui/Dockerfile ``` Note the structure. The current single-stage Dockerfile is likely just `FROM nginx + COPY public/ + COPY nginx.conf`. The new file is multi-stage. - [ ] **Step 2: Rewrite Dockerfile as two-stage build** Replace `services/web-ui/Dockerfile` with: ```dockerfile # ── Stage 1: build CSS bundle ───────────────────────────────── FROM node:20-alpine AS css-build WORKDIR /build # Copy only the files needed to install deps (better cache layering) COPY package.json package-lock.json* ./ RUN npm install --no-audit --no-fund # Copy source CSS + tailwind config + every HTML file (tailwind # scans HTML to determine which utilities to emit) COPY tailwind.config.js postcss.config.js ./ COPY src/ ./src/ COPY public/ ./public/ # Build into public/dist/app.css RUN npx tailwindcss -i ./src/css/app.css -o ./public/dist/app.css --minify # ── Stage 2: runtime ────────────────────────────────────────── FROM nginx:1.27-alpine COPY --from=css-build /build/public/ /usr/share/nginx/html/ COPY nginx.conf /etc/nginx/conf.d/default.conf ``` - [ ] **Step 3: Commit** ```bash HOME=/root git add services/web-ui/Dockerfile HOME=/root git commit -m "web-ui: two-stage Dockerfile (Node build + nginx runtime)" ``` --- ### Task 19: Modify nginx.conf to cache /dist/* aggressively **Files:** - Modify: `services/web-ui/nginx.conf` - [ ] **Step 1: Read existing nginx.conf** ```bash cat /opt/wild-dragon/services/web-ui/nginx.conf ``` - [ ] **Step 2: Add a location block for /dist/ and /fonts/** Add the following inside the existing `server` block (above the catch-all `location /`): ```nginx location /dist/ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } location /fonts/ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } ``` - [ ] **Step 3: Commit** ```bash HOME=/root git add services/web-ui/nginx.conf HOME=/root git commit -m "web-ui: aggressive caching for /dist/ and /fonts/" ``` --- ### Task 20: Build and verify the new image locally on zampp1 **Files:** none modified, verification only. - [ ] **Step 1: Build the new image** On zampp1: ```bash cd /opt/wild-dragon docker compose build web-ui 2>&1 | tail -40 ``` Expected: build succeeds. Look for `tailwindcss` running and `app.css` being emitted. No errors. - [ ] **Step 2: Restart the container with the new image** ```bash docker compose up -d web-ui sleep 3 docker ps --filter name=wild-dragon-web-ui --format 'table {{.Names}}\t{{.Status}}' ``` Expected: container running, recent uptime. - [ ] **Step 3: Verify the new CSS bundle is being served** ```bash curl -sk -o /dev/null -w 'HTTP=%{http_code} size=%{size_download}\n' http://localhost:47434/dist/app.css ``` Expected: HTTP=200, size > 10000 bytes (minified CSS bundle). - [ ] **Step 4: Verify fonts are being served** ```bash for f in inter-400 inter-500 inter-600 jetbrains-mono-400 jetbrains-mono-600; do STATUS=$(curl -sk -o /dev/null -w '%{http_code}/%{size_download}' "http://localhost:47434/fonts/${f}.woff2") echo " ${f}.woff2: $STATUS" done ``` Expected: each line ends in `: 200/` where size is 50000-200000. - [ ] **Step 5: Verify existing pages STILL WORK (no regression)** ```bash for p in home recorders cluster index login editor; do STATUS=$(curl -sk -o /dev/null -w '%{http_code}' "http://localhost:47434/${p}.html") echo " ${p}.html: $STATUS" done ``` Expected: every page returns 200. (No page references the new CSS yet, but the build must not have broken anything.) - [ ] **Step 6: Verify the recorders page specifically still has its codec UI intact** ```bash curl -sk http://localhost:47434/recorders.html | grep -cE 'codec-tab|sdi-card-wrap|min-height: 0|BMDCards.render' ``` Expected: 4+ matches (the in-page CSS + JS from the previous rewrite is still there). - [ ] **Step 7: Commit a verification note** Nothing to commit — this is verification only. If all steps pass, move on. If any fails, stop and debug before continuing. --- ### Task 21: Smoke-test sample HTML against the new primitives **Files:** - Create: `services/web-ui/public/_primitives-smoke.html` A one-off page that exercises every primitive class so we can visually QA the bundle without migrating a real page yet. Will be deleted at the end of wave 4. - [ ] **Step 1: Write the smoke-test page** Write `services/web-ui/public/_primitives-smoke.html`: ```html Primitives smoke test

Asset cards

00:30 2
DRP_B004_081606_V1_0099.mov
Alissa Morris · Oct 14th, 2024
00:05 V2
DRP_A015_0815OF_V1_0023.mov
Alissa Morris · Oct 14th, 2024

Operational cards

Studio A SRT Recording
Receiving · 12450 fr · 30 fps 00:23:14
Last: Oct 14th, 2024 4:00 PM
zampp2 Online
CPU42%
Memory71%
DeckLink Duo 2 · 4 ports

List rows

Name Image Status Actions
wild-dragon-mam-api-1 wild-dragon-mam-api:latest Up
wild-dragon-web-ui-1 wild-dragon-web-ui:latest Up

Buttons

Form controls

Letters, numbers, dashes. Used in clip filenames.

Field group

Master recording

Empty state

No recorders yet
Create a recorder to ingest live streams via SRT, RTMP, or SDI.
``` - [ ] **Step 2: Rebuild the web-ui image so the smoke page ships** ```bash cd /opt/wild-dragon docker compose up -d --build web-ui 2>&1 | tail -8 ``` Expected: image rebuilt, container restarted, exit code 0. - [ ] **Step 3: Verify the smoke page renders** ```bash curl -sk -o /dev/null -w 'HTTP=%{http_code} size=%{size_download}\n' http://localhost:47434/_primitives-smoke.html ``` Expected: HTTP=200, size > 5000 bytes. - [ ] **Step 4: Inspect the rendered CSS for known primitive classes** ```bash curl -sk http://localhost:47434/dist/app.css | grep -cE '\.wd-sidebar|\.wd-card-asset|\.wd-card-op|\.wd-slide-panel|\.wd-field-group|\.wd-toast|\.wd-btn|\.wd-input' ``` Expected: at least 30 matches. - [ ] **Step 5: Commit the smoke page** ```bash HOME=/root git add services/web-ui/public/_primitives-smoke.html HOME=/root git commit -m "web-ui: add _primitives-smoke.html for wave-1 QA (delete at wave-4 end)" ``` - [ ] **Step 6: User QA gate** Stop. Ask user to load `http://:47434/_primitives-smoke.html` in their browser and visually QA the new primitives. If any primitive looks wrong, file a new task to fix it. Do not proceed to wave 2 until the user signs off on the visual quality of the smoke page. --- ### Task 22: Push everything to forge.wilddragon.net **Files:** none modified. - [ ] **Step 1: Push commits to main** ```bash cd /opt/wild-dragon HOME=/root git push origin main 2>&1 | tail -5 ``` Expected: push succeeds, several commits land on `origin/main`. - [ ] **Step 2: Verify the new files exist in the remote** ```bash curl -s -H "Authorization: token " \ "https://forge.wilddragon.net/api/v1/repos/zgaetano/wild-dragon/contents/services/web-ui/src/css/app.css?ref=main" \ | head -c 200 ``` Expected: JSON response with `"sha"` field, not 404. --- ## Wave 2 / 3 / 4 placeholder These waves are **out of scope for this plan document**. Each will get its own detailed plan after the prior wave ships and is verified. - **Wave 2** (`docs/superpowers/plans/2026-05-2X-ui-shell-rework-wave-2-plan.md`): migrate `login.html`, `home.html`, `settings.html`, `tokens.html`, `users.html`, `containers.html` to the new primitives. One task per page. - **Wave 3** (`docs/superpowers/plans/2026-05-2X-ui-shell-rework-wave-3-plan.md`): migrate `index.html`, `projects.html`, `upload.html`, `jobs.html`, `api-tokens.html`. One task per page. - **Wave 4** (`docs/superpowers/plans/2026-05-2X-ui-shell-rework-wave-4-plan.md`): migrate `recorders.html`, `cluster.html`, `capture.html`. One task per page, plus a final task to delete `_primitives-smoke.html`. --- ## Self-review notes (For the implementer / reviewer.) - **Spec coverage:** Every numbered section of the spec maps to at least one task. Tokens → task 4. Sidebar → task 6. Topbar → task 7. Cards (4 families) → tasks 12, 13, 14. Forms → tasks 9, 10. Slide-panel → task 11. Motion → task 5. Badges/toasts → task 15. A11y (tertiary lightness bump) → task 4. Build/theme port → tasks 1-3, 18, 19. Self-hosted fonts → task 17. Smoke test → task 21. - **Placeholders:** none. Every code step has the actual code. - **Type consistency:** class names use the `wd-` prefix throughout. Sidebar active state uses `.is-active` everywhere. Slide-panel uses `.is-open` everywhere. List-row selected uses `.is-selected`. - **Open risk:** the Inter / JetBrains Mono download URLs in task 17 step 1 depend on those GitHub repos remaining at the same paths. If they 404, fall back to fonts.google.com woff2 downloads or vendor the files in manually.