Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a390cc8
feat: add shared OKLCH design tokens
VictorGjn Apr 2, 2026
7cc9561
refactor: rewrite theme.ts to use OKLCH CSS custom properties via var…
VictorGjn Apr 2, 2026
1853dd4
refactor: set data-theme attribute on documentElement in themeStore
VictorGjn Apr 2, 2026
6616766
feat(Modal): add inline focus trap and use CSS custom properties
VictorGjn Apr 2, 2026
fc3c464
fix(Tabs): min font 11/12px, ARIA tabs pattern, CSS vars, remove uppe…
VictorGjn Apr 2, 2026
079eef4
refactor(styles): rewrite globals.css to use shared OKLCH --m-* tokens
VictorGjn Apr 2, 2026
f89dce6
fix(WizardLayout): tabpanel id/aria-labelledby, tab ids, Geist font name
VictorGjn Apr 2, 2026
7f2da09
fix(Button): replace hardcoded colors with CSS custom properties
VictorGjn Apr 2, 2026
cb0e231
fix(RuntimePanel): replace hardcoded colors with CSS custom properties
VictorGjn Apr 2, 2026
4a696cf
fix(RuntimePanel): replace remaining border hex colors with color-mix
VictorGjn Apr 2, 2026
493363a
fix(AgentLibrary): deduplicate toast system, replace hardcoded colors
VictorGjn Apr 2, 2026
de5fdef
feat: extract WorkflowCard component with design-token colors
VictorGjn Apr 2, 2026
8e9e823
refactor: thin AgentBuilder orchestrator (~140 lines)
VictorGjn Apr 2, 2026
2a16f7f
refactor: extract AgentBuilder helper components (OKLCH tokens)
VictorGjn Apr 3, 2026
3b30438
refactor: extract AgentBuilder helper components (OKLCH tokens)
VictorGjn Apr 3, 2026
315eb7a
refactor: extract AgentBuilder helper components (OKLCH tokens)
VictorGjn Apr 3, 2026
a90d7c5
refactor: extract IdentitySection from AgentBuilder
VictorGjn Apr 3, 2026
ee661d2
refactor: extract PersonaSection from AgentBuilder
VictorGjn Apr 3, 2026
aacd976
refactor: extract ConstraintsSection from AgentBuilder
VictorGjn Apr 3, 2026
9e86c78
refactor: extract ObjectivesSection from AgentBuilder
VictorGjn Apr 3, 2026
d06d283
fix: FactBadge background uses color-mix instead of broken string con…
VictorGjn Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 22 additions & 54 deletions src/components/AgentLibrary.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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<Agent[]>([]);
const [loading, setLoading] = useState(true);
Expand All @@ -56,7 +49,6 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) {
const [deleteTarget, setDeleteTarget] = useState<Agent | null>(null);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [cloningId, setCloningId] = useState<string | null>(null);
const [toasts, setToasts] = useState<Toast[]>([]);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const t = useTheme();

Expand All @@ -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();
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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,
}}
/>
Expand All @@ -276,13 +264,13 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) {
<div
className="mb-6 flex items-start gap-4 p-5 rounded-xl"
style={{
background: t.isDark ? 'rgba(254,80,0,0.07)' : 'rgba(254,80,0,0.05)',
border: `1px solid ${t.isDark ? 'rgba(254,80,0,0.25)' : 'rgba(254,80,0,0.2)'}`,
background: t.isDark ? 'color-mix(in srgb, var(--m-accent) 7%, transparent)' : 'color-mix(in srgb, var(--m-accent) 5%, transparent)',
border: `1px solid ${t.isDark ? 'color-mix(in srgb, var(--m-accent) 25%, transparent)' : 'color-mix(in srgb, var(--m-accent) 20%, transparent)'}`,
}}
>
<Rocket size={28} style={{ color: '#FE5000', flexShrink: 0, marginTop: 2 }} aria-hidden="true" />
<Rocket size={28} style={{ color: 'var(--m-accent)', flexShrink: 0, marginTop: 2 }} aria-hidden="true" />
<div>
<div className="text-base font-bold mb-1" style={{ color: t.textPrimary, fontFamily: "'Geist Sans', sans-serif" }}>
<div className="text-base font-bold mb-1" style={{ color: t.textPrimary, fontFamily: "var(--m-font-sans), sans-serif" }}>
Welcome to Modular Studio
</div>
<div className="text-sm mb-3" style={{ color: t.textSecondary }}>
Expand All @@ -292,7 +280,7 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) {
type="button"
onClick={handleNewAgentClick}
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold border-none cursor-pointer"
style={{ background: '#FE5000', color: '#fff' }}
style={{ background: 'var(--m-accent)', color: '#fff' }}
>
<Plus size={14} aria-hidden="true" />
Get Started
Expand Down Expand Up @@ -325,17 +313,17 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) {
border: `1px solid ${t.border}`,
boxShadow: `0 2px 8px ${t.isDark ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.06)'}`,
}}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = '#FE5000'; }}
onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--m-accent)'; }}
onMouseLeave={(e) => { e.currentTarget.style.borderColor = t.border; }}
>
<div className="p-4">
{/* Avatar and Name */}
<div className="flex items-start gap-3 mb-3">
<div className="w-8 h-8 rounded-full flex items-center justify-center shrink-0" style={{ background: '#FE500015' }}>
<Bot size={16} style={{ color: '#FE5000' }} />
<div className="w-8 h-8 rounded-full flex items-center justify-center shrink-0" style={{ background: 'color-mix(in srgb, var(--m-accent) 8%, transparent)' }}>
<Bot size={16} style={{ color: 'var(--m-accent)' }} />
</div>
<div className="min-w-0 flex-1">
<h3 className="text-base font-semibold mb-1 truncate" style={{ color: t.textPrimary, fontFamily: "'Geist Sans', sans-serif" }}>
<h3 className="text-base font-semibold mb-1 truncate" style={{ color: t.textPrimary, fontFamily: "var(--m-font-sans), sans-serif" }}>
{agent.name}
</h3>
<p className="text-sm line-clamp-2" style={{ color: t.textSecondary }}>
Expand All @@ -349,7 +337,7 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) {
<div className="flex flex-wrap gap-1 mb-3">
{agent.tags.slice(0, 3).map((tag) => (
<span key={tag} className="text-[11px] px-1.5 py-0.5 rounded"
style={{ background: t.surfaceElevated, color: t.textDim, fontFamily: "'Geist Mono', monospace" }}>
style={{ background: t.surfaceElevated, color: t.textDim, fontFamily: "var(--m-font-mono), monospace" }}>
{tag}
</span>
))}
Expand All @@ -365,7 +353,7 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) {
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 text-xs" style={{ color: t.textDim }}>
<Clock size={12} />
<span style={{ fontFamily: "'Geist Mono', monospace" }}>{formatDate(agent.updatedAt)}</span>
<span style={{ fontFamily: "var(--m-font-mono), monospace" }}>{formatDate(agent.updatedAt)}</span>
</div>
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
<button
Expand All @@ -376,7 +364,7 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) {
onClick={(e) => handleClone(agent, e)}
className="flex items-center justify-center w-6 h-6 rounded cursor-pointer border-none bg-transparent transition-colors"
style={{ color: cloningId === agent.id ? t.textFaint : t.textDim }}
onMouseEnter={(e) => { e.currentTarget.style.color = '#FE5000'; }}
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--m-accent)'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = cloningId === agent.id ? t.textFaint : t.textDim; }}
>
{cloningId === agent.id ? <span className="animate-spin text-[10px]" aria-hidden="true">⟳</span> : <Copy size={13} aria-hidden="true" />}
Expand All @@ -389,7 +377,7 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) {
onClick={(e) => { e.stopPropagation(); setDeleteTarget(agent); }}
className="flex items-center justify-center w-6 h-6 rounded cursor-pointer border-none bg-transparent transition-colors"
style={{ color: deletingId === agent.id ? t.textFaint : t.textDim }}
onMouseEnter={(e) => { e.currentTarget.style.color = '#ff4d4f'; }}
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--m-error)'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = deletingId === agent.id ? t.textFaint : t.textDim; }}
>
{deletingId === agent.id ? <span className="animate-spin text-[10px]" aria-hidden="true">⟳</span> : <Trash2 size={13} aria-hidden="true" />}
Expand Down Expand Up @@ -423,27 +411,7 @@ export function AgentLibrary({ onSelectAgent, onNewAgent }: AgentLibraryProps) {
</div>
</Modal>

{/* Toast notifications */}
<div className="fixed bottom-5 right-5 z-[300] flex flex-col gap-2 pointer-events-none">
{toasts.map((toast) => (
<div
key={toast.id}
className="flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium pointer-events-auto"
style={{
background: t.surfaceOpaque,
border: `1px solid ${toast.type === 'success' ? '#22c55e40' : '#ff4d4f40'}`,
color: t.textPrimary,
boxShadow: `0 4px 16px ${t.isDark ? 'rgba(0,0,0,0.4)' : 'rgba(0,0,0,0.12)'}`,
fontFamily: "'Geist Sans', sans-serif",
}}
>
{toast.type === 'success'
? <CheckCircle size={15} style={{ color: '#22c55e', flexShrink: 0 }} />
: <XCircle size={15} style={{ color: '#ff4d4f', flexShrink: 0 }} />}
{toast.message}
</div>
))}
</div>

</div>
);
}
10 changes: 5 additions & 5 deletions src/components/ds/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
const fontSizes = { sm: 12, md: 13 };

const variants: Record<string, { bg: string; color: string; border: string; hoverBg: string }> = {
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];
Expand All @@ -38,7 +38,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(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,
Expand Down
57 changes: 49 additions & 8 deletions src/components/ds/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,59 @@ export interface ModalProps {
width?: number;
}

function getFocusableElements(container: HTMLElement) {
return container.querySelectorAll<HTMLElement>(
'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<HTMLDivElement>(null);
const previouslyFocusedRef = useRef<HTMLElement | null>(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]);
Expand All @@ -32,7 +73,7 @@ export function Modal({ open, onClose, title, children, footer, width = 520 }: M

return createPortal(
<div className="fixed inset-0 z-[200] flex items-center justify-center" onClick={onClose}>
<div className="absolute inset-0" style={{ background: 'rgba(0,0,0,0.5)', backdropFilter: 'blur(4px)' }} />
<div className="absolute inset-0" style={{ background: 'var(--m-overlay, rgba(0,0,0,0.5))', backdropFilter: 'blur(4px)' }} />
<div
ref={panelRef}
role="dialog"
Expand All @@ -43,22 +84,22 @@ export function Modal({ open, onClose, title, children, footer, width = 520 }: M
className="relative flex flex-col rounded-xl overflow-hidden outline-none"
style={{
width, maxWidth: '90vw', maxHeight: '80vh',
background: t.surfaceOpaque,
border: `1px solid ${t.border}`,
background: 'var(--m-surface-opaque)',
border: '1px solid var(--m-border)',
boxShadow: `0 16px 48px ${t.isDark ? 'rgba(0,0,0,0.6)' : 'rgba(0,0,0,0.15)'}`,
}}
>
{title && (
<div className="flex items-center justify-between px-4 py-3 shrink-0" style={{ borderBottom: `1px solid ${t.borderSubtle}` }}>
<span id={titleId} className="text-[17px] font-bold" style={{ fontFamily: "'Geist Mono', monospace", color: t.textPrimary }}>{title}</span>
<button type="button" onClick={onClose} aria-label="Close dialog" className="flex items-center justify-center w-7 h-7 rounded-md cursor-pointer border-none bg-transparent" style={{ color: t.textDim }}>
<div className="flex items-center justify-between px-4 py-3 shrink-0" style={{ borderBottom: '1px solid var(--m-border-subtle)' }}>
<span id={titleId} className="text-[17px] font-bold" style={{ fontFamily: "'Geist Mono', monospace", color: 'var(--m-text-primary)' }}>{title}</span>
<button type="button" onClick={onClose} aria-label="Close dialog" className="flex items-center justify-center w-7 h-7 rounded-md cursor-pointer border-none bg-transparent" style={{ color: 'var(--m-text-dim)' }}>
<X size={14} aria-hidden="true" />
</button>
</div>
)}
<div className="flex-1 overflow-y-auto">{children}</div>
{footer && (
<div className="flex items-center justify-end gap-2 px-4 py-3 shrink-0" style={{ borderTop: `1px solid ${t.borderSubtle}` }}>
<div className="flex items-center justify-end gap-2 px-4 py-3 shrink-0" style={{ borderTop: '1px solid var(--m-border-subtle)' }}>
{footer}
</div>
)}
Expand Down
Loading
Loading