From a390cc8ab04cd136ac26ad113b77efd4c4e22919 Mon Sep 17 00:00:00 2001 From: VictorGjn <56982152+VictorGjn@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:37:59 +0200 Subject: [PATCH 01/21] feat: add shared OKLCH design tokens --- src/styles/tokens.css | 135 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/styles/tokens.css diff --git a/src/styles/tokens.css b/src/styles/tokens.css new file mode 100644 index 0000000..739da61 --- /dev/null +++ b/src/styles/tokens.css @@ -0,0 +1,135 @@ +/* ═══════════════════════════════════════════════════════════ + MODULAR Design Tokens (OKLCH) + Single source of truth for both Studio and Crew + ═══════════════════════════════════════════════════════════ */ + +:root { + /* ── Accent ── */ + --m-accent: oklch(0.63 0.24 38); + --m-accent-hover: oklch(0.58 0.22 38); + --m-accent-glow: oklch(0.63 0.24 38 / 0.25); + --m-accent-bg: oklch(0.63 0.24 38 / 0.08); + + /* ── Semantic status ── */ + --m-success: oklch(0.72 0.19 155); + --m-success-bg: oklch(0.72 0.19 155 / 0.07); + --m-success-glow: oklch(0.72 0.19 155 / 0.5); + --m-error: oklch(0.60 0.25 20); + --m-error-bg: oklch(0.60 0.25 20 / 0.08); + --m-error-glow: oklch(0.60 0.25 20 / 0.5); + --m-warning: oklch(0.78 0.18 80); + --m-warning-bg: oklch(0.78 0.18 80 / 0.08); + --m-warning-glow: oklch(0.78 0.18 80 / 0.5); + --m-info: oklch(0.60 0.12 245); + --m-info-bg: oklch(0.60 0.12 245 / 0.08); + + /* ── Semantic domains ── */ + --m-knowledge: oklch(0.60 0.12 245); + --m-discovery: oklch(0.72 0.19 155); + --m-intel: oklch(0.68 0.16 60); + --m-agents: oklch(0.55 0.18 310); + + /* ── Cable/port roles ── */ + --m-cable-skills: oklch(0.80 0.18 90); + --m-cable-mcp: oklch(0.72 0.19 155); + --m-cable-knowledge: oklch(0.60 0.25 20); + + /* ── Fact epistemic types ── */ + --m-fact-observation: oklch(0.60 0.12 245); + --m-fact-inference: oklch(0.80 0.18 90); + --m-fact-decision: oklch(0.72 0.19 155); + --m-fact-hypothesis: oklch(0.55 0.18 310); + --m-fact-contract: oklch(0.63 0.24 38); + + /* ── Sizing ── */ + --m-radius-sm: 6px; + --m-radius: 8px; + --m-radius-lg: 12px; + --m-radius-xl: 16px; + --m-radius-full: 9999px; + + /* ── Spacing ── */ + --m-space-1: 4px; + --m-space-2: 8px; + --m-space-3: 12px; + --m-space-4: 16px; + --m-space-6: 24px; + --m-space-8: 32px; + --m-space-12: 48px; + + /* ── Typography ── */ + --m-font-sans: 'Geist', system-ui, -apple-system, sans-serif; + --m-font-mono: 'Geist Mono', 'SF Mono', 'Fira Code', monospace; + --m-text-xs: 11px; + --m-text-sm: 12px; + --m-text-base: 13px; + --m-text-md: 14px; + --m-text-lg: 16px; + --m-text-xl: 18px; + --m-text-2xl: 24px; + --m-text-3xl: 32px; + + /* ── Layout ── */ + --m-topbar-h: 48px; + + /* ── Transitions ── */ + --m-t-fast: 120ms ease; + --m-t-base: 150ms ease; + --m-t-slide: 250ms cubic-bezier(0.4, 0, 0.2, 1); +} + +/* ── Dark Theme (default) ── */ +[data-theme="dark"], :root { + --m-bg: oklch(0.15 0.005 280); + --m-surface: oklch(0.20 0.005 280 / 0.9); + --m-surface-opaque: oklch(0.20 0.005 280); + --m-surface-elevated: oklch(0.23 0.005 280); + --m-surface-hover: oklch(0.21 0.005 280); + --m-border: oklch(0.26 0.005 280); + --m-border-subtle: oklch(0.22 0.005 280); + --m-text-primary: oklch(0.95 0 0); + --m-text-secondary: oklch(0.65 0 0); + --m-text-muted: oklch(0.50 0 0); + --m-text-dim: oklch(0.42 0 0); + --m-text-faint: oklch(0.35 0 0); + --m-input-bg: oklch(0.16 0.005 280); + --m-badge-bg: oklch(0.23 0.005 280); + --m-dot-grid: oklch(0.22 0.005 280); + --m-scrollbar-track: oklch(0.18 0.005 280); + --m-scrollbar-thumb: oklch(0.30 0.005 280); + --m-shadow-card: 0 2px 8px oklch(0 0 0 / 0.3); + --m-overlay: oklch(0 0 0 / 0.5); + color-scheme: dark; +} + +/* ── Light Theme ── */ +[data-theme="light"] { + --m-bg: oklch(0.95 0.005 260); + --m-surface: oklch(1.0 0 0 / 0.95); + --m-surface-opaque: oklch(1.0 0 0); + --m-surface-elevated: oklch(0.94 0.005 260); + --m-surface-hover: oklch(0.97 0.005 260); + --m-border: oklch(0.83 0.005 260); + --m-border-subtle: oklch(0.88 0.005 260); + --m-text-primary: oklch(0.18 0.01 280); + --m-text-secondary: oklch(0.35 0.01 280); + --m-text-muted: oklch(0.50 0.01 280); + --m-text-dim: oklch(0.55 0.01 280); + --m-text-faint: oklch(0.62 0.01 280); + --m-input-bg: oklch(0.97 0.005 260); + --m-badge-bg: oklch(0.91 0.005 260); + --m-dot-grid: oklch(0.85 0.005 260); + --m-scrollbar-track: oklch(0.93 0.005 260); + --m-scrollbar-thumb: oklch(0.78 0.005 260); + --m-shadow-card: 0 2px 8px oklch(0 0 0 / 0.08); + --m-overlay: oklch(0 0 0 / 0.18); + color-scheme: light; +} + +/* ── Reduced motion ── */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + transition: none !important; + animation: none !important; + } +} From 7cc9561fb03188388cb6b5024f20bb28c2938fba Mon Sep 17 00:00:00 2001 From: VictorGjn <56982152+VictorGjn@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:40:47 +0200 Subject: [PATCH 02/21] refactor: rewrite theme.ts to use OKLCH CSS custom properties via var(--m-*) --- src/theme.ts | 194 ++++++++++++++++----------------------------------- 1 file changed, 60 insertions(+), 134 deletions(-) diff --git a/src/theme.ts b/src/theme.ts index 268b3c8..69631ef 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -1,139 +1,65 @@ import { useThemeStore } from './store/themeStore'; -const dark = { - bg: '#111114', - surface: 'rgba(28, 28, 32, 0.9)', - surfaceOpaque: '#1c1c20', - surfaceElevated: '#25252a', - surfaceHover: '#1f1f24', - border: '#2a2a30', - borderSubtle: '#222226', - textPrimary: '#f0f0f0', - textSecondary: '#888', - textMuted: '#666', - textDim: '#555', - textFaint: '#444', - inputBg: '#141417', - badgeBg: '#25252a', - dotGrid: '#222228', - minimapBg: '#111114', - minimapMask: 'rgba(17,17,20,0.8)', - minimapNode: '#25252a', - controlsBg: '#1c1c20', - controlsBorder: '#2a2a30', - // Tile - tileActiveBg: '#25252a', - tileHoverBg: '#1f1f24', - tileBg: '#1c1c20', - tileBorderHover: '#3a3a40', - // Agent preview - agentBg: '#151210', - agentBorder: '#2d2720', - agentLabel: '#9a8e82', - agentMeta: '#6d6058', - agentArrow: '#6d6058', - agentLineNum: '#2d2720', - agentText: '#8a7e72', - // Token budget - tokenLabel: '#999', - tokenDivider: '#777', - tokenTrackBg: '#25252a', - // Jack port ring in light vs dark - jackRingBase: '#0a0a0a', - jackLabelOnRing: '#999', - jackLabelBeside: '#666', - // Cable shadow - cableShadow: 'rgba(0,0,0,0.4)', - cableHighlight: 'rgba(255,255,255,0.06)', - // Response - responseBg: 'rgba(28, 28, 32, 0.9)', - responseText: '#bbb', - // Status - statusSuccess: '#00ff88', - statusError: '#ff3344', - statusWarning: '#ffaa00', - statusInfo: '#3498db', - statusSuccessBg: 'rgba(0,255,136,0.07)', - statusErrorBg: 'rgba(255,51,68,0.08)', - statusWarningBg: 'rgba(255,170,0,0.08)', - statusSuccessGlow: '0 0 6px rgba(0,255,136,0.5)', - statusErrorGlow: '0 0 6px rgba(255,51,68,0.5)', - statusWarningGlow: '0 0 6px rgba(255,170,0,0.5)', - // Semantic cable/port colors (these are "role" colors, not status) - cableSkills: '#f1c40f', - cableMcp: '#2ecc71', - cableKnowledge: '#e74c3c', -}; - -const light = { - bg: '#f0f1f3', - surface: 'rgba(255,255,255,0.95)', - surfaceOpaque: '#ffffff', - surfaceElevated: '#eeeef2', - surfaceHover: '#f5f5f8', - border: '#ccccd4', - borderSubtle: '#dddde2', - textPrimary: '#1a1a20', - textSecondary: '#3a3a45', - textMuted: '#555560', - textDim: '#5c5c66', - textFaint: '#71717a', - inputBg: '#f5f5f8', - badgeBg: '#e5e5ea', - dotGrid: '#d0d0d8', - minimapBg: '#f0f1f3', - minimapMask: 'rgba(240,241,243,0.8)', - minimapNode: '#dddde2', - controlsBg: '#ffffff', - controlsBorder: '#ccccd4', - // Tile - tileActiveBg: '#e8e8ee', - tileHoverBg: '#f0f0f5', - tileBg: '#ffffff', - tileBorderHover: '#bbbbc4', - // Agent preview - agentBg: '#f8f8fa', - agentBorder: '#dddde2', - agentLabel: '#555560', - agentMeta: '#888890', - agentArrow: '#888890', - agentLineNum: '#ccccd4', - agentText: '#666670', - // Token budget - tokenLabel: '#555560', - tokenDivider: '#888890', - tokenTrackBg: '#dddde2', - // Jack port - jackRingBase: '#e8e8ee', - jackLabelOnRing: '#555', - jackLabelBeside: '#777', - // Cable shadow - cableShadow: 'rgba(0,0,0,0.12)', - cableHighlight: 'rgba(255,255,255,0.3)', - // Response - responseBg: 'rgba(255,255,255,0.95)', - responseText: '#444', - // Status - statusSuccess: '#16a34a', - statusError: '#dc2626', - statusWarning: '#ca8a04', - statusInfo: '#2563eb', - statusSuccessBg: 'rgba(22,163,74,0.08)', - statusErrorBg: 'rgba(220,38,38,0.08)', - statusWarningBg: 'rgba(202,138,4,0.08)', - statusSuccessGlow: '0 0 6px rgba(22,163,74,0.3)', - statusErrorGlow: '0 0 6px rgba(220,38,38,0.3)', - statusWarningGlow: '0 0 6px rgba(202,138,4,0.3)', - // Semantic cable/port colors - cableSkills: '#B45309', - cableMcp: '#16a34a', - cableKnowledge: '#dc2626', -}; - -export type ThemePalette = typeof dark; +export type ThemePalette = { [key: string]: string; isDark: boolean }; export function useTheme(): ThemePalette & { isDark: boolean } { - const theme = useThemeStore((s) => s.theme); - const palette = theme === 'dark' ? dark : light; - return { ...palette, isDark: theme === 'dark' }; + const theme = useThemeStore(s => s.theme); + const isDark = theme === 'dark'; + return { + isDark, + bg: 'var(--m-bg)', + surface: 'var(--m-surface)', + surfaceOpaque: 'var(--m-surface-opaque)', + surfaceElevated: 'var(--m-surface-elevated)', + surfaceHover: 'var(--m-surface-hover)', + border: 'var(--m-border)', + borderSubtle: 'var(--m-border-subtle)', + textPrimary: 'var(--m-text-primary)', + textSecondary: 'var(--m-text-secondary)', + textMuted: 'var(--m-text-muted)', + textDim: 'var(--m-text-dim)', + textFaint: 'var(--m-text-faint)', + inputBg: 'var(--m-input-bg)', + badgeBg: 'var(--m-badge-bg)', + dotGrid: 'var(--m-dot-grid)', + minimapBg: 'var(--m-bg)', + minimapMask: isDark ? 'oklch(0.15 0.005 280 / 0.8)' : 'oklch(0.95 0.005 260 / 0.8)', + minimapNode: 'var(--m-surface-elevated)', + controlsBg: 'var(--m-surface-opaque)', + controlsBorder: 'var(--m-border)', + tileActiveBg: 'var(--m-surface-elevated)', + tileHoverBg: 'var(--m-surface-hover)', + tileBg: 'var(--m-surface-opaque)', + tileBorderHover: isDark ? 'oklch(0.32 0.005 280)' : 'oklch(0.75 0.005 260)', + agentBg: isDark ? 'oklch(0.16 0.01 50)' : 'oklch(0.97 0.005 50)', + agentBorder: isDark ? 'oklch(0.24 0.01 50)' : 'oklch(0.88 0.005 50)', + agentLabel: isDark ? 'oklch(0.60 0.02 50)' : 'oklch(0.45 0.02 50)', + agentMeta: isDark ? 'oklch(0.48 0.02 50)' : 'oklch(0.55 0.02 50)', + agentArrow: isDark ? 'oklch(0.48 0.02 50)' : 'oklch(0.55 0.02 50)', + agentLineNum: isDark ? 'oklch(0.24 0.01 50)' : 'oklch(0.85 0.005 50)', + agentText: isDark ? 'oklch(0.55 0.02 50)' : 'oklch(0.50 0.02 50)', + tokenLabel: 'var(--m-text-muted)', + tokenDivider: 'var(--m-text-secondary)', + tokenTrackBg: 'var(--m-surface-elevated)', + jackRingBase: isDark ? 'oklch(0.10 0 0)' : 'oklch(0.92 0.005 260)', + jackLabelOnRing: 'var(--m-text-muted)', + jackLabelBeside: 'var(--m-text-dim)', + cableShadow: isDark ? 'oklch(0 0 0 / 0.4)' : 'oklch(0 0 0 / 0.12)', + cableHighlight: isDark ? 'oklch(1 0 0 / 0.06)' : 'oklch(1 0 0 / 0.3)', + responseBg: 'var(--m-surface)', + responseText: 'var(--m-text-secondary)', + statusSuccess: 'var(--m-success)', + statusError: 'var(--m-error)', + statusWarning: 'var(--m-warning)', + statusInfo: 'var(--m-info)', + statusSuccessBg: 'var(--m-success-bg)', + statusErrorBg: 'var(--m-error-bg)', + statusWarningBg: 'var(--m-warning-bg)', + statusSuccessGlow: 'var(--m-success-glow)', + statusErrorGlow: 'var(--m-error-glow)', + statusWarningGlow: 'var(--m-warning-glow)', + cableSkills: 'var(--m-cable-skills)', + cableMcp: 'var(--m-cable-mcp)', + cableKnowledge: 'var(--m-cable-knowledge)', + }; } From 1853dd485e7a7b86dd8806dff65a64484db9a4c6 Mon Sep 17 00:00:00 2001 From: VictorGjn <56982152+VictorGjn@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:40:48 +0200 Subject: [PATCH 03/21] refactor: set data-theme attribute on documentElement in themeStore --- src/store/themeStore.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/store/themeStore.ts b/src/store/themeStore.ts index 0f21166..a546071 100644 --- a/src/store/themeStore.ts +++ b/src/store/themeStore.ts @@ -11,11 +11,19 @@ const stored = typeof localStorage !== 'undefined' ? (localStorage.getItem('modular-theme') as Theme | null) : null; +const initial: Theme = stored === 'light' ? 'light' : 'dark'; + +// Set data-theme on initial load +if (typeof document !== 'undefined') { + document.documentElement.setAttribute('data-theme', initial); +} + export const useThemeStore = create((set, get) => ({ - theme: stored === 'light' ? 'light' : 'dark', + theme: initial, toggleTheme: () => { const next = get().theme === 'dark' ? 'light' : 'dark'; localStorage.setItem('modular-theme', next); + document.documentElement.setAttribute('data-theme', next); set({ theme: next }); }, })); From 6616766c4195d1e1c79fd00a5a28b6dc24b3557d Mon Sep 17 00:00:00 2001 From: VictorGjn <56982152+VictorGjn@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:40:49 +0200 Subject: [PATCH 04/21] feat(Modal): add inline focus trap and use CSS custom properties --- src/components/ds/Modal.tsx | 57 +++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/src/components/ds/Modal.tsx b/src/components/ds/Modal.tsx index 9ea05eb..640a00a 100644 --- a/src/components/ds/Modal.tsx +++ b/src/components/ds/Modal.tsx @@ -12,18 +12,59 @@ export interface ModalProps { width?: number; } +function getFocusableElements(container: HTMLElement) { + return container.querySelectorAll( + 'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])' + ); +} + export function Modal({ open, onClose, title, children, footer, width = 520 }: ModalProps) { const t = useTheme(); const panelRef = useRef(null); + const previouslyFocusedRef = useRef(null); const titleId = title ? `modal-title-${title.toLowerCase().replace(/\s+/g, '-')}` : undefined; + // Store previously focused element and restore on close + useEffect(() => { + if (open) { + previouslyFocusedRef.current = document.activeElement as HTMLElement; + } else if (previouslyFocusedRef.current) { + previouslyFocusedRef.current.focus(); + previouslyFocusedRef.current = null; + } + }, [open]); + + // Escape handler + focus trap useEffect(() => { if (!open) return; - const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; + const handleKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + return; + } + if (e.key === 'Tab' && panelRef.current) { + const focusable = getFocusableElements(panelRef.current); + if (focusable.length === 0) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey) { + if (document.activeElement === first || document.activeElement === panelRef.current) { + e.preventDefault(); + last.focus(); + } + } else { + if (document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + } + }; document.addEventListener('keydown', handleKey); return () => document.removeEventListener('keydown', handleKey); }, [open, onClose]); + // Auto-focus panel on open useEffect(() => { if (open) panelRef.current?.focus(); }, [open]); @@ -32,7 +73,7 @@ export function Modal({ open, onClose, title, children, footer, width = 520 }: M return createPortal(
-
+
{title && ( -
- {title} -
)}
{children}
{footer && ( -
+
{footer}
)} From fc3c4641c941b7d0ad9546f9daa6870522c64905 Mon Sep 17 00:00:00 2001 From: VictorGjn <56982152+VictorGjn@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:40:55 +0200 Subject: [PATCH 05/21] fix(Tabs): min font 11/12px, ARIA tabs pattern, CSS vars, remove uppercase --- src/components/ds/Tabs.tsx | 104 ++++++++++++++++++++++++++----------- 1 file changed, 73 insertions(+), 31 deletions(-) diff --git a/src/components/ds/Tabs.tsx b/src/components/ds/Tabs.tsx index ba60997..8c907a6 100644 --- a/src/components/ds/Tabs.tsx +++ b/src/components/ds/Tabs.tsx @@ -1,4 +1,4 @@ -import { type ReactNode } from 'react'; +import { type ReactNode, useRef, useCallback } from 'react'; import { useTheme } from '../../theme'; export interface Tab { @@ -17,39 +17,81 @@ export interface TabsProps { export function Tabs({ tabs, active, onChange, size = 'sm' }: TabsProps) { const t = useTheme(); - const fontSize = size === 'sm' ? 9 : 10; + const fontSize = size === 'sm' ? 11 : 12; const py = size === 'sm' ? 6 : 8; + const tabRefs = useRef<(HTMLButtonElement | null)[]>([]); + + const handleKeyDown = useCallback((e: React.KeyboardEvent, index: number) => { + let nextIndex: number | null = null; + switch (e.key) { + case 'ArrowLeft': + e.preventDefault(); + nextIndex = index === 0 ? tabs.length - 1 : index - 1; + break; + case 'ArrowRight': + e.preventDefault(); + nextIndex = index === tabs.length - 1 ? 0 : index + 1; + break; + case 'Home': + e.preventDefault(); + nextIndex = 0; + break; + case 'End': + e.preventDefault(); + nextIndex = tabs.length - 1; + break; + } + if (nextIndex !== null) { + tabRefs.current[nextIndex]?.focus(); + onChange(tabs[nextIndex].id); + } + }, [tabs, onChange]); return ( -
- {tabs.map((tab) => ( - - ))} +
+ {tabs.map((tab, index) => { + const isActive = active === tab.id; + return ( + + ); + })}
); } From 079eef452ceb83251b16c7fe1afcb645218231d8 Mon Sep 17 00:00:00 2001 From: VictorGjn <56982152+VictorGjn@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:41:08 +0200 Subject: [PATCH 06/21] refactor(styles): rewrite globals.css to use shared OKLCH --m-* tokens - Import tokens.css as single source of truth - Map --m-* vars to Tailwind --color-* via @theme block - Replace all hardcoded hex colors with var(--m-*) references - Add Firefox scrollbar support (scrollbar-width/scrollbar-color) - Scope theme transitions to .theme-transition class only - Use Geist (not Geist Sans) for font-sans via --m-font-sans - Keep focus-visible, sr-only, base reset, and React Flow overrides - Remove separate dark/light scrollbar and React Flow theme blocks (tokens.css handles theme switching via data-theme attribute) --- src/styles/globals.css | 226 +++++++++++++++++++++-------------------- 1 file changed, 116 insertions(+), 110 deletions(-) diff --git a/src/styles/globals.css b/src/styles/globals.css index 4dc3da8..19a65e7 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1,26 +1,48 @@ +@import "./tokens.css"; @import "tailwindcss"; +/* ═══════════════════════════════════════════════════════════ + Tailwind Theme Integration + Maps --m-* design tokens → Tailwind --color-* utilities + ═══════════════════════════════════════════════════════════ */ @theme { - --color-bg: #111114; - --color-surface: #1c1c20; - --color-surface-elevated: #25252a; - --color-border: #2a2a30; - --color-accent: #FE5000; - --color-accent-dim: #CC4000; - --color-text-primary: #f0f0f0; - --color-text-secondary: #888; - --color-text-muted: #555; - --color-led-green: #00ff88; - --color-led-red: #ff3344; - --color-led-amber: #ffaa00; - --color-knowledge: #3498db; - --color-discovery: #2ecc71; - --color-intel: #e67e22; - --color-agents: #9b59b6; - --font-mono: 'Geist Mono', monospace; - --font-sans: 'Geist Sans', sans-serif; -} - + --color-bg: var(--m-bg); + --color-surface: var(--m-surface-opaque); + --color-surface-elevated: var(--m-surface-elevated); + --color-surface-hover: var(--m-surface-hover); + --color-border: var(--m-border); + --color-border-subtle: var(--m-border-subtle); + --color-accent: var(--m-accent); + --color-accent-hover: var(--m-accent-hover); + --color-accent-glow: var(--m-accent-glow); + --color-accent-bg: var(--m-accent-bg); + --color-text-primary: var(--m-text-primary); + --color-text-secondary: var(--m-text-secondary); + --color-text-muted: var(--m-text-muted); + --color-text-dim: var(--m-text-dim); + --color-text-faint: var(--m-text-faint); + --color-success: var(--m-success); + --color-success-bg: var(--m-success-bg); + --color-error: var(--m-error); + --color-error-bg: var(--m-error-bg); + --color-warning: var(--m-warning); + --color-warning-bg: var(--m-warning-bg); + --color-info: var(--m-info); + --color-info-bg: var(--m-info-bg); + --color-knowledge: var(--m-knowledge); + --color-discovery: var(--m-discovery); + --color-intel: var(--m-intel); + --color-agents: var(--m-agents); + --color-input-bg: var(--m-input-bg); + --color-badge-bg: var(--m-badge-bg); + --color-overlay: var(--m-overlay); + --font-sans: var(--m-font-sans); + --font-mono: var(--m-font-mono); +} + +/* ═══════════════════════════════════════════════════════════ + Base Reset + ═══════════════════════════════════════════════════════════ */ @layer base { * { margin: 0; @@ -29,13 +51,13 @@ } } -/* A3 — Focus indicators */ +/* ── Focus indicators ── */ :focus-visible { - outline: 2px solid var(--color-accent, #6366F1); + outline: 2px solid var(--m-accent); outline-offset: 2px; } -/* Accessibility: aria-live region for canvas announcements */ +/* ── Screen-reader only ── */ .sr-only { position: absolute; width: 1px; @@ -48,32 +70,56 @@ border-width: 0; } +/* ═══════════════════════════════════════════════════════════ + Root Layout + ═══════════════════════════════════════════════════════════ */ html, body, #root { width: 100%; height: 100%; overflow: hidden; - background: var(--color-bg); - font-family: var(--font-sans); - color: var(--color-text-primary); + background: var(--m-bg); + font-family: var(--m-font-sans); + color: var(--m-text-primary); } -/* Scrollbar — base sizing (applies to both themes) */ -::-webkit-scrollbar { width: 6px; height: 6px; } +/* ═══════════════════════════════════════════════════════════ + Theme Transitions + Scoped to panels/modals — NOT applied to all elements. + Add .theme-transition to elements that should animate + on theme change. + ═══════════════════════════════════════════════════════════ */ +.theme-transition { + transition: background-color var(--m-t-base), color var(--m-t-base), border-color var(--m-t-base); +} -/* Scrollbar — dark theme (default) */ -.dark ::-webkit-scrollbar-track, -[data-theme="dark"] ::-webkit-scrollbar-track { background: #111114; } -.dark ::-webkit-scrollbar-thumb, -[data-theme="dark"] ::-webkit-scrollbar-thumb { background: #2a2a30; border-radius: 3px; } -.dark ::-webkit-scrollbar-thumb:hover, -[data-theme="dark"] ::-webkit-scrollbar-thumb:hover { background: #3a3a40; } +/* ═══════════════════════════════════════════════════════════ + Scrollbars + ═══════════════════════════════════════════════════════════ */ -/* Scrollbar — light theme */ -[data-theme="light"] ::-webkit-scrollbar-track { background: #f0f1f3; } -[data-theme="light"] ::-webkit-scrollbar-thumb { background: #ccccd4; border-radius: 3px; } -[data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: #aaaaB0; } +/* Firefox */ +* { + scrollbar-width: thin; + scrollbar-color: var(--m-scrollbar-thumb) var(--m-scrollbar-track); +} -/* Remove default ReactFlow node background/border/shadow */ +/* WebKit (Chrome, Safari, Edge) — base sizing */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: var(--m-scrollbar-track); } +::-webkit-scrollbar-thumb { background: var(--m-scrollbar-thumb); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: var(--m-scrollbar-thumb); filter: brightness(1.2); } + +/* Tab bar: hidden scrollbar */ +.tab-scrollbar-hidden { + scrollbar-width: none; + -ms-overflow-style: none; +} +.tab-scrollbar-hidden::-webkit-scrollbar { + display: none; +} + +/* ═══════════════════════════════════════════════════════════ + React Flow — Node Reset + ═══════════════════════════════════════════════════════════ */ .react-flow__node { background: transparent !important; border: none !important; @@ -83,8 +129,8 @@ html, body, #root { /* ── React Flow Controls (zoom +/- and fit) ── */ .react-flow__controls { - box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important; - border-radius: 8px !important; + box-shadow: var(--m-shadow-card) !important; + border-radius: var(--m-radius) !important; overflow: hidden !important; } .react-flow__controls-button { @@ -93,14 +139,14 @@ html, body, #root { display: flex !important; align-items: center !important; justify-content: center !important; - background: #1c1c20 !important; + background: var(--m-surface-opaque) !important; border: none !important; - border-bottom: 1px solid #2a2a30 !important; - color: #f0f0f0 !important; - fill: #f0f0f0 !important; + border-bottom: 1px solid var(--m-border) !important; + color: var(--m-text-primary) !important; + fill: var(--m-text-primary) !important; } .react-flow__controls-button:hover { - background: #25252a !important; + background: var(--m-surface-elevated) !important; } .react-flow__controls-button:last-child { border-bottom: none !important; @@ -113,79 +159,42 @@ html, body, #root { /* ── React Flow Minimap ── */ .react-flow__minimap { - background: #1c1c20 !important; - border: 1px solid #2a2a30 !important; - border-radius: 8px !important; - box-shadow: 0 2px 8px rgba(0,0,0,0.2) !important; + background: var(--m-surface-opaque) !important; + border: 1px solid var(--m-border) !important; + border-radius: var(--m-radius) !important; + box-shadow: var(--m-shadow-card) !important; } .react-flow__minimap-mask { - fill: rgba(17,17,20,0.85) !important; + fill: var(--m-overlay) !important; } .react-flow__minimap-node { - fill: #3a3a42 !important; - stroke: #555 !important; + fill: var(--m-border) !important; + stroke: var(--m-text-muted) !important; stroke-width: 1 !important; } -/* ── Light mode overrides ── */ -[data-theme="light"] .react-flow__controls-button { - background: #ffffff !important; - border-bottom-color: #dddde2 !important; - color: #1a1a20 !important; - fill: #1a1a20 !important; -} -[data-theme="light"] .react-flow__controls-button:hover { - background: #f0f0f5 !important; -} -[data-theme="light"] .react-flow__minimap { - background: #ffffff !important; - border-color: #ccccd4 !important; -} -[data-theme="light"] .react-flow__minimap-mask { - fill: rgba(240,241,243,0.85) !important; -} -[data-theme="light"] .react-flow__minimap-node { - fill: #ccccd4 !important; - stroke: #999 !important; +/* ── Edge labels ── */ +.react-flow__edge-textwrapper .react-flow__edge-text { + fill: var(--m-text-secondary) !important; } -/* ── Placeholder text contrast ── */ +/* ── Placeholder text ── */ ::placeholder { - color: #666 !important; + color: var(--m-text-muted) !important; opacity: 1 !important; } -[data-theme="light"] ::placeholder { - color: #888 !important; -} - -/* ── Edge labels ── */ -.react-flow__edge-textwrapper .react-flow__edge-text { - fill: #888 !important; -} -[data-theme="light"] .react-flow__edge-textwrapper .react-flow__edge-text { - fill: #555 !important; -} -/* ── Import spinner animation ── */ +/* ═══════════════════════════════════════════════════════════ + Animations + ═══════════════════════════════════════════════════════════ */ @keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -/* Tab bar horizontal scrolling without scrollbar */ -.tab-scrollbar-hidden { - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* Internet Explorer and Edge */ -} -.tab-scrollbar-hidden::-webkit-scrollbar { - display: none; /* Chrome, Safari, Opera */ + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } } -/* Accessibility: respect reduced motion preference */ +/* ═══════════════════════════════════════════════════════════ + Accessibility: Reduced Motion + ═══════════════════════════════════════════════════════════ */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; @@ -194,21 +203,18 @@ html, body, #root { } } -/* ── Mobile responsiveness (< 768px) ── */ +/* ═══════════════════════════════════════════════════════════ + Mobile Responsiveness (< 768px) + ═══════════════════════════════════════════════════════════ */ @media (max-width: 767px) { - /* Wizard content: full-width, remove side padding */ .wizard-content-inner { padding-left: 1rem !important; padding-right: 1rem !important; max-width: 100% !important; } - - /* Hide prev/next edge navigation arrows on mobile (use tabs instead) */ .wizard-edge-nav { display: none !important; } - - /* FAB moves closer to edge on mobile */ .floating-run-btn { right: 12px !important; bottom: 72px !important; From f89dce6a1c6365b14b6ca87b38741994181ea435 Mon Sep 17 00:00:00 2001 From: VictorGjn <56982152+VictorGjn@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:41:21 +0200 Subject: [PATCH 07/21] fix(WizardLayout): tabpanel id/aria-labelledby, tab ids, Geist font name --- src/layouts/WizardLayout.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/layouts/WizardLayout.tsx b/src/layouts/WizardLayout.tsx index 0b52104..c5b9d28 100644 --- a/src/layouts/WizardLayout.tsx +++ b/src/layouts/WizardLayout.tsx @@ -238,7 +238,7 @@ export function WizardLayout() { > {/* Skip Link */} { tabRefs.current[index] = el; }} type="button" role="tab" + id={`tab-${tab.id}`} aria-selected={isActive} aria-controls={`tabpanel-${tab.id}`} tabIndex={isActive ? 0 : -1} @@ -353,7 +354,7 @@ export function WizardLayout() { }} aria-hidden="true" /> - + {tab.label} {isTabCompleted && ( @@ -378,7 +379,7 @@ export function WizardLayout() { {/* Tab Content */}
Date: Thu, 2 Apr 2026 23:43:09 +0200 Subject: [PATCH 08/21] fix(Button): replace hardcoded colors with CSS custom properties --- src/components/ds/Button.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/ds/Button.tsx b/src/components/ds/Button.tsx index e8be5f3..f6517ec 100644 --- a/src/components/ds/Button.tsx +++ b/src/components/ds/Button.tsx @@ -20,10 +20,10 @@ export const Button = forwardRef(function Button const fontSizes = { sm: 12, md: 13 }; const variants: Record = { - primary: { bg: '#FE5000', color: '#fff', border: 'transparent', hoverBg: '#e54700' }, - secondary: { bg: t.surfaceElevated, color: t.textSecondary, border: t.border, hoverBg: t.isDark ? '#2a2a30' : '#eee' }, - ghost: { bg: 'transparent', color: t.textSecondary, border: 'transparent', hoverBg: t.isDark ? '#ffffff08' : '#00000008' }, - danger: { bg: t.statusErrorBg, color: t.statusError, border: 'transparent', hoverBg: t.isDark ? '#ff4d4f20' : '#ff4d4f15' }, + primary: { bg: 'var(--m-accent)', color: '#fff', border: 'transparent', hoverBg: 'var(--m-accent-hover)' }, + secondary: { bg: 'var(--m-surface-elevated)', color: t.textSecondary, border: 'var(--m-border)', hoverBg: 'var(--m-surface-hover)' }, + ghost: { bg: 'transparent', color: t.textSecondary, border: 'transparent', hoverBg: t.isDark ? 'oklch(1 0 0 / 0.03)' : 'oklch(0 0 0 / 0.03)' }, + danger: { bg: t.statusErrorBg, color: t.statusError, border: 'transparent', hoverBg: 'var(--m-error-hover-bg)' }, }; const v = variants[variant]; @@ -38,7 +38,7 @@ export const Button = forwardRef(function Button height: heights[size], padding: paddings[size], fontSize: fontSizes[size], - fontFamily: "'Geist Mono', monospace", + fontFamily: "var(--m-font-mono), monospace", background: v.bg, color: v.color, borderColor: v.border, From cb0e2317f57db97bd33b740ab9ed5e1fefd52ec3 Mon Sep 17 00:00:00 2001 From: VictorGjn <56982152+VictorGjn@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:44:02 +0200 Subject: [PATCH 09/21] fix(RuntimePanel): replace hardcoded colors with CSS custom properties --- src/panels/RuntimePanel.tsx | 56 ++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/panels/RuntimePanel.tsx b/src/panels/RuntimePanel.tsx index 88f8949..416be58 100644 --- a/src/panels/RuntimePanel.tsx +++ b/src/panels/RuntimePanel.tsx @@ -4,17 +4,17 @@ import { useRuntimeStore, type ExtractedFact, type RuntimeAgentState } from '../ import { Loader2, CheckCircle, XCircle, Clock, Brain, Maximize2, Minimize2, ChevronDown, ChevronRight, Copy, Check, Zap } from 'lucide-react'; const FACT_COLORS: Record = { - observation: '#3498db', - inference: '#f1c40f', - decision: '#2ecc71', - hypothesis: '#9b59b6', - contract: '#FE5000', + observation: 'var(--m-fact-observation)', + inference: 'var(--m-fact-inference)', + decision: 'var(--m-fact-decision)', + hypothesis: 'var(--m-fact-hypothesis)', + contract: 'var(--m-fact-contract)', }; function FactBadge({ fact }: { fact: ExtractedFact }) { - const color = FACT_COLORS[fact.epistemicType] ?? '#888'; + const color = FACT_COLORS[fact.epistemicType] ?? 'var(--m-text-dim)'; return ( - + {fact.key} ); @@ -29,7 +29,7 @@ function CopyButton({ text }: { text: string }) { style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 2, opacity: 0.6 }} title="Copy output" > - {copied ? : } + {copied ? : } ); } @@ -40,19 +40,19 @@ function AgentCard({ agent, expanded: forceExpanded }: { agent: RuntimeAgentStat const statusIcon = { waiting: , - running: , - completed: , - error: , + running: , + completed: , + error: , }[agent.status]; const output = agent.status === 'completed' ? agent.output : agent.currentMessage; const hasLongOutput = (output?.length ?? 0) > 300; return ( -
+
{statusIcon} - + {agent.name} {agent.isAgentSdk && ( @@ -61,9 +61,9 @@ function AgentCard({ agent, expanded: forceExpanded }: { agent: RuntimeAgentStat fontSize: 10, padding: '2px 6px', borderRadius: 4, - background: '#FE500015', - color: '#FE5000', - fontFamily: "'Geist Mono', monospace", + background: 'color-mix(in srgb, var(--m-accent) 8%, transparent)', + color: 'var(--m-accent)', + fontFamily: "var(--m-font-mono), monospace", fontWeight: 600, border: '1px solid #FE500030', }} @@ -95,7 +95,7 @@ function AgentCard({ agent, expanded: forceExpanded }: { agent: RuntimeAgentStat @@ -108,8 +108,8 @@ function AgentCard({ agent, expanded: forceExpanded }: { agent: RuntimeAgentStat {agent.toolCalls.length > 0 && (
- - + + Tool Calls ({agent.toolCalls.length})
@@ -121,13 +121,13 @@ function AgentCard({ agent, expanded: forceExpanded }: { agent: RuntimeAgentStat fontSize: 12, padding: 6, borderRadius: 4, - background: '#2ecc7115', + background: 'color-mix(in srgb, var(--m-success) 8%, transparent)', border: '1px solid #2ecc7130', color: t.textSecondary, - fontFamily: "'Geist Mono', monospace", + fontFamily: "var(--m-font-mono), monospace", }} > -
+
{tc.tool}
{tc.args && ( @@ -157,8 +157,8 @@ function SharedFacts({ facts }: { facts: ExtractedFact[] }) { return (
- - + + Shared Memory ({facts.length})
@@ -203,9 +203,9 @@ export function RuntimeResults() { const content = (
- {status === 'running' && } - {status === 'completed' && } - {status === 'error' && } + {status === 'running' && } + {status === 'completed' && } + {status === 'error' && } {status === 'running' ? 'Running...' : status === 'completed' ? 'Completed' : 'Error'} @@ -221,7 +221,7 @@ export function RuntimeResults() {
{error && ( -
{error}
+
{error}
)}
From 4a696cfa1f26e2192515629466d250acf66865f6 Mon Sep 17 00:00:00 2001 From: VictorGjn <56982152+VictorGjn@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:44:41 +0200 Subject: [PATCH 10/21] fix(RuntimePanel): replace remaining border hex colors with color-mix --- src/panels/RuntimePanel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/RuntimePanel.tsx b/src/panels/RuntimePanel.tsx index 416be58..ae3e5fa 100644 --- a/src/panels/RuntimePanel.tsx +++ b/src/panels/RuntimePanel.tsx @@ -65,7 +65,7 @@ function AgentCard({ agent, expanded: forceExpanded }: { agent: RuntimeAgentStat color: 'var(--m-accent)', fontFamily: "var(--m-font-mono), monospace", fontWeight: 600, - border: '1px solid #FE500030', + border: '1px solid color-mix(in srgb, var(--m-accent) 19%, transparent)', }} > Agent SDK @@ -122,7 +122,7 @@ function AgentCard({ agent, expanded: forceExpanded }: { agent: RuntimeAgentStat padding: 6, borderRadius: 4, background: 'color-mix(in srgb, var(--m-success) 8%, transparent)', - border: '1px solid #2ecc7130', + border: '1px solid color-mix(in srgb, var(--m-success) 19%, transparent)', color: t.textSecondary, fontFamily: "var(--m-font-mono), monospace", }} From 493363af9d55e920b6c98e9de81bff43da841df9 Mon Sep 17 00:00:00 2001 From: VictorGjn <56982152+VictorGjn@users.noreply.github.com> Date: Thu, 2 Apr 2026 23:45:31 +0200 Subject: [PATCH 11/21] fix(AgentLibrary): deduplicate toast system, replace hardcoded colors --- src/components/AgentLibrary.tsx | 76 ++++++++++----------------------- 1 file changed, 22 insertions(+), 54 deletions(-) diff --git a/src/components/AgentLibrary.tsx b/src/components/AgentLibrary.tsx index c1f680b..d80a158 100644 --- a/src/components/AgentLibrary.tsx +++ b/src/components/AgentLibrary.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; -import { Plus, Clock, Bot, Search, Trash2, Copy, ArrowUpDown, CheckCircle, XCircle, Rocket } from 'lucide-react'; +import { Plus, Clock, Bot, Search, Trash2, Copy, ArrowUpDown, Rocket } from 'lucide-react'; import { useTheme } from '../theme'; import { useConsoleStore } from '../store/consoleStore'; import { Button } from './ds/Button'; @@ -10,6 +10,7 @@ import { Select } from './ds/Select'; import { API_BASE } from '../config'; import { DEMO_PRESETS } from '../store/demoPresets'; import { TemplateCard } from './TemplateCard'; +import { useToastStore } from '../store/toastStore'; interface Agent { id: string; @@ -20,12 +21,6 @@ interface Agent { updatedAt: string; } -interface Toast { - id: number; - type: 'success' | 'error'; - message: string; -} - interface AgentLibraryProps { onSelectAgent: (agentId: string) => void; onNewAgent: () => void; @@ -44,8 +39,6 @@ const TEMPLATE_LIST = Object.entries(DEMO_PRESETS).map(([id, preset]) => ({ tags: preset.agentMeta.tags, })); -let _toastSeq = 0; - export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) { const [agents, setAgents] = useState([]); const [loading, setLoading] = useState(true); @@ -56,7 +49,6 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) { const [deleteTarget, setDeleteTarget] = useState(null); const [deletingId, setDeletingId] = useState(null); const [cloningId, setCloningId] = useState(null); - const [toasts, setToasts] = useState([]); const debounceRef = useRef | null>(null); const t = useTheme(); @@ -67,11 +59,7 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) { return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, [searchQuery]); - const showToast = useCallback((type: Toast['type'], message: string) => { - const id = ++_toastSeq; - setToasts((prev) => [...prev, { id, type, message }]); - setTimeout(() => setToasts((prev) => prev.filter((t) => t.id !== id)), 3500); - }, []); + const addToast = useToastStore((s) => s.addToast); const handleUseTemplate = useCallback((presetId: string) => { const { loadDemoPreset } = useConsoleStore.getState(); @@ -130,9 +118,9 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) { const res = await fetch(`${API_BASE}/agents/${encodeURIComponent(deleteTarget.id)}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Delete failed'); setAgents((prev) => prev.filter((a) => a.id !== deleteTarget.id)); - showToast('success', `"${deleteTarget.name}" deleted`); + addToast(`"${deleteTarget.name}" deleted`, 'success'); } catch { - showToast('error', `Failed to delete "${deleteTarget.name}"`); + addToast(`Failed to delete "${deleteTarget.name}"`, 'error'); } finally { setDeletingId(null); } @@ -165,10 +153,10 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) { }); if (!saveRes.ok) throw new Error('Failed to save clone'); - showToast('success', `Cloned as "${cloned.agentMeta.name}"`); + addToast(`Cloned as "${cloned.agentMeta.name}"`, 'success'); onSelectAgent(newId); } catch { - showToast('error', `Failed to clone "${agent.name}"`); + addToast(`Failed to clone "${agent.name}"`, 'error'); } finally { setCloningId(null); } @@ -255,7 +243,7 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) { background: t.inputBg, border: `1px solid ${t.border}`, color: t.textPrimary, - fontFamily: "'Geist Sans', sans-serif", + fontFamily: "var(--m-font-sans), sans-serif", fontSize: 13, }} /> @@ -276,13 +264,13 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) {
-