diff --git a/docs/DESIGN_SYSTEM_REMEDIATION_PLAYBOOK.md b/docs/DESIGN_SYSTEM_REMEDIATION_PLAYBOOK.md new file mode 100644 index 00000000..60ac90a8 --- /dev/null +++ b/docs/DESIGN_SYSTEM_REMEDIATION_PLAYBOOK.md @@ -0,0 +1,1742 @@ +# Design System Remediation Playbook + +> **Purpose:** Agent-executable playbook. Every task has exact file paths, code templates, verification commands, and binary pass/fail criteria. A coding agent should be able to execute this document end-to-end without human intervention. +> +> **Companion doc:** `DESIGN_SYSTEM_AUDIT.md` — the audit findings this playbook remediates. +> +> **Commit convention:** `feat(design-system): ` or `fix(design-system): ` + +--- + +## Prerequisites + +```bash +# All work happens relative to the workspace root +WORKSPACE="$HOME/code/mygh" + +# Verify repos exist +ls $WORKSPACE/learning_ai_common_plat # ← token generator lives here +ls $WORKSPACE/learning_ai_clock # ChronoMind +ls $WORKSPACE/learning_ai_fastgap # NomGap +ls $WORKSPACE/learning_ai_jarvis_jr # JarvisJr +ls $WORKSPACE/learning_ai_trails # ActionTrail +ls $WORKSPACE/learning_ai_flowmonk # FlowMonk +ls $WORKSPACE/learning_ai_notes # NoteLett +ls $WORKSPACE/learning_ai_local_memory_gpt # LocalMemGPT +ls $WORKSPACE/learning_ai_local_llms # Local LLM Lab + +# pnpm must be available (common-plat is a pnpm workspace) +pnpm --version # must be >= 9 +``` + +## Repo Map (Reference) + +| Alias | Repo | Web Source | CSS Prefix | Backend Port | +| ------ | ------------------------------ | ------------------------- | ---------- | ------------ | +| `PLAT` | `learning_ai_common_plat` | `packages/design-tokens/` | N/A | N/A | +| `CM` | `learning_ai_clock` | `web/src/` | `--cm-` | 4011 | +| `NG` | `learning_ai_fastgap` | `web/src/` | `--ng-` | 4013 | +| `JJ` | `learning_ai_jarvis_jr` | `web/src/` | `--jj-` | 4012 | +| `AT` | `learning_ai_trails` | `web/src/` | `--at-` | 4018 | +| `FM` | `learning_ai_flowmonk` | `web/src/` | `--fm-` | 4017 | +| `NL` | `learning_ai_notes` | `web/src/` | `--nl-` | 4016 | +| `LMG` | `learning_ai_local_memory_gpt` | `web/src/` | `--lmg-` | 4019 | +| `LLM` | `learning_ai_local_llms` | `dashboard/src/` | `--llm-` | 3000 | + +--- + +# Phase 1: Foundation Fix + +> **Goal:** Make the design token system actually consumable. Currently tokens are defined but no app imports them. +> **Depends on:** Nothing — this is the starting phase. +> **Estimated effort:** 1-2 days + +--- + +## Task 1.1: Add Missing Product Palettes to Token JSON + +**File:** `$WORKSPACE/learning_ai_common_plat/packages/design-tokens/tokens/bytelyst.tokens.json` + +**What:** Add `color.actiontrail`, `color.notelett`, `color.localmemgpt`, and `color.localllmlab` sections. Currently only 7 of 11 products have palettes. + +**Action:** Open the token JSON and add these 4 new product palette objects at the same level as `color.chronomind`, `color.nomgap`, etc. Use the product's existing `globals.css` as the source of truth for color values. + +For each missing product, extract the product-specific colors from their `globals.css`: + +```bash +# ActionTrail colors (from learning_ai_trails/web/src/app/globals.css) +grep -E '^\s+--at-' $WORKSPACE/learning_ai_trails/web/src/app/globals.css + +# NoteLett colors (from learning_ai_notes/web/src/app/globals.css, skip --ml- leaks) +grep -E '^\s+--nl-' $WORKSPACE/learning_ai_notes/web/src/app/globals.css + +# LocalMemGPT colors (from learning_ai_local_memory_gpt/web/src/app/globals.css) +grep -E '^\s+--lmg-' $WORKSPACE/learning_ai_local_memory_gpt/web/src/app/globals.css + +# Local LLM Lab colors (from learning_ai_local_llms/dashboard/src/app/globals.css) +grep -E '^\s+--llm-' $WORKSPACE/learning_ai_local_llms/dashboard/src/app/globals.css +``` + +Add JSON entries following the existing pattern. Example for ActionTrail: + +```json +"actiontrail": { + "bgCanvas": "#0B0D11", + "bgElevated": "#12151C", + "surfaceCard": "#171B24", + "textPrimary": "#E8ECF4", + "textSecondary": "#8B95A8", + "accent": "#6C8EEF", + "success": "#34D399", + "warning": "#F59E0B", + "danger": "#EF4444", + "riskCritical": "#DC2626", + "riskHigh": "#F97316", + "riskMedium": "#EAB308", + "riskLow": "#22C55E" +} +``` + +Repeat for `notelett`, `localmemgpt`, `localllmlab` — extracting values from each app's `globals.css`. + +**Verify:** + +```bash +cd $WORKSPACE/learning_ai_common_plat +node -e " +const t = require('./packages/design-tokens/tokens/bytelyst.tokens.json'); +const required = ['actiontrail','notelett','localmemgpt','localllmlab']; +const found = required.filter(p => t.color[p]); +console.log('Found:', found.length, '/', required.length); +if (found.length !== required.length) { console.log('MISSING:', required.filter(p => !t.color[p])); process.exit(1); } +console.log('PASS'); +" +``` + +**Pass criteria:** All 4 products exist in `color.*` → script prints `PASS`. + +--- + +## Task 1.2: Extend Token Generator for Per-Product CSS + +**File:** `$WORKSPACE/learning_ai_common_plat/packages/design-tokens/scripts/generate.ts` + +**What:** The current `generateCSS()` function hardcodes the `--ml-` prefix. Refactor it to accept a product name and prefix, then loop over all products. + +**Action:** Add a new function `generateProductCSS(productId, prefix, productColors)` and call it for every product that has a palette in the JSON. Also keep the existing `tokens.css` (shared/semantic) output. + +Add this mapping object near the top of `generate.ts` (after the `tokens` const): + +```typescript +const PRODUCT_CSS_MAP: Record = { + mindlyst: { prefix: 'ml', colorsKey: 'brain' }, // existing (uses color.brain for gradients) + chronomind: { prefix: 'cm', colorsKey: 'chronomind' }, + jarvisjr: { prefix: 'jj', colorsKey: 'jarvisjr' }, + nomgap: { prefix: 'ng', colorsKey: 'nomgap' }, + actiontrail: { prefix: 'at', colorsKey: 'actiontrail' }, + flowmonk: { prefix: 'fm', colorsKey: 'flowmonk' }, + notelett: { prefix: 'nl', colorsKey: 'notelett' }, + localmemgpt: { prefix: 'lmg', colorsKey: 'localmemgpt' }, + localllmlab: { prefix: 'llm', colorsKey: 'localllmlab' }, + lysnrai: { prefix: 'lys', colorsKey: 'lysnrai' }, + peakpulse: { prefix: 'pp', colorsKey: 'peakpulse' }, +}; +``` + +Add a new function: + +```typescript +function generateProductCSS(productId: string, prefix: string, colorsKey: string): string { + const productColors = tokens.color[colorsKey]; + if (!productColors) return `/* No palette found for ${productId} (color.${colorsKey}) */\n`; + + const lines: string[] = [ + `/* Auto-generated ${productId} tokens from bytelyst.tokens.json — do not edit manually */`, + '', + ':root {', + ]; + + // Semantic colors (dark as default) — shared across all products + for (const [key, value] of Object.entries(tokens.color.semantic.dark)) { + lines.push(` --${prefix}-${camelToKebab(key)}: ${value};`); + } + lines.push(''); + + // Product-specific colors + lines.push(` /* ${productId} product colors */`); + for (const [key, value] of Object.entries(productColors)) { + if (typeof value === 'string') { + lines.push(` --${prefix}-${camelToKebab(key)}: ${value};`); + } else if (typeof value === 'object' && value !== null && 'from' in value) { + // Gradient — emit from/to + lines.push(` --${prefix}-${camelToKebab(key)}-from: ${(value as any).from};`); + lines.push(` --${prefix}-${camelToKebab(key)}-to: ${(value as any).to};`); + } + } + lines.push(''); + + // Typography + for (const [key, value] of Object.entries(tokens.typography.fontFamily)) { + const cssVal = typeof value === 'string' ? value.replace(/'/g, '"') : value; + lines.push(` --${prefix}-font-${key}: ${cssVal};`); + } + lines.push(''); + + // Font sizes + for (const [key, value] of Object.entries(tokens.typography.fontSize)) { + lines.push(` --${prefix}-fs-${key}: ${value}px;`); + } + lines.push(''); + + // Spacing + for (const [key, value] of Object.entries(tokens.spacing)) { + lines.push(` --${prefix}-space-${key}: ${value === 0 ? '0' : `${value}px`};`); + } + lines.push(''); + + // Radius + for (const [key, value] of Object.entries(tokens.radius)) { + lines.push(` --${prefix}-radius-${key}: ${value}px;`); + } + lines.push(''); + + // Elevation + for (const [key, value] of Object.entries(tokens.elevation)) { + if (key === 'none') continue; + lines.push(` --${prefix}-elevation-${key}: ${value};`); + } + lines.push(''); + + // Motion + for (const [key, value] of Object.entries(tokens.motion.duration)) { + if (key === 'instant') continue; + lines.push(` --${prefix}-motion-${key}: ${value}ms;`); + } + lines.push(` --${prefix}-easing-standard: ${tokens.motion.easing.standard};`); + + lines.push('}', ''); + + // Light theme overrides + lines.push('[data-theme="light"] {'); + for (const [key, value] of Object.entries(tokens.color.semantic.light)) { + const darkVal = (tokens.color.semantic.dark as Record)[key]; + if (value !== darkVal) { + lines.push(` --${prefix}-${camelToKebab(key)}: ${value};`); + } + } + lines.push('}', ''); + + return lines.join('\n'); +} +``` + +Update the "Write all" section at the bottom of `generate.ts`: + +```typescript +// ── Write all ──────────────────────────────────────────────────────── +// Shared semantic tokens (backward compatible) +writeFileSync(resolve(outDir, 'tokens.css'), generateCSS()); +writeFileSync(resolve(outDir, 'tokens.ts'), generateTS()); +writeFileSync(resolve(outDir, 'MindLystTokens.kt'), generateKotlin()); +writeFileSync(resolve(outDir, 'MindLystTheme.swift'), generateSwift()); + +// Per-product CSS +for (const [productId, config] of Object.entries(PRODUCT_CSS_MAP)) { + const css = generateProductCSS(productId, config.prefix, config.colorsKey); + writeFileSync(resolve(outDir, `${productId}.css`), css); +} + +console.log( + `Generated 4 shared + ${Object.keys(PRODUCT_CSS_MAP).length} product token files in generated/` +); +``` + +**Verify:** + +```bash +cd $WORKSPACE/learning_ai_common_plat/packages/design-tokens +npx tsx scripts/generate.ts + +# Check per-product files exist +for p in chronomind jarvisjr nomgap actiontrail flowmonk notelett localmemgpt localllmlab lysnrai peakpulse mindlyst; do + if [ -f "generated/${p}.css" ]; then echo "PASS: ${p}.css exists"; else echo "FAIL: ${p}.css missing"; exit 1; fi +done + +# Spot-check: chronomind.css should have --cm- prefix +grep -c '\-\-cm-' generated/chronomind.css | xargs -I{} test {} -gt 20 && echo "PASS: --cm- prefix count > 20" || echo "FAIL" + +# Spot-check: nomgap.css should have --ng- prefix, NOT --ml- +grep -c '\-\-ng-' generated/nomgap.css | xargs -I{} test {} -gt 20 && echo "PASS: --ng- prefix" || echo "FAIL" +grep -c '\-\-ml-' generated/nomgap.css | xargs -I{} test {} -eq 0 && echo "PASS: no --ml- leak" || echo "FAIL: --ml- leak detected" +``` + +**Pass criteria:** 11 product CSS files generated. Each uses its correct prefix. No `--ml-` leaks in non-MindLyst files. + +**Commit:** `feat(design-tokens): per-product CSS generation for all 11 products` + +--- + +## Task 1.3: Wire Web Apps to Import Generated Tokens + +**What:** Each product web app should import its generated CSS file from `@bytelyst/design-tokens` instead of hand-writing CSS custom properties. + +**This task is app-by-app.** For each app below: + +1. Add `@bytelyst/design-tokens` to `package.json` (or verify it's a `file:` ref if not published) +2. Replace hand-written CSS custom properties in `globals.css` with an `@import` of the generated file +3. Keep any product-specific CSS that is NOT token-related (layout rules, component styles, etc.) + +### Sub-task 1.3a: Publish design-tokens package + +First, ensure the package is consumable. Add an `exports` field to the design-tokens `package.json`: + +**File:** `$WORKSPACE/learning_ai_common_plat/packages/design-tokens/package.json` + +Ensure it has: + +```json +{ + "exports": { + ".": "./generated/tokens.ts", + "./css": "./generated/tokens.css", + "./css/*": "./generated/*.css", + "./tokens": "./generated/tokens.ts", + "./kotlin": "./generated/MindLystTokens.kt", + "./swift": "./generated/MindLystTheme.swift" + } +} +``` + +Run `pnpm build` in common-plat to ensure it builds cleanly. + +### Sub-task 1.3b: Wire each web app + +For each product web app, add the dependency and import: + +**Pattern for each app:** + +1. In the app's `package.json`, add: + + ```json + "@bytelyst/design-tokens": "file:../../learning_ai_common_plat/packages/design-tokens" + ``` + + (Adjust the relative path based on the repo location) + +2. In `globals.css`, replace the hand-written `:root { --xx-... }` block with: + + ```css + @import '@bytelyst/design-tokens/css/.css'; + ``` + +3. Keep any product-specific overrides or non-token CSS rules that follow the `:root` block. + +**App-specific instructions:** + +| App | `package.json` path | `globals.css` path | Import line | +| ----------- | ----------------------------------------------- | ------------------------- | -------------------------------------------------------- | +| ChronoMind | `learning_ai_clock/web/package.json` | `web/src/app/globals.css` | `@import "@bytelyst/design-tokens/css/chronomind.css";` | +| NomGap | `learning_ai_fastgap/web/package.json` | `web/src/app/globals.css` | `@import "@bytelyst/design-tokens/css/nomgap.css";` | +| JarvisJr | `learning_ai_jarvis_jr/web/package.json` | `web/src/app/globals.css` | `@import "@bytelyst/design-tokens/css/jarvisjr.css";` | +| ActionTrail | `learning_ai_trails/web/package.json` | `web/src/app/globals.css` | `@import "@bytelyst/design-tokens/css/actiontrail.css";` | +| FlowMonk | `learning_ai_flowmonk/web/package.json` | `web/src/app/globals.css` | `@import "@bytelyst/design-tokens/css/flowmonk.css";` | +| NoteLett | `learning_ai_notes/web/package.json` | `web/src/app/globals.css` | `@import "@bytelyst/design-tokens/css/notelett.css";` | +| LocalMemGPT | `learning_ai_local_memory_gpt/web/package.json` | `web/src/app/globals.css` | `@import "@bytelyst/design-tokens/css/localmemgpt.css";` | + +**Important:** When replacing `:root {}` blocks: + +- Remove ALL lines that define `--xx-bg-canvas`, `--xx-text-primary`, `--xx-font-*`, `--xx-space-*`, `--xx-radius-*`, etc. — these now come from the generated file +- KEEP any product-specific CSS that is layout, animation, or component styling (e.g., `body { ... }`, `.sidebar { ... }`, `@keyframes`) +- KEEP the `@import "tailwindcss";` line if present (it must stay) +- If the app has a `[data-theme="light"]` block, remove it — the generated CSS includes light overrides + +**Verify (per app):** + +```bash +# Example for ChronoMind: +cd $WORKSPACE/learning_ai_clock/web +grep -c '@bytelyst/design-tokens' package.json # must be >= 1 +grep -c 'design-tokens/css/chronomind' src/app/globals.css # must be >= 1 +grep -c '\-\-cm-bg-canvas' src/app/globals.css # must be 0 (removed hand-written) +npm run typecheck # must pass +npm run build -- --webpack # must pass (use --webpack if needed per AGENTS.md) +``` + +**Pass criteria:** Each app's `globals.css` imports from `@bytelyst/design-tokens`, has zero hand-written token definitions, and builds cleanly. + +**Commit (per app):** `feat(design-system): wire web to generated design tokens` + +--- + +## Task 1.4: Fix NomGap and NoteLett `--ml-*` CSS Prefix Leaks + +**Files:** + +- `$WORKSPACE/learning_ai_fastgap/web/src/app/globals.css` +- `$WORKSPACE/learning_ai_notes/web/src/app/globals.css` + +**What:** Both apps have `--ml-*` prefixed CSS variables that should be `--ng-*` and `--nl-*` respectively. After Task 1.3, these should be replaced by the generated import, but verify no leaks remain. + +**Verify:** + +```bash +# NomGap: zero --ml- refs (should all be --ng-) +grep -c '\-\-ml-' $WORKSPACE/learning_ai_fastgap/web/src/app/globals.css +# Must be 0 + +# NoteLett: zero --ml- refs (should all be --nl-) +grep -c '\-\-ml-' $WORKSPACE/learning_ai_notes/web/src/app/globals.css +# Must be 0 +``` + +If any remain after Task 1.3, do a find-and-replace: `--ml-` → `--ng-` in NomGap, `--ml-` → `--nl-` in NoteLett. Then also update any component `.tsx` files that reference `--ml-*` variables: + +```bash +# Find all --ml- refs in NomGap web components +grep -rn '\-\-ml-' $WORKSPACE/learning_ai_fastgap/web/src/ + +# Find all --ml- refs in NoteLett web components +grep -rn '\-\-ml-' $WORKSPACE/learning_ai_notes/web/src/ +``` + +Replace each occurrence with the correct product prefix. + +**Pass criteria:** `grep -rn '\-\-ml-' /web/src/` returns 0 results for both NomGap and NoteLett. + +**Commit:** `fix(design-system): remove --ml- prefix leaks from NomGap and NoteLett` + +--- + +# Phase 2: Shared Component Library + +> **Goal:** Create `@bytelyst/ui` so product teams stop reinventing Button, Toast, Modal, etc. +> **Depends on:** Phase 1 (tokens must be generated so components can consume them) +> **Estimated effort:** 3-5 days + +--- + +## Task 2.1: Scaffold `@bytelyst/ui` Package + +**Action:** Create the package in the common-plat monorepo. + +```bash +cd $WORKSPACE/learning_ai_common_plat +mkdir -p packages/ui/src/components +``` + +**File:** `$WORKSPACE/learning_ai_common_plat/packages/ui/package.json` + +```json +{ + "name": "@bytelyst/ui", + "version": "0.1.0", + "type": "module", + "exports": { + ".": "./src/index.ts", + "./button": "./src/components/Button.tsx", + "./toast": "./src/components/Toast.tsx", + "./modal": "./src/components/Modal.tsx", + "./confirm-dialog": "./src/components/ConfirmDialog.tsx", + "./badge": "./src/components/Badge.tsx", + "./empty-state": "./src/components/EmptyState.tsx", + "./sidebar": "./src/components/Sidebar.tsx" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.0", + "@radix-ui/react-toast": "^1.2.0", + "@radix-ui/react-alert-dialog": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "lucide-react": "^0.460.0", + "clsx": "^2.1.0" + }, + "devDependencies": { + "typescript": "^5.7.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0" + } +} +``` + +**File:** `$WORKSPACE/learning_ai_common_plat/packages/ui/tsconfig.json` + +```json +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "outDir": "dist", + "rootDir": "src", + "declaration": true + }, + "include": ["src"] +} +``` + +**File:** `$WORKSPACE/learning_ai_common_plat/packages/ui/src/index.ts` + +```typescript +export { Button, type ButtonProps } from './components/Button.js'; +export { Toast, ToastProvider, useToast, type ToastMessage } from './components/Toast.js'; +export { Modal, type ModalProps } from './components/Modal.js'; +export { ConfirmDialog, type ConfirmDialogProps } from './components/ConfirmDialog.js'; +export { Badge, type BadgeProps } from './components/Badge.js'; +export { EmptyState, type EmptyStateProps } from './components/EmptyState.js'; +``` + +**Verify:** + +```bash +cd $WORKSPACE/learning_ai_common_plat +ls packages/ui/package.json && echo "PASS: package.json exists" +ls packages/ui/src/index.ts && echo "PASS: index.ts exists" +``` + +--- + +## Task 2.2: Build Button Component + +**File:** `$WORKSPACE/learning_ai_common_plat/packages/ui/src/components/Button.tsx` + +```tsx +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { clsx } from 'clsx'; +import { Loader2 } from 'lucide-react'; + +export interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'outline'; + size?: 'sm' | 'md' | 'lg'; + loading?: boolean; + asChild?: boolean; +} + +export const Button = React.forwardRef( + ( + { variant = 'primary', size = 'md', loading, asChild, className, children, disabled, ...props }, + ref + ) => { + const Comp = asChild ? Slot : 'button'; + + const baseStyles = + 'inline-flex items-center justify-center font-medium rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'; + + const variants: Record = { + primary: 'bg-[var(--bl-accent,#5A8CFF)] text-white hover:opacity-90', + secondary: + 'bg-[var(--bl-surface-card,#1a1a2e)] text-[var(--bl-text-primary,#fff)] border border-[var(--bl-border,#2a2a4a)] hover:bg-[var(--bl-surface-muted,#252540)]', + ghost: + 'text-[var(--bl-text-secondary,#a0a0b0)] hover:bg-[var(--bl-surface-muted,#252540)] hover:text-[var(--bl-text-primary,#fff)]', + destructive: 'bg-red-600 text-white hover:bg-red-700', + outline: + 'border border-[var(--bl-border,#2a2a4a)] text-[var(--bl-text-primary,#fff)] hover:bg-[var(--bl-surface-muted,#252540)]', + }; + + const sizes: Record = { + sm: 'h-8 px-3 text-xs gap-1.5', + md: 'h-10 px-4 text-sm gap-2', + lg: 'h-12 px-6 text-base gap-2.5', + }; + + return ( + + {loading && } + {children} + + ); + } +); + +Button.displayName = 'Button'; +``` + +**Verify:** + +```bash +cd $WORKSPACE/learning_ai_common_plat +npx tsc --noEmit -p packages/ui/tsconfig.json 2>&1 | head -5 +# Must have 0 errors (some warnings OK if peer deps not installed locally) +``` + +--- + +## Task 2.3: Build Toast Component + +**File:** `$WORKSPACE/learning_ai_common_plat/packages/ui/src/components/Toast.tsx` + +```tsx +'use client'; + +import * as React from 'react'; +import { clsx } from 'clsx'; +import { X, CheckCircle, AlertTriangle, Info, AlertCircle } from 'lucide-react'; + +export interface ToastMessage { + id: string; + type: 'success' | 'error' | 'warning' | 'info'; + title: string; + description?: string; + duration?: number; +} + +type ToastListener = (toasts: ToastMessage[]) => void; +let globalToasts: ToastMessage[] = []; +const listeners = new Set(); +function notifyListeners() { + listeners.forEach(l => l([...globalToasts])); +} + +export function toast(msg: Omit) { + const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; + globalToasts = [...globalToasts, { ...msg, id }]; + notifyListeners(); + const duration = msg.duration ?? 5000; + if (duration > 0) setTimeout(() => dismissToast(id), duration); +} + +export function dismissToast(id: string) { + globalToasts = globalToasts.filter(t => t.id !== id); + notifyListeners(); +} + +export function useToast() { + return { toast, dismiss: dismissToast }; +} + +const icons: Record = { + success: , + error: , + warning: , + info: , +}; + +export function Toast({ message, onDismiss }: { message: ToastMessage; onDismiss: () => void }) { + return ( +
+ {icons[message.type]} +
+

{message.title}

+ {message.description && ( +

+ {message.description} +

+ )} +
+ +
+ ); +} + +export function ToastProvider({ children }: { children: React.ReactNode }) { + const [toasts, setToasts] = React.useState([]); + React.useEffect(() => { + listeners.add(setToasts); + return () => { + listeners.delete(setToasts); + }; + }, []); + + return ( + <> + {children} +
+ {toasts.map(t => ( + dismissToast(t.id)} /> + ))} +
+ + ); +} +``` + +--- + +## Task 2.4: Build Modal Component + +**File:** `$WORKSPACE/learning_ai_common_plat/packages/ui/src/components/Modal.tsx` + +```tsx +'use client'; + +import * as React from 'react'; +import * as Dialog from '@radix-ui/react-dialog'; +import { clsx } from 'clsx'; +import { X } from 'lucide-react'; + +export interface ModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description?: string; + size?: 'sm' | 'md' | 'lg' | 'full'; + children: React.ReactNode; +} + +const sizes: Record = { + sm: 'max-w-sm', + md: 'max-w-lg', + lg: 'max-w-2xl', + full: 'max-w-[90vw]', +}; + +export function Modal({ + open, + onOpenChange, + title, + description, + size = 'md', + children, +}: ModalProps) { + return ( + + + + + {title} + {description && ( + + {description} + + )} +
{children}
+ + + +
+
+
+ ); +} +``` + +--- + +## Task 2.5: Build ConfirmDialog Component + +**File:** `$WORKSPACE/learning_ai_common_plat/packages/ui/src/components/ConfirmDialog.tsx` + +```tsx +'use client'; + +import * as React from 'react'; +import * as AlertDialog from '@radix-ui/react-alert-dialog'; +import { clsx } from 'clsx'; +import { AlertTriangle } from 'lucide-react'; +import { Button } from './Button.js'; + +export interface ConfirmDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description: string; + confirmLabel?: string; + cancelLabel?: string; + variant?: 'destructive' | 'warning'; + loading?: boolean; + onConfirm: () => void | Promise; +} + +export function ConfirmDialog({ + open, + onOpenChange, + title, + description, + confirmLabel = 'Confirm', + cancelLabel = 'Cancel', + variant = 'destructive', + loading, + onConfirm, +}: ConfirmDialogProps) { + return ( + + + + +
+
+ +
+
+ {title} + + {description} + +
+
+
+ + + + + + +
+
+
+
+ ); +} +``` + +--- + +## Task 2.6: Build Badge Component + +**File:** `$WORKSPACE/learning_ai_common_plat/packages/ui/src/components/Badge.tsx` + +```tsx +import * as React from 'react'; +import { clsx } from 'clsx'; + +export interface BadgeProps extends React.HTMLAttributes { + variant?: 'success' | 'warning' | 'error' | 'info' | 'neutral'; + size?: 'sm' | 'md'; + dot?: boolean; +} + +const variantStyles: Record = { + success: 'bg-green-500/10 text-green-400 border-green-500/20', + warning: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20', + error: 'bg-red-500/10 text-red-400 border-red-500/20', + info: 'bg-blue-500/10 text-blue-400 border-blue-500/20', + neutral: + 'bg-[var(--bl-surface-muted,#252540)] text-[var(--bl-text-secondary,#a0a0b0)] border-[var(--bl-border,#2a2a4a)]', +}; + +export function Badge({ + variant = 'neutral', + size = 'sm', + dot, + className, + children, + ...props +}: BadgeProps) { + return ( + + {dot && ( + + )} + {children} + + ); +} +``` + +--- + +## Task 2.7: Build EmptyState Component + +**File:** `$WORKSPACE/learning_ai_common_plat/packages/ui/src/components/EmptyState.tsx` + +```tsx +import * as React from 'react'; +import { clsx } from 'clsx'; +import { Inbox } from 'lucide-react'; +import { Button } from './Button.js'; + +export interface EmptyStateProps { + icon?: React.ReactNode; + title: string; + description?: string; + actionLabel?: string; + onAction?: () => void; + className?: string; +} + +export function EmptyState({ + icon, + title, + description, + actionLabel, + onAction, + className, +}: EmptyStateProps) { + return ( +
+
+ {icon ?? } +
+

{title}

+ {description && ( +

+ {description} +

+ )} + {actionLabel && onAction && ( + + )} +
+ ); +} +``` + +**Verify all components build:** + +```bash +cd $WORKSPACE/learning_ai_common_plat +pnpm install # install new deps +npx tsc --noEmit -p packages/ui/tsconfig.json +# Must exit 0 with no errors +echo "PASS: @bytelyst/ui type-checks cleanly" +``` + +**Commit:** `feat(ui): create @bytelyst/ui with Button, Toast, Modal, ConfirmDialog, Badge, EmptyState` + +--- + +# Phase 3: UX Quality Bar + +> **Goal:** Every product web app meets the minimum UX quality bar from the audit's non-negotiable checklist. +> **Depends on:** Phase 2 (components from `@bytelyst/ui` are used here) +> **Estimated effort:** 2-3 days + +--- + +## Task 3.1: Add `not-found.tsx` to All Product Web Apps + +**What:** 6 of 7 product web apps have no custom 404 page. Users hitting a bad URL see the default Next.js 404. + +**Apps needing this file:** NomGap, JarvisJr, ActionTrail, FlowMonk, NoteLett, LocalMemGPT + +**Template:** Create `src/app/not-found.tsx` in each app. Use ChronoMind's as the reference pattern but replace the CSS prefix. + +For each app, create `not-found.tsx` at `/web/src/app/not-found.tsx`: + +```tsx +import Link from 'next/link'; + +export default function NotFound() { + return ( +
+
+

+ 404 +

+

+ Page not found +

+

+ The page you are looking for does not exist or has been moved. +

+ + Go home + +
+
+ ); +} +``` + +**Prefix substitutions:** + +| App | Replace `PREFIX` with | +| ----------- | --------------------- | +| NomGap | `ng` | +| JarvisJr | `jj` | +| ActionTrail | `at` | +| FlowMonk | `fm` | +| NoteLett | `nl` | +| LocalMemGPT | `lmg` | + +**Verify:** + +```bash +for repo in learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + if [ -f "$WORKSPACE/$repo/web/src/app/not-found.tsx" ]; then echo "PASS: $repo"; else echo "FAIL: $repo"; fi +done +``` + +**Pass criteria:** All 6 files exist. Build passes for each app. + +--- + +## Task 3.2: Add `error.tsx` to All Product Web Apps + +**What:** Only ChronoMind has an error boundary. All others show the default Next.js error page. + +**Apps needing this file:** NomGap, JarvisJr, ActionTrail, FlowMonk, NoteLett, LocalMemGPT + +**Template:** Create `src/app/error.tsx` in each app: + +```tsx +'use client'; + +import { useEffect } from 'react'; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error('Unhandled error:', error); + }, [error]); + + return ( +
+
+

+ Something went wrong +

+

+ {error.message || 'An unexpected error occurred.'} +

+ +
+
+ ); +} +``` + +Same prefix substitutions as Task 3.1. + +**Verify:** + +```bash +for repo in learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + if [ -f "$WORKSPACE/$repo/web/src/app/error.tsx" ]; then echo "PASS: $repo"; else echo "FAIL: $repo"; fi +done +``` + +--- + +## Task 3.3: Add `loading.tsx` to All Product Web Apps + +**What:** Zero apps have a loading state for route transitions. + +**Apps needing this file:** ALL 7 product web apps + ChronoMind + +**Template:** Create `src/app/loading.tsx`: + +```tsx +export default function Loading() { + return ( +
+
+
+

+ Loading... +

+
+
+ ); +} +``` + +**Verify:** + +```bash +for repo in learning_ai_clock learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + if [ -f "$WORKSPACE/$repo/web/src/app/loading.tsx" ]; then echo "PASS: $repo"; else echo "FAIL: $repo"; fi +done +``` + +**Commit (Tasks 3.1-3.3):** `feat(design-system): add not-found, error, loading pages to all product web apps` + +--- + +## Task 3.4: Add Focus-Visible CSS to All `globals.css` + +**What:** 5 of 7 apps have zero `focus-visible` refs. Keyboard users cannot see focus. + +**Action:** Add these rules to each app's `globals.css` (after the token import or `:root` block): + +```css +/* Focus-visible — keyboard accessibility */ +*:focus-visible { + outline: 2px solid var(--PREFIX-accent, #5a8cff); + outline-offset: 2px; + border-radius: 4px; +} + +/* Remove default focus ring for mouse users */ +*:focus:not(:focus-visible) { + outline: none; +} +``` + +**Files to modify** (use correct prefix for each): + +| File | Prefix | +| ------------------------------------------------------ | ------ | +| `learning_ai_fastgap/web/src/app/globals.css` | `ng` | +| `learning_ai_jarvis_jr/web/src/app/globals.css` | `jj` | +| `learning_ai_trails/web/src/app/globals.css` | `at` | +| `learning_ai_flowmonk/web/src/app/globals.css` | `fm` | +| `learning_ai_notes/web/src/app/globals.css` | `nl` | +| `learning_ai_local_memory_gpt/web/src/app/globals.css` | `lmg` | +| `learning_ai_local_llms/dashboard/src/app/globals.css` | `llm` | + +(ChronoMind already has 17 focus-visible refs — skip it.) + +**Verify:** + +```bash +for repo in learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + count=$(grep -c 'focus-visible' "$WORKSPACE/$repo/web/src/app/globals.css" 2>/dev/null) + if [ "$count" -ge 2 ]; then echo "PASS: $repo ($count refs)"; else echo "FAIL: $repo ($count refs)"; fi +done +``` + +**Commit:** `feat(design-system): add focus-visible CSS to all product web apps` + +--- + +## Task 3.5: Add ToastProvider to Apps Missing It + +**What:** 4 of 7 apps have no toast system (JarvisJr, FlowMonk, NoteLett, LocalMemGPT). + +**Action:** For each app missing toast: + +1. Add `@bytelyst/ui` to `package.json` +2. Import and wrap in `layout.tsx` (or `providers.tsx` if it exists) + +In the app's root layout (e.g., `src/app/layout.tsx`), wrap `{children}` with ``: + +```tsx +import { ToastProvider } from '@bytelyst/ui/toast'; + +// In the layout body: +{children}; +``` + +**Files to modify:** + +| App | Layout file | +| ----------- | ----------------------------------------------------- | +| JarvisJr | `learning_ai_jarvis_jr/web/src/app/layout.tsx` | +| FlowMonk | `learning_ai_flowmonk/web/src/app/layout.tsx` | +| NoteLett | `learning_ai_notes/web/src/app/layout.tsx` | +| LocalMemGPT | `learning_ai_local_memory_gpt/web/src/app/layout.tsx` | + +**Verify:** + +```bash +for repo in learning_ai_jarvis_jr learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + count=$(grep -c 'ToastProvider' "$WORKSPACE/$repo/web/src/app/layout.tsx" 2>/dev/null) + if [ "$count" -ge 1 ]; then echo "PASS: $repo"; else echo "FAIL: $repo"; fi +done +``` + +**Commit:** `feat(design-system): add ToastProvider to JarvisJr, FlowMonk, NoteLett, LocalMemGPT` + +--- + +## Task 3.6: Add Confirmation Dialogs Before Delete Actions + +**What:** 5 of 7 apps perform delete actions without confirmation. + +**Action:** For each app, find all delete/remove handlers and wrap them with `ConfirmDialog`: + +```bash +# Find delete actions across all product web apps +for repo in learning_ai_clock learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + echo "--- $repo ---" + grep -rn 'delete\|remove\|destroy' "$WORKSPACE/$repo/web/src/" --include="*.tsx" -l 2>/dev/null +done +``` + +For each file with a delete action, add: + +1. A `useState` for the confirm dialog open state +2. Store the item-to-delete in state +3. Wrap the delete button's `onClick` to open the dialog instead of deleting directly +4. The `` component with the actual delete in `onConfirm` + +**Pattern:** + +```tsx +import { ConfirmDialog } from '@bytelyst/ui/confirm-dialog'; + +// In the component: +const [deleteTarget, setDeleteTarget] = useState(null); + +// Replace: onClick={() => handleDelete(item.id)} +// With: onClick={() => setDeleteTarget(item.id)} + +// Add before closing tag: + { + if (!open) setDeleteTarget(null); + }} + title="Delete item?" + description="This action cannot be undone. The item will be permanently removed." + confirmLabel="Delete" + variant="destructive" + onConfirm={() => { + handleDelete(deleteTarget!); + setDeleteTarget(null); + }} +/>; +``` + +**Verify:** + +```bash +for repo in learning_ai_clock learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + count=$(grep -rn 'ConfirmDialog' "$WORKSPACE/$repo/web/src/" --include="*.tsx" 2>/dev/null | wc -l) + deletes=$(grep -rn 'delete\|remove' "$WORKSPACE/$repo/web/src/" --include="*.tsx" -l 2>/dev/null | wc -l) + echo "$repo: $count ConfirmDialog refs, $deletes files with delete actions" +done +``` + +**Pass criteria:** Every file that has a delete action also has a ConfirmDialog import. + +**Commit:** `feat(design-system): add ConfirmDialog before all destructive actions` + +--- + +## Task 3.7: Add Empty States to All List Views + +**What:** Most apps show nothing when a list is empty. + +**Action:** For each app, find all list renders and add an `EmptyState` fallback: + +```bash +# Find list renders (map calls) across product web apps +for repo in learning_ai_clock learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + echo "--- $repo ---" + grep -rn '\.map(' "$WORKSPACE/$repo/web/src/" --include="*.tsx" -l 2>/dev/null +done +``` + +**Pattern:** For every `items.map(...)` render, add a guard: + +```tsx +import { EmptyState } from '@bytelyst/ui/empty-state'; + +// Replace: +{ + items.map(item => ); +} + +// With: +{ + items.length === 0 ? ( + { + /* open create flow */ + }} + /> + ) : ( + items.map(item => ) + ); +} +``` + +**Verify:** + +```bash +for repo in learning_ai_clock learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + count=$(grep -rn 'EmptyState' "$WORKSPACE/$repo/web/src/" --include="*.tsx" 2>/dev/null | wc -l) + echo "$repo: $count EmptyState refs" +done +# Each app should have >= 1 +``` + +**Commit:** `feat(design-system): add EmptyState to all list views` + +--- + +# Phase 4: Accessibility + +> **Goal:** WCAG 2.1 AA baseline across all web surfaces. +> **Depends on:** Phase 3 (components with ARIA are already in place) +> **Estimated effort:** 2-3 days + +--- + +## Task 4.1: Add `@axe-core/playwright` to All E2E Suites + +**Action:** For each product web app with Playwright specs: + +1. Add the dependency: + + ```bash + cd $WORKSPACE//web && npm install -D @axe-core/playwright + ``` + +2. Add an accessibility check to the first spec file. In each app's main E2E spec (e.g., `e2e/navigation.spec.ts`), add: + +```typescript +import AxeBuilder from '@axe-core/playwright'; + +test('should have no critical accessibility violations', async ({ page }) => { + await page.goto('/'); + const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa']).analyze(); + expect(results.violations.filter(v => v.impact === 'critical')).toHaveLength(0); +}); +``` + +**Repos:** learning_ai_clock, learning_ai_fastgap, learning_ai_jarvis_jr, learning_ai_trails, learning_ai_flowmonk, learning_ai_notes, learning_ai_local_memory_gpt + +**Verify:** + +```bash +for repo in learning_ai_clock learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + count=$(grep -rn 'axe-core\|AxeBuilder' "$WORKSPACE/$repo/web/e2e/" 2>/dev/null | wc -l) + if [ "$count" -ge 1 ]; then echo "PASS: $repo"; else echo "FAIL: $repo"; fi +done +``` + +**Commit:** `feat(design-system): add @axe-core/playwright a11y tests to all product web apps` + +--- + +## Task 4.2: Add ARIA Labels to All Icon-Only Buttons + +**Action:** Find all ` + +// After: + +``` + +Common labels: `"Close"`, `"Delete"`, `"Edit"`, `"Menu"`, `"Search"`, `"Settings"`, `"Filter"`, `"Refresh"`, `"Copy"`, `"Expand"`, `"Collapse"` + +**Verify:** + +```bash +for repo in learning_ai_clock learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + total=$(grep -rn '/dev/null | wc -l) + labeled=$(grep -rn '/dev/null | grep -c 'aria-label') + echo "$repo: $labeled/$total buttons have aria-label" +done +``` + +**Pass criteria:** >80% of buttons have `aria-label` (icon-only buttons must be 100%). + +**Commit:** `feat(design-system): add aria-label to all icon-only buttons` + +--- + +## Task 4.3: Add Semantic `
` Landmark to All Layouts + +**Action:** Every app's main content area should be wrapped in `
`. Check each layout file: + +```bash +for repo in learning_ai_clock learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + count=$(grep -rn '/dev/null | wc -l) + echo "$repo: $count
refs" +done +``` + +For any app with 0 refs, find the layout that wraps `{children}` and add `
`: + +```tsx +// Before: +
+ {children} +
+ +// After: +
+ {children} +
+``` + +**Verify:** Re-run the grep. All apps must have >= 1 `
` tag. + +**Commit:** `feat(design-system): add semantic
landmark to all web app layouts` + +--- + +# Phase 5: Polish + +> **Goal:** Elevate from functional to delightful — dark/light theme, responsive, visual regression, CI. +> **Depends on:** Phases 1-4 +> **Estimated effort:** 3-5 days + +--- + +## Task 5.1: Add Dark/Light Theme Toggle to All Web Apps + +**What:** Only 3 of 7 apps have a theme toggle. All define dark tokens; most also have light tokens in the generated CSS. + +**Action:** Create a shared hook or copy the pattern from LocalMemGPT's `use-theme.ts` into each app. + +**Pattern** (create at `/web/src/lib/use-theme.ts`): + +```typescript +'use client'; + +import { useState, useEffect, useCallback } from 'react'; + +type Theme = 'dark' | 'light' | 'system'; + +export function useTheme() { + const [theme, setThemeState] = useState('system'); + + useEffect(() => { + const saved = localStorage.getItem('theme') as Theme | null; + if (saved) setThemeState(saved); + }, []); + + const setTheme = useCallback((t: Theme) => { + setThemeState(t); + localStorage.setItem('theme', t); + const resolved = + t === 'system' + ? window.matchMedia('(prefers-color-scheme: light)').matches + ? 'light' + : 'dark' + : t; + document.documentElement.setAttribute('data-theme', resolved); + }, []); + + useEffect(() => { + const resolved = + theme === 'system' + ? window.matchMedia('(prefers-color-scheme: light)').matches + ? 'light' + : 'dark' + : theme; + document.documentElement.setAttribute('data-theme', resolved); + }, [theme]); + + return { theme, setTheme }; +} +``` + +Add a toggle button in each app's settings or sidebar. + +**Verify:** + +```bash +for repo in learning_ai_clock learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + if [ -f "$WORKSPACE/$repo/web/src/lib/use-theme.ts" ]; then echo "PASS: $repo"; else echo "FAIL: $repo"; fi +done +``` + +**Commit:** `feat(design-system): add dark/light theme toggle to all product web apps` + +--- + +## Task 5.2: Add Token Drift CI Check + +**What:** No CI job verifies that `bytelyst.tokens.json` changes result in regenerated output files. + +**Action:** Add a CI step to the common-plat CI workflow. + +**File:** `$WORKSPACE/learning_ai_common_plat/.gitea/workflows/ci.yml` (or `.github/workflows/`) + +Add this job: + +```yaml +token-drift: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Regenerate tokens + run: npx tsx packages/design-tokens/scripts/generate.ts + - name: Check for drift + run: | + if git diff --exit-code packages/design-tokens/generated/; then + echo "PASS: No token drift detected" + else + echo "FAIL: Token drift detected — run 'npx tsx packages/design-tokens/scripts/generate.ts' and commit" + git diff packages/design-tokens/generated/ + exit 1 + fi +``` + +**Verify:** + +```bash +grep -c 'token-drift' $WORKSPACE/learning_ai_common_plat/.gitea/workflows/ci.yml +# Must be >= 1 +``` + +**Commit:** `feat(ci): add token drift detection to CI pipeline` + +--- + +## Task 5.3: Add Visual Regression Testing + +**What:** Zero apps have visual regression tests. + +**Action:** For each product web app with Playwright, add screenshot tests for key pages. + +In each app's E2E directory, add a spec: + +```typescript +// e2e/visual-regression.spec.ts +import { test, expect } from '@playwright/test'; + +test.describe('visual regression', () => { + test('dashboard matches snapshot', async ({ page }) => { + await page.goto('/dashboard'); + await page.waitForLoadState('networkidle'); + await expect(page).toHaveScreenshot('dashboard.png', { maxDiffPixelRatio: 0.01 }); + }); + + test('settings matches snapshot', async ({ page }) => { + await page.goto('/settings'); + await page.waitForLoadState('networkidle'); + await expect(page).toHaveScreenshot('settings.png', { maxDiffPixelRatio: 0.01 }); + }); +}); +``` + +Adjust routes based on each app's actual pages (check `src/app/` for route names). + +**First run** will create baseline screenshots. Subsequent runs will compare against them. + +**Verify:** + +```bash +for repo in learning_ai_clock learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + if [ -f "$WORKSPACE/$repo/web/e2e/visual-regression.spec.ts" ]; then echo "PASS: $repo"; else echo "FAIL: $repo"; fi +done +``` + +**Commit:** `feat(design-system): add visual regression Playwright specs to all product web apps` + +--- + +## Task 5.4: Add Design Review PR Template + +**What:** No repo requires screenshots for UI changes. + +**Action:** Create a PR template in every product repo: + +**File:** `/.github/pull_request_template.md` + +```markdown +## Summary + + + +## Screenshots + + + +| Before | After | +| ------------------- | ------------------- | +| | | + +## Checklist + +- [ ] Design tokens used (no hardcoded hex) +- [ ] Focus-visible works (keyboard nav) +- [ ] ARIA labels on icon-only buttons +- [ ] Empty state handled for list views +- [ ] Confirmation dialog on destructive actions +- [ ] Loading state on async operations +- [ ] Dark/light theme tested +``` + +**Repos:** All 7 product repos + common-plat. + +**Verify:** + +```bash +for repo in learning_ai_clock learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt learning_ai_common_plat; do + if [ -f "$WORKSPACE/$repo/.github/pull_request_template.md" ]; then echo "PASS: $repo"; else echo "FAIL: $repo"; fi +done +``` + +**Commit:** `feat(design-system): add design review PR template to all repos` + +--- + +# Final Verification + +Run this comprehensive check after all 5 phases are complete: + +```bash +WORKSPACE="$HOME/code/mygh" +PASS=0; FAIL=0 + +echo "=== PHASE 1: Token Foundation ===" +# All product palettes exist +node -e "const t=require('$WORKSPACE/learning_ai_common_plat/packages/design-tokens/tokens/bytelyst.tokens.json'); const p=['actiontrail','notelett','localmemgpt','localllmlab']; const ok=p.every(x=>t.color[x]); console.log(ok?'PASS':'FAIL',': palettes'); process.exit(ok?0:1)" && ((PASS++)) || ((FAIL++)) + +# Per-product CSS files exist +for p in chronomind jarvisjr nomgap actiontrail flowmonk notelett localmemgpt; do + [ -f "$WORKSPACE/learning_ai_common_plat/packages/design-tokens/generated/${p}.css" ] && ((PASS++)) || { echo "FAIL: ${p}.css"; ((FAIL++)); } +done + +# No --ml- leaks +grep -rq '\-\-ml-' "$WORKSPACE/learning_ai_fastgap/web/src/app/globals.css" && { echo "FAIL: NomGap --ml- leak"; ((FAIL++)); } || ((PASS++)) +grep -rq '\-\-ml-' "$WORKSPACE/learning_ai_notes/web/src/app/globals.css" && { echo "FAIL: NoteLett --ml- leak"; ((FAIL++)); } || ((PASS++)) + +echo "" +echo "=== PHASE 2: Component Library ===" +[ -f "$WORKSPACE/learning_ai_common_plat/packages/ui/src/components/Button.tsx" ] && ((PASS++)) || ((FAIL++)) +[ -f "$WORKSPACE/learning_ai_common_plat/packages/ui/src/components/Toast.tsx" ] && ((PASS++)) || ((FAIL++)) +[ -f "$WORKSPACE/learning_ai_common_plat/packages/ui/src/components/Modal.tsx" ] && ((PASS++)) || ((FAIL++)) +[ -f "$WORKSPACE/learning_ai_common_plat/packages/ui/src/components/ConfirmDialog.tsx" ] && ((PASS++)) || ((FAIL++)) +[ -f "$WORKSPACE/learning_ai_common_plat/packages/ui/src/components/Badge.tsx" ] && ((PASS++)) || ((FAIL++)) +[ -f "$WORKSPACE/learning_ai_common_plat/packages/ui/src/components/EmptyState.tsx" ] && ((PASS++)) || ((FAIL++)) + +echo "" +echo "=== PHASE 3: UX Quality Bar ===" +for repo in learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + [ -f "$WORKSPACE/$repo/web/src/app/not-found.tsx" ] && ((PASS++)) || { echo "FAIL: $repo not-found.tsx"; ((FAIL++)); } + [ -f "$WORKSPACE/$repo/web/src/app/error.tsx" ] && ((PASS++)) || { echo "FAIL: $repo error.tsx"; ((FAIL++)); } + [ -f "$WORKSPACE/$repo/web/src/app/loading.tsx" ] && ((PASS++)) || { echo "FAIL: $repo loading.tsx"; ((FAIL++)); } +done + +echo "" +echo "=== PHASE 4: Accessibility ===" +for repo in learning_ai_clock learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + grep -rq 'AxeBuilder\|axe-core' "$WORKSPACE/$repo/web/e2e/" 2>/dev/null && ((PASS++)) || { echo "FAIL: $repo axe-core"; ((FAIL++)); } + grep -rq '/dev/null && ((PASS++)) || { echo "FAIL: $repo
"; ((FAIL++)); } +done + +echo "" +echo "=== PHASE 5: Polish ===" +for repo in learning_ai_clock learning_ai_fastgap learning_ai_jarvis_jr learning_ai_trails learning_ai_flowmonk learning_ai_notes learning_ai_local_memory_gpt; do + [ -f "$WORKSPACE/$repo/web/src/lib/use-theme.ts" ] && ((PASS++)) || { echo "FAIL: $repo use-theme"; ((FAIL++)); } + [ -f "$WORKSPACE/$repo/web/e2e/visual-regression.spec.ts" ] && ((PASS++)) || { echo "FAIL: $repo visual-regression"; ((FAIL++)); } + [ -f "$WORKSPACE/$repo/.github/pull_request_template.md" ] && ((PASS++)) || { echo "FAIL: $repo PR template"; ((FAIL++)); } +done +grep -q 'token-drift' "$WORKSPACE/learning_ai_common_plat/.gitea/workflows/ci.yml" && ((PASS++)) || { echo "FAIL: token drift CI"; ((FAIL++)); } + +echo "" +echo "========================================" +echo "RESULTS: $PASS PASS / $FAIL FAIL" +echo "========================================" +[ $FAIL -eq 0 ] && echo "ALL CHECKS PASSED" || echo "SOME CHECKS FAILED" +``` + +**Target:** 0 FAIL on all checks. + +--- + +_Generated by Windsurf Cascade — agent-executable design system remediation playbook._ +_Companion audit: `DESIGN_SYSTEM_AUDIT.md`_