learning_ai_common_plat/docs/DESIGN_SYSTEM_REMEDIATION_PLAYBOOK.md

57 KiB

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

# 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:

# 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:

"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:

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):

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:

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:

// ── 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:

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:

{
  "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:

    "@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:

    @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):

# 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:

# 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:

# 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.

cd $WORKSPACE/learning_ai_common_plat
mkdir -p packages/ui/src/components

File: $WORKSPACE/learning_ai_common_plat/packages/ui/package.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

{
  "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

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:

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

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:

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

'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

'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

'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

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

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:

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:

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:

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:

'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:

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:

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:

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):

/* 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:

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>:

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:

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:

# 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:

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:

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:

# 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:

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:

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:

    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:

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:

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:

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>":

// 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:

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:

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>:

// 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):

'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:

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:

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:

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:

// 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:

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

## 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:

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:

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