feat: Implement v2 design system with indigo brand, dark mode, and status tokens

Complete frontend design overhaul replacing achromatic shadcn/ui defaults with
an indigo-branded (#6366F1), status-aware, dark-mode-enabled token system.

Phase 1 — Theme Foundation:
- Replace all CSS tokens in index.css with v2 light/dark mode values
- Add 24 status tokens (6 statuses × 4 variants), 22 terminal tokens,
  7 diff tokens, 5 shadow tokens, 9 transition/animation tokens,
  10 z-index tokens, 10-step extended indigo scale
- Install Geist Sans/Mono variable fonts (public/fonts/)
- Extend tailwind.config.ts with all new token utilities
- Add dark mode flash-prevention script in index.html
- Add status-pulse and shimmer keyframe animations
- Add global focus-visible styles and reduced-motion media query

Phase 2 — ThemeProvider + Toggle:
- ThemeProvider context with system preference listener
- 3-state ThemeToggle (Sun/Monitor/Moon)
- Radix tooltip primitive for tooltips
- localStorage persistence with 'cw-theme' key

Phase 3 — Shared Components + Token Migration:
- StatusDot: mapEntityStatus() maps raw statuses to 6 semantic variants
- StatusBadge: uses status token bg/fg/border classes
- Badge: 6 new status variants + xs size
- EmptyState, ErrorState, SaveIndicator shared patterns
- CommandPalette: Cmd+K search with fuzzy matching, keyboard nav
- Skeleton with shimmer animation + SkeletonCard composite layouts
- KeyboardShortcutHint, NavBadge, enhanced Sonner config
- Migrate ALL hardcoded Tailwind colors to token classes across
  AgentOutputViewer, review/*, ProgressBar, AccountCard,
  InitiativeHeader, DependencyIndicator, PipelineTaskCard,
  PreviewPanel, ChangeSetBanner, MessageCard, PhaseDetailPanel

Phase 4 — App Layout Overhaul:
- Single 48px row header with CW logo, nav with NavBadge counts,
  Cmd+K search button, ThemeToggle, HealthDot
- Remove max-w-7xl from header/main; pages control own widths
- ConnectionBanner for offline/reconnecting states
- BrowserTitleUpdater with running/questions counts
- useGlobalKeyboard (1-4 nav, Cmd+K), useConnectionStatus hooks
- Per-page width wrappers (initiatives max-w-6xl, settings max-w-4xl)

Phase 5 — Page-Level Token Migration:
- ReviewSidebar: all hardcoded green/orange/red → status/diff tokens
- CommentThread: resolved state → status-success tokens
- Settings health: green → status-success-dot
This commit is contained in:
Lukas May
2026-03-03 11:43:09 +01:00
parent 34578d39c6
commit 04c212da92
50 changed files with 2428 additions and 238 deletions

View File

@@ -4,6 +4,13 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Codewalk District</title>
<script>
(function() {
var t = localStorage.getItem('cw-theme') || 'system';
var d = t === 'dark' || (t === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
if (d) document.documentElement.classList.add('dark');
})();
</script>
</head>
<body>
<div id="root"></div>

View File

@@ -14,6 +14,7 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.75.0",
"@tanstack/react-router": "^1.158.0",
"@tiptap/extension-link": "^3.19.0",
@@ -28,6 +29,7 @@
"@trpc/react-query": "^11.9.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"geist": "^1.7.0",
"lucide-react": "^0.563.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",

Binary file not shown.

Binary file not shown.

View File

@@ -35,10 +35,10 @@ function UsageBar({
}) {
const color =
utilization >= 90
? "bg-destructive"
? "bg-status-error-dot"
: utilization >= 70
? "bg-yellow-500"
: "bg-green-500";
? "bg-status-warning-dot"
: "bg-status-success-dot";
const resetText = resetsAt ? formatResetTime(resetsAt) : null;
return (
<div className="flex items-center gap-2 text-xs">
@@ -96,13 +96,13 @@ export function AccountCard({ account }: { account: AccountData }) {
const hasWarning = account.credentialsValid && !account.isExhausted && account.error;
const statusIcon = !account.credentialsValid ? (
<XCircle className="h-5 w-5 shrink-0 text-destructive" />
<XCircle className="h-5 w-5 shrink-0 text-status-error-fg" />
) : account.isExhausted ? (
<AlertTriangle className="h-5 w-5 shrink-0 text-yellow-500" />
<AlertTriangle className="h-5 w-5 shrink-0 text-status-warning-fg" />
) : hasWarning ? (
<AlertTriangle className="h-5 w-5 shrink-0 text-yellow-500" />
<AlertTriangle className="h-5 w-5 shrink-0 text-status-warning-fg" />
) : (
<CheckCircle2 className="h-5 w-5 shrink-0 text-green-500" />
<CheckCircle2 className="h-5 w-5 shrink-0 text-status-success-fg" />
);
const statusText = !account.credentialsValid
@@ -197,7 +197,7 @@ export function AccountCard({ account }: { account: AccountData }) {
{/* Error / warning message */}
{account.error && (
<p className={`pl-8 text-xs ${hasWarning ? 'text-yellow-600 dark:text-yellow-500' : 'text-destructive'}`}>
<p className={`pl-8 text-xs ${hasWarning ? 'text-status-warning-fg' : 'text-destructive'}`}>
{account.error}
</p>
)}

View File

@@ -95,21 +95,21 @@ export function AgentOutputViewer({ agentId, agentName, status, onStop }: AgentO
const hasOutput = messages.length > 0;
return (
<div className="flex flex-col h-full rounded-lg border overflow-hidden">
<div className="flex flex-col h-full rounded-lg border border-terminal-border overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between border-b bg-zinc-900 px-4 py-2">
<div className="flex items-center justify-between border-b border-terminal-border bg-terminal px-4 py-2">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-zinc-100">
<span className="text-sm font-medium text-terminal-fg font-mono">
{agentName ? `Output: ${agentName}` : "Agent Output"}
</span>
{subscription.error && (
<div className="flex items-center gap-1 text-red-400" title={subscription.error.message}>
<div className="flex items-center gap-1 text-terminal-error" title={subscription.error.message}>
<AlertCircle className="h-3 w-3" />
<span className="text-xs">Connection error</span>
</div>
)}
{subscription.isConnecting && (
<span className="text-xs text-yellow-400">Connecting...</span>
<span className="text-xs text-terminal-warning">Connecting...</span>
)}
</div>
<div className="flex items-center gap-2">
@@ -128,7 +128,7 @@ export function AgentOutputViewer({ agentId, agentName, status, onStop }: AgentO
variant="ghost"
size="sm"
onClick={() => setFollow(!follow)}
className="h-7 text-zinc-400 hover:text-zinc-100 hover:bg-zinc-800"
className="h-7 text-terminal-muted hover:text-terminal-fg hover:bg-white/5"
>
{follow ? (
<>
@@ -147,7 +147,7 @@ export function AgentOutputViewer({ agentId, agentName, status, onStop }: AgentO
variant="ghost"
size="sm"
onClick={scrollToBottom}
className="h-7 text-zinc-400 hover:text-zinc-100 hover:bg-zinc-800"
className="h-7 text-terminal-muted hover:text-terminal-fg hover:bg-white/5"
>
<ArrowDown className="mr-1 h-3 w-3" />
Jump to bottom
@@ -160,73 +160,73 @@ export function AgentOutputViewer({ agentId, agentName, status, onStop }: AgentO
<div
ref={containerRef}
onScroll={handleScroll}
className="flex-1 overflow-y-auto bg-zinc-900 p-4"
className="flex-1 overflow-y-auto bg-terminal p-4"
>
{isLoading ? (
<div className="text-zinc-500 text-sm">Loading output...</div>
<div className="text-terminal-muted text-sm">Loading output...</div>
) : !hasOutput ? (
<div className="text-zinc-500 text-sm">No output yet...</div>
<div className="text-terminal-muted text-sm">No output yet...</div>
) : (
<div className="space-y-2">
{messages.map((message, index) => (
<div key={index} className={getMessageStyling(message.type)}>
{message.type === 'system' && (
<div className="flex items-center gap-2">
<Badge variant="secondary" className="text-xs">System</Badge>
<span className="text-xs text-zinc-400">{message.content}</span>
<Badge variant="secondary" className="text-xs bg-terminal-border text-terminal-system">System</Badge>
<span className="text-xs text-terminal-muted">{message.content}</span>
</div>
)}
{message.type === 'text' && (
<div className="font-mono text-sm whitespace-pre-wrap text-zinc-100">
<div className="font-mono text-sm whitespace-pre-wrap text-terminal-fg">
{message.content}
</div>
)}
{message.type === 'tool_call' && (
<div className="border-l-2 border-blue-500 pl-3 py-1">
<div className="border-l-2 border-terminal-tool pl-3 py-1">
<Badge variant="default" className="mb-1 text-xs">
{message.meta?.toolName}
</Badge>
<div className="font-mono text-xs text-zinc-300 whitespace-pre-wrap">
<div className="font-mono text-xs text-terminal-muted whitespace-pre-wrap">
{message.content}
</div>
</div>
)}
{message.type === 'tool_result' && (
<div className="border-l-2 border-green-500 pl-3 py-1 bg-zinc-800/30">
<Badge variant="outline" className="mb-1 text-xs">
<div className="border-l-2 border-terminal-result pl-3 py-1 bg-white/[0.02]">
<Badge variant="outline" className="mb-1 text-xs text-terminal-result border-terminal-result">
Result
</Badge>
<div className="font-mono text-xs text-zinc-300 whitespace-pre-wrap">
<div className="font-mono text-xs text-terminal-muted whitespace-pre-wrap">
{message.content}
</div>
</div>
)}
{message.type === 'error' && (
<div className="border-l-2 border-red-500 pl-3 py-1 bg-red-900/20">
<div className="border-l-2 border-terminal-error pl-3 py-1 bg-terminal-error/10">
<Badge variant="destructive" className="mb-1 text-xs">
Error
</Badge>
<div className="font-mono text-xs text-red-200 whitespace-pre-wrap">
<div className="font-mono text-xs text-terminal-error whitespace-pre-wrap">
{message.content}
</div>
</div>
)}
{message.type === 'session_end' && (
<div className="border-t border-zinc-700 pt-2 mt-4">
<div className="border-t border-terminal-border pt-2 mt-4">
<div className="flex items-center gap-2">
<Badge variant={message.meta?.isError ? "destructive" : "default"} className="text-xs">
{message.content}
</Badge>
{message.meta?.cost && (
<span className="text-xs text-zinc-500">${message.meta.cost.toFixed(4)}</span>
<span className="text-xs text-terminal-muted">${message.meta.cost.toFixed(4)}</span>
)}
{message.meta?.duration && (
<span className="text-xs text-zinc-500">{(message.meta.duration / 1000).toFixed(1)}s</span>
<span className="text-xs text-terminal-muted">{(message.meta.duration / 1000).toFixed(1)}s</span>
)}
</div>
</div>

View File

@@ -0,0 +1,23 @@
import { useEffect } from "react";
import { trpc } from "@/lib/trpc";
export function BrowserTitleUpdater() {
const agents = trpc.listAgents.useQuery(undefined, {
refetchInterval: 10000,
});
const runningCount = agents.data?.filter((a) => a.status === "running").length ?? 0;
const questionsCount = agents.data?.filter((a) => a.status === "waiting_for_input").length ?? 0;
useEffect(() => {
const parts: string[] = [];
if (questionsCount > 0) parts.push(`${questionsCount} question${questionsCount > 1 ? "s" : ""}`);
if (runningCount > 0) parts.push(`${runningCount} running`);
document.title = parts.length > 0
? `(${parts.join(", ")}) Codewalk District`
: "Codewalk District";
}, [runningCount, questionsCount]);
return null;
}

View File

@@ -87,11 +87,11 @@ export function ChangeSetBanner({ changeSet, onDismiss }: ChangeSetBannerProps)
</div>
{conflicts && (
<div className="rounded border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950 p-2 space-y-2">
<p className="text-xs font-medium text-amber-800 dark:text-amber-200">
<div className="rounded border border-status-warning-border bg-status-warning-bg p-2 space-y-2">
<p className="text-xs font-medium text-status-warning-fg">
Conflicts detected:
</p>
<ul className="text-xs text-amber-700 dark:text-amber-300 list-disc pl-4 space-y-0.5">
<ul className="text-xs text-status-warning-fg list-disc pl-4 space-y-0.5">
{conflicts.map((c, i) => (
<li key={i}>{c}</li>
))}

View File

@@ -0,0 +1,231 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { useNavigate } from "@tanstack/react-router";
import { Search, Briefcase, Bot, Settings, ArrowRight } from "lucide-react";
import { trpc } from "@/lib/trpc";
import { cn } from "@/lib/utils";
interface CommandItem {
id: string;
label: string;
group: string;
path: string;
icon: typeof Briefcase;
meta?: string;
}
function fuzzyMatch(query: string, text: string): boolean {
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
const lower = text.toLowerCase();
return terms.every((term) => lower.includes(term));
}
const RECENT_KEY = "cw-command-recent";
function getRecent(): string[] {
try {
return JSON.parse(sessionStorage.getItem(RECENT_KEY) ?? "[]");
} catch {
return [];
}
}
function addRecent(id: string) {
const recent = getRecent().filter((r) => r !== id);
recent.unshift(id);
sessionStorage.setItem(RECENT_KEY, JSON.stringify(recent.slice(0, 5)));
}
interface CommandPaletteProps {
open: boolean;
onClose: () => void;
}
export function CommandPalette({ open, onClose }: CommandPaletteProps) {
const [query, setQuery] = useState("");
const [activeIndex, setActiveIndex] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const navigate = useNavigate();
const initiatives = trpc.listInitiatives.useQuery(undefined, { enabled: open });
const agents = trpc.listAgents.useQuery(undefined, { enabled: open });
// Build items list
const items: CommandItem[] = [];
// Static navigation items
const staticItems: CommandItem[] = [
{ id: "nav-initiatives", label: "Initiatives", group: "Navigation", path: "/initiatives", icon: Briefcase },
{ id: "nav-agents", label: "Agents", group: "Navigation", path: "/agents", icon: Bot },
{ id: "nav-inbox", label: "Inbox", group: "Navigation", path: "/inbox", icon: ArrowRight },
{ id: "nav-settings", label: "Settings", group: "Navigation", path: "/settings/health", icon: Settings },
];
for (const item of staticItems) items.push(item);
if (initiatives.data) {
for (const init of initiatives.data) {
items.push({
id: `initiative-${init.id}`,
label: init.name,
group: "Initiatives",
path: `/initiatives/${init.id}`,
icon: Briefcase,
meta: init.status,
});
}
}
if (agents.data) {
for (const agent of agents.data) {
items.push({
id: `agent-${agent.id}`,
label: agent.name,
group: "Agents",
path: "/agents",
icon: Bot,
meta: agent.status,
});
}
}
// Filter
const filtered = query
? items.filter((item) => fuzzyMatch(query, `${item.label} ${item.meta ?? ""}`))
: items;
// Group
const groups = new Map<string, CommandItem[]>();
for (const item of filtered) {
const list = groups.get(item.group) ?? [];
list.push(item);
groups.set(item.group, list);
}
// Flat list for keyboard nav
const flatItems = Array.from(groups.values()).flat();
const select = useCallback(
(item: CommandItem) => {
addRecent(item.id);
onClose();
navigate({ to: item.path });
},
[navigate, onClose],
);
// Focus input when opened
useEffect(() => {
if (open) {
setQuery("");
setActiveIndex(0);
requestAnimationFrame(() => inputRef.current?.focus());
}
}, [open]);
// Reset active index on filter change
useEffect(() => {
setActiveIndex(0);
}, [query]);
// Scroll active item into view
useEffect(() => {
const el = listRef.current?.querySelector('[data-active="true"]');
el?.scrollIntoView({ block: "nearest" });
}, [activeIndex]);
function handleKeyDown(e: React.KeyboardEvent) {
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setActiveIndex((i) => Math.min(i + 1, flatItems.length - 1));
break;
case "ArrowUp":
e.preventDefault();
setActiveIndex((i) => Math.max(i - 1, 0));
break;
case "Enter":
e.preventDefault();
if (flatItems[activeIndex]) select(flatItems[activeIndex]);
break;
case "Escape":
e.preventDefault();
onClose();
break;
}
}
if (!open) return null;
let flatIndex = 0;
return (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-command bg-black/50"
onClick={onClose}
/>
{/* Palette */}
<div className="fixed left-1/2 top-[20%] z-command w-full max-w-lg -translate-x-1/2 rounded-lg border bg-popover shadow-xl dark:bg-[hsl(var(--surface-3))]">
{/* Search */}
<div className="flex items-center gap-2 border-b px-3">
<Search className="h-4 w-4 shrink-0 text-muted-foreground" />
<input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Search initiatives, agents, settings..."
className="h-11 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
/>
<kbd className="hidden sm:inline-flex items-center gap-0.5 rounded border bg-muted px-1.5 py-0.5 text-[10px] font-mono text-muted-foreground">
ESC
</kbd>
</div>
{/* Results */}
<div ref={listRef} className="max-h-[300px] overflow-y-auto p-1">
{flatItems.length === 0 ? (
<p className="px-3 py-6 text-center text-sm text-muted-foreground">
No results
</p>
) : (
Array.from(groups.entries()).map(([group, groupItems]) => (
<div key={group}>
<p className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
{group}
</p>
{groupItems.map((item) => {
const isActive = flatIndex === activeIndex;
const idx = flatIndex;
flatIndex++;
const Icon = item.icon;
return (
<button
key={item.id}
data-active={isActive}
className={cn(
"flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors",
isActive
? "bg-accent text-accent-foreground"
: "text-foreground hover:bg-muted",
)}
onClick={() => select(item)}
onMouseEnter={() => setActiveIndex(idx)}
>
<Icon className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="flex-1 truncate text-left">{item.label}</span>
{item.meta && (
<span className="text-xs text-muted-foreground">{item.meta}</span>
)}
</button>
);
})}
</div>
))
)}
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,32 @@
import { WifiOff, Loader2 } from "lucide-react";
import type { ConnectionState } from "@/hooks/useConnectionStatus";
interface ConnectionBannerProps {
state: ConnectionState;
}
export function ConnectionBanner({ state }: ConnectionBannerProps) {
if (state === "connected") return null;
return (
<div
className={`flex items-center justify-center gap-2 px-4 py-1.5 text-xs font-medium ${
state === "disconnected"
? "bg-status-error-bg text-status-error-fg"
: "bg-status-warning-bg text-status-warning-fg"
}`}
>
{state === "disconnected" ? (
<>
<WifiOff className="h-3.5 w-3.5" />
Offline connection lost
</>
) : (
<>
<Loader2 className="h-3.5 w-3.5 animate-spin" />
Reconnecting...
</>
)}
</div>
);
}

View File

@@ -21,7 +21,7 @@ export function DependencyIndicator({
const names = blockedBy.map((item) => item.name).join(", ");
return (
<div className={cn("pl-8 text-sm text-amber-600", className)}>
<div className={cn("pl-8 text-sm text-status-warning-fg", className)}>
<span className="font-mono">^</span> blocked by: {names}
</div>
);

View File

@@ -0,0 +1,25 @@
import type { LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
interface EmptyStateProps {
icon?: LucideIcon;
title: string;
description?: string;
action?: React.ReactNode;
className?: string;
}
export function EmptyState({ icon: Icon, title, description, action, className }: EmptyStateProps) {
return (
<div className={cn("flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed p-8 text-center", className)}>
{Icon && <Icon className="h-8 w-8 text-muted-foreground/50" />}
<div className="space-y-1">
<p className="text-sm font-medium text-foreground">{title}</p>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</div>
{action}
</div>
);
}

View File

@@ -0,0 +1,25 @@
import { AlertCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface ErrorStateProps {
message?: string;
onRetry?: () => void;
className?: string;
}
export function ErrorState({ message, onRetry, className }: ErrorStateProps) {
return (
<div className={cn("flex flex-col items-center justify-center gap-3 py-12", className)}>
<AlertCircle className="h-8 w-8 text-status-error-fg" />
<p className="text-sm text-status-error-fg">
{message ?? "Something went wrong"}
</p>
{onRetry && (
<Button variant="outline" size="sm" onClick={onRetry}>
Retry
</Button>
)}
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { trpc } from "@/lib/trpc";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
export function HealthDot() {
const health = trpc.healthCheck.useQuery(undefined, {
refetchInterval: 30000,
retry: 1,
});
const isHealthy = health.data && !health.isError;
const isLoading = health.isLoading;
return (
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
"inline-block h-2 w-2 rounded-full",
isLoading && "bg-status-neutral-dot animate-status-pulse",
isHealthy && "bg-status-success-dot",
!isHealthy && !isLoading && "bg-status-error-dot",
)}
/>
</TooltipTrigger>
<TooltipContent>
{isLoading
? "Checking server..."
: isHealthy
? "Server connected"
: "Server disconnected"}
</TooltipContent>
</Tooltip>
);
}

View File

@@ -104,8 +104,8 @@ export function InitiativeHeader({
variant="outline"
className={`cursor-pointer select-none transition-colors ${
initiative.executionMode === "yolo"
? "border-orange-300 text-orange-700 text-[10px] hover:bg-orange-50"
: "border-blue-300 text-blue-700 text-[10px] hover:bg-blue-50"
? "border-status-warning-border text-status-warning-fg text-[10px] hover:bg-status-warning-bg"
: "border-status-active-border text-status-active-fg text-[10px] hover:bg-status-active-bg"
}`}
onClick={toggleExecutionMode}
>

View File

@@ -0,0 +1,55 @@
import { cn } from "@/lib/utils";
interface KeyboardShortcutHintProps {
keys: string[];
className?: string;
}
function formatKey(key: string): string {
const isMac = navigator.platform.toUpperCase().includes("MAC");
switch (key.toLowerCase()) {
case "mod":
case "cmd":
return isMac ? "\u2318" : "Ctrl";
case "shift":
return isMac ? "\u21E7" : "Shift";
case "alt":
case "option":
return isMac ? "\u2325" : "Alt";
case "enter":
case "return":
return "\u21B5";
case "escape":
case "esc":
return "Esc";
case "backspace":
return "\u232B";
case "delete":
return "Del";
case "arrowup":
return "\u2191";
case "arrowdown":
return "\u2193";
case "arrowleft":
return "\u2190";
case "arrowright":
return "\u2192";
default:
return key.toUpperCase();
}
}
export function KeyboardShortcutHint({ keys, className }: KeyboardShortcutHintProps) {
return (
<span className={cn("inline-flex items-center gap-0.5", className)}>
{keys.map((key, i) => (
<kbd
key={i}
className="inline-flex h-5 min-w-5 items-center justify-center rounded border bg-muted px-1 text-[10px] font-mono text-muted-foreground"
>
{formatKey(key)}
</kbd>
))}
</span>
);
}

View File

@@ -43,7 +43,7 @@ export function MessageCard({
<span
className={cn(
"text-sm",
requiresResponse ? "text-orange-500" : "text-muted-foreground",
requiresResponse ? "text-status-warning-dot" : "text-muted-foreground",
)}
>
{requiresResponse ? "\u25CF" : "\u25CB"}

View File

@@ -0,0 +1,21 @@
import { cn } from "@/lib/utils";
interface NavBadgeProps {
count: number;
className?: string;
}
export function NavBadge({ count, className }: NavBadgeProps) {
if (count <= 0) return null;
return (
<span
className={cn(
"inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold text-primary-foreground",
className,
)}
>
{count > 99 ? "99+" : count}
</span>
);
}

View File

@@ -13,7 +13,7 @@ export function ProgressBar({ completed, total, className }: ProgressBarProps) {
percentage === 0
? ""
: percentage === 100
? "bg-green-500"
? "bg-status-success-dot"
: "bg-primary";
return (

View File

@@ -0,0 +1,44 @@
import { Check, Loader2, AlertCircle } from "lucide-react";
import { cn } from "@/lib/utils";
type SaveStatus = "idle" | "saving" | "saved" | "error";
interface SaveIndicatorProps {
status: SaveStatus;
className?: string;
}
export function SaveIndicator({ status, className }: SaveIndicatorProps) {
if (status === "idle") return null;
return (
<span
className={cn(
"inline-flex items-center gap-1 text-xs transition-opacity duration-normal",
status === "saved" && "text-status-success-fg",
status === "saving" && "text-muted-foreground",
status === "error" && "text-status-error-fg",
className,
)}
>
{status === "saving" && (
<>
<Loader2 className="h-3 w-3 animate-spin" />
Saving...
</>
)}
{status === "saved" && (
<>
<Check className="h-3 w-3" />
Saved
</>
)}
{status === "error" && (
<>
<AlertCircle className="h-3 w-3" />
Save failed
</>
)}
</span>
);
}

View File

@@ -2,10 +2,19 @@ import { cn } from "@/lib/utils";
interface SkeletonProps {
className?: string;
variant?: "line" | "circle" | "rect";
}
export function Skeleton({ className }: SkeletonProps) {
export function Skeleton({ className, variant = "rect" }: SkeletonProps) {
return (
<div className={cn("animate-pulse rounded-md bg-muted", className)} />
<div
className={cn(
"animate-shimmer bg-gradient-to-r from-muted via-muted/60 to-muted bg-[length:200%_100%]",
variant === "circle" && "rounded-full",
variant === "line" && "h-4 rounded",
variant === "rect" && "rounded-md",
className,
)}
/>
);
}

View File

@@ -0,0 +1,71 @@
import { Skeleton } from "./Skeleton";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
type Layout = "agent-card" | "initiative-card" | "conversation-card" | "project-card" | "account-card";
interface SkeletonCardProps {
layout?: Layout;
className?: string;
}
export function SkeletonCard({ layout = "initiative-card", className }: SkeletonCardProps) {
return (
<Card className={cn("overflow-hidden", className)}>
<CardContent className="p-4">
{layout === "agent-card" && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Skeleton variant="circle" className="h-2 w-2" />
<Skeleton className="h-4 w-28" />
<Skeleton className="ml-auto h-5 w-14 rounded-full" />
</div>
<Skeleton className="h-3 w-40" />
</div>
)}
{layout === "initiative-card" && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Skeleton className="h-5 w-40" />
<Skeleton className="h-5 w-16 rounded-full" />
</div>
<Skeleton className="h-3 w-56" />
<Skeleton className="h-2 w-full rounded-full" />
</div>
)}
{layout === "conversation-card" && (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Skeleton variant="circle" className="h-6 w-6" />
<Skeleton className="h-4 w-32" />
<Skeleton className="ml-auto h-3 w-16" />
</div>
<Skeleton className="h-3 w-full" />
<Skeleton className="h-3 w-3/4" />
</div>
)}
{layout === "project-card" && (
<div className="space-y-2">
<Skeleton className="h-4 w-28" />
<Skeleton className="h-3 w-48" />
</div>
)}
{layout === "account-card" && (
<div className="space-y-3">
<div className="flex items-center gap-3">
<Skeleton variant="circle" className="h-5 w-5" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-3 w-28" />
</div>
</div>
<div className="space-y-1.5 pl-8">
<Skeleton className="h-2 w-full rounded-full" />
<Skeleton className="h-2 w-3/4 rounded-full" />
</div>
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -1,23 +1,16 @@
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import { mapEntityStatus, type StatusVariant } from "./StatusDot";
const statusStyles: Record<string, string> = {
// Initiative statuses
active: "bg-blue-100 text-blue-800 hover:bg-blue-100/80 border-blue-200",
completed: "bg-green-100 text-green-800 hover:bg-green-100/80 border-green-200",
archived: "bg-gray-100 text-gray-800 hover:bg-gray-100/80 border-gray-200",
// Phase statuses
pending: "bg-gray-100 text-gray-800 hover:bg-gray-100/80 border-gray-200",
approved: "bg-amber-100 text-amber-800 hover:bg-amber-100/80 border-amber-200",
in_progress: "bg-blue-100 text-blue-800 hover:bg-blue-100/80 border-blue-200",
blocked: "bg-red-100 text-red-800 hover:bg-red-100/80 border-red-200",
pending_review: "bg-amber-100 text-amber-800 hover:bg-amber-100/80 border-amber-200",
pending_approval: "bg-amber-100 text-amber-800 hover:bg-amber-100/80 border-amber-200",
const statusStyles: Record<StatusVariant, string> = {
active: "bg-status-active-bg text-status-active-fg border-status-active-border",
success: "bg-status-success-bg text-status-success-fg border-status-success-border",
warning: "bg-status-warning-bg text-status-warning-fg border-status-warning-border",
error: "bg-status-error-bg text-status-error-fg border-status-error-border",
neutral: "bg-status-neutral-bg text-status-neutral-fg border-status-neutral-border",
urgent: "bg-status-urgent-bg text-status-urgent-fg border-status-urgent-border",
};
const defaultStyle = "bg-gray-100 text-gray-800 hover:bg-gray-100/80 border-gray-200";
function formatStatusText(status: string): string {
return status.replace(/_/g, " ").toUpperCase();
}
@@ -29,10 +22,11 @@ interface StatusBadgeProps {
export function StatusBadge({ status, className }: StatusBadgeProps) {
if (!status) return null;
const style = statusStyles[status] ?? defaultStyle;
const variant = mapEntityStatus(status);
const style = statusStyles[variant];
return (
<Badge className={cn(style, className)}>
<Badge className={cn(style, "hover:opacity-90", className)}>
{formatStatusText(status)}
</Badge>
);

View File

@@ -1,76 +1,99 @@
import { cn } from "@/lib/utils";
/**
* Color mapping for different status values.
* Uses semantic colors that work well as small dots.
*/
const statusColors: Record<string, string> = {
// Task statuses
pending: "bg-gray-400",
pending_approval: "bg-yellow-400",
in_progress: "bg-blue-500",
completed: "bg-green-500",
blocked: "bg-red-500",
export type StatusVariant = "active" | "success" | "warning" | "error" | "neutral" | "urgent";
// Agent statuses
idle: "bg-gray-400",
running: "bg-blue-500",
waiting_for_input: "bg-yellow-400",
stopped: "bg-gray-600",
crashed: "bg-red-500",
const dotColors: Record<StatusVariant, string> = {
active: "bg-status-active-dot",
success: "bg-status-success-dot",
warning: "bg-status-warning-dot",
error: "bg-status-error-dot",
neutral: "bg-status-neutral-dot",
urgent: "bg-status-urgent-dot",
};
// Initiative/Phase statuses
active: "bg-blue-500",
archived: "bg-gray-400",
/** Maps raw entity status strings to semantic StatusVariant tokens. */
export function mapEntityStatus(rawStatus: string): StatusVariant {
switch (rawStatus) {
// Active / in-progress
case "running":
case "in_progress":
case "active":
case "building":
case "responded":
return "active";
// Message statuses
read: "bg-green-500",
responded: "bg-blue-500",
// Success / completed
case "completed":
case "read":
case "low":
return "success";
// Priority indicators
low: "bg-green-400",
medium: "bg-yellow-400",
high: "bg-red-400",
} as const;
// Warning / needs attention
case "waiting_for_input":
case "pending_approval":
case "pending_review":
case "approved":
case "exhausted":
case "medium":
return "warning";
const defaultColor = "bg-gray-400";
// Error / failed
case "crashed":
case "blocked":
case "failed":
case "high":
return "error";
// Urgent
// (reserved for future use, no current raw statuses map here)
// Neutral / idle
case "pending":
case "idle":
case "stopped":
case "exited":
case "archived":
default:
return "neutral";
}
}
interface StatusDotProps {
status: string;
size?: "sm" | "md" | "lg";
pulse?: boolean;
label?: string;
className?: string;
title?: string;
}
/**
* Small colored dot to indicate status at a glance.
* More compact than StatusBadge for use in lists or tight spaces.
*/
export function StatusDot({
status,
size = "md",
pulse = false,
label,
className,
title
}: StatusDotProps) {
const sizeClasses = {
sm: "h-2 w-2",
md: "h-3 w-3",
lg: "h-4 w-4"
lg: "h-4 w-4",
};
const color = statusColors[status] ?? defaultColor;
const displayTitle = title ?? status.replace(/_/g, " ").toLowerCase();
const variant = mapEntityStatus(status);
const color = dotColors[variant];
const displayLabel = label ?? status.replace(/_/g, " ").toLowerCase();
return (
<div
<span
role="status"
aria-label={displayLabel}
className={cn(
"rounded-full",
"inline-block shrink-0 rounded-full",
sizeClasses[size],
color,
className
pulse && "animate-status-pulse",
className,
)}
title={displayTitle}
aria-label={`Status: ${displayTitle}`}
/>
);
}
}

View File

@@ -0,0 +1,38 @@
import { Sun, Monitor, Moon } from 'lucide-react';
import { useTheme, type Theme } from '../lib/theme';
const options: { value: Theme; icon: typeof Sun; label: string }[] = [
{ value: 'light', icon: Sun, label: 'Light' },
{ value: 'system', icon: Monitor, label: 'System' },
{ value: 'dark', icon: Moon, label: 'Dark' },
];
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<div
className="inline-flex items-center rounded-md border bg-muted p-0.5"
role="radiogroup"
aria-label="Theme"
>
{options.map(({ value, icon: Icon, label }) => (
<button
key={value}
role="radio"
aria-checked={theme === value}
aria-label={label}
title={label}
onClick={() => setTheme(value)}
className={`inline-flex items-center justify-center rounded-sm p-1.5 transition-colors duration-fast ${
theme === value
? 'bg-background text-foreground shadow-xs'
: 'text-muted-foreground hover:text-foreground'
}`}
>
<Icon className="h-3.5 w-3.5" />
</button>
))}
</div>
);
}

View File

@@ -268,8 +268,8 @@ export function PhaseDetailPanel({
{/* Pending review banner */}
{isPendingReview && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-4 py-3 dark:border-amber-800 dark:bg-amber-950">
<p className="text-sm font-medium text-amber-800 dark:text-amber-200">
<div className="rounded-md border border-status-warning-border bg-status-warning-bg px-4 py-3">
<p className="text-sm font-medium text-status-warning-fg">
This phase is pending review. Switch to the{" "}
<span className="font-semibold">Review</span> tab to view the diff and approve.
</p>
@@ -322,7 +322,7 @@ export function PhaseDetailPanel({
<span
className={
dep.status === "completed"
? "text-green-600"
? "text-status-success-fg"
: "text-muted-foreground"
}
>

View File

@@ -5,11 +5,11 @@ import { useExecutionContext } from "@/components/execution";
import type { SerializedTask } from "@/components/TaskRow";
const statusConfig: Record<string, { icon: typeof Clock; color: string; spin?: boolean }> = {
pending: { icon: Clock, color: "text-muted-foreground" },
pending_approval: { icon: AlertTriangle, color: "text-yellow-500" },
in_progress: { icon: Loader2, color: "text-blue-500", spin: true },
completed: { icon: CheckCircle2, color: "text-green-500" },
blocked: { icon: Ban, color: "text-red-500" },
pending: { icon: Clock, color: "text-status-neutral-fg" },
pending_approval: { icon: AlertTriangle, color: "text-status-warning-dot" },
in_progress: { icon: Loader2, color: "text-status-active-dot", spin: true },
completed: { icon: CheckCircle2, color: "text-status-success-dot" },
blocked: { icon: Ban, color: "text-status-error-dot" },
};
interface PipelineTaskCardProps {

View File

@@ -16,7 +16,7 @@ export function CommentThread({ comments, onResolve, onUnresolve }: CommentThrea
key={comment.id}
className={`rounded border p-2.5 text-xs space-y-1.5 ${
comment.resolved
? "border-green-200 dark:border-green-900 bg-green-50/50 dark:bg-green-950/10"
? "border-status-success-border bg-status-success-bg/50"
: "border-border bg-card"
}`}
>
@@ -27,7 +27,7 @@ export function CommentThread({ comments, onResolve, onUnresolve }: CommentThrea
{formatTime(comment.createdAt)}
</span>
{comment.resolved && (
<span className="flex items-center gap-0.5 text-green-600 text-[10px] font-medium">
<span className="flex items-center gap-0.5 text-status-success-fg text-[10px] font-medium">
<Check className="h-3 w-3" />
Resolved
</span>

View File

@@ -42,13 +42,13 @@ export function FileCard({
<span className="truncate flex-1">{file.newPath}</span>
<span className="flex items-center gap-2 shrink-0 text-xs">
{file.additions > 0 && (
<span className="flex items-center gap-0.5 text-green-600">
<span className="flex items-center gap-0.5 text-diff-add-fg">
<Plus className="h-3 w-3" />
{file.additions}
</span>
)}
{file.deletions > 0 && (
<span className="flex items-center gap-0.5 text-red-600">
<span className="flex items-center gap-0.5 text-diff-remove-fg">
<Minus className="h-3 w-3" />
{file.deletions}
</span>

View File

@@ -49,7 +49,7 @@ export function HunkRows({
<tr>
<td
colSpan={3}
className="px-3 py-1 text-muted-foreground bg-blue-50 dark:bg-blue-950/30 text-[11px] select-none"
className="px-3 py-1 text-muted-foreground bg-diff-hunk-bg text-[11px] select-none"
>
{hunk.header}
</td>

View File

@@ -37,16 +37,16 @@ export function LineWithComments({
const bgClass =
line.type === "added"
? "bg-green-50 dark:bg-green-950/20"
? "bg-diff-add-bg"
: line.type === "removed"
? "bg-red-50 dark:bg-red-950/20"
? "bg-diff-remove-bg"
: "";
const gutterBgClass =
line.type === "added"
? "bg-green-100 dark:bg-green-950/40"
? "bg-diff-add-bg"
: line.type === "removed"
? "bg-red-100 dark:bg-red-950/40"
? "bg-diff-remove-bg"
: "bg-muted/30";
const prefix =
@@ -54,9 +54,9 @@ export function LineWithComments({
const textColorClass =
line.type === "added"
? "text-green-800 dark:text-green-300"
? "text-diff-add-fg"
: line.type === "removed"
? "text-red-800 dark:text-red-300"
? "text-diff-remove-fg"
: "";
return (
@@ -81,7 +81,7 @@ export function LineWithComments({
{/* Comment button gutter */}
<td className={`w-6 min-w-6 ${gutterBgClass} align-top`}>
<button
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 hover:text-blue-600"
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 hover:text-primary"
onClick={onStartComment}
title="Add comment"
>
@@ -123,7 +123,7 @@ export function LineWithComments({
<tr>
<td
colSpan={3}
className="px-3 py-2 bg-blue-50/50 dark:bg-blue-950/20 border-y border-blue-200 dark:border-blue-900"
className="px-3 py-2 bg-diff-hunk-bg border-y border-status-active-border"
>
<CommentForm
ref={formRef}

View File

@@ -81,13 +81,13 @@ export function PreviewPanel({
// Building state
if (startMutation.isPending) {
return (
<div className="flex items-center gap-3 rounded-lg border border-blue-200 dark:border-blue-900 bg-blue-50 dark:bg-blue-950/20 px-4 py-3">
<Loader2 className="h-4 w-4 animate-spin text-blue-600 dark:text-blue-400" />
<div className="flex items-center gap-3 rounded-lg border border-status-active-border bg-status-active-bg px-4 py-3">
<Loader2 className="h-4 w-4 animate-spin text-status-active-dot" />
<div className="flex-1">
<p className="text-sm font-medium text-blue-700 dark:text-blue-300">
<p className="text-sm font-medium text-status-active-fg">
Building preview...
</p>
<p className="text-xs text-blue-600/70 dark:text-blue-400/70">
<p className="text-xs text-status-active-fg/70">
Building containers and starting services
</p>
</div>
@@ -104,14 +104,14 @@ export function PreviewPanel({
<div
className={`flex items-center gap-3 rounded-lg border px-4 py-3 ${
isBuilding
? "border-blue-200 dark:border-blue-900 bg-blue-50 dark:bg-blue-950/20"
: "border-green-200 dark:border-green-900 bg-green-50 dark:bg-green-950/20"
? "border-status-active-border bg-status-active-bg"
: "border-status-success-border bg-status-success-bg"
}`}
>
{isBuilding ? (
<Loader2 className="h-4 w-4 animate-spin text-blue-600 dark:text-blue-400" />
<Loader2 className="h-4 w-4 animate-spin text-status-active-dot" />
) : (
<CircleDot className="h-4 w-4 text-green-600 dark:text-green-400" />
<CircleDot className="h-4 w-4 text-status-success-dot" />
)}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground">
@@ -122,7 +122,7 @@ export function PreviewPanel({
href={url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
className="text-xs text-primary hover:underline flex items-center gap-1"
>
{url}
<ExternalLink className="h-3 w-3" />
@@ -146,10 +146,10 @@ export function PreviewPanel({
// Failed state
if (preview && preview.status === "failed") {
return (
<div className="flex items-center gap-3 rounded-lg border border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-950/20 px-4 py-3">
<CircleX className="h-4 w-4 text-red-600 dark:text-red-400" />
<div className="flex items-center gap-3 rounded-lg border border-status-error-border bg-status-error-bg px-4 py-3">
<CircleX className="h-4 w-4 text-status-error-dot" />
<div className="flex-1">
<p className="text-sm font-medium text-red-700 dark:text-red-300">
<p className="text-sm font-medium text-status-error-fg">
Preview failed
</p>
</div>

View File

@@ -51,7 +51,7 @@ export function ReviewSidebar({
<div className="space-y-3">
<div className="flex items-start justify-between gap-2">
<h3 className="text-sm font-semibold leading-tight">{title}</h3>
<StatusBadge status={status} />
<ReviewStatusBadge status={status} />
</div>
<p className="text-xs text-muted-foreground leading-relaxed">
{description}
@@ -94,13 +94,13 @@ export function ReviewSidebar({
</>
)}
{status === "approved" && (
<div className="flex items-center gap-2 rounded-md bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-900 px-3 py-2 text-xs text-green-700 dark:text-green-400">
<div className="flex items-center gap-2 rounded-md bg-status-success-bg border border-status-success-border px-3 py-2 text-xs text-status-success-fg">
<Check className="h-4 w-4" />
<span className="font-medium">Approved</span>
</div>
)}
{status === "changes_requested" && (
<div className="flex items-center gap-2 rounded-md bg-orange-50 dark:bg-orange-950/20 border border-orange-200 dark:border-orange-900 px-3 py-2 text-xs text-orange-700 dark:text-orange-400">
<div className="flex items-center gap-2 rounded-md bg-status-warning-bg border border-status-warning-border px-3 py-2 text-xs text-status-warning-fg">
<X className="h-4 w-4" />
<span className="font-medium">Changes Requested</span>
</div>
@@ -118,13 +118,13 @@ export function ReviewSidebar({
{comments.length} comment{comments.length !== 1 ? "s" : ""}
</span>
{resolvedCount > 0 && (
<span className="flex items-center gap-1 text-green-600">
<span className="flex items-center gap-1 text-status-success-fg">
<CheckCircle2 className="h-3 w-3" />
{resolvedCount} resolved
</span>
)}
{unresolvedCount > 0 && (
<span className="flex items-center gap-1 text-orange-600">
<span className="flex items-center gap-1 text-status-warning-fg">
<Circle className="h-3 w-3" />
{unresolvedCount} open
</span>
@@ -142,11 +142,11 @@ export function ReviewSidebar({
<FileCode className="h-3 w-3 text-muted-foreground" />
{files.length} file{files.length !== 1 ? "s" : ""}
</span>
<span className="flex items-center gap-0.5 text-green-600">
<span className="flex items-center gap-0.5 text-diff-add-fg">
<Plus className="h-3 w-3" />
{totalAdditions}
</span>
<span className="flex items-center gap-0.5 text-red-600">
<span className="flex items-center gap-0.5 text-diff-remove-fg">
<Minus className="h-3 w-3" />
{totalDeletions}
</span>
@@ -179,8 +179,8 @@ export function ReviewSidebar({
{fileCommentCount}
</span>
)}
<span className="text-green-600 text-[10px]">+{file.additions}</span>
<span className="text-red-600 text-[10px]">-{file.deletions}</span>
<span className="text-diff-add-fg text-[10px]">+{file.additions}</span>
<span className="text-diff-remove-fg text-[10px]">-{file.deletions}</span>
</span>
</button>
);
@@ -190,23 +190,23 @@ export function ReviewSidebar({
);
}
function StatusBadge({ status }: { status: ReviewStatus }) {
function ReviewStatusBadge({ status }: { status: ReviewStatus }) {
if (status === "approved") {
return (
<Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 border-green-200 dark:border-green-800 text-[10px]">
<Badge variant="success" size="xs">
Approved
</Badge>
);
}
if (status === "changes_requested") {
return (
<Badge className="bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200 border-orange-200 dark:border-orange-800 text-[10px]">
<Badge variant="warning" size="xs">
Changes Requested
</Badge>
);
}
return (
<Badge variant="secondary" className="text-[10px]">
<Badge variant="secondary" size="xs">
Pending Review
</Badge>
);

View File

@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"inline-flex items-center rounded-full border font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
@@ -15,10 +15,27 @@ const badgeVariants = cva(
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
active:
"border-status-active-border bg-status-active-bg text-status-active-fg",
success:
"border-status-success-border bg-status-success-bg text-status-success-fg",
warning:
"border-status-warning-border bg-status-warning-bg text-status-warning-fg",
error:
"border-status-error-border bg-status-error-bg text-status-error-fg",
neutral:
"border-status-neutral-border bg-status-neutral-bg text-status-neutral-fg",
urgent:
"border-status-urgent-border bg-status-urgent-bg text-status-urgent-fg",
},
size: {
sm: "px-2.5 py-0.5 text-xs",
xs: "px-1.5 py-0 text-[10px]",
},
},
defaultVariants: {
variant: "default",
size: "sm",
},
}
)
@@ -27,9 +44,9 @@ export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
function Badge({ className, variant, size, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
<div className={cn(badgeVariants({ variant, size }), className)} {...props} />
)
}

View File

@@ -4,9 +4,13 @@ export function Toaster() {
return (
<SonnerToaster
position="bottom-right"
visibleToasts={3}
closeButton
offset={16}
richColors
toastOptions={{
className: "font-sans",
duration: 4000,
}}
/>
);

View File

@@ -0,0 +1,25 @@
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from '../../lib/utils';
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
function TooltipContent({
className,
sideOffset = 4,
...props
}: React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Content
sideOffset={sideOffset}
className={cn(
'z-tooltip overflow-hidden rounded-md bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95',
className,
)}
{...props}
/>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@@ -0,0 +1,31 @@
import { useState, useEffect } from "react";
export type ConnectionState = "connected" | "reconnecting" | "disconnected";
export function useConnectionStatus(): ConnectionState {
const [state, setState] = useState<ConnectionState>("connected");
useEffect(() => {
function handleOnline() {
setState("connected");
}
function handleOffline() {
setState("disconnected");
}
window.addEventListener("online", handleOnline);
window.addEventListener("offline", handleOffline);
// Initial state
if (!navigator.onLine) {
setState("disconnected");
}
return () => {
window.removeEventListener("online", handleOnline);
window.removeEventListener("offline", handleOffline);
};
}, []);
return state;
}

View File

@@ -0,0 +1,54 @@
import { useEffect } from "react";
import { useNavigate } from "@tanstack/react-router";
const NAV_KEYS: Record<string, string> = {
"1": "/initiatives",
"2": "/agents",
"3": "/inbox",
"4": "/settings/health",
};
interface UseGlobalKeyboardOptions {
onCommandK: () => void;
}
export function useGlobalKeyboard({ onCommandK }: UseGlobalKeyboardOptions) {
const navigate = useNavigate();
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
// Skip if user is typing in an input/textarea/contenteditable
const target = e.target as HTMLElement;
if (
target.tagName === "INPUT" ||
target.tagName === "TEXTAREA" ||
target.isContentEditable
) {
// But still allow Cmd+K even in inputs
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
onCommandK();
}
return;
}
// Cmd+K / Ctrl+K — command palette
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
onCommandK();
return;
}
// Number keys 1-4 for quick nav (no modifier)
if (!e.metaKey && !e.ctrlKey && !e.altKey) {
const path = NAV_KEYS[e.key];
if (path) {
navigate({ to: path });
}
}
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [navigate, onCommandK]);
}

View File

@@ -1,51 +1,307 @@
@font-face {
font-family: 'Geist Sans';
src: url('/fonts/Geist-Variable.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Geist Mono';
src: url('/fonts/GeistMono-Variable.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
/* Core palette — Indigo brand */
--background: 0 0% 99%;
--foreground: 240 6% 10%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--card-foreground: 240 6% 10%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
--popover-foreground: 240 6% 10%;
--primary: 239 84% 67%;
--primary-foreground: 0 0% 100%;
--secondary: 240 5% 96%;
--secondary-foreground: 240 4% 16%;
--muted: 240 5% 93%;
--muted-foreground: 240 4% 46%;
--accent: 226 100% 97%;
--accent-foreground: 239 84% 67%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%;
--border: 240 6% 90%;
--input: 240 6% 90%;
--ring: 239 84% 67%;
--radius: 0.375rem;
/* Extended indigo scale */
--indigo-50: 226 100% 97%;
--indigo-100: 228 96% 93%;
--indigo-200: 232 92% 86%;
--indigo-300: 235 88% 78%;
--indigo-400: 237 86% 72%;
--indigo-500: 239 84% 67%;
--indigo-600: 243 75% 59%;
--indigo-700: 245 58% 51%;
--indigo-800: 244 47% 42%;
--indigo-900: 242 47% 34%;
/* Status tokens — active */
--status-active-bg: 210 100% 95%;
--status-active-fg: 210 100% 40%;
--status-active-border: 210 100% 80%;
--status-active-dot: 210 100% 50%;
/* Status tokens — success */
--status-success-bg: 142 72% 94%;
--status-success-fg: 142 72% 29%;
--status-success-border: 142 72% 80%;
--status-success-dot: 142 72% 45%;
/* Status tokens — warning */
--status-warning-bg: 38 92% 95%;
--status-warning-fg: 38 92% 30%;
--status-warning-border: 38 92% 80%;
--status-warning-dot: 38 92% 50%;
/* Status tokens — error */
--status-error-bg: 0 84% 95%;
--status-error-fg: 0 84% 40%;
--status-error-border: 0 84% 80%;
--status-error-dot: 0 84% 50%;
/* Status tokens — neutral */
--status-neutral-bg: 240 5% 96%;
--status-neutral-fg: 240 4% 46%;
--status-neutral-border: 240 6% 90%;
--status-neutral-dot: 240 4% 46%;
/* Status tokens — urgent */
--status-urgent-bg: 270 91% 95%;
--status-urgent-fg: 270 91% 40%;
--status-urgent-border: 270 91% 80%;
--status-urgent-dot: 270 91% 55%;
/* Terminal tokens (always-dark aesthetic) */
--terminal-bg: 240 6% 7%;
--terminal-fg: 120 100% 80%;
--terminal-muted: 240 5% 55%;
--terminal-border: 240 4% 16%;
--terminal-selection: 239 84% 67%;
--terminal-system: 240 5% 55%;
--terminal-tool: 217 91% 60%;
--terminal-result: 142 72% 45%;
--terminal-error: 0 84% 60%;
--terminal-cursor: 120 100% 65%;
--terminal-selection-bg: 239 84% 67% / 0.25;
--terminal-link: 217 91% 70%;
--terminal-warning: 38 92% 60%;
--terminal-line-number: 240 5% 35%;
--terminal-ansi-black: 240 6% 7%;
--terminal-ansi-red: 0 84% 60%;
--terminal-ansi-green: 142 72% 45%;
--terminal-ansi-yellow: 38 92% 60%;
--terminal-ansi-blue: 217 91% 60%;
--terminal-ansi-magenta: 270 91% 65%;
--terminal-ansi-cyan: 195 80% 55%;
--terminal-ansi-white: 240 5% 85%;
/* Diff tokens */
--diff-add-bg: 142 72% 94%;
--diff-add-fg: 142 72% 29%;
--diff-add-border: 142 72% 80%;
--diff-remove-bg: 0 84% 95%;
--diff-remove-fg: 0 84% 40%;
--diff-remove-border: 0 84% 80%;
--diff-hunk-bg: 226 100% 97%;
/* Shadow tokens */
--shadow-xs: 0 1px 2px hsl(0 0% 0% / 0.04);
--shadow-sm: 0 1px 3px hsl(0 0% 0% / 0.06), 0 1px 2px hsl(0 0% 0% / 0.04);
--shadow-md: 0 4px 6px hsl(0 0% 0% / 0.06), 0 2px 4px hsl(0 0% 0% / 0.04);
--shadow-lg: 0 10px 15px hsl(0 0% 0% / 0.06), 0 4px 6px hsl(0 0% 0% / 0.04);
--shadow-xl: 0 20px 25px hsl(0 0% 0% / 0.08), 0 8px 10px hsl(0 0% 0% / 0.04);
/* Transition & animation tokens */
--duration-instant: 50ms;
--duration-fast: 100ms;
--duration-normal: 200ms;
--duration-slow: 350ms;
--duration-glacial: 500ms;
--ease-default: cubic-bezier(0.2, 0, 0, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
/* Z-index scale */
--z-base: 0;
--z-raised: 1;
--z-sticky: 10;
--z-sidebar: 20;
--z-dropdown: 30;
--z-overlay: 40;
--z-modal: 50;
--z-toast: 60;
--z-command: 70;
--z-tooltip: 80;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--background: 240 6% 7%;
--foreground: 240 5% 96%;
--card: 240 5% 11%;
--card-foreground: 240 5% 96%;
--popover: 240 5% 16%;
--popover-foreground: 240 5% 96%;
--primary: 239 84% 67%;
--primary-foreground: 0 0% 100%;
--secondary: 240 4% 16%;
--secondary-foreground: 240 5% 96%;
--muted: 240 4% 16%;
--muted-foreground: 240 5% 65%;
--accent: 239 40% 16%;
--accent-foreground: 239 84% 75%;
--destructive: 0 63% 31%;
--destructive-foreground: 0 0% 100%;
--border: 240 4% 16%;
--input: 240 4% 16%;
--ring: 239 84% 67%;
/* Surface level 3 for command palette */
--surface-3: 240 5% 21%;
/* Status tokens — dark mode */
--status-active-bg: 210 100% 12%;
--status-active-fg: 210 100% 70%;
--status-active-border: 210 100% 25%;
--status-active-dot: 210 100% 50%;
--status-success-bg: 142 72% 10%;
--status-success-fg: 142 72% 65%;
--status-success-border: 142 72% 22%;
--status-success-dot: 142 72% 45%;
--status-warning-bg: 38 92% 10%;
--status-warning-fg: 38 92% 65%;
--status-warning-border: 38 92% 22%;
--status-warning-dot: 38 92% 50%;
--status-error-bg: 0 84% 12%;
--status-error-fg: 0 84% 65%;
--status-error-border: 0 84% 25%;
--status-error-dot: 0 84% 50%;
--status-neutral-bg: 240 4% 16%;
--status-neutral-fg: 240 5% 65%;
--status-neutral-border: 240 4% 22%;
--status-neutral-dot: 240 5% 50%;
--status-urgent-bg: 270 91% 12%;
--status-urgent-fg: 270 91% 70%;
--status-urgent-border: 270 91% 25%;
--status-urgent-dot: 270 91% 55%;
/* Terminal tokens — dark mode */
--terminal-bg: 240 5% 11%;
--terminal-fg: 120 100% 80%;
--terminal-muted: 240 5% 55%;
--terminal-border: 240 4% 16%;
--terminal-selection: 239 84% 67%;
--terminal-system: 240 5% 55%;
--terminal-tool: 217 91% 60%;
--terminal-result: 142 72% 45%;
--terminal-error: 0 84% 60%;
--terminal-cursor: 120 100% 65%;
--terminal-selection-bg: 239 84% 67% / 0.25;
--terminal-link: 217 91% 70%;
--terminal-warning: 38 92% 60%;
--terminal-line-number: 240 5% 35%;
--terminal-ansi-black: 240 6% 7%;
--terminal-ansi-red: 0 84% 60%;
--terminal-ansi-green: 142 72% 45%;
--terminal-ansi-yellow: 38 92% 60%;
--terminal-ansi-blue: 217 91% 60%;
--terminal-ansi-magenta: 270 91% 65%;
--terminal-ansi-cyan: 195 80% 55%;
--terminal-ansi-white: 240 5% 85%;
/* Diff tokens — dark mode */
--diff-add-bg: 142 72% 10%;
--diff-add-fg: 142 72% 65%;
--diff-add-border: 142 72% 22%;
--diff-remove-bg: 0 84% 12%;
--diff-remove-fg: 0 84% 65%;
--diff-remove-border: 0 84% 25%;
--diff-hunk-bg: 239 40% 16%;
/* Shadow tokens — dark mode (inset highlights + ambient glow) */
--shadow-xs: none;
--shadow-sm: inset 0 1px 0 hsl(0 0% 100% / 0.04);
--shadow-md: inset 0 1px 0 hsl(0 0% 100% / 0.05), 0 2px 8px hsl(0 0% 0% / 0.3);
--shadow-lg: inset 0 1px 0 hsl(0 0% 100% / 0.06), 0 4px 16px hsl(0 0% 0% / 0.4);
--shadow-xl: inset 0 1px 0 hsl(0 0% 100% / 0.06), 0 8px 32px hsl(0 0% 0% / 0.5);
}
}
/* Keyframes */
@keyframes status-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* Global focus-visible styles */
*:focus-visible {
outline: 2px solid hsl(var(--ring));
outline-offset: 2px;
border-radius: var(--radius);
}
*:focus:not(:focus-visible) {
outline: none;
}
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
outline: none;
box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(var(--ring));
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}

View File

@@ -1,41 +1,86 @@
import { Link } from '@tanstack/react-router'
import { Search } from 'lucide-react'
import { ThemeToggle } from '@/components/ThemeToggle'
import { HealthDot } from '@/components/HealthDot'
import { NavBadge } from '@/components/NavBadge'
import { trpc } from '@/lib/trpc'
const navItems = [
{ label: 'Initiatives', to: '/initiatives' },
{ label: 'Agents', to: '/agents' },
{ label: 'Inbox', to: '/inbox' },
{ label: 'Settings', to: '/settings' },
{ label: 'Initiatives', to: '/initiatives', badgeKey: null },
{ label: 'Agents', to: '/agents', badgeKey: 'running' as const },
{ label: 'Inbox', to: '/inbox', badgeKey: 'questions' as const },
{ label: 'Settings', to: '/settings', badgeKey: null },
] as const
export function AppLayout({ children }: { children: React.ReactNode }) {
interface AppLayoutProps {
children: React.ReactNode
onOpenCommandPalette?: () => void
}
export function AppLayout({ children, onOpenCommandPalette }: AppLayoutProps) {
const agents = trpc.listAgents.useQuery(undefined, {
refetchInterval: 10000,
})
const badgeCounts = {
running: agents.data?.filter((a) => a.status === 'running').length ?? 0,
questions: agents.data?.filter((a) => a.status === 'waiting_for_input').length ?? 0,
}
return (
<div className="flex h-screen flex-col bg-background">
{/* Header */}
<header className="shrink-0 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="mx-auto flex h-14 max-w-7xl items-center justify-between px-6">
<Link to="/initiatives" className="text-lg font-bold tracking-tight">
Codewalk District
</Link>
</div>
{/* Navigation */}
<nav className="mx-auto flex max-w-7xl gap-1 px-6 pb-2">
{navItems.map((item) => (
<Link
key={item.label}
to={item.to}
className="rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground"
activeProps={{
className: 'rounded-md px-3 py-1.5 text-sm font-medium text-foreground bg-muted',
}}
>
{item.label}
{/* Single-row 48px header */}
<header className="z-sticky shrink-0 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex h-12 items-center justify-between px-4">
{/* Left: Logo + Nav */}
<div className="flex items-center gap-6">
<Link to="/initiatives" className="flex items-center gap-2 text-sm font-bold tracking-tight">
<span className="inline-flex h-6 w-6 items-center justify-center rounded bg-primary text-[10px] font-bold text-primary-foreground">
CW
</span>
<span className="hidden sm:inline">Codewalk District</span>
</Link>
))}
</nav>
<nav className="flex items-center gap-0.5">
{navItems.map((item) => (
<Link
key={item.label}
to={item.to}
className="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors duration-fast hover:text-foreground"
activeProps={{
className: 'flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-foreground bg-accent',
}}
>
{item.label}
{item.badgeKey && (
<NavBadge count={badgeCounts[item.badgeKey]} />
)}
</Link>
))}
</nav>
</div>
{/* Right: Cmd+K, Theme toggle, Health, Workspace */}
<div className="flex items-center gap-3">
{onOpenCommandPalette && (
<button
onClick={onOpenCommandPalette}
className="flex items-center gap-1.5 rounded-md border bg-muted/50 px-2.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
<Search className="h-3 w-3" />
<span className="hidden sm:inline">Search</span>
<kbd className="ml-1 hidden rounded border bg-background px-1 py-0.5 text-[10px] font-mono sm:inline">
K
</kbd>
</button>
)}
<ThemeToggle />
<HealthDot />
</div>
</div>
</header>
{/* Page content */}
<main className="mx-auto flex-1 min-h-0 w-full max-w-7xl overflow-auto px-6 py-6">
{/* Page content — no max-width here, pages control their own */}
<main className="flex-1 min-h-0 w-full overflow-auto px-6 py-6">
{children}
</main>
</div>

View File

@@ -0,0 +1,54 @@
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react';
export type Theme = 'light' | 'dark' | 'system';
interface ThemeContextValue {
theme: Theme;
setTheme: (t: Theme) => void;
isDark: boolean;
}
const ThemeContext = createContext<ThemeContextValue>(null!);
function applyTheme(theme: Theme) {
const isDark =
theme === 'dark' ||
(theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', isDark);
localStorage.setItem('cw-theme', theme);
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setThemeState] = useState<Theme>(
() => (localStorage.getItem('cw-theme') as Theme) || 'system'
);
const [systemDark, setSystemDark] = useState(
() => window.matchMedia('(prefers-color-scheme: dark)').matches
);
const isDark = theme === 'dark' || (theme === 'system' && systemDark);
const setTheme = useCallback((t: Theme) => {
setThemeState(t);
applyTheme(t);
}, []);
useEffect(() => {
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e: MediaQueryListEvent) => {
setSystemDark(e.matches);
if (theme === 'system') applyTheme('system');
};
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme, isDark }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);

View File

@@ -3,6 +3,8 @@ import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { trpc, createTRPCClient } from './lib/trpc';
import { createMutationCache } from './lib/invalidation';
import { ThemeProvider } from './lib/theme';
import { TooltipProvider } from './components/ui/tooltip';
import App from './App';
import './index.css';
@@ -26,11 +28,15 @@ function Root() {
const [trpcClient] = useState(createTRPCClient);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</trpc.Provider>
<ThemeProvider>
<TooltipProvider delayDuration={300}>
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</trpc.Provider>
</TooltipProvider>
</ThemeProvider>
);
}

View File

@@ -1,20 +1,45 @@
import { useState, useCallback } from 'react'
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
import { AppLayout } from '../layouts/AppLayout'
import { ErrorBoundary } from '../components/ErrorBoundary'
import { Toaster } from '../components/ui/sonner'
import { Button } from '../components/ui/button'
import { CommandPalette } from '../components/CommandPalette'
import { ConnectionBanner } from '../components/ConnectionBanner'
import { BrowserTitleUpdater } from '../components/BrowserTitleUpdater'
import { useConnectionStatus } from '../hooks/useConnectionStatus'
import { useGlobalKeyboard } from '../hooks/useGlobalKeyboard'
export const Route = createRootRoute({
component: () => (
function RootLayout() {
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false)
const connectionState = useConnectionStatus()
const openCommandPalette = useCallback(() => {
setCommandPaletteOpen(true)
}, [])
useGlobalKeyboard({ onCommandK: openCommandPalette })
return (
<>
<AppLayout>
<ConnectionBanner state={connectionState} />
<AppLayout onOpenCommandPalette={openCommandPalette}>
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
</AppLayout>
<CommandPalette
open={commandPaletteOpen}
onClose={() => setCommandPaletteOpen(false)}
/>
<BrowserTitleUpdater />
<Toaster />
</>
),
)
}
export const Route = createRootRoute({
component: RootLayout,
notFoundComponent: () => (
<div className="flex flex-col items-center justify-center gap-4 py-12">
<h1 className="text-2xl font-bold">Page not found</h1>

View File

@@ -264,7 +264,7 @@ function AgentsPage() {
</span>
{agent.status === "waiting_for_input" && (
<span
className="text-xs text-yellow-600 hover:underline cursor-pointer"
className="text-xs text-status-warning-fg hover:underline cursor-pointer"
onClick={(e) => {
e.stopPropagation();
handleGoToInbox();

View File

@@ -31,7 +31,7 @@ function DashboardPage() {
]);
return (
<div className="space-y-6">
<div className="mx-auto max-w-6xl space-y-6">
{/* Page header */}
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Initiatives</h1>

View File

@@ -11,7 +11,7 @@ const settingsTabs = [
function SettingsLayout() {
return (
<div className="space-y-4">
<div className="mx-auto max-w-4xl space-y-4">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold tracking-tight">Settings</h1>
</div>

View File

@@ -93,7 +93,7 @@ function HealthCheckPage() {
</CardHeader>
<CardContent>
<div className="flex items-center gap-3">
<CheckCircle2 className="h-5 w-5 text-green-500" />
<CheckCircle2 className="h-5 w-5 text-status-success-dot" />
<div>
<p className="text-sm font-medium">Running</p>
<p className="text-xs text-muted-foreground">
@@ -149,7 +149,7 @@ function HealthCheckPage() {
<Card key={project.id}>
<CardContent className="flex items-center gap-3 py-4">
{project.repoExists ? (
<CheckCircle2 className="h-5 w-5 shrink-0 text-green-500" />
<CheckCircle2 className="h-5 w-5 shrink-0 text-status-success-dot" />
) : (
<XCircle className="h-5 w-5 shrink-0 text-destructive" />
)}

View File

@@ -1,6 +1,7 @@
import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate";
import typography from "@tailwindcss/typography";
import defaultTheme from "tailwindcss/defaultTheme";
export default {
darkMode: "class",
@@ -14,6 +15,10 @@ export default {
},
},
extend: {
fontFamily: {
sans: ["Geist Sans", ...defaultTheme.fontFamily.sans],
mono: ["Geist Mono", ...defaultTheme.fontFamily.mono],
},
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
@@ -48,12 +53,104 @@ export default {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
indigo: {
50: "hsl(var(--indigo-50))",
100: "hsl(var(--indigo-100))",
200: "hsl(var(--indigo-200))",
300: "hsl(var(--indigo-300))",
400: "hsl(var(--indigo-400))",
500: "hsl(var(--indigo-500))",
600: "hsl(var(--indigo-600))",
700: "hsl(var(--indigo-700))",
800: "hsl(var(--indigo-800))",
900: "hsl(var(--indigo-900))",
},
status: {
"active-bg": "hsl(var(--status-active-bg))",
"active-fg": "hsl(var(--status-active-fg))",
"active-border": "hsl(var(--status-active-border))",
"active-dot": "hsl(var(--status-active-dot))",
"success-bg": "hsl(var(--status-success-bg))",
"success-fg": "hsl(var(--status-success-fg))",
"success-border": "hsl(var(--status-success-border))",
"success-dot": "hsl(var(--status-success-dot))",
"warning-bg": "hsl(var(--status-warning-bg))",
"warning-fg": "hsl(var(--status-warning-fg))",
"warning-border": "hsl(var(--status-warning-border))",
"warning-dot": "hsl(var(--status-warning-dot))",
"error-bg": "hsl(var(--status-error-bg))",
"error-fg": "hsl(var(--status-error-fg))",
"error-border": "hsl(var(--status-error-border))",
"error-dot": "hsl(var(--status-error-dot))",
"neutral-bg": "hsl(var(--status-neutral-bg))",
"neutral-fg": "hsl(var(--status-neutral-fg))",
"neutral-border": "hsl(var(--status-neutral-border))",
"neutral-dot": "hsl(var(--status-neutral-dot))",
"urgent-bg": "hsl(var(--status-urgent-bg))",
"urgent-fg": "hsl(var(--status-urgent-fg))",
"urgent-border": "hsl(var(--status-urgent-border))",
"urgent-dot": "hsl(var(--status-urgent-dot))",
},
terminal: {
DEFAULT: "hsl(var(--terminal-bg))",
fg: "hsl(var(--terminal-fg))",
muted: "hsl(var(--terminal-muted))",
border: "hsl(var(--terminal-border))",
system: "hsl(var(--terminal-system))",
tool: "hsl(var(--terminal-tool))",
result: "hsl(var(--terminal-result))",
error: "hsl(var(--terminal-error))",
cursor: "hsl(var(--terminal-cursor))",
link: "hsl(var(--terminal-link))",
warning: "hsl(var(--terminal-warning))",
"line-number": "hsl(var(--terminal-line-number))",
},
diff: {
"add-bg": "hsl(var(--diff-add-bg))",
"add-fg": "hsl(var(--diff-add-fg))",
"add-border": "hsl(var(--diff-add-border))",
"remove-bg": "hsl(var(--diff-remove-bg))",
"remove-fg": "hsl(var(--diff-remove-fg))",
"remove-border":"hsl(var(--diff-remove-border))",
"hunk-bg": "hsl(var(--diff-hunk-bg))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
boxShadow: {
xs: "var(--shadow-xs)",
sm: "var(--shadow-sm)",
md: "var(--shadow-md)",
lg: "var(--shadow-lg)",
xl: "var(--shadow-xl)",
},
transitionDuration: {
instant: "var(--duration-instant)",
fast: "var(--duration-fast)",
normal: "var(--duration-normal)",
slow: "var(--duration-slow)",
},
transitionTimingFunction: {
default: "var(--ease-default)",
in: "var(--ease-in)",
out: "var(--ease-out)",
spring: "var(--ease-spring)",
},
zIndex: {
base: "var(--z-base)",
raised: "var(--z-raised)",
sticky: "var(--z-sticky)",
sidebar: "var(--z-sidebar)",
dropdown: "var(--z-dropdown)",
overlay: "var(--z-overlay)",
modal: "var(--z-modal)",
toast: "var(--z-toast)",
command: "var(--z-command)",
tooltip: "var(--z-tooltip)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
@@ -63,10 +160,20 @@ export default {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
"status-pulse": {
"0%, 100%": { opacity: "1" },
"50%": { opacity: "0.4" },
},
shimmer: {
"0%": { backgroundPosition: "-200% 0" },
"100%": { backgroundPosition: "200% 0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"status-pulse": "status-pulse 2s ease-in-out infinite",
shimmer: "shimmer 1.5s infinite",
},
},
},

View File

@@ -10,9 +10,29 @@
| TanStack Router | File-based routing |
| tRPC React Query | Type-safe API client with caching |
| Tailwind CSS | Utility-first styling |
| shadcn/ui | Component library (button, card, dialog, dropdown, input, label, textarea, badge, sonner) |
| shadcn/ui | Component library (button, card, dialog, dropdown, input, label, textarea, badge, sonner, tooltip) |
| Tiptap | Rich text editor (ProseMirror-based) |
| Lucide | Icon library |
| Geist Sans/Mono | Typography (variable fonts in `public/fonts/`) |
## Design System (v2)
Theme spec: `docs/wireframes/v2/theme.md`
- **Brand**: Indigo (#6366F1) — `--primary` is indigo, not black
- **Dark mode**: 3-state toggle (light/system/dark), persisted in `localStorage('cw-theme')`
- **Status tokens**: 6 semantic statuses (active/success/warning/error/neutral/urgent) with bg/fg/border/dot variants. Use `bg-status-{status}-bg`, `text-status-{status}-fg`, etc.
- **Terminal tokens**: Always-dark surface for agent output. Use `bg-terminal`, `text-terminal-fg`, etc.
- **Diff tokens**: `bg-diff-add-bg`, `text-diff-remove-fg`, etc.
- **Shadows**: 5-level system (xs-xl). Dark mode uses inset highlights + ambient glow.
- **Transitions**: `duration-fast`, `duration-normal`, `ease-default`, `ease-spring`
- **Z-index**: Named scale from `z-base` to `z-tooltip`
- **Radius**: 6px base (down from 8px)
- **Flash prevention**: Inline `<script>` in `index.html` reads theme before paint
### Status-to-Entity Mapping
Use `mapEntityStatus(rawStatus)` from `StatusDot.tsx` to convert raw entity statuses to semantic tokens. `StatusDot` and `StatusBadge` both use this automatically.
## Path Alias
@@ -41,13 +61,25 @@ The initiative detail page has three tabs managed via local state (not URL param
|-----------|---------|
| `InitiativeHeader` | Initiative name, project badges, inline-editable execution mode & branch |
| `InitiativeContent` | Content tab with page tree + editor |
| `StatusBadge` | Colored status indicator |
| `StatusDot` | Small colored dot using status tokens, with pulse animation |
| `StatusBadge` | Colored badge using status tokens |
| `TaskRow` | Task list item with status, priority, category |
| `QuestionForm` | Agent question form with options |
| `InboxDetailPanel` | Agent message detail + response form |
| `ProjectPicker` | Checkbox list for project selection |
| `RegisterProjectDialog` | Dialog to register new git project |
| `Skeleton` | Loading placeholder |
| `Skeleton` | Loading placeholder with shimmer animation |
| `SkeletonCard` | Composite skeleton layouts (agent-card, initiative-card, etc.) |
| `EmptyState` | Shared empty state with icon, title, description, action |
| `ErrorState` | Shared error state with retry |
| `SaveIndicator` | Saving/saved/error status indicator |
| `CommandPalette` | Cmd+K search palette (initiatives, agents, navigation) |
| `ThemeToggle` | 3-state theme toggle (Sun/Monitor/Moon) |
| `NavBadge` | Numeric badge for nav items |
| `KeyboardShortcutHint` | Formatted keyboard shortcut display |
| `ConnectionBanner` | Offline/reconnecting state banner |
| `HealthDot` | Server health indicator with tooltip |
| `BrowserTitleUpdater` | Dynamic document.title with agent counts |
### Editor Components (`src/components/editor/`)
| Component | Purpose |
@@ -84,7 +116,7 @@ The initiative detail page has three tabs managed via local state (not URL param
| `ProposalCard` | Individual proposal display |
### UI Primitives (`src/components/ui/`)
shadcn/ui components: badge, button, card, dialog, dropdown-menu, input, label, sonner, textarea.
shadcn/ui components: badge (6 status variants + xs size), button, card, dialog, dropdown-menu, input, label, select, sonner, textarea, tooltip.
## Custom Hooks (`src/hooks/`)
@@ -93,6 +125,12 @@ shadcn/ui components: badge, button, card, dialog, dropdown-menu, input, label,
| `useRefineAgent` | Manages refine agent lifecycle for initiative |
| `useDetailAgent` | Manages detail agent for phase planning |
| `useAgentOutput` | Subscribes to live agent output stream |
| `useConnectionStatus` | Tracks online/offline/reconnecting state |
| `useGlobalKeyboard` | Global keyboard shortcuts (1-4 nav, Cmd+K) |
## Theme (`src/lib/theme.tsx`)
`ThemeProvider` wraps the app root. `useTheme()` returns `{ theme, setTheme, isDark }`. The provider listens for OS `prefers-color-scheme` changes when in `system` mode.
## tRPC Client

901
package-lock.json generated
View File

@@ -55,6 +55,7 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.75.0",
"@tanstack/react-router": "^1.158.0",
"@tiptap/extension-link": "^3.19.0",
@@ -69,6 +70,7 @@
"@trpc/react-query": "^11.9.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"geist": "^1.7.0",
"lucide-react": "^0.563.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -1051,6 +1053,17 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@emnapi/runtime": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild-kit/core-utils": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
@@ -1967,6 +1980,497 @@
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@img/colour": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"peer": true,
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"peer": true,
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
"cpu": [
"ppc64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"peer": true,
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
"cpu": [
"riscv64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"peer": true,
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"peer": true,
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"peer": true,
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"peer": true,
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"peer": true,
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
"cpu": [
"ppc64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
"cpu": [
"riscv64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/runtime": "^1.7.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
@@ -2055,6 +2559,149 @@
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
"license": "MIT"
},
"node_modules/@next/env": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz",
"integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==",
"license": "MIT",
"peer": true
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz",
"integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz",
"integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz",
"integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz",
"integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz",
"integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz",
"integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz",
"integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz",
"integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 10"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2740,6 +3387,58 @@
}
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
"integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-visually-hidden": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@@ -3293,6 +3992,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
@@ -4644,7 +5353,6 @@
"version": "2.9.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -4789,7 +5497,6 @@
"version": "1.0.30001767",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz",
"integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -4872,6 +5579,13 @@
"url": "https://polar.sh/cva"
}
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT",
"peer": true
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -6016,6 +6730,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/geist": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/geist/-/geist-1.7.0.tgz",
"integrity": "sha512-ZaoiZwkSf0DwwB1ncdLKp+ggAldqxl5L1+SXaNIBGkPAqcu+xjVJLxlf3/S8vLt9UHx1xu5fz3lbzKCj5iOVdQ==",
"license": "SIL OPEN FONT LICENSE",
"peerDependencies": {
"next": ">=13.2.0"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -6728,6 +7451,108 @@
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT"
},
"node_modules/next": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
"integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@next/env": "16.1.6",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.1.6",
"@next/swc-darwin-x64": "16.1.6",
"@next/swc-linux-arm64-gnu": "16.1.6",
"@next/swc-linux-arm64-musl": "16.1.6",
"@next/swc-linux-x64-gnu": "16.1.6",
"@next/swc-linux-x64-musl": "16.1.6",
"@next/swc-win32-arm64-msvc": "16.1.6",
"@next/swc-win32-x64-msvc": "16.1.6",
"sharp": "^0.34.4"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.51.1",
"babel-plugin-react-compiler": "*",
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"@playwright/test": {
"optional": true
},
"babel-plugin-react-compiler": {
"optional": true
},
"sass": {
"optional": true
}
}
},
"node_modules/next/node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"peer": true,
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/node-abi": {
"version": "3.87.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
@@ -6903,7 +7728,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -7950,6 +8774,52 @@
"seroval": "^1.0"
}
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": {
"@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
"semver": "^7.7.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.5",
"@img/sharp-darwin-x64": "0.34.5",
"@img/sharp-libvips-darwin-arm64": "1.2.4",
"@img/sharp-libvips-darwin-x64": "1.2.4",
"@img/sharp-libvips-linux-arm": "1.2.4",
"@img/sharp-libvips-linux-arm64": "1.2.4",
"@img/sharp-libvips-linux-ppc64": "1.2.4",
"@img/sharp-libvips-linux-riscv64": "1.2.4",
"@img/sharp-libvips-linux-s390x": "1.2.4",
"@img/sharp-libvips-linux-x64": "1.2.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
"@img/sharp-linux-arm": "0.34.5",
"@img/sharp-linux-arm64": "0.34.5",
"@img/sharp-linux-ppc64": "0.34.5",
"@img/sharp-linux-riscv64": "0.34.5",
"@img/sharp-linux-s390x": "0.34.5",
"@img/sharp-linux-x64": "0.34.5",
"@img/sharp-linuxmusl-arm64": "0.34.5",
"@img/sharp-linuxmusl-x64": "0.34.5",
"@img/sharp-wasm32": "0.34.5",
"@img/sharp-win32-arm64": "0.34.5",
"@img/sharp-win32-ia32": "0.34.5",
"@img/sharp-win32-x64": "0.34.5"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -8083,7 +8953,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -8168,6 +9037,30 @@
"node": ">=0.10.0"
}
},
"node_modules/styled-jsx": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
"integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
"license": "MIT",
"peer": true,
"dependencies": {
"client-only": "0.0.1"
},
"engines": {
"node": ">= 12.0.0"
},
"peerDependencies": {
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"babel-plugin-macros": {
"optional": true
}
}
},
"node_modules/sucrase": {
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",