# 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): <task description>` or `fix(design-system): <task description>`

---

## 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<string, { prefix: string; colorsKey: string }> = {
  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<string, unknown>)[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/<productid>.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 <ProductName> 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-' <repo>/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<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'ghost' | 'destructive' | 'outline';
  size?: 'sm' | 'md' | 'lg';
  loading?: boolean;
  asChild?: boolean;
}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (
    { 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<string, string> = {
      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<string, string> = {
      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 (
      <Comp
        ref={ref}
        className={clsx(baseStyles, variants[variant], sizes[size], className)}
        disabled={disabled || loading}
        {...props}
      >
        {loading && <Loader2 className="h-4 w-4 animate-spin" />}
        {children}
      </Comp>
    );
  }
);

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<ToastListener>();
function notifyListeners() {
  listeners.forEach(l => l([...globalToasts]));
}

export function toast(msg: Omit<ToastMessage, 'id'>) {
  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<string, React.ReactNode> = {
  success: <CheckCircle className="h-5 w-5 text-green-400" />,
  error: <AlertCircle className="h-5 w-5 text-red-400" />,
  warning: <AlertTriangle className="h-5 w-5 text-yellow-400" />,
  info: <Info className="h-5 w-5 text-blue-400" />,
};

export function Toast({ message, onDismiss }: { message: ToastMessage; onDismiss: () => void }) {
  return (
    <div
      role="alert"
      className={clsx(
        'flex items-start gap-3 rounded-lg border p-4 shadow-lg backdrop-blur-sm',
        'bg-[var(--bl-surface-card,#1a1a2e)] border-[var(--bl-border,#2a2a4a)] text-[var(--bl-text-primary,#fff)]'
      )}
    >
      {icons[message.type]}
      <div className="flex-1 min-w-0">
        <p className="text-sm font-medium">{message.title}</p>
        {message.description && (
          <p className="mt-1 text-xs text-[var(--bl-text-secondary,#a0a0b0)]">
            {message.description}
          </p>
        )}
      </div>
      <button
        onClick={onDismiss}
        className="text-[var(--bl-text-tertiary,#666)] hover:text-[var(--bl-text-primary,#fff)]"
        aria-label="Dismiss notification"
      >
        <X className="h-4 w-4" />
      </button>
    </div>
  );
}

export function ToastProvider({ children }: { children: React.ReactNode }) {
  const [toasts, setToasts] = React.useState<ToastMessage[]>([]);
  React.useEffect(() => {
    listeners.add(setToasts);
    return () => {
      listeners.delete(setToasts);
    };
  }, []);

  return (
    <>
      {children}
      <div
        className="fixed bottom-4 right-4 z-[9999] flex flex-col gap-2 max-w-sm"
        aria-live="polite"
      >
        {toasts.map(t => (
          <Toast key={t.id} message={t} onDismiss={() => dismissToast(t.id)} />
        ))}
      </div>
    </>
  );
}
```

---

## 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<string, string> = {
  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 (
    <Dialog.Root open={open} onOpenChange={onOpenChange}>
      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 z-[9998] bg-black/60 backdrop-blur-sm" />
        <Dialog.Content
          className={clsx(
            'fixed left-1/2 top-1/2 z-[9999] -translate-x-1/2 -translate-y-1/2',
            'w-full rounded-xl border p-6 shadow-xl',
            'bg-[var(--bl-bg-elevated,#12151c)] border-[var(--bl-border,#2a2a4a)] text-[var(--bl-text-primary,#fff)]',
            'focus:outline-none',
            sizes[size]
          )}
        >
          <Dialog.Title className="text-lg font-semibold">{title}</Dialog.Title>
          {description && (
            <Dialog.Description className="mt-1 text-sm text-[var(--bl-text-secondary,#a0a0b0)]">
              {description}
            </Dialog.Description>
          )}
          <div className="mt-4">{children}</div>
          <Dialog.Close asChild>
            <button
              className="absolute right-4 top-4 text-[var(--bl-text-tertiary,#666)] hover:text-[var(--bl-text-primary,#fff)]"
              aria-label="Close dialog"
            >
              <X className="h-4 w-4" />
            </button>
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}
```

---

## 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<void>;
}

export function ConfirmDialog({
  open,
  onOpenChange,
  title,
  description,
  confirmLabel = 'Confirm',
  cancelLabel = 'Cancel',
  variant = 'destructive',
  loading,
  onConfirm,
}: ConfirmDialogProps) {
  return (
    <AlertDialog.Root open={open} onOpenChange={onOpenChange}>
      <AlertDialog.Portal>
        <AlertDialog.Overlay className="fixed inset-0 z-[9998] bg-black/60 backdrop-blur-sm" />
        <AlertDialog.Content
          className={clsx(
            'fixed left-1/2 top-1/2 z-[9999] -translate-x-1/2 -translate-y-1/2',
            'w-full max-w-md rounded-xl border p-6 shadow-xl',
            'bg-[var(--bl-bg-elevated,#12151c)] border-[var(--bl-border,#2a2a4a)] text-[var(--bl-text-primary,#fff)]',
            'focus:outline-none'
          )}
        >
          <div className="flex items-start gap-3">
            <div
              className={clsx(
                'mt-0.5 rounded-full p-2',
                variant === 'destructive'
                  ? 'bg-red-500/10 text-red-400'
                  : 'bg-yellow-500/10 text-yellow-400'
              )}
            >
              <AlertTriangle className="h-5 w-5" />
            </div>
            <div>
              <AlertDialog.Title className="text-lg font-semibold">{title}</AlertDialog.Title>
              <AlertDialog.Description className="mt-2 text-sm text-[var(--bl-text-secondary,#a0a0b0)]">
                {description}
              </AlertDialog.Description>
            </div>
          </div>
          <div className="mt-6 flex justify-end gap-3">
            <AlertDialog.Cancel asChild>
              <Button variant="ghost">{cancelLabel}</Button>
            </AlertDialog.Cancel>
            <AlertDialog.Action asChild>
              <Button
                variant={variant === 'destructive' ? 'destructive' : 'primary'}
                loading={loading}
                onClick={onConfirm}
              >
                {confirmLabel}
              </Button>
            </AlertDialog.Action>
          </div>
        </AlertDialog.Content>
      </AlertDialog.Portal>
    </AlertDialog.Root>
  );
}
```

---

## 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<HTMLSpanElement> {
  variant?: 'success' | 'warning' | 'error' | 'info' | 'neutral';
  size?: 'sm' | 'md';
  dot?: boolean;
}

const variantStyles: Record<string, string> = {
  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 (
    <span
      className={clsx(
        'inline-flex items-center gap-1.5 rounded-full border font-medium',
        size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-3 py-1 text-sm',
        variantStyles[variant],
        className
      )}
      {...props}
    >
      {dot && (
        <span
          className={clsx(
            'h-1.5 w-1.5 rounded-full',
            variant === 'success'
              ? 'bg-green-400'
              : variant === 'error'
                ? 'bg-red-400'
                : variant === 'warning'
                  ? 'bg-yellow-400'
                  : variant === 'info'
                    ? 'bg-blue-400'
                    : 'bg-gray-400'
          )}
        />
      )}
      {children}
    </span>
  );
}
```

---

## 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 (
    <div
      className={clsx(
        'flex flex-col items-center justify-center py-16 px-4 text-center',
        className
      )}
    >
      <div className="mb-4 text-[var(--bl-text-tertiary,#555)]">
        {icon ?? <Inbox className="h-12 w-12" />}
      </div>
      <h3 className="text-lg font-medium text-[var(--bl-text-primary,#fff)]">{title}</h3>
      {description && (
        <p className="mt-2 max-w-sm text-sm text-[var(--bl-text-secondary,#a0a0b0)]">
          {description}
        </p>
      )}
      {actionLabel && onAction && (
        <Button variant="primary" size="sm" className="mt-4" onClick={onAction}>
          {actionLabel}
        </Button>
      )}
    </div>
  );
}
```

**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 `<repo>/web/src/app/not-found.tsx`:

```tsx
import Link from 'next/link';

export default function NotFound() {
  return (
    <div
      className="flex min-h-screen items-center justify-center p-4"
      style={{ background: 'var(--PREFIX-bg-canvas)' }}
    >
      <div className="text-center">
        <h1 className="text-6xl font-bold" style={{ color: 'var(--PREFIX-accent)' }}>
          404
        </h1>
        <p className="mt-4 text-lg" style={{ color: 'var(--PREFIX-text-secondary)' }}>
          Page not found
        </p>
        <p className="mt-2 text-sm" style={{ color: 'var(--PREFIX-text-tertiary)' }}>
          The page you are looking for does not exist or has been moved.
        </p>
        <Link
          href="/"
          className="mt-6 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
          style={{
            background: 'var(--PREFIX-accent)',
            color: '#fff',
          }}
        >
          Go home
        </Link>
      </div>
    </div>
  );
}
```

**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 (
    <div
      className="flex min-h-screen items-center justify-center p-4"
      style={{ background: 'var(--PREFIX-bg-canvas)' }}
    >
      <div className="text-center">
        <h1 className="text-4xl font-bold" style={{ color: 'var(--PREFIX-danger, #EF4444)' }}>
          Something went wrong
        </h1>
        <p className="mt-4 text-sm" style={{ color: 'var(--PREFIX-text-secondary)' }}>
          {error.message || 'An unexpected error occurred.'}
        </p>
        <button
          onClick={reset}
          className="mt-6 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
          style={{ background: 'var(--PREFIX-accent)', color: '#fff' }}
        >
          Try again
        </button>
      </div>
    </div>
  );
}
```

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 (
    <div
      className="flex min-h-screen items-center justify-center"
      style={{ background: 'var(--PREFIX-bg-canvas)' }}
    >
      <div className="flex flex-col items-center gap-3">
        <div
          className="h-8 w-8 animate-spin rounded-full border-2 border-t-transparent"
          style={{ borderColor: 'var(--PREFIX-accent)', borderTopColor: 'transparent' }}
        />
        <p className="text-sm" style={{ color: 'var(--PREFIX-text-secondary)' }}>
          Loading...
        </p>
      </div>
    </div>
  );
}
```

**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 `<ToastProvider>`:

```tsx
import { ToastProvider } from '@bytelyst/ui/toast';

// In the layout body:
<ToastProvider>{children}</ToastProvider>;
```

**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 `<ConfirmDialog>` component with the actual delete in `onConfirm`

**Pattern:**

```tsx
import { ConfirmDialog } from '@bytelyst/ui/confirm-dialog';

// In the component:
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);

// Replace: onClick={() => handleDelete(item.id)}
// With:    onClick={() => setDeleteTarget(item.id)}

// Add before closing tag:
<ConfirmDialog
  open={deleteTarget !== null}
  onOpenChange={open => {
    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 => <ItemCard key={item.id} {...item} />);
}

// With:
{
  items.length === 0 ? (
    <EmptyState
      title="No items yet"
      description="Create your first item to get started."
      actionLabel="Create"
      onAction={() => {
        /* open create flow */
      }}
    />
  ) : (
    items.map(item => <ItemCard key={item.id} {...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/<repo>/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 `<button>` elements that contain only an icon (SVG/Lucide) with no text:

```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
  echo "--- $repo ---"
  grep -rn '<button' "$WORKSPACE/$repo/web/src/" --include="*.tsx" | grep -v 'aria-label' | head -10
done
```

For each match, add `aria-label="<descriptive action>"`:

```tsx
// Before:
<button onClick={onClose}><X className="h-4 w-4" /></button>

// After:
<button onClick={onClose} aria-label="Close"><X className="h-4 w-4" /></button>
```

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 '<button' "$WORKSPACE/$repo/web/src/" --include="*.tsx" 2>/dev/null | wc -l)
  labeled=$(grep -rn '<button' "$WORKSPACE/$repo/web/src/" --include="*.tsx" 2>/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 `<main>` Landmark to All Layouts

**Action:** Every app's main content area should be wrapped in `<main>`. 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 '<main' "$WORKSPACE/$repo/web/src/" --include="*.tsx" 2>/dev/null | wc -l)
  echo "$repo: $count <main> refs"
done
```

For any app with 0 refs, find the layout that wraps `{children}` and add `<main>`:

```tsx
// Before:
<div className="flex-1 overflow-auto p-6">
  {children}
</div>

// After:
<main className="flex-1 overflow-auto p-6">
  {children}
</main>
```

**Verify:** Re-run the grep. All apps must have >= 1 `<main>` tag.

**Commit:** `feat(design-system): add semantic <main> 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 `<repo>/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<Theme>('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:** `<repo>/.github/pull_request_template.md`

```markdown
## Summary

<!-- Brief description of the change -->

## Screenshots

<!-- REQUIRED for any UI change. Before/after screenshots. -->

| Before              | After               |
| ------------------- | ------------------- |
| <!-- screenshot --> | <!-- screenshot --> |

## 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 '<main' "$WORKSPACE/$repo/web/src/" 2>/dev/null && ((PASS++)) || { echo "FAIL: $repo <main>"; ((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`_
