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:
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
BIN
apps/web/public/fonts/Geist-Variable.woff2
Normal file
BIN
apps/web/public/fonts/Geist-Variable.woff2
Normal file
Binary file not shown.
BIN
apps/web/public/fonts/GeistMono-Variable.woff2
Normal file
BIN
apps/web/public/fonts/GeistMono-Variable.woff2
Normal file
Binary file not shown.
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
23
apps/web/src/components/BrowserTitleUpdater.tsx
Normal file
23
apps/web/src/components/BrowserTitleUpdater.tsx
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
231
apps/web/src/components/CommandPalette.tsx
Normal file
231
apps/web/src/components/CommandPalette.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
32
apps/web/src/components/ConnectionBanner.tsx
Normal file
32
apps/web/src/components/ConnectionBanner.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
25
apps/web/src/components/EmptyState.tsx
Normal file
25
apps/web/src/components/EmptyState.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
25
apps/web/src/components/ErrorState.tsx
Normal file
25
apps/web/src/components/ErrorState.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
35
apps/web/src/components/HealthDot.tsx
Normal file
35
apps/web/src/components/HealthDot.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
55
apps/web/src/components/KeyboardShortcutHint.tsx
Normal file
55
apps/web/src/components/KeyboardShortcutHint.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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"}
|
||||
|
||||
21
apps/web/src/components/NavBadge.tsx
Normal file
21
apps/web/src/components/NavBadge.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
44
apps/web/src/components/SaveIndicator.tsx
Normal file
44
apps/web/src/components/SaveIndicator.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
71
apps/web/src/components/SkeletonCard.tsx
Normal file
71
apps/web/src/components/SkeletonCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
38
apps/web/src/components/ThemeToggle.tsx
Normal file
38
apps/web/src/components/ThemeToggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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} />
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,13 @@ export function Toaster() {
|
||||
return (
|
||||
<SonnerToaster
|
||||
position="bottom-right"
|
||||
visibleToasts={3}
|
||||
closeButton
|
||||
offset={16}
|
||||
richColors
|
||||
toastOptions={{
|
||||
className: "font-sans",
|
||||
duration: 4000,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
25
apps/web/src/components/ui/tooltip.tsx
Normal file
25
apps/web/src/components/ui/tooltip.tsx
Normal 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 };
|
||||
31
apps/web/src/hooks/useConnectionStatus.ts
Normal file
31
apps/web/src/hooks/useConnectionStatus.ts
Normal 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;
|
||||
}
|
||||
54
apps/web/src/hooks/useGlobalKeyboard.ts
Normal file
54
apps/web/src/hooks/useGlobalKeyboard.ts
Normal 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]);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
54
apps/web/src/lib/theme.tsx
Normal file
54
apps/web/src/lib/theme.tsx
Normal 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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
)}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
901
package-lock.json
generated
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user