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>orfix(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:
- Add
@bytelyst/design-tokenstopackage.json(or verify it's afile:ref if not published) - Replace hand-written CSS custom properties in
globals.csswith an@importof the generated file - 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:
-
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)
-
In
globals.css, replace the hand-written:root { --xx-... }block with:@import '@bytelyst/design-tokens/css/<productid>.css'; -
Keep any product-specific overrides or non-token CSS rules that follow the
:rootblock.
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/uiso 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/uiare 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:
- Add
@bytelyst/uitopackage.json - Import and wrap in
layout.tsx(orproviders.tsxif 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:
- A
useStatefor the confirm dialog open state - Store the item-to-delete in state
- Wrap the delete button's
onClickto open the dialog instead of deleting directly - The
<ConfirmDialog>component with the actual delete inonConfirm
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:
-
Add the dependency:
cd $WORKSPACE/<repo>/web && npm install -D @axe-core/playwright -
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