History

light mode (#4)

* init components * use them * Fix select trigger styling in dark mode and add open state highlight Remove dark mode background bleed on select triggers and add accent background when popup is open. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> ---------

Julius Marminge committed 9 months ago

Commit c51f2521 parent 70a9a0452 files · +4872 −918Browse files

Unified · Split

52 files
  1. apps/desktop/scripts/smoke-test.mjs +2 −2
  2. apps/renderer/components.json +25 −0
  3. apps/renderer/index.html +1 −1
  4. apps/renderer/package.json +5 −0
  5. apps/renderer/src/App.tsx +3 −3
  6. apps/renderer/src/components/ChatMarkdown.tsx +1 −1
  7. apps/renderer/src/components/ChatView.tsx +255 −754
  8. apps/renderer/src/components/DiffPanel.tsx +18 −10
  9. apps/renderer/src/components/Sidebar.tsx +95 −119
  10. apps/renderer/src/components/ui/accordion.tsx +67 −0
  11. apps/renderer/src/components/ui/alert-dialog.tsx +167 −0
  12. apps/renderer/src/components/ui/alert.tsx +80 −0
  13. apps/renderer/src/components/ui/autocomplete.tsx +308 −0
  14. apps/renderer/src/components/ui/avatar.tsx +44 −0
  15. apps/renderer/src/components/ui/badge.tsx +58 −0
  16. apps/renderer/src/components/ui/button.tsx +71 −0
  17. apps/renderer/src/components/ui/card.tsx +242 −0
  18. apps/renderer/src/components/ui/checkbox-group.tsx +14 −0
  19. apps/renderer/src/components/ui/checkbox.tsx +58 −0
  20. apps/renderer/src/components/ui/collapsible.tsx +43 −0
  21. apps/renderer/src/components/ui/combobox.tsx +422 −0
  22. apps/renderer/src/components/ui/command.tsx +262 −0
  23. apps/renderer/src/components/ui/dialog.tsx +194 −0
  24. apps/renderer/src/components/ui/empty.tsx +127 −0
  25. apps/renderer/src/components/ui/field.tsx +72 −0
  26. apps/renderer/src/components/ui/fieldset.tsx +27 −0
  27. apps/renderer/src/components/ui/form.tsx +15 −0
  28. apps/renderer/src/components/ui/input-group.tsx +99 −0
  29. apps/renderer/src/components/ui/input.tsx +64 −0
  30. apps/renderer/src/components/ui/kbd.tsx +28 −0
  31. apps/renderer/src/components/ui/label.tsx +26 −0
  32. apps/renderer/src/components/ui/menu.tsx +308 −0
  33. apps/renderer/src/components/ui/popover.tsx +102 −0
  34. apps/renderer/src/components/ui/progress.tsx +79 −0
  35. apps/renderer/src/components/ui/radio-group.tsx +34 −0
  36. apps/renderer/src/components/ui/scroll-area.tsx +62 −0
  37. apps/renderer/src/components/ui/select.tsx +187 −0
  38. apps/renderer/src/components/ui/separator.tsx +23 −0
  39. apps/renderer/src/components/ui/sheet.tsx +201 −0
  40. apps/renderer/src/components/ui/skeleton.tsx +16 −0
  41. apps/renderer/src/components/ui/spinner.tsx +18 −0
  42. apps/renderer/src/components/ui/switch.tsx +25 −0
  43. apps/renderer/src/components/ui/table.tsx +126 −0
  44. apps/renderer/src/components/ui/tabs.tsx +85 −0
  45. apps/renderer/src/components/ui/textarea.tsx +49 −0
  46. apps/renderer/src/components/ui/toast.tsx +267 −0
  47. apps/renderer/src/hooks/useTheme.ts +90 −0
  48. apps/renderer/src/index.css +194 −26
  49. apps/renderer/src/lib/utils.ts +6 −0
  50. apps/renderer/tsconfig.json +5 −1
  51. apps/renderer/vite.config.ts +6 −0
  52. bun.lock +96 −1
apps/desktop/scripts/smoke-test.mjs+2−2
@@ -13,7 +13,7 @@ · 12 unmodified lines
1313 * the first render). For production mode (no React DevTools message), we
1414 * instead check that no fatal errors appeared.
1515 */
16−import { spawn, execSync } from "node:child_process";
16+import { execSync, spawn } from "node:child_process";
1717 import { dirname, resolve } from "node:path";
1818 import { fileURLToPath } from "node:url";
1919
@@ -75,7 +75,7 @@ child.on("exit", () => { · 55 unmodified lines
7575 for (const f of failures) {
7676 console.error(` • ${f}`);
7777 }
78− console.error("\nFull output:\n" + output);
78+ console.error(`\nFull output:\n${output}`);
7979 process.exit(1);
8080 }
8181
apps/renderer/components.json+25−0
@@ -0,0 +1,25 @@
1+{
2+ "$schema": "https://ui.shadcn.com/schema.json",
3+ "style": "new-york",
4+ "rsc": false,
5+ "tsx": true,
6+ "tailwind": {
7+ "config": "",
8+ "css": "src/index.css",
9+ "baseColor": "zinc",
10+ "cssVariables": true,
11+ "prefix": ""
12+ },
13+ "iconLibrary": "lucide",
14+ "rtl": false,
15+ "aliases": {
16+ "components": "#/components",
17+ "utils": "#/lib/utils",
18+ "ui": "#/components/ui",
19+ "lib": "#/lib",
20+ "hooks": "#/hooks"
21+ },
22+ "registries": {
23+ "@coss": "https://coss.com/ui/r/{name}.json"
24+ }
25+}
apps/renderer/index.html+1−1
@@ -5,7 +5,7 @@ · 4 unmodified lines
55 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
66 <title>Electron Todo</title>
77 </head>
8− <body class="bg-slate-100">
8+ <body>
99 <div id="root"></div>
1010 <script type="module" src="/src/main.tsx"></script>
1111 </body>
apps/renderer/package.json+5−0
@@ -12,12 +12,17 @@ · 11 unmodified lines
1212 },
1313 "dependencies": {
1414 "@acme/contracts": "workspace:*",
15+ "@base-ui/react": "^1.1.0",
16+ "class-variance-authority": "^0.7.1",
17+ "clsx": "^2.1.1",
1518 "highlight.js": "^11.11.1",
19+ "lucide-react": "^0.563.0",
1620 "react": "^19.0.0",
1721 "react-dom": "^19.0.0",
1822 "react-markdown": "^10.1.0",
1923 "rehype-highlight": "^7.0.2",
2024 "remark-gfm": "^4.0.1",
25+ "tailwind-merge": "^3.4.0",
2126 "zod": "^3.24.1"
2227 },
2328 "devDependencies": {
apps/renderer/src/App.tsx+3−3
@@ -31,10 +31,10 @@ function Layout() { · 30 unmodified lines
3131
3232 if (!api) {
3333 return (
34− <div className="flex h-screen flex-col bg-[#0c0c0c] text-[#e0e0e0]">
34+ <div className="flex h-screen flex-col bg-background text-foreground">
3535 <div className="drag-region h-[52px] shrink-0" />
3636 <div className="flex flex-1 items-center justify-center">
37− <p className="text-sm text-[#a0a0a0]/60">
37+ <p className="text-sm text-muted-foreground">
3838 Native bridge unavailable. Launch through Electron.
3939 </p>
4040 </div>
@@ -43,7 +43,7 @@ function Layout() { · 2 unmodified lines
4343 }
4444
4545 return (
46− <div className="flex h-screen overflow-hidden bg-[#0c0c0c] text-[#e0e0e0]">
46+ <div className="flex h-screen overflow-hidden bg-background text-foreground">
4747 <EventRouter />
4848 <Sidebar />
4949 <ChatView />
apps/renderer/src/components/ChatMarkdown.tsx+1−1
@@ -15,7 +15,7 @@ const markdownComponents: Components = { · 14 unmodified lines
1515
1616 export default function ChatMarkdown({ text }: ChatMarkdownProps) {
1717 return (
18− <div className="chat-markdown text-sm leading-relaxed text-[#d0d0d0]">
18+ <div className="chat-markdown text-sm leading-relaxed text-foreground/80">
1919 <ReactMarkdown
2020 remarkPlugins={[remarkGfm]}
2121 rehypePlugins={[[rehypeHighlight, { detect: true, ignoreMissing: true }]]}
apps/renderer/src/components/ChatView.tsx+255−754
@@ -1,7 +1,6 @@
1−import type { ProviderApprovalDecision, ProviderEvent } from "@acme/contracts";
1+import { ArrowUpIcon, LoaderCircleIcon, SquareIcon } from "lucide-react";
22 import {
33 type FormEvent,
4− Fragment,
54 type KeyboardEvent,
65 useEffect,
76 useMemo,
@@ -9,7 +8,6 @@ import { · 1 unmodified line
98 useState,
109 } from "react";
1110
12−import { EDITORS, type EditorId } from "@acme/contracts";
1311 import {
1412 DEFAULT_MODEL,
1513 DEFAULT_REASONING,
@@ -21,100 +19,44 @@ import { · 5 unmodified lines
2119 derivePhase,
2220 deriveTimelineEntries,
2321 deriveWorkLogEntries,
24− formatDuration,
2522 formatElapsed,
2623 formatTimestamp,
2724 readNativeApi,
2825 } from "../session-logic";
2926 import { useStore } from "../store";
3027 import ChatMarkdown from "./ChatMarkdown";
28+import { Button } from "./ui/button";
29+import {
30+ InputGroup,
31+ InputGroupAddon,
32+ InputGroupText,
33+ InputGroupTextarea,
34+} from "./ui/input-group";
35+import {
36+ Select,
37+ SelectItem,
38+ SelectPopup,
39+ SelectTrigger,
40+ SelectValue,
41+} from "./ui/select";
3142
3243 function formatMessageMeta(createdAt: string, duration: string | null): string {
3344 if (!duration) return formatTimestamp(createdAt);
3445 return `${formatTimestamp(createdAt)} • ${duration}`;
3546 }
3647
37−const FILE_MANAGER_LABEL = navigator.platform.includes("Mac")
38− ? "Finder"
39− : navigator.platform.includes("Win")
40− ? "Explorer"
41− : "Files";
42−
43−function editorLabel(editor: (typeof EDITORS)[number]): string {
44− return editor.command ? editor.label : FILE_MANAGER_LABEL;
48+function statusLabel(phase: string): string {
49+ if (phase === "running") return "Thinking / working";
50+ if (phase === "connecting") return "Connecting";
51+ if (phase === "ready") return "Ready";
52+ return "Disconnected";
4553 }
4654
47−const LAST_EDITOR_KEY = "codething:last-editor";
48−
4955 function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {
50− if (tone === "error") return "text-rose-300/50";
51− if (tone === "tool") return "text-[#8a8a8a]";
52− if (tone === "thinking") return "text-[#707070]";
53− return "text-[#606060]";
54−}
55−
56−interface PendingApprovalCard {
57− requestId: string;
58− requestKind: "command" | "file-change";
59− createdAt: string;
60− detail?: string;
61−}
62−
63−function asRecord(value: unknown): Record<string, unknown> | undefined {
64− if (!value || typeof value !== "object") return undefined;
65− return value as Record<string, unknown>;
66−}
67−
68−function asString(value: unknown): string | undefined {
69− return typeof value === "string" ? value : undefined;
70−}
71−
72−function approvalDetail(event: ProviderEvent): string | undefined {
73− const payload = asRecord(event.payload);
74− const command = asString(payload?.command);
75− if (command) return command;
76− return asString(payload?.reason);
77−}
78−
79−function derivePendingApprovals(
80− events: ProviderEvent[],
81−): PendingApprovalCard[] {
82− const pending = new Map<string, PendingApprovalCard>();
83− const ordered = [...events].reverse();
84−
85− for (const event of ordered) {
86− if (
87− event.method === "session/closed" ||
88− event.method === "session/exited"
89− ) {
90− pending.clear();
91− continue;
92− }
93−
94− const requestId =
95− event.requestId ?? asString(asRecord(event.payload)?.requestId);
96− if (!requestId) continue;
97−
98− if (
99− event.kind === "request" &&
100− (event.requestKind === "command" || event.requestKind === "file-change")
101− ) {
102− const detail = approvalDetail(event);
103− pending.set(requestId, {
104− requestId,
105− requestKind: event.requestKind,
106− createdAt: event.createdAt,
107− ...(detail ? { detail } : {}),
108− });
109− continue;
110− }
111−
112− if (event.method === "item/requestApproval/decision") {
113− pending.delete(requestId);
114− }
115− }
116−
117− return Array.from(pending.values());
56+ if (tone === "error") return "text-destructive/50";
57+ if (tone === "tool") return "text-muted-foreground";
58+ if (tone === "thinking") return "text-muted-foreground/70";
59+ return "text-muted-foreground/60";
11860 }
11961
12062 export default function ChatView() {
@@ -123,46 +65,30 @@ export default function ChatView() { · 2 unmodified lines
12365 const [prompt, setPrompt] = useState("");
12466 const [isSending, setIsSending] = useState(false);
12567 const [isConnecting, setIsConnecting] = useState(false);
126− const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
127− const [isEditorMenuOpen, setIsEditorMenuOpen] = useState(false);
128− const [lastEditor, setLastEditor] = useState<EditorId>(() => {
129− const stored = localStorage.getItem(LAST_EDITOR_KEY);
130− return EDITORS.some((e) => e.id === stored)
131− ? (stored as EditorId)
132− : EDITORS[0].id;
133− });
13468 const [selectedEffort, setSelectedEffort] =
13569 useState<string>(DEFAULT_REASONING);
136− const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false);
137− const [respondingRequestIds, setRespondingRequestIds] = useState<string[]>(
138− [],
139− );
14070 const [nowTick, setNowTick] = useState(() => Date.now());
14171 const messagesEndRef = useRef<HTMLDivElement>(null);
142− const textareaRef = useRef<HTMLTextAreaElement>(null);
143− const modelMenuRef = useRef<HTMLDivElement>(null);
144− const editorMenuRef = useRef<HTMLDivElement>(null);
14572
14673 const activeThread = state.threads.find((t) => t.id === state.activeThreadId);
147− const activeProject = state.projects.find((p) => p.id === activeThread?.projectId);
74+ const activeProject = state.projects.find(
75+ (p) => p.id === activeThread?.projectId,
76+ );
14877 const selectedModel = resolveModelSlug(
14978 activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL,
15079 );
15180 const phase = derivePhase(activeThread?.session ?? null);
15281 const isWorking = phase === "running" || isSending || isConnecting;
82+ const activeTurnId = activeThread?.session?.activeTurnId;
15383 const nowIso = new Date(nowTick).toISOString();
15484 const modelOptions = MODEL_OPTIONS;
15585 const workLogEntries = useMemo(
156− () => deriveWorkLogEntries(activeThread?.events ?? [], undefined),
157− [activeThread?.events],
158− );
159− const pendingApprovals = useMemo(
160− () => derivePendingApprovals(activeThread?.events ?? []),
161− [activeThread?.events],
86+ () => deriveWorkLogEntries(activeThread?.events ?? [], activeTurnId),
87+ [activeThread?.events, activeTurnId],
16288 );
16389 const assistantCompletionByItemId = useMemo(() => {
16490 const map = new Map<string, string>();
165− const ordered = [...(activeThread?.events ?? [])].toReversed();
91+ const ordered = [...(activeThread?.events ?? [])].reverse();
16692 for (const event of ordered) {
16793 if (event.method !== "item/completed") continue;
16894 if (!event.itemId) continue;
@@ -171,113 +97,27 @@ export default function ChatView() { · 2 unmodified lines
17197 return map;
17298 }, [activeThread?.events]);
17399 const timelineEntries = useMemo(
174− () => deriveTimelineEntries(activeThread?.messages ?? [], workLogEntries),
175− [activeThread?.messages, workLogEntries],
100+ () =>
101+ deriveTimelineEntries(
102+ activeThread?.messages ?? [],
103+ isWorking ? workLogEntries : [],
104+ ),
105+ [activeThread?.messages, isWorking, workLogEntries],
176106 );
177− const completionSummary = useMemo(() => {
178− if (!activeThread?.latestTurnStartedAt) return null;
179− if (!activeThread.latestTurnCompletedAt) return null;
180− if (workLogEntries.length === 0) return null;
181−
182− if (
183− typeof activeThread.latestTurnDurationMs === "number" &&
184− Number.isFinite(activeThread.latestTurnDurationMs) &&
185− activeThread.latestTurnDurationMs >= 0
186− ) {
187− return `Worked for ${formatDuration(activeThread.latestTurnDurationMs)}`;
188− }
189−
190− const elapsed = formatElapsed(
191− activeThread.latestTurnStartedAt,
192− activeThread.latestTurnCompletedAt,
193− );
194− return elapsed ? `Worked for ${elapsed}` : null;
195− }, [
196− activeThread?.latestTurnStartedAt,
197− activeThread?.latestTurnCompletedAt,
198− activeThread?.latestTurnDurationMs,
199− workLogEntries.length,
200− ]);
201− const completionDividerBeforeEntryId = useMemo(() => {
202− if (!activeThread?.latestTurnStartedAt) return null;
203− if (!activeThread.latestTurnCompletedAt) return null;
204− if (workLogEntries.length === 0) return null;
205−
206− const turnStartedAt = Date.parse(activeThread.latestTurnStartedAt);
207− if (Number.isNaN(turnStartedAt)) return null;
208−
209− const entry = timelineEntries.find((timelineEntry) => {
210− if (timelineEntry.kind !== "message") return false;
211− if (timelineEntry.message.role !== "assistant") return false;
212− const messageAt = Date.parse(timelineEntry.message.createdAt);
213− return !Number.isNaN(messageAt) && messageAt >= turnStartedAt;
214− });
215− return entry?.id ?? null;
216− }, [
217− activeThread?.latestTurnStartedAt,
218− activeThread?.latestTurnCompletedAt,
219− timelineEntries,
220− workLogEntries.length,
221− ]);
222− const runtimeSessionConfig =
223− state.runtimeMode === "full-access"
224− ? ({
225− approvalPolicy: "never",
226− sandboxMode: "danger-full-access",
227− } as const)
228− : ({
229− approvalPolicy: "on-request",
230− sandboxMode: "workspace-write",
231− } as const);
232−
233− const handleRuntimeModeChange = async (
234− mode: "approval-required" | "full-access",
235− ) => {
236− if (mode === state.runtimeMode) return;
237− dispatch({ type: "SET_RUNTIME_MODE", mode });
238− if (!api) return;
239−
240− const sessionIds = state.threads
241− .map((t) => t.session)
242− .filter(
243− (s): s is NonNullable<typeof s> =>
244− s !== null && s.status !== "closed",
245− )
246− .map((s) => s.sessionId);
247−
248− if (sessionIds.length === 0) return;
249−
250− setIsSwitchingRuntimeMode(true);
251− try {
252− await Promise.all(
253− sessionIds.map((id) =>
254− api.providers.stopSession({ sessionId: id }).catch(() => undefined),
255− ),
256− );
257− } finally {
258− setIsSwitchingRuntimeMode(false);
259− }
260− };
261107
262108 // Auto-scroll on new messages
263109 const messageCount = activeThread?.messages.length ?? 0;
264110 const workLogCount = workLogEntries.length;
111+ // biome-ignore lint/correctness/useExhaustiveDependencies: trigger on message count change
265112 useEffect(() => {
266113 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
267114 }, [messageCount]);
115+ // biome-ignore lint/correctness/useExhaustiveDependencies: auto-scroll while active work-log events stream in
268116 useEffect(() => {
269117 if (phase !== "running") return;
270118 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
271119 }, [phase, workLogCount]);
272120
273− // Auto-resize textarea
274− useEffect(() => {
275− const ta = textareaRef.current;
276− if (!ta) return;
277− ta.style.height = "auto";
278− ta.style.height = `${Math.min(ta.scrollHeight, 200)}px`;
279− }, [prompt]);
280−
281121 useEffect(() => {
282122 if (phase !== "running") return;
283123 const timer = window.setInterval(() => {
@@ -288,63 +128,6 @@ export default function ChatView() { · 4 unmodified lines
288128 };
289129 }, [phase]);
290130
291− useEffect(() => {
292− if (!isModelMenuOpen) return;
293−
294− const handleClickOutside = (event: MouseEvent) => {
295− if (!modelMenuRef.current) return;
296− if (event.target instanceof Node && !modelMenuRef.current.contains(event.target)) {
297− setIsModelMenuOpen(false);
298− }
299− };
300−
301− window.addEventListener("mousedown", handleClickOutside);
302− return () => {
303− window.removeEventListener("mousedown", handleClickOutside);
304− };
305− }, [isModelMenuOpen]);
306−
307− useEffect(() => {
308− if (!isEditorMenuOpen) return;
309−
310− const handleClickOutside = (event: MouseEvent) => {
311− if (!editorMenuRef.current) return;
312− if (
313− event.target instanceof Node &&
314− !editorMenuRef.current.contains(event.target)
315− ) {
316− setIsEditorMenuOpen(false);
317− }
318− };
319−
320− window.addEventListener("mousedown", handleClickOutside);
321− return () => {
322− window.removeEventListener("mousedown", handleClickOutside);
323− };
324− }, [isEditorMenuOpen]);
325−
326− // Cmd+O / Ctrl+O to open in last-used editor
327− useEffect(() => {
328− const handler = (e: globalThis.KeyboardEvent) => {
329− if (e.key === "o" && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
330− if (api && activeProject) {
331− e.preventDefault();
332− void api.shell.openInEditor(activeProject.cwd, lastEditor);
333− }
334− }
335− };
336− window.addEventListener("keydown", handler);
337− return () => window.removeEventListener("keydown", handler);
338− }, [api, activeProject, lastEditor]);
339−
340− const openInEditor = (editorId: EditorId) => {
341− if (!api || !activeProject) return;
342− void api.shell.openInEditor(activeProject.cwd, editorId);
343− setLastEditor(editorId);
344− localStorage.setItem(LAST_EDITOR_KEY, editorId);
345− setIsEditorMenuOpen(false);
346− };
347−
348131 const ensureSession = async (): Promise<string | null> => {
349132 if (!api || !activeThread || !activeProject) return null;
350133 if (activeThread.session && activeThread.session.status !== "closed") {
@@ -357,8 +140,6 @@ export default function ChatView() { · 6 unmodified lines
357140 provider: "codex",
358141 cwd: activeProject.cwd || undefined,
359142 model: selectedModel || undefined,
360− approvalPolicy: runtimeSessionConfig.approvalPolicy,
361− sandboxMode: runtimeSessionConfig.sandboxMode,
362143 });
363144 dispatch({
364145 type: "UPDATE_SESSION",
@@ -386,7 +167,8 @@ export default function ChatView() { · 21 unmodified lines
386167
387168 // Auto-title from first message
388169 if (activeThread.messages.length === 0) {
389− const title = trimmed.length > 50 ? `${trimmed.slice(0, 50)}...` : trimmed;
170+ const title =
171+ trimmed.length > 50 ? `${trimmed.slice(0, 50)}...` : trimmed;
390172 dispatch({
391173 type: "SET_THREAD_TITLE",
392174 threadId: activeThread.id,
@@ -437,37 +219,6 @@ export default function ChatView() { · 44 unmodified lines
437219 });
438220 };
439221
440− const onRespondToApproval = async (
441− requestId: string,
442− decision: ProviderApprovalDecision,
443− ) => {
444− if (!api || !activeThread?.session) return;
445−
446− setRespondingRequestIds((existing) =>
447− existing.includes(requestId) ? existing : [...existing, requestId],
448− );
449− try {
450− await api.providers.respondToRequest({
451− sessionId: activeThread.session.sessionId,
452− requestId,
453− decision,
454− });
455− } catch (err) {
456− dispatch({
457− type: "SET_ERROR",
458− threadId: activeThread.id,
459− error:
460− err instanceof Error
461− ? err.message
462− : "Failed to submit approval decision.",
463− });
464− } finally {
465− setRespondingRequestIds((existing) =>
466− existing.filter((id) => id !== requestId),
467− );
468− }
469− };
470−
471222 const onModelSelect = (model: string) => {
472223 if (!activeThread) return;
473224 dispatch({
@@ -475,7 +226,6 @@ export default function ChatView() { · 1 unmodified line
475226 threadId: activeThread.id,
476227 model: resolveModelSlug(model),
477228 });
478− setIsModelMenuOpen(false);
479229 };
480230
481231 const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
@@ -488,11 +238,13 @@ export default function ChatView() { · 6 unmodified lines
488238 // Empty state: no active thread
489239 if (!activeThread) {
490240 return (
491− <div className="flex flex-1 flex-col bg-[#0c0c0c] text-[#a0a0a0]/40">
241+ <div className="flex flex-1 flex-col bg-background text-muted-foreground/60">
492242 <div className="drag-region h-[52px] shrink-0" />
493243 <div className="flex flex-1 items-center justify-center">
494244 <div className="text-center">
495− <p className="text-sm">Select a thread or create a new one to get started.</p>
245+ <p className="text-sm">
246+ Select a thread or create a new one to get started.
247+ </p>
496248 </div>
497249 </div>
498250 </div>
@@ -500,55 +252,61 @@ export default function ChatView() { · 1 unmodified line
500252 }
501253
502254 return (
503− <div className="flex flex-1 flex-col bg-[#0c0c0c]">
255+ <div className="flex flex-1 flex-col bg-background">
504256 {/* Top bar */}
505− <header className="drag-region flex items-center justify-between border-b border-white/8 px-5 pt-[28px] pb-3">
257+ <header className="drag-region flex items-center justify-between border-b px-5 pt-[28px] pb-3">
506258 <div className="flex items-center gap-3">
507− <h2 className="text-sm font-medium text-[#e0e0e0]">
259+ <h2 className="text-sm font-medium text-foreground">
508260 {activeThread.title}
509261 </h2>
262+ {activeProject && (
263+ <span className="rounded-full bg-accent px-2 py-0.5 text-[10px] text-muted-foreground/70">
264+ {activeProject.name}
265+ </span>
266+ )}
510267 </div>
511268 <div className="flex items-center gap-3">
512− {/* Open in editor */}
513− {activeProject && (
514− <div className="relative" ref={editorMenuRef}>
515− <button
516− type="button"
517− className="rounded-md px-2 py-1 text-[10px] text-[#a0a0a0]/40 transition-colors duration-150 hover:text-[#a0a0a0]/60"
518− onClick={() => setIsEditorMenuOpen((v) => !v)}
519− >
520− Open in&hellip;
521− </button>
522− {isEditorMenuOpen && (
523− <div className="absolute right-0 top-full z-50 mt-1 min-w-[120px] rounded-md border border-white/[0.08] bg-[#1b1b1d] py-1 shadow-xl">
524− {EDITORS.map((editor) => (
525− <button
526− key={editor.id}
527− type="button"
528− className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[11px] text-[#e0e0e0] hover:bg-white/[0.06]"
529− onClick={() => openInEditor(editor.id)}
530− >
531− {editorLabel(editor)}
532− {editor.id === lastEditor && (
533− <kbd className="ml-auto text-[9px] text-[#a0a0a0]/40">
534− {navigator.platform.includes("Mac")
535− ? "\u2318O"
536− : "Ctrl+O"}
537− </kbd>
538− )}
539− </button>
540− ))}
541− </div>
269+ {/* Status indicator */}
270+ <div
271+ className={`inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-[10px] ${
272+ phase === "running"
273+ ? "border-sky-400/35 bg-sky-500/8 text-sky-700 dark:text-sky-100"
274+ : phase === "connecting"
275+ ? "border-amber-400/35 bg-amber-500/8 text-amber-700 dark:text-amber-100"
276+ : phase === "ready"
277+ ? "border-emerald-400/35 bg-emerald-500/8 text-emerald-700 dark:text-emerald-100"
278+ : "border-border bg-secondary text-muted-foreground"
279+ }`}
280+ >
281+ <span className="relative inline-flex h-2.5 w-2.5">
282+ {(phase === "running" || phase === "connecting") && (
283+ <span
284+ className={`absolute inline-flex h-full w-full animate-ping rounded-full ${
285+ phase === "running" ? "bg-sky-300/70" : "bg-amber-300/70"
286+ }`}
287+ />
542288 )}
543− </div>
544− )}
289+ <span
290+ className={`relative inline-flex h-2.5 w-2.5 rounded-full ${
291+ phase === "running"
292+ ? "bg-sky-200"
293+ : phase === "connecting"
294+ ? "bg-amber-200"
295+ : phase === "ready"
296+ ? "bg-emerald-200"
297+ : "bg-muted-foreground/40"
298+ }`}
299+ />
300+ </span>
301+ <span>{statusLabel(phase)}</span>
302+ </div>
545303 {/* Diff toggle */}
546304 <button
547305 type="button"
548306 className={`rounded-md px-2 py-1 text-[10px] transition-colors duration-150 ${
549307 state.diffOpen
550− ? "bg-white/10 text-white"
551− : "text-[#a0a0a0]/40 hover:text-[#a0a0a0]/60"
308+ ? "bg-accent text-accent-foreground"
309+ : "text-muted-foreground/60 hover:text-muted-foreground"
552310 }`}
553311 onClick={() => dispatch({ type: "TOGGLE_DIFF" })}
554312 >
@@ -559,192 +317,105 @@ export default function ChatView() { · 4 unmodified lines
559317
560318 {/* Error banner */}
561319 {activeThread.error && (
562− <div className="mx-4 mt-3 rounded-lg border border-rose-400/20 bg-rose-900/20 px-3 py-2 text-xs text-rose-200">
320+ <div className="mx-4 mt-3 rounded-lg border border-destructive/20 bg-destructive/10 px-3 py-2 text-xs text-destructive-foreground">
563321 {activeThread.error}
564322 </div>
565323 )}
566324
567− {pendingApprovals.length > 0 && (
568− <div className="mx-4 mt-3 space-y-2">
569− {pendingApprovals.map((approval) => {
570− const isResponding = respondingRequestIds.includes(
571− approval.requestId,
572− );
573− return (
574− <div
575− key={approval.requestId}
576− className="rounded-lg border border-amber-300/20 bg-amber-500/[0.07] px-3 py-2"
577− >
578− <p className="text-xs font-medium text-amber-100">
579− {approval.requestKind === "command"
580− ? "Command approval requested"
581− : "File-change approval requested"}
582− </p>
583− {approval.detail && (
584− <p
585− className="mt-1 truncate font-mono text-[11px] text-amber-100/75"
586− title={approval.detail}
587− >
588− {approval.detail}
589− </p>
590− )}
591− <div className="mt-2 flex flex-wrap gap-1.5">
592− <button
593− type="button"
594− className="rounded-md border border-white/[0.15] bg-white/[0.08] px-2 py-1 text-[11px] text-[#e8e8e8] transition-colors duration-150 hover:bg-white/[0.13] disabled:cursor-not-allowed disabled:opacity-50"
595− disabled={isResponding}
596− onClick={() =>
597− void onRespondToApproval(approval.requestId, "accept")
598− }
599− >
600− Approve once
601− </button>
602− <button
603− type="button"
604− className="rounded-md border border-sky-300/30 bg-sky-500/[0.15] px-2 py-1 text-[11px] text-sky-100 transition-colors duration-150 hover:bg-sky-500/[0.22] disabled:cursor-not-allowed disabled:opacity-50"
605− disabled={isResponding}
606− onClick={() =>
607− void onRespondToApproval(
608− approval.requestId,
609− "acceptForSession",
610− )
611− }
612− >
613− Always allow this session
614− </button>
615− <button
616− type="button"
617− className="rounded-md border border-white/[0.15] px-2 py-1 text-[11px] text-[#d8d8d8] transition-colors duration-150 hover:bg-white/[0.06] disabled:cursor-not-allowed disabled:opacity-50"
618− disabled={isResponding}
619− onClick={() =>
620− void onRespondToApproval(approval.requestId, "decline")
621− }
622− >
623− Decline
624− </button>
625− <button
626− type="button"
627− className="rounded-md border border-rose-300/30 bg-rose-500/[0.12] px-2 py-1 text-[11px] text-rose-100 transition-colors duration-150 hover:bg-rose-500/[0.2] disabled:cursor-not-allowed disabled:opacity-50"
628− disabled={isResponding}
629− onClick={() =>
630− void onRespondToApproval(approval.requestId, "cancel")
631− }
632− >
633− Cancel turn
634− </button>
635− </div>
636− </div>
637− );
638− })}
639− </div>
640− )}
641−
642325 {/* Messages */}
643326 <div className="flex-1 overflow-y-auto px-5 py-4">
644327 {activeThread.messages.length === 0 && !isWorking ? (
645328 <div className="flex h-full items-center justify-center">
646− <p className="text-sm text-[#a0a0a0]/30">Send a message to start the conversation.</p>
329+ <p className="text-sm text-muted-foreground/40">
330+ Send a message to start the conversation.
331+ </p>
647332 </div>
648333 ) : (
649334 <div className="mx-auto max-w-3xl space-y-4">
650− {timelineEntries.map((timelineEntry, index) => (
651− <Fragment key={timelineEntry.id}>
652− {timelineEntry.kind === "message" &&
653− timelineEntry.message.role === "assistant" &&
654− (completionDividerBeforeEntryId === timelineEntry.id ||
655− timelineEntries[index - 1]?.kind === "work") && (
656− <div className="my-3 flex items-center gap-3">
657− <span className="h-px flex-1 bg-white/[0.1]" />
658− <span className="rounded-full border border-white/[0.12] bg-[#121212] px-2.5 py-1 text-[10px] uppercase tracking-[0.14em] text-[#9a9a9a]/80">
659− {completionSummary
660− ? `Response • ${completionSummary}`
661− : "Response"}
662− </span>
663− <span className="h-px flex-1 bg-white/[0.1]" />
335+ {timelineEntries.map((timelineEntry) =>
336+ timelineEntry.kind === "work" ? (
337+ <div
338+ key={timelineEntry.id}
339+ className="border-l-2 border-border/60 pl-4 py-1"
340+ >
341+ <p
342+ className={`py-[2px] text-[12px] leading-relaxed ${workToneClass(timelineEntry.entry.tone)}`}
343+ >
344+ {timelineEntry.entry.detail ? (
345+ <>
346+ {timelineEntry.entry.label}
347+ <span className="ml-1.5 font-mono text-[11px] opacity-60">
348+ {timelineEntry.entry.detail}
349+ </span>
350+ </>
351+ ) : (
352+ timelineEntry.entry.label
353+ )}
354+ </p>
355+ </div>
356+ ) : (
357+ <div key={timelineEntry.id}>
358+ {timelineEntry.message.role === "user" ? (
359+ <div className="flex justify-end">
360+ <div className="max-w-[80%] rounded-2xl rounded-br-sm border border-border bg-accent px-4 py-3">
361+ <pre className="whitespace-pre-wrap wrap-break-word font-mono text-sm leading-relaxed text-foreground">
362+ {timelineEntry.message.text}
363+ </pre>
364+ <p className="mt-1.5 text-right text-[10px] text-muted-foreground/40">
365+ {formatTimestamp(timelineEntry.message.createdAt)}
366+ </p>
367+ </div>
664368 </div>
665− )}
666− {timelineEntry.kind === "work" ? (
667− <div className="flex items-start gap-2 py-0.5 pl-1.5">
668− <span className="mt-[7px] h-1.5 w-1.5 shrink-0 rounded-full bg-white/[0.18]" />
669− <p
670− className={`py-[2px] text-[11px] leading-relaxed ${workToneClass(timelineEntry.entry.tone)}`}
671− >
672− {timelineEntry.entry.detail ? (
673− <>
674− {timelineEntry.entry.label}
675− <span
676− className="ml-1.5 inline-block max-w-[70ch] truncate align-bottom font-mono text-[11px] opacity-60"
677− title={timelineEntry.entry.detail}
678− >
679− {timelineEntry.entry.detail}
369+ ) : (
370+ <div className="border-l-2 border-border pl-4">
371+ <ChatMarkdown
372+ text={
373+ timelineEntry.message.text ||
374+ (timelineEntry.message.streaming
375+ ? ""
376+ : "(empty response)")
377+ }
378+ />
379+ {timelineEntry.message.streaming && (
380+ <div className="pt-1.5">
381+ <span className="inline-flex items-center gap-2 rounded-full border border-sky-400/25 bg-sky-500/8 px-2 py-0.5 text-[10px] text-sky-700/90 dark:text-sky-100/90">
382+ <span className="inline-flex gap-1">
383+ <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-500/80 dark:bg-sky-100/80" />
384+ <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-500/80 dark:bg-sky-100/80 [animation-delay:150ms]" />
385+ <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-500/80 dark:bg-sky-100/80 [animation-delay:300ms]" />
386+ </span>
387+ <span>Thinking</span>
680388 </span>
681− </>
682− ) : (
683− timelineEntry.entry.label
389+ </div>
684390 )}
685− </p>
686− </div>
687− ) : timelineEntry.message.role === "user" ? (
688− <div className="flex justify-end">
689− <div className="max-w-[80%] rounded-2xl rounded-br-sm border border-white/[0.08] bg-white/[0.05] px-4 py-3">
690− <pre className="whitespace-pre-wrap break-words font-mono text-sm leading-relaxed text-[#e0e0e0]">
691− {timelineEntry.message.text}
692− </pre>
693− <p className="mt-1.5 text-right text-[10px] text-[#a0a0a0]/30">
694− {formatTimestamp(timelineEntry.message.createdAt)}
391+ <p className="mt-1.5 text-[10px] text-muted-foreground/40">
392+ {formatMessageMeta(
393+ timelineEntry.message.createdAt,
394+ timelineEntry.message.streaming
395+ ? formatElapsed(
396+ timelineEntry.message.createdAt,
397+ nowIso,
398+ )
399+ : formatElapsed(
400+ timelineEntry.message.createdAt,
401+ assistantCompletionByItemId.get(
402+ timelineEntry.message.id,
403+ ),
404+ ),
405+ )}
695406 </p>
696407 </div>
697− </div>
698− ) : (
699− <div className="px-1 py-0.5">
700− <ChatMarkdown
701− text={
702− timelineEntry.message.text ||
703− (timelineEntry.message.streaming
704− ? ""
705− : "(empty response)")
706− }
707− />
708− {timelineEntry.message.streaming && (
709− <div className="pt-1.5">
710− <span className="inline-flex items-center gap-2 rounded-full border border-sky-400/25 bg-sky-500/[0.08] px-2 py-0.5 text-[10px] text-sky-100/90">
711− <span className="inline-flex gap-1">
712− <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-100/80" />
713− <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-100/80 [animation-delay:150ms]" />
714− <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-100/80 [animation-delay:300ms]" />
715− </span>
716− <span>Thinking</span>
717− </span>
718− </div>
719− )}
720− <p className="mt-1.5 text-[10px] text-[#a0a0a0]/30">
721− {formatMessageMeta(
722− timelineEntry.message.createdAt,
723− timelineEntry.message.streaming
724− ? formatElapsed(
725− timelineEntry.message.createdAt,
726− nowIso,
727− )
728− : formatElapsed(
729− timelineEntry.message.createdAt,
730− assistantCompletionByItemId.get(
731− timelineEntry.message.id,
732− ),
733− ),
734− )}
735− </p>
736− </div>
737− )}
738− </Fragment>
739− ))}
408+ )}
409+ </div>
410+ ),
411+ )}
740412 {isWorking && (
741− <div className="flex items-center gap-2 py-0.5 pl-1.5">
742− <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-white/[0.18]" />
413+ <div className="border-l-2 border-border/60 pl-4 py-1">
743414 <div className="flex items-center pt-1">
744415 <span className="inline-flex items-center gap-[3px]">
745− <span className="h-1 w-1 rounded-full bg-white/20 animate-pulse" />
746− <span className="h-1 w-1 rounded-full bg-white/20 animate-pulse [animation-delay:200ms]" />
747− <span className="h-1 w-1 rounded-full bg-white/20 animate-pulse [animation-delay:400ms]" />
416+ <span className="h-1 w-1 rounded-full bg-foreground/20 animate-pulse" />
417+ <span className="h-1 w-1 rounded-full bg-foreground/20 animate-pulse [animation-delay:200ms]" />
418+ <span className="h-1 w-1 rounded-full bg-foreground/20 animate-pulse [animation-delay:400ms]" />
748419 </span>
749420 </div>
750421 </div>
@@ -757,271 +428,101 @@ export default function ChatView() { · 6 unmodified lines
757428 {/* Input bar */}
758429 <div className="px-5 pb-4 pt-2">
759430 <form onSubmit={onSend} className="mx-auto max-w-3xl">
760− <div className="group rounded-[20px] border border-white/8 bg-[#141416] transition-colors duration-200 focus-within:border-white/16">
761− {/* Textarea area */}
762− <div className="px-4 pt-4 pb-2">
763− <textarea
764− ref={textareaRef}
765− className="w-full resize-none bg-transparent text-[14px] leading-relaxed text-[#e0e0e0] placeholder:text-[#a0a0a0]/35 focus:outline-none"
766− rows={2}
767− value={prompt}
768− onChange={(e) => setPrompt(e.target.value)}
769− onKeyDown={onKeyDown}
770− placeholder={
771− phase === "disconnected" ? "Ask for follow-up changes" : "Ask anything..."
772− }
773− disabled={isSending || isConnecting}
774− />
775− </div>
776−
777− {/* Bottom toolbar */}
778− <div className="flex items-center justify-between px-3 pb-3">
779− <div className="flex items-center gap-1">
780− {/* Model picker */}
781− <div className="relative" ref={modelMenuRef}>
782− <button
783− type="button"
784− className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/6 hover:text-[#d0d0d0]"
785− onClick={() => setIsModelMenuOpen((open) => !open)}
786− >
787− <span className="max-w-[180px] truncate">{selectedModel}</span>
788− <svg
789− width="10"
790− height="10"
791− viewBox="0 0 10 10"
792− fill="none"
793− className="opacity-50"
794− aria-hidden="true"
795− >
796− <path
797− d="M2.5 4L5 6.5L7.5 4"
798− stroke="currentColor"
799− strokeWidth="1.2"
800− strokeLinecap="round"
801− strokeLinejoin="round"
802− />
803− </svg>
804− </button>
805− {isModelMenuOpen && (
806− <div className="absolute bottom-full left-0 z-20 mb-2 w-[320px] rounded-2xl border border-white/10 bg-[#1b1b1d]/95 p-2 shadow-[0_16px_40px_rgba(0,0,0,0.55)] backdrop-blur">
807− <p className="px-2 py-1 text-[11px] text-[#a0a0a0]/70">Select model</p>
808− <div className="max-h-72 overflow-y-auto">
809− {modelOptions.map((model) => {
810− const isSelected = model === selectedModel;
811− return (
812− <button
813− key={model}
814− type="button"
815− className={`mb-0.5 flex w-full items-center justify-between gap-2 rounded-xl px-2 py-2 text-left font-mono text-sm transition-colors duration-150 ${
816− isSelected
817− ? "bg-white/8 text-white"
818− : "text-[#d4d4d4] hover:bg-white/5"
819− }`}
820− onClick={() => onModelSelect(model)}
821− >
822− <span className="truncate">{model}</span>
823− <span
824− className={`pt-0.5 text-sm ${
825− isSelected ? "text-white" : "text-transparent"
826− }`}
827− >
828− ✓
829− </span>
830− </button>
831− );
832− })}
833− </div>
834− </div>
835− )}
836− </div>
837−
838− {/* Divider */}
839− <div className="mx-0.5 h-4 w-px bg-white/8" />
431+ <InputGroup className="rounded-[20px] before:rounded-[19px] **:[textarea]:max-h-[200px]">
432+ <InputGroupTextarea
433+ value={prompt}
434+ onChange={(e) => setPrompt(e.target.value)}
435+ onKeyDown={onKeyDown}
436+ placeholder={
437+ phase === "disconnected"
438+ ? "Ask for follow-up changes"
439+ : "Ask anything..."
440+ }
441+ disabled={isSending || isConnecting}
442+ rows={2}
443+ className="font-sans!"
444+ />
445+ <InputGroupAddon align="block-end">
446+ {/* Model picker */}
447+ <Select
448+ value={selectedModel}
449+ onValueChange={(val) => val && onModelSelect(val)}
450+ >
451+ <SelectTrigger
452+ size="sm"
453+ className="w-auto min-w-0 border-0 shadow-none bg-transparent dark:bg-transparent before:hidden data-[popup-open]:bg-accent dark:data-[popup-open]:bg-accent"
454+ >
455+ <SelectValue />
456+ </SelectTrigger>
457+ <SelectPopup side="top" align="start">
458+ {modelOptions.map((model) => (
459+ <SelectItem key={model} value={model}>
460+ {model}
461+ </SelectItem>
462+ ))}
463+ </SelectPopup>
464+ </Select>
840465
841− {/* Reasoning effort */}
842− <label
843− className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/6 hover:text-[#d0d0d0]"
844− htmlFor="reasoning-effort"
466+ {/* Reasoning effort */}
467+ <Select
468+ value={selectedEffort}
469+ onValueChange={(val) => val && setSelectedEffort(val)}
470+ >
471+ <SelectTrigger
472+ size="sm"
473+ className="w-auto min-w-0 border-0 shadow-none bg-transparent dark:bg-transparent before:hidden data-[popup-open]:bg-accent dark:data-[popup-open]:bg-accent capitalize"
845474 >
846− <span>{selectedEffort.charAt(0).toUpperCase() + selectedEffort.slice(1)}</span>
847− <select
848− id="reasoning-effort"
849− className="absolute opacity-0 w-0 h-0"
850− value={selectedEffort}
851− onChange={(event) => setSelectedEffort(event.target.value)}
852− >
853− {REASONING_OPTIONS.map((effort) => (
854− <option key={effort} value={effort} className="bg-[#1b1b1d]">
855− {effort}
856− {effort === DEFAULT_REASONING ? " (default)" : ""}
857− </option>
858− ))}
859− </select>
860− <svg
861− width="10"
862− height="10"
863− viewBox="0 0 10 10"
864− fill="none"
865− className="opacity-50"
866− aria-hidden="true"
867− >
868− <path
869− d="M2.5 4L5 6.5L7.5 4"
870− stroke="currentColor"
871− strokeWidth="1.2"
872− strokeLinecap="round"
873− strokeLinejoin="round"
874− />
875− </svg>
876− </label>
475+ <SelectValue />
476+ </SelectTrigger>
477+ <SelectPopup side="top" align="start">
478+ {REASONING_OPTIONS.map((effort) => (
479+ <SelectItem key={effort} value={effort}>
480+ {effort.charAt(0).toUpperCase() + effort.slice(1)}
481+ {effort === DEFAULT_REASONING ? " (default)" : ""}
482+ </SelectItem>
483+ ))}
484+ </SelectPopup>
485+ </Select>
877486
878− {/* Divider */}
879− <div className="mx-0.5 h-4 w-px bg-white/[0.08]" />
487+ {activeProject && (
488+ <InputGroupText className="ml-auto text-muted-foreground/40">
489+ {activeProject.name}
490+ </InputGroupText>
491+ )}
880492
881− {/* Runtime mode toggle */}
882− <button
883− type="button"
884− className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/[0.06] hover:text-[#d0d0d0]"
885− disabled={isSwitchingRuntimeMode}
886− onClick={() =>
887− void handleRuntimeModeChange(
888− state.runtimeMode === "full-access"
889− ? "approval-required"
890− : "full-access",
891− )
892− }
893− title={
894− state.runtimeMode === "full-access"
895− ? "Full access — click to require approvals"
896− : "Approval required — click for full access"
493+ {phase === "running" ? (
494+ <Button
495+ variant="destructive"
496+ size="icon-sm"
497+ className="rounded-full"
498+ onClick={() => void onInterrupt()}
499+ aria-label="Stop generation"
500+ >
501+ <SquareIcon className="size-3" />
502+ </Button>
503+ ) : (
504+ <Button
505+ type="submit"
506+ size="icon-sm"
507+ className="rounded-full"
508+ disabled={isSending || isConnecting || !prompt.trim()}
509+ aria-label={
510+ isConnecting
511+ ? "Connecting"
512+ : isSending
513+ ? "Sending"
514+ : "Send message"
897515 }
898516 >
899− {state.runtimeMode === "full-access" ? (
900− <svg
901− width="14"
902− height="14"
903− viewBox="0 0 14 14"
904− fill="none"
905− aria-hidden="true"
906− >
907− <rect
908− x="2"
909− y="5.5"
910− width="10"
911− height="7"
912− rx="1.5"
913− stroke="currentColor"
914− strokeWidth="1.3"
915− />
916− <path
917− d="M9.5 5.5V4a2.5 2.5 0 0 0-5 0"
918− stroke="currentColor"
919− strokeWidth="1.3"
920− strokeLinecap="round"
921− />
922− </svg>
517+ {isConnecting || isSending ? (
518+ <LoaderCircleIcon className="animate-spin" />
923519 ) : (
924− <svg
925− width="14"
926− height="14"
927− viewBox="0 0 14 14"
928− fill="none"
929− aria-hidden="true"
930− >
931− <rect
932− x="2"
933− y="5.5"
934− width="10"
935− height="7"
936− rx="1.5"
937− stroke="currentColor"
938− strokeWidth="1.3"
939− />
940− <path
941− d="M4.5 5.5V4a2.5 2.5 0 0 1 5 0v1.5"
942− stroke="currentColor"
943− strokeWidth="1.3"
944− strokeLinecap="round"
945− />
946− </svg>
520+ <ArrowUpIcon />
947521 )}
948− <span>
949− {state.runtimeMode === "full-access"
950− ? "Full access"
951− : "Supervised"}
952− </span>
953− </button>
954− </div>
955−
956− {/* Right side: send / stop button */}
957− <div className="flex items-center gap-2">
958− {phase === "running" ? (
959− <button
960− type="button"
961− className="flex h-8 w-8 items-center justify-center rounded-full bg-rose-500/90 text-white transition-all duration-150 hover:bg-rose-500 hover:scale-105"
962− onClick={() => void onInterrupt()}
963− aria-label="Stop generation"
964− >
965− <svg
966− width="12"
967− height="12"
968− viewBox="0 0 12 12"
969− fill="currentColor"
970− aria-hidden="true"
971− >
972− <rect x="2" y="2" width="8" height="8" rx="1.5" />
973− </svg>
974− </button>
975− ) : (
976− <button
977− type="submit"
978− className="flex h-8 w-8 items-center justify-center rounded-full bg-white/90 text-[#0c0c0c] transition-all duration-150 hover:bg-white hover:scale-105 disabled:opacity-30 disabled:hover:scale-100"
979− disabled={isSending || isConnecting || !prompt.trim()}
980− aria-label={
981− isConnecting ? "Connecting" : isSending ? "Sending" : "Send message"
982− }
983− >
984− {isConnecting || isSending ? (
985− <svg
986− width="14"
987− height="14"
988− viewBox="0 0 14 14"
989− fill="none"
990− className="animate-spin"
991− aria-hidden="true"
992− >
993− <circle
994− cx="7"
995− cy="7"
996− r="5.5"
997− stroke="currentColor"
998− strokeWidth="1.5"
999− strokeLinecap="round"
1000− strokeDasharray="20 12"
1001− />
1002− </svg>
1003− ) : (
1004− <svg
1005− width="14"
1006− height="14"
1007− viewBox="0 0 14 14"
1008− fill="none"
1009− aria-hidden="true"
1010− >
1011− <path
1012− d="M7 11.5V2.5M7 2.5L3 6.5M7 2.5L11 6.5"
1013− stroke="currentColor"
1014− strokeWidth="1.8"
1015− strokeLinecap="round"
1016− strokeLinejoin="round"
1017− />
1018− </svg>
1019− )}
1020− </button>
1021− )}
1022− </div>
1023− </div>
1024− </div>
522+ </Button>
523+ )}
524+ </InputGroupAddon>
525+ </InputGroup>
1025526 </form>
1026527 </div>
1027528 </div>
apps/renderer/src/components/DiffPanel.tsx+18−10
@@ -4,13 +4,15 @@ export default function DiffPanel() { · 3 unmodified lines
44 const { dispatch } = useStore();
55
66 return (
7− <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-white/[0.08] bg-[#141414]">
7+ <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-border bg-card">
88 {/* Header */}
9− <div className="drag-region flex items-center justify-between border-b border-white/[0.08] px-4 pt-[28px] pb-3">
10− <h3 className="text-xs font-medium text-[#e0e0e0]">Uncommitted changes</h3>
9+ <div className="drag-region flex items-center justify-between border-b border-border px-4 pt-[28px] pb-3">
10+ <h3 className="text-xs font-medium text-foreground">
11+ Uncommitted changes
12+ </h3>
1113 <button
1214 type="button"
13− className="text-[#a0a0a0]/40 transition-colors duration-150 hover:text-[#a0a0a0]/70"
15+ className="text-muted-foreground/60 transition-colors duration-150 hover:text-muted-foreground"
1416 onClick={() => dispatch({ type: "TOGGLE_DIFF" })}
1517 >
1618 <span className="text-sm">&times;</span>
@@ -20,30 +22,36 @@ export default function DiffPanel() { · 3 unmodified lines
2022 {/* Placeholder content */}
2123 <div className="flex-1 overflow-y-auto p-4">
2224 <div className="space-y-3">
23− <div className="rounded-lg border border-white/[0.06] bg-white/[0.02] p-3">
25+ <div className="rounded-lg border border-border/80 bg-muted p-3">
2426 <div className="mb-2 flex items-center gap-2">
2527 <span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[9px] font-medium text-emerald-400">
2628 M
2729 </span>
28− <span className="font-mono text-xs text-[#a0a0a0]/60">src/App.tsx</span>
30+ <span className="font-mono text-xs text-muted-foreground/80">
31+ src/App.tsx
32+ </span>
2933 </div>
3034 <div className="space-y-0.5 font-mono text-[11px]">
3135 <div className="text-red-400/60">- const oldValue = "hello";</div>
32− <div className="text-emerald-400/60">+ const newValue = "world";</div>
36+ <div className="text-emerald-400/60">
37+ + const newValue = "world";
38+ </div>
3339 </div>
3440 </div>
3541
36− <div className="rounded-lg border border-white/[0.06] bg-white/[0.02] p-3">
42+ <div className="rounded-lg border border-border/80 bg-muted p-3">
3743 <div className="flex items-center gap-2">
3844 <span className="rounded bg-blue-500/20 px-1.5 py-0.5 text-[9px] font-medium text-blue-400">
3945 A
4046 </span>
41− <span className="font-mono text-xs text-[#a0a0a0]/60">src/utils.ts</span>
47+ <span className="font-mono text-xs text-muted-foreground/80">
48+ src/utils.ts
49+ </span>
4250 </div>
4351 </div>
4452 </div>
4553
46− <p className="mt-6 text-center text-[10px] text-[#a0a0a0]/25">
54+ <p className="mt-6 text-center text-[10px] text-muted-foreground/30">
4755 Diff integration coming soon
4856 </p>
4957 </div>
apps/renderer/src/components/Sidebar.tsx+95−119
@@ -1,10 +1,11 @@
1−import { useCallback, useEffect, useMemo, useState } from "react";
2−import { DEFAULT_MODEL } from "../model-logic";
1+import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
2+import { useMemo, useState } from "react";
3+import { useTheme } from "../hooks/useTheme";
4+import { DEFAULT_MODEL, MODEL_OPTIONS, resolveModelSlug } from "../model-logic";
35 import { readNativeApi } from "../session-logic";
46 import { useStore } from "../store";
57 import type { Project } from "../types";
68
7−
89 function formatRelativeTime(iso: string): string {
910 const diff = Date.now() - new Date(iso).getTime();
1011 const minutes = Math.floor(diff / 60_000);
@@ -15,58 +16,53 @@ function formatRelativeTime(iso: string): string { · 4 unmodified lines
1516 return `${Math.floor(hours / 24)}d ago`;
1617 }
1718
18−function threadStatusLabel(
19− status: "connecting" | "ready" | "running" | "error" | "closed" | undefined,
20−): "Working" | "Connecting" | null {
21− if (status === "running") return "Working";
22− if (status === "connecting") return "Connecting";
23− return null;
24−}
19+const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const;
2520
2621 export default function Sidebar() {
2722 const { state, dispatch } = useStore();
2823 const api = useMemo(() => readNativeApi(), []);
24+ const { theme, setTheme } = useTheme();
2925 const [addingProject, setAddingProject] = useState(false);
3026 const [newCwd, setNewCwd] = useState("");
27+ const [newModel, setNewModel] = useState(DEFAULT_MODEL);
3128 const [isPickingFolder, setIsPickingFolder] = useState(false);
3229
3330 const handleAddProject = () => {
3431 const cwd = newCwd.trim();
3532 if (!cwd) return;
3633 const name = cwd.split("/").filter(Boolean).pop() ?? "project";
34+ const normalizedModel = resolveModelSlug(newModel);
3735 const project: Project = {
3836 id: crypto.randomUUID(),
3937 name,
4038 cwd,
41− model: DEFAULT_MODEL,
39+ model: normalizedModel,
4240 expanded: true,
4341 };
4442 dispatch({ type: "ADD_PROJECT", project });
4543 setNewCwd("");
44+ setNewModel(DEFAULT_MODEL);
4645 setAddingProject(false);
4746 };
4847
49− const handleNewThread = useCallback(
50− (projectId: string) => {
51− dispatch({
52− type: "ADD_THREAD",
53− thread: {
54− id: crypto.randomUUID(),
55− projectId,
56− title: "New thread",
57− model:
58− state.projects.find((p) => p.id === projectId)?.model ??
59− DEFAULT_MODEL,
60− session: null,
61− messages: [],
62− events: [],
63− error: null,
64− createdAt: new Date().toISOString(),
65− },
66− });
67− },
68− [dispatch, state.projects],
69− );
48+ const handleNewThread = (projectId: string) => {
49+ dispatch({
50+ type: "ADD_THREAD",
51+ thread: {
52+ id: crypto.randomUUID(),
53+ projectId,
54+ title: "New thread",
55+ model:
56+ state.projects.find((p) => p.id === projectId)?.model ??
57+ DEFAULT_MODEL,
58+ session: null,
59+ messages: [],
60+ events: [],
61+ error: null,
62+ createdAt: new Date().toISOString(),
63+ },
64+ });
65+ };
7066
7167 const handlePickFolder = async () => {
7268 if (!api || isPickingFolder) return;
@@ -80,62 +76,40 @@ export default function Sidebar() { · 7 unmodified lines
8076 }
8177 };
8278
83− useEffect(() => {
84− const onWindowKeyDown = (event: KeyboardEvent) => {
85− const isNewThreadShortcut =
86− event.metaKey &&
87− event.shiftKey &&
88− !event.ctrlKey &&
89− !event.altKey &&
90− event.key.toLowerCase() === "o";
91− if (!isNewThreadShortcut) return;
92−
93− const target = event.target;
94− if (target instanceof HTMLElement) {
95− const tagName = target.tagName;
96− if (
97− target.isContentEditable ||
98− tagName === "INPUT" ||
99− tagName === "TEXTAREA" ||
100− tagName === "SELECT"
101− ) {
102− return;
103− }
104− }
105−
106− const activeThread = state.threads.find(
107− (t) => t.id === state.activeThreadId,
108− );
109− const projectId = activeThread?.projectId ?? state.projects[0]?.id;
110− if (!projectId) return;
111−
112− event.preventDefault();
113− handleNewThread(projectId);
114− };
115−
116− window.addEventListener("keydown", onWindowKeyDown);
117− return () => {
118− window.removeEventListener("keydown", onWindowKeyDown);
119− };
120− }, [handleNewThread, state.activeThreadId, state.projects, state.threads]);
121−
12279 return (
123− <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-white/8 bg-[#141414]">
80+ <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-border bg-card">
12481 {/* Drag region / traffic light space */}
12582 <div className="drag-region h-[52px] shrink-0" />
12683 {/* Branding */}
12784 <div className="flex items-center gap-2 px-4 pb-4">
128− <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-white text-xs font-bold text-[#0c0c0c]">
85+ <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-xs font-bold text-primary-foreground">
12986 CT
13087 </div>
131− <span className="text-sm font-semibold tracking-tight text-[#e0e0e0]">CodeThing</span>
88+ <span className="flex-1 text-sm font-semibold tracking-tight text-foreground">
89+ CodeThing
90+ </span>
91+ <button
92+ type="button"
93+ className="rounded-md p-1.5 text-muted-foreground/80 transition-colors duration-150 hover:bg-accent hover:text-muted-foreground"
94+ onClick={() => setTheme(THEME_CYCLE[theme])}
95+ aria-label={`Theme: ${theme}`}
96+ title={`Theme: ${theme}`}
97+ >
98+ {theme === "system" ? (
99+ <MonitorIcon className="size-3.5" />
100+ ) : theme === "light" ? (
101+ <SunIcon className="size-3.5" />
102+ ) : (
103+ <MoonIcon className="size-3.5" />
104+ )}
105+ </button>
132106 </div>
133107
134108 {/* New thread (global) */}
135109 <div className="px-3 pb-3">
136110 <button
137111 type="button"
138− className="flex w-full items-center gap-2 rounded-lg border border-white/8 bg-white/4 px-3 py-2 text-xs text-[#a0a0a0]/80 transition-colors duration-150 hover:bg-white/[0.07]"
112+ className="flex w-full items-center gap-2 rounded-lg border border-border bg-secondary px-3 py-2 text-xs text-muted-foreground transition-colors duration-150 hover:bg-accent"
139113 onClick={() => {
140114 if (state.projects.length === 0) {
141115 setAddingProject(true);
@@ -145,7 +119,7 @@ export default function Sidebar() { · 3 unmodified lines
145119 if (firstProject) handleNewThread(firstProject.id);
146120 }}
147121 >
148− <span className="text-white">+</span>
122+ <span className="text-foreground">+</span>
149123 New thread
150124 </button>
151125 </div>
@@ -153,46 +127,43 @@ export default function Sidebar() { · 1 unmodified line
153127 {/* Project list */}
154128 <nav className="flex-1 overflow-y-auto px-2">
155129 {state.projects.map((project) => {
156− const threads = state.threads
157− .filter((t) => t.projectId === project.id)
158− .sort((a, b) => {
159− const byDate =
160− new Date(b.createdAt).getTime() -
161− new Date(a.createdAt).getTime();
162− if (byDate !== 0) return byDate;
163− return b.id.localeCompare(a.id);
164− });
130+ const threads = state.threads.filter(
131+ (t) => t.projectId === project.id,
132+ );
165133 return (
166134 <div key={project.id} className="mb-1">
167135 {/* Project header */}
168136 <button
169137 type="button"
170− className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 hover:bg-white/5"
171− onClick={() => dispatch({ type: "TOGGLE_PROJECT", projectId: project.id })}
138+ className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 hover:bg-accent"
139+ onClick={() =>
140+ dispatch({ type: "TOGGLE_PROJECT", projectId: project.id })
141+ }
172142 >
173− <span className="text-[10px] text-[#a0a0a0]/50">
143+ <span className="text-[10px] text-muted-foreground/70">
174144 {project.expanded ? "▼" : "▶"}
175145 </span>
176− <span className="flex-1 truncate text-xs font-medium text-[#e0e0e0]/90">
146+ <span className="flex-1 truncate text-xs font-medium text-foreground/90">
177147 {project.name}
178148 </span>
179− <span className="text-[10px] text-[#a0a0a0]/40">{threads.length}</span>
149+ <span className="text-[10px] text-muted-foreground/60">
150+ {threads.length}
151+ </span>
180152 </button>
181153
182154 {/* Threads */}
183155 {project.expanded && (
184− <div className="ml-2 border-l border-white/6 pl-2">
156+ <div className="ml-2 border-l border-border/80 pl-2">
185157 {threads.map((thread) => {
186158 const isActive = state.activeThreadId === thread.id;
187− const threadStatus = threadStatusLabel(
188− thread.session?.status,
189− );
190159 return (
191160 <button
192161 key={thread.id}
193162 type="button"
194163 className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 ${
195− isActive ? "bg-white/10 text-white" : "text-[#a0a0a0]/70 hover:bg-white/4"
164+ isActive
165+ ? "bg-accent text-foreground"
166+ : "text-muted-foreground hover:bg-secondary"
196167 }`}
197168 onClick={() =>
198169 dispatch({
@@ -201,20 +172,10 @@ export default function Sidebar() { · 2 unmodified lines
201172 })
202173 }
203174 >
204− <div className="flex min-w-0 flex-1 items-center gap-1.5">
205− {threadStatus && (
206− <span className="inline-flex items-center gap-1 text-[10px] text-[#7ed8ff]/80">
207− <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-300/80" />
208− <span className="hidden xl:inline">
209− {threadStatus}
210− </span>
211− </span>
212− )}
213− <span className="min-w-0 flex-1 truncate text-xs">
214− {thread.title}
215− </span>
216− </div>
217− <span className="shrink-0 text-[10px] text-[#a0a0a0]/30">
175+ <span className="flex-1 truncate text-xs">
176+ {thread.title}
177+ </span>
178+ <span className="shrink-0 text-[10px] text-muted-foreground/40">
218179 {formatRelativeTime(thread.createdAt)}
219180 </span>
220181 </button>
@@ -224,7 +185,7 @@ export default function Sidebar() { · 3 unmodified lines
224185 {/* New thread within project */}
225186 <button
226187 type="button"
227− className="flex w-full items-center gap-1 px-2 py-1 text-[10px] text-[#a0a0a0]/40 transition-colors duration-150 hover:text-[#a0a0a0]/60"
188+ className="flex w-full items-center gap-1 px-2 py-1 text-[10px] text-muted-foreground/60 transition-colors duration-150 hover:text-muted-foreground/80"
228189 onClick={() => handleNewThread(project.id)}
229190 >
230191 <span>+</span> New thread
@@ -236,7 +197,7 @@ export default function Sidebar() { · 5 unmodified lines
236197 })}
237198
238199 {state.projects.length === 0 && !addingProject && (
239− <div className="px-2 pt-4 text-center text-xs text-[#a0a0a0]/40">
200+ <div className="px-2 pt-4 text-center text-xs text-muted-foreground/60">
240201 No projects yet.
241202 <br />
242203 Add one to get started.
@@ -246,12 +207,12 @@ export default function Sidebar() { · 3 unmodified lines
246207
247208 {/* Add project form */}
248209 {addingProject ? (
249− <div className="border-t border-white/8 p-3">
250− <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-[#a0a0a0]/50">
210+ <div className="border-t border-border p-3">
211+ <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70">
251212 Add project
252213 </p>
253214 <input
254− className="mb-2 w-full rounded-md border border-white/10 bg-white/4 px-2 py-1.5 font-mono text-xs text-[#e0e0e0] placeholder:text-[#a0a0a0]/30 focus:border-white/30 focus:outline-none"
215+ className="mb-2 w-full rounded-md border border-border bg-secondary px-2 py-1.5 font-mono text-xs text-foreground placeholder:text-muted-foreground/40 focus:border-ring focus:outline-none"
255216 placeholder="/path/to/project"
256217 value={newCwd}
257218 onChange={(e) => setNewCwd(e.target.value)}
@@ -263,24 +224,39 @@ export default function Sidebar() { · 5 unmodified lines
263224 {api && (
264225 <button
265226 type="button"
266− className="mb-2 flex w-full items-center justify-center rounded-md border border-white/10 px-2 py-1.5 text-xs text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/4 disabled:cursor-not-allowed disabled:opacity-60"
227+ className="mb-2 flex w-full items-center justify-center rounded-md border border-border px-2 py-1.5 text-xs text-muted-foreground transition-colors duration-150 hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
267228 onClick={() => void handlePickFolder()}
268229 disabled={isPickingFolder}
269230 >
270231 {isPickingFolder ? "Picking folder..." : "Browse for folder"}
271232 </button>
272233 )}
234+ <select
235+ className="mb-2 w-full rounded-md border border-border bg-secondary px-2 py-1.5 font-mono text-xs text-foreground placeholder:text-muted-foreground/40 focus:border-ring focus:outline-none"
236+ value={newModel}
237+ onChange={(e) => setNewModel(e.target.value)}
238+ onKeyDown={(e) => {
239+ if (e.key === "Enter") handleAddProject();
240+ if (e.key === "Escape") setAddingProject(false);
241+ }}
242+ >
243+ {MODEL_OPTIONS.map((model) => (
244+ <option key={model} value={model} className="bg-card">
245+ {model}
246+ </option>
247+ ))}
248+ </select>
273249 <div className="flex gap-2">
274250 <button
275251 type="button"
276− className="flex-1 rounded-md bg-white px-2 py-1 text-xs font-medium text-[#0c0c0c] transition-colors duration-150 hover:bg-white/90"
252+ className="flex-1 rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors duration-150 hover:bg-primary/90"
277253 onClick={handleAddProject}
278254 >
279255 Add
280256 </button>
281257 <button
282258 type="button"
283− className="flex-1 rounded-md border border-white/10 px-2 py-1 text-xs text-[#a0a0a0]/60 transition-colors duration-150 hover:bg-white/4"
259+ className="flex-1 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground/80 transition-colors duration-150 hover:bg-secondary"
284260 onClick={() => setAddingProject(false)}
285261 >
286262 Cancel
@@ -288,10 +264,10 @@ export default function Sidebar() { · 1 unmodified line
288264 </div>
289265 </div>
290266 ) : (
291− <div className="border-t border-white/8 p-3">
267+ <div className="border-t border-border p-3">
292268 <button
293269 type="button"
294− className="flex w-full items-center justify-center gap-1 rounded-md border border-dashed border-white/12 py-2 text-xs text-[#a0a0a0]/50 transition-colors duration-150 hover:border-white/20 hover:text-[#a0a0a0]/70"
270+ className="flex w-full items-center justify-center gap-1 rounded-md border border-dashed border-border py-2 text-xs text-muted-foreground/70 transition-colors duration-150 hover:border-ring hover:text-muted-foreground"
295271 onClick={() => setAddingProject(true)}
296272 >
297273 + Add project
apps/renderer/src/components/ui/accordion.tsx+67−0
@@ -0,0 +1,67 @@
1+import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
2+import { ChevronDownIcon } from "lucide-react";
3+
4+import { cn } from "#/lib/utils";
5+
6+function Accordion(props: AccordionPrimitive.Root.Props) {
7+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
8+}
9+
10+function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
11+ return (
12+ <AccordionPrimitive.Item
13+ className={cn("border-b last:border-b-0", className)}
14+ data-slot="accordion-item"
15+ {...props}
16+ />
17+ );
18+}
19+
20+function AccordionTrigger({
21+ className,
22+ children,
23+ ...props
24+}: AccordionPrimitive.Trigger.Props) {
25+ return (
26+ <AccordionPrimitive.Header className="flex">
27+ <AccordionPrimitive.Trigger
28+ className={cn(
29+ "flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left font-medium text-sm outline-none transition-all focus-visible:ring-[3px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-64 data-panel-open:*:data-[slot=accordion-indicator]:rotate-180",
30+ className,
31+ )}
32+ data-slot="accordion-trigger"
33+ {...props}
34+ >
35+ {children}
36+ <ChevronDownIcon
37+ className="pointer-events-none size-4 shrink-0 translate-y-0.5 opacity-80 transition-transform duration-200 ease-in-out"
38+ data-slot="accordion-indicator"
39+ />
40+ </AccordionPrimitive.Trigger>
41+ </AccordionPrimitive.Header>
42+ );
43+}
44+
45+function AccordionPanel({
46+ className,
47+ children,
48+ ...props
49+}: AccordionPrimitive.Panel.Props) {
50+ return (
51+ <AccordionPrimitive.Panel
52+ className="h-(--accordion-panel-height) overflow-hidden text-muted-foreground text-sm transition-[height] duration-200 ease-in-out data-ending-style:h-0 data-starting-style:h-0"
53+ data-slot="accordion-panel"
54+ {...props}
55+ >
56+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
57+ </AccordionPrimitive.Panel>
58+ );
59+}
60+
61+export {
62+ Accordion,
63+ AccordionItem,
64+ AccordionTrigger,
65+ AccordionPanel,
66+ AccordionPanel as AccordionContent,
67+};
apps/renderer/src/components/ui/alert-dialog.tsx+167−0
@@ -0,0 +1,167 @@
1+import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
2+
3+import { cn } from "#/lib/utils";
4+
5+const AlertDialogCreateHandle = AlertDialogPrimitive.createHandle;
6+
7+const AlertDialog = AlertDialogPrimitive.Root;
8+
9+const AlertDialogPortal = AlertDialogPrimitive.Portal;
10+
11+function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) {
12+ return (
13+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
14+ );
15+}
16+
17+function AlertDialogBackdrop({
18+ className,
19+ ...props
20+}: AlertDialogPrimitive.Backdrop.Props) {
21+ return (
22+ <AlertDialogPrimitive.Backdrop
23+ className={cn(
24+ "fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 ease-out data-ending-style:opacity-0 data-starting-style:opacity-0",
25+ className,
26+ )}
27+ data-slot="alert-dialog-backdrop"
28+ {...props}
29+ />
30+ );
31+}
32+
33+function AlertDialogViewport({
34+ className,
35+ ...props
36+}: AlertDialogPrimitive.Viewport.Props) {
37+ return (
38+ <AlertDialogPrimitive.Viewport
39+ className={cn(
40+ "fixed inset-0 z-50 grid grid-rows-[1fr_auto_3fr] justify-items-center p-4",
41+ className,
42+ )}
43+ data-slot="alert-dialog-viewport"
44+ {...props}
45+ />
46+ );
47+}
48+
49+function AlertDialogPopup({
50+ className,
51+ bottomStickOnMobile = true,
52+ ...props
53+}: AlertDialogPrimitive.Popup.Props & {
54+ bottomStickOnMobile?: boolean;
55+}) {
56+ return (
57+ <AlertDialogPortal>
58+ <AlertDialogBackdrop />
59+ <AlertDialogViewport
60+ className={cn(
61+ bottomStickOnMobile &&
62+ "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12",
63+ )}
64+ >
65+ <AlertDialogPrimitive.Popup
66+ className={cn(
67+ "-translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 max-w-lg scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border bg-popover not-dark:bg-clip-padding text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
68+ bottomStickOnMobile &&
69+ "max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4 max-sm:before:hidden max-sm:before:rounded-none",
70+ className,
71+ )}
72+ data-slot="alert-dialog-popup"
73+ {...props}
74+ />
75+ </AlertDialogViewport>
76+ </AlertDialogPortal>
77+ );
78+}
79+
80+function AlertDialogHeader({
81+ className,
82+ ...props
83+}: React.ComponentProps<"div">) {
84+ return (
85+ <div
86+ className={cn(
87+ "flex flex-col gap-2 p-6 text-center max-sm:pb-4 sm:text-left",
88+ className,
89+ )}
90+ data-slot="alert-dialog-header"
91+ {...props}
92+ />
93+ );
94+}
95+
96+function AlertDialogFooter({
97+ className,
98+ variant = "default",
99+ ...props
100+}: React.ComponentProps<"div"> & {
101+ variant?: "default" | "bare";
102+}) {
103+ return (
104+ <div
105+ className={cn(
106+ "flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end sm:rounded-b-[calc(var(--radius-2xl)-1px)]",
107+ variant === "default" && "border-t bg-muted/72 py-4",
108+ variant === "bare" && "pb-6",
109+ className,
110+ )}
111+ data-slot="alert-dialog-footer"
112+ {...props}
113+ />
114+ );
115+}
116+
117+function AlertDialogTitle({
118+ className,
119+ ...props
120+}: AlertDialogPrimitive.Title.Props) {
121+ return (
122+ <AlertDialogPrimitive.Title
123+ className={cn(
124+ "font-heading font-semibold text-xl leading-none",
125+ className,
126+ )}
127+ data-slot="alert-dialog-title"
128+ {...props}
129+ />
130+ );
131+}
132+
133+function AlertDialogDescription({
134+ className,
135+ ...props
136+}: AlertDialogPrimitive.Description.Props) {
137+ return (
138+ <AlertDialogPrimitive.Description
139+ className={cn("text-muted-foreground text-sm", className)}
140+ data-slot="alert-dialog-description"
141+ {...props}
142+ />
143+ );
144+}
145+
146+function AlertDialogClose(props: AlertDialogPrimitive.Close.Props) {
147+ return (
148+ <AlertDialogPrimitive.Close data-slot="alert-dialog-close" {...props} />
149+ );
150+}
151+
152+export {
153+ AlertDialogCreateHandle,
154+ AlertDialog,
155+ AlertDialogPortal,
156+ AlertDialogBackdrop,
157+ AlertDialogBackdrop as AlertDialogOverlay,
158+ AlertDialogTrigger,
159+ AlertDialogPopup,
160+ AlertDialogPopup as AlertDialogContent,
161+ AlertDialogHeader,
162+ AlertDialogFooter,
163+ AlertDialogTitle,
164+ AlertDialogDescription,
165+ AlertDialogClose,
166+ AlertDialogViewport,
167+};
apps/renderer/src/components/ui/alert.tsx+80−0
@@ -0,0 +1,80 @@
1+import { type VariantProps, cva } from "class-variance-authority";
2+import type * as React from "react";
3+
4+import { cn } from "#/lib/utils";
5+
6+const alertVariants = cva(
7+ "relative grid w-full items-start gap-x-2 gap-y-0.5 rounded-xl border px-3.5 py-3 text-card-foreground text-sm has-[>svg]:has-data-[slot=alert-action]:grid-cols-[calc(var(--spacing)*4)_1fr_auto] has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-data-[slot=alert-action]:grid-cols-[1fr_auto] has-[>svg]:gap-x-2 [&>svg]:h-lh [&>svg]:w-4",
8+ {
9+ defaultVariants: {
10+ variant: "default",
11+ },
12+ variants: {
13+ variant: {
14+ default:
15+ "bg-transparent dark:bg-input/32 [&>svg]:text-muted-foreground",
16+ error:
17+ "border-destructive/32 bg-destructive/4 [&>svg]:text-destructive",
18+ info: "border-info/32 bg-info/4 [&>svg]:text-info",
19+ success: "border-success/32 bg-success/4 [&>svg]:text-success",
20+ warning: "border-warning/32 bg-warning/4 [&>svg]:text-warning",
21+ },
22+ },
23+ },
24+);
25+
26+function Alert({
27+ className,
28+ variant,
29+ ...props
30+}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
31+ return (
32+ <div
33+ className={cn(alertVariants({ variant }), className)}
34+ data-slot="alert"
35+ role="alert"
36+ {...props}
37+ />
38+ );
39+}
40+
41+function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
42+ return (
43+ <div
44+ className={cn("font-medium [svg~&]:col-start-2", className)}
45+ data-slot="alert-title"
46+ {...props}
47+ />
48+ );
49+}
50+
51+function AlertDescription({
52+ className,
53+ ...props
54+}: React.ComponentProps<"div">) {
55+ return (
56+ <div
57+ className={cn(
58+ "flex flex-col gap-2.5 text-muted-foreground [svg~&]:col-start-2",
59+ className,
60+ )}
61+ data-slot="alert-description"
62+ {...props}
63+ />
64+ );
65+}
66+
67+function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
68+ return (
69+ <div
70+ className={cn(
71+ "flex gap-1 max-sm:col-start-2 max-sm:mt-2 sm:row-start-1 sm:row-end-3 sm:self-center sm:[[data-slot=alert-description]~&]:col-start-2 sm:[[data-slot=alert-title]~&]:col-start-2 sm:[svg~&]:col-start-2 sm:[svg~[data-slot=alert-description]~&]:col-start-3 sm:[svg~[data-slot=alert-title]~&]:col-start-3",
72+ className,
73+ )}
74+ data-slot="alert-action"
75+ {...props}
76+ />
77+ );
78+}
79+
80+export { Alert, AlertTitle, AlertDescription, AlertAction };
apps/renderer/src/components/ui/autocomplete.tsx+308−0
@@ -0,0 +1,308 @@
1+import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete";
2+import { ChevronsUpDownIcon, XIcon } from "lucide-react";
3+
4+import { Input } from "#/components/ui/input";
5+import { ScrollArea } from "#/components/ui/scroll-area";
6+import { cn } from "#/lib/utils";
7+
8+const Autocomplete = AutocompletePrimitive.Root;
9+
10+function AutocompleteInput({
11+ className,
12+ showTrigger = false,
13+ showClear = false,
14+ startAddon,
15+ size,
16+ ...props
17+}: Omit<AutocompletePrimitive.Input.Props, "size"> & {
18+ showTrigger?: boolean;
19+ showClear?: boolean;
20+ startAddon?: React.ReactNode;
21+ size?: "sm" | "default" | "lg" | number;
22+ ref?: React.Ref<HTMLInputElement>;
23+}) {
24+ const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;
25+
26+ return (
27+ <div className="relative not-has-[>*.w-full]:w-fit w-full text-foreground has-disabled:opacity-64">
28+ {startAddon && (
29+ <div
30+ aria-hidden="true"
31+ className="[&_svg]:-mx-0.5 pointer-events-none absolute inset-y-0 start-px z-10 flex items-center ps-[calc(--spacing(3)-1px)] opacity-80 has-[+[data-size=sm]]:ps-[calc(--spacing(2.5)-1px)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4"
32+ data-slot="autocomplete-start-addon"
33+ >
34+ {startAddon}
35+ </div>
36+ )}
37+ <AutocompletePrimitive.Input
38+ className={cn(
39+ startAddon &&
40+ "data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7.5)-1px)] *:data-[slot=autocomplete-input]:ps-[calc(--spacing(8.5)-1px)] sm:data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7)-1px)] sm:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(8)-1px)]",
41+ sizeValue === "sm"
42+ ? "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-6.5"
43+ : "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-7",
44+ className,
45+ )}
46+ data-slot="autocomplete-input"
47+ render={<Input nativeInput size={sizeValue} />}
48+ {...props}
49+ />
50+ {showTrigger && (
51+ <AutocompleteTrigger
52+ className={cn(
53+ "-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
54+ sizeValue === "sm" ? "end-0" : "end-0.5",
55+ )}
56+ >
57+ <ChevronsUpDownIcon />
58+ </AutocompleteTrigger>
59+ )}
60+ {showClear && (
61+ <AutocompleteClear
62+ className={cn(
63+ "-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
64+ sizeValue === "sm" ? "end-0" : "end-0.5",
65+ )}
66+ >
67+ <XIcon />
68+ </AutocompleteClear>
69+ )}
70+ </div>
71+ );
72+}
73+
74+function AutocompletePopup({
75+ className,
76+ children,
77+ side = "bottom",
78+ sideOffset = 4,
79+ alignOffset,
80+ align = "start",
81+ ...props
82+}: AutocompletePrimitive.Popup.Props & {
83+ align?: AutocompletePrimitive.Positioner.Props["align"];
84+ sideOffset?: AutocompletePrimitive.Positioner.Props["sideOffset"];
85+ alignOffset?: AutocompletePrimitive.Positioner.Props["alignOffset"];
86+ side?: AutocompletePrimitive.Positioner.Props["side"];
87+}) {
88+ return (
89+ <AutocompletePrimitive.Portal>
90+ <AutocompletePrimitive.Positioner
91+ align={align}
92+ alignOffset={alignOffset}
93+ className="z-50 select-none"
94+ data-slot="autocomplete-positioner"
95+ side={side}
96+ sideOffset={sideOffset}
97+ >
98+ <span
99+ className={cn(
100+ "relative flex max-h-full min-w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
101+ className,
102+ )}
103+ >
104+ <AutocompletePrimitive.Popup
105+ className="flex max-h-[min(var(--available-height),23rem)] flex-1 flex-col text-foreground"
106+ data-slot="autocomplete-popup"
107+ {...props}
108+ >
109+ {children}
110+ </AutocompletePrimitive.Popup>
111+ </span>
112+ </AutocompletePrimitive.Positioner>
113+ </AutocompletePrimitive.Portal>
114+ );
115+}
116+
117+function AutocompleteItem({
118+ className,
119+ children,
120+ ...props
121+}: AutocompletePrimitive.Item.Props) {
122+ return (
123+ <AutocompletePrimitive.Item
124+ className={cn(
125+ "flex min-h-8 cursor-default select-none items-center rounded-sm px-2 py-1 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm",
126+ className,
127+ )}
128+ data-slot="autocomplete-item"
129+ {...props}
130+ >
131+ {children}
132+ </AutocompletePrimitive.Item>
133+ );
134+}
135+
136+function AutocompleteSeparator({
137+ className,
138+ ...props
139+}: AutocompletePrimitive.Separator.Props) {
140+ return (
141+ <AutocompletePrimitive.Separator
142+ className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}
143+ data-slot="autocomplete-separator"
144+ {...props}
145+ />
146+ );
147+}
148+
149+function AutocompleteGroup({
150+ className,
151+ ...props
152+}: AutocompletePrimitive.Group.Props) {
153+ return (
154+ <AutocompletePrimitive.Group
155+ className={cn("[[role=group]+&]:mt-1.5", className)}
156+ data-slot="autocomplete-group"
157+ {...props}
158+ />
159+ );
160+}
161+
162+function AutocompleteGroupLabel({
163+ className,
164+ ...props
165+}: AutocompletePrimitive.GroupLabel.Props) {
166+ return (
167+ <AutocompletePrimitive.GroupLabel
168+ className={cn(
169+ "px-2 py-1.5 font-medium text-muted-foreground text-xs",
170+ className,
171+ )}
172+ data-slot="autocomplete-group-label"
173+ {...props}
174+ />
175+ );
176+}
177+
178+function AutocompleteEmpty({
179+ className,
180+ ...props
181+}: AutocompletePrimitive.Empty.Props) {
182+ return (
183+ <AutocompletePrimitive.Empty
184+ className={cn(
185+ "not-empty:p-2 text-center text-base text-muted-foreground sm:text-sm",
186+ className,
187+ )}
188+ data-slot="autocomplete-empty"
189+ {...props}
190+ />
191+ );
192+}
193+
194+function AutocompleteRow({
195+ className,
196+ ...props
197+}: AutocompletePrimitive.Row.Props) {
198+ return (
199+ <AutocompletePrimitive.Row
200+ className={className}
201+ data-slot="autocomplete-row"
202+ {...props}
203+ />
204+ );
205+}
206+
207+function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) {
208+ return (
209+ <AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />
210+ );
211+}
212+
213+function AutocompleteList({
214+ className,
215+ ...props
216+}: AutocompletePrimitive.List.Props) {
217+ return (
218+ <ScrollArea scrollbarGutter scrollFade>
219+ <AutocompletePrimitive.List
220+ className={cn(
221+ "not-empty:scroll-py-1 not-empty:p-1 in-data-has-overflow-y:pe-3",
222+ className,
223+ )}
224+ data-slot="autocomplete-list"
225+ {...props}
226+ />
227+ </ScrollArea>
228+ );
229+}
230+
231+function AutocompleteClear({
232+ className,
233+ ...props
234+}: AutocompletePrimitive.Clear.Props) {
235+ return (
236+ <AutocompletePrimitive.Clear
237+ className={cn(
238+ "-translate-y-1/2 absolute end-0.5 top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-[color,background-color,box-shadow,opacity] pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
239+ className,
240+ )}
241+ data-slot="autocomplete-clear"
242+ {...props}
243+ >
244+ <XIcon />
245+ </AutocompletePrimitive.Clear>
246+ );
247+}
248+
249+function AutocompleteStatus({
250+ className,
251+ ...props
252+}: AutocompletePrimitive.Status.Props) {
253+ return (
254+ <AutocompletePrimitive.Status
255+ className={cn(
256+ "px-3 py-2 font-medium text-muted-foreground text-xs empty:m-0 empty:p-0",
257+ className,
258+ )}
259+ data-slot="autocomplete-status"
260+ {...props}
261+ />
262+ );
263+}
264+
265+function AutocompleteCollection({
266+ ...props
267+}: AutocompletePrimitive.Collection.Props) {
268+ return (
269+ <AutocompletePrimitive.Collection
270+ data-slot="autocomplete-collection"
271+ {...props}
272+ />
273+ );
274+}
275+
276+function AutocompleteTrigger({
277+ className,
278+ ...props
279+}: AutocompletePrimitive.Trigger.Props) {
280+ return (
281+ <AutocompletePrimitive.Trigger
282+ className={className}
283+ data-slot="autocomplete-trigger"
284+ {...props}
285+ />
286+ );
287+}
288+
289+const useAutocompleteFilter = AutocompletePrimitive.useFilter;
290+
291+export {
292+ Autocomplete,
293+ AutocompleteInput,
294+ AutocompleteTrigger,
295+ AutocompletePopup,
296+ AutocompleteItem,
297+ AutocompleteSeparator,
298+ AutocompleteGroup,
299+ AutocompleteGroupLabel,
300+ AutocompleteEmpty,
301+ AutocompleteValue,
302+ AutocompleteList,
303+ AutocompleteClear,
304+ AutocompleteStatus,
305+ AutocompleteRow,
306+ AutocompleteCollection,
307+ useAutocompleteFilter,
308+};
apps/renderer/src/components/ui/avatar.tsx+44−0
@@ -0,0 +1,44 @@
1+import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
2+
3+import { cn } from "#/lib/utils";
4+
5+function Avatar({ className, ...props }: AvatarPrimitive.Root.Props) {
6+ return (
7+ <AvatarPrimitive.Root
8+ className={cn(
9+ "inline-flex size-8 shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-background align-middle font-medium text-xs",
10+ className,
11+ )}
12+ data-slot="avatar"
13+ {...props}
14+ />
15+ );
16+}
17+
18+function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
19+ return (
20+ <AvatarPrimitive.Image
21+ className={cn("size-full object-cover", className)}
22+ data-slot="avatar-image"
23+ {...props}
24+ />
25+ );
26+}
27+
28+function AvatarFallback({
29+ className,
30+ ...props
31+}: AvatarPrimitive.Fallback.Props) {
32+ return (
33+ <AvatarPrimitive.Fallback
34+ className={cn(
35+ "flex size-full items-center justify-center rounded-full bg-muted",
36+ className,
37+ )}
38+ data-slot="avatar-fallback"
39+ {...props}
40+ />
41+ );
42+}
43+
44+export { Avatar, AvatarImage, AvatarFallback };
apps/renderer/src/components/ui/badge.tsx+58−0
@@ -0,0 +1,58 @@
1+import { mergeProps } from "@base-ui/react/merge-props";
2+import { useRender } from "@base-ui/react/use-render";
3+import { type VariantProps, cva } from "class-variance-authority";
4+
5+import { cn } from "#/lib/utils";
6+
7+const badgeVariants = cva(
8+ "relative inline-flex shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-sm border border-transparent font-medium outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-3.5 sm:[&_svg:not([class*='size-'])]:size-3 [&_svg]:pointer-events-none [&_svg]:shrink-0 [button,a&]:cursor-pointer [button,a&]:pointer-coarse:after:absolute [button,a&]:pointer-coarse:after:size-full [button,a&]:pointer-coarse:after:min-h-11 [button,a&]:pointer-coarse:after:min-w-11",
9+ {
10+ defaultVariants: {
11+ size: "default",
12+ variant: "default",
13+ },
14+ variants: {
15+ size: {
16+ default:
17+ "h-5.5 min-w-5.5 px-[calc(--spacing(1)-1px)] text-sm sm:h-4.5 sm:min-w-4.5 sm:text-xs",
18+ lg: "h-6.5 min-w-6.5 px-[calc(--spacing(1.5)-1px)] text-base sm:h-5.5 sm:min-w-5.5 sm:text-sm",
19+ sm: "h-5 min-w-5 rounded-[calc(var(--radius-sm)-2px)] px-[calc(--spacing(1)-1px)] text-xs sm:h-4 sm:min-w-4 sm:text-[.625rem]",
20+ },
21+ variant: {
22+ default:
23+ "bg-primary text-primary-foreground [button,a&]:hover:bg-primary/90",
24+ destructive:
25+ "bg-destructive text-white [button,a&]:hover:bg-destructive/90",
26+ error:
27+ "bg-destructive/8 text-destructive-foreground dark:bg-destructive/16",
28+ info: "bg-info/8 text-info-foreground dark:bg-info/16",
29+ outline:
30+ "border-input bg-background text-foreground dark:bg-input/32 [button,a&]:hover:bg-accent/50 dark:[button,a&]:hover:bg-input/48",
31+ secondary:
32+ "bg-secondary text-secondary-foreground [button,a&]:hover:bg-secondary/90",
33+ success: "bg-success/8 text-success-foreground dark:bg-success/16",
34+ warning: "bg-warning/8 text-warning-foreground dark:bg-warning/16",
35+ },
36+ },
37+ },
38+);
39+
40+interface BadgeProps extends useRender.ComponentProps<"span"> {
41+ variant?: VariantProps<typeof badgeVariants>["variant"];
42+ size?: VariantProps<typeof badgeVariants>["size"];
43+}
44+
45+function Badge({ className, variant, size, render, ...props }: BadgeProps) {
46+ const defaultProps = {
47+ className: cn(badgeVariants({ className, size, variant })),
48+ "data-slot": "badge",
49+ };
50+
51+ return useRender({
52+ defaultTagName: "span",
53+ props: mergeProps<"span">(defaultProps, props),
54+ render,
55+ });
56+}
57+
58+export { Badge, badgeVariants };
apps/renderer/src/components/ui/button.tsx+71−0
@@ -0,0 +1,71 @@
1+import { mergeProps } from "@base-ui/react/merge-props";
2+import { useRender } from "@base-ui/react/use-render";
3+import { type VariantProps, cva } from "class-variance-authority";
4+import type * as React from "react";
5+
6+import { cn } from "#/lib/utils";
7+
8+const buttonVariants = cva(
9+ "[&_svg]:-mx-0.5 relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
10+ {
11+ defaultVariants: {
12+ size: "default",
13+ variant: "default",
14+ },
15+ variants: {
16+ size: {
17+ default: "h-9 px-[calc(--spacing(3)-1px)] sm:h-8",
18+ icon: "size-9 sm:size-8",
19+ "icon-lg": "size-10 sm:size-9",
20+ "icon-sm": "size-8 sm:size-7",
21+ "icon-xl":
22+ "size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",
23+ "icon-xs":
24+ "size-7 rounded-md before:rounded-[calc(var(--radius-md)-1px)] sm:size-6 not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-4 sm:not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3.5",
25+ lg: "h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9",
26+ sm: "h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:h-7",
27+ xl: "h-11 px-[calc(--spacing(4)-1px)] text-lg sm:h-10 sm:text-base [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",
28+ xs: "h-7 gap-1 rounded-md px-[calc(--spacing(2)-1px)] text-sm before:rounded-[calc(var(--radius-md)-1px)] sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5",
29+ },
30+ variant: {
31+ default:
32+ "not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-primary/90",
33+ destructive:
34+ "not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-destructive bg-destructive text-white shadow-destructive/24 shadow-xs [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-destructive/90",
35+ "destructive-outline":
36+ "border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:border-destructive/32 [:hover,[data-pressed]]:bg-destructive/4",
37+ ghost:
38+ "border-transparent text-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent",
39+ link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline",
40+ outline:
41+ "border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-accent/50 dark:[:hover,[data-pressed]]:bg-input/64",
42+ secondary:
43+ "border-transparent bg-secondary text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80 [:hover,[data-pressed]]:bg-secondary/90",
44+ },
45+ },
46+ },
47+);
48+
49+interface ButtonProps extends useRender.ComponentProps<"button"> {
50+ variant?: VariantProps<typeof buttonVariants>["variant"];
51+ size?: VariantProps<typeof buttonVariants>["size"];
52+}
53+
54+function Button({ className, variant, size, render, ...props }: ButtonProps) {
55+ const typeValue: React.ButtonHTMLAttributes<HTMLButtonElement>["type"] =
56+ render ? undefined : "button";
57+
58+ const defaultProps = {
59+ className: cn(buttonVariants({ className, size, variant })),
60+ "data-slot": "button",
61+ type: typeValue,
62+ };
63+
64+ return useRender({
65+ defaultTagName: "button",
66+ props: mergeProps<"button">(defaultProps, props),
67+ render,
68+ });
69+}
70+
71+export { Button, buttonVariants };
apps/renderer/src/components/ui/card.tsx+242−0
@@ -0,0 +1,242 @@
1+import { mergeProps } from "@base-ui/react/merge-props";
2+import { useRender } from "@base-ui/react/use-render";
3+
4+import { cn } from "#/lib/utils";
5+
6+function Card({
7+ className,
8+ render,
9+ ...props
10+}: useRender.ComponentProps<"div">) {
11+ const defaultProps = {
12+ className: cn(
13+ "relative flex flex-col rounded-2xl border bg-card not-dark:bg-clip-padding text-card-foreground shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
14+ className,
15+ ),
16+ "data-slot": "card",
17+ };
18+
19+ return useRender({
20+ defaultTagName: "div",
21+ props: mergeProps<"div">(defaultProps, props),
22+ render,
23+ });
24+}
25+
26+function CardFrame({
27+ className,
28+ render,
29+ ...props
30+}: useRender.ComponentProps<"div">) {
31+ const defaultProps = {
32+ className: cn(
33+ "flex flex-col relative rounded-2xl border bg-card before:bg-muted/72 not-dark:bg-clip-padding text-card-foreground shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)] *:data-[slot=card]:-m-px *:not-last:data-[slot=card]:rounded-b-xl *:not-last:data-[slot=card]:before:rounded-b-[calc(var(--radius-xl)-1px)] *:not-first:data-[slot=card]:rounded-t-xl *:not-first:data-[slot=card]:before:rounded-t-[calc(var(--radius-xl)-1px)] *:data-[slot=card]:[clip-path:inset(-1rem_1px)] *:data-[slot=card]:first:[clip-path:inset(1px_1px_-1rem_1px_round_calc(var(--radius-2xl)-1px))] *:data-[slot=card]:last:[clip-path:inset(-1rem_1px_1px_1px_round_calc(var(--radius-2xl)-1px))] *:data-[slot=card]:shadow-none *:data-[slot=card]:before:hidden *:data-[slot=card]:bg-clip-padding",
34+ className,
35+ ),
36+ "data-slot": "card-frame",
37+ };
38+
39+ return useRender({
40+ defaultTagName: "div",
41+ props: mergeProps<"div">(defaultProps, props),
42+ render,
43+ });
44+}
45+
46+function CardFrameHeader({
47+ className,
48+ render,
49+ ...props
50+}: useRender.ComponentProps<"div">) {
51+ const defaultProps = {
52+ className: cn("relative flex flex-col px-6 py-4", className),
53+ "data-slot": "card-frame-header",
54+ };
55+
56+ return useRender({
57+ defaultTagName: "div",
58+ props: mergeProps<"div">(defaultProps, props),
59+ render,
60+ });
61+}
62+
63+function CardFrameTitle({
64+ className,
65+ render,
66+ ...props
67+}: useRender.ComponentProps<"div">) {
68+ const defaultProps = {
69+ className: cn("font-semibold text-sm", className),
70+ "data-slot": "card-frame-title",
71+ };
72+
73+ return useRender({
74+ defaultTagName: "div",
75+ props: mergeProps<"div">(defaultProps, props),
76+ render,
77+ });
78+}
79+
80+function CardFrameDescription({
81+ className,
82+ render,
83+ ...props
84+}: useRender.ComponentProps<"div">) {
85+ const defaultProps = {
86+ className: cn("text-muted-foreground text-sm", className),
87+ "data-slot": "card-frame-description",
88+ };
89+
90+ return useRender({
91+ defaultTagName: "div",
92+ props: mergeProps<"div">(defaultProps, props),
93+ render,
94+ });
95+}
96+
97+function CardFrameFooter({
98+ className,
99+ render,
100+ ...props
101+}: useRender.ComponentProps<"div">) {
102+ const defaultProps = {
103+ className: cn("px-6 py-4", className),
104+ "data-slot": "card-frame-footer",
105+ };
106+
107+ return useRender({
108+ defaultTagName: "div",
109+ props: mergeProps<"div">(defaultProps, props),
110+ render,
111+ });
112+}
113+
114+function CardHeader({
115+ className,
116+ render,
117+ ...props
118+}: useRender.ComponentProps<"div">) {
119+ const defaultProps = {
120+ className: cn(
121+ "grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pb-4 has-data-[slot=card-action]:grid-cols-[1fr_auto]",
122+ className,
123+ ),
124+ "data-slot": "card-header",
125+ };
126+
127+ return useRender({
128+ defaultTagName: "div",
129+ props: mergeProps<"div">(defaultProps, props),
130+ render,
131+ });
132+}
133+
134+function CardTitle({
135+ className,
136+ render,
137+ ...props
138+}: useRender.ComponentProps<"div">) {
139+ const defaultProps = {
140+ className: cn("font-semibold text-lg leading-none", className),
141+ "data-slot": "card-title",
142+ };
143+
144+ return useRender({
145+ defaultTagName: "div",
146+ props: mergeProps<"div">(defaultProps, props),
147+ render,
148+ });
149+}
150+
151+function CardDescription({
152+ className,
153+ render,
154+ ...props
155+}: useRender.ComponentProps<"div">) {
156+ const defaultProps = {
157+ className: cn("text-muted-foreground text-sm", className),
158+ "data-slot": "card-description",
159+ };
160+
161+ return useRender({
162+ defaultTagName: "div",
163+ props: mergeProps<"div">(defaultProps, props),
164+ render,
165+ });
166+}
167+
168+function CardAction({
169+ className,
170+ render,
171+ ...props
172+}: useRender.ComponentProps<"div">) {
173+ const defaultProps = {
174+ className: cn(
175+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end inline-flex",
176+ className,
177+ ),
178+ "data-slot": "card-action",
179+ };
180+
181+ return useRender({
182+ defaultTagName: "div",
183+ props: mergeProps<"div">(defaultProps, props),
184+ render,
185+ });
186+}
187+
188+function CardPanel({
189+ className,
190+ render,
191+ ...props
192+}: useRender.ComponentProps<"div">) {
193+ const defaultProps = {
194+ className: cn(
195+ "flex-1 p-6 in-[[data-slot=card]:has(>[data-slot=card-header]:not(.border-b))]:pt-0 in-[[data-slot=card]:has(>[data-slot=card-footer]:not(.border-t))]:pb-0",
196+ className,
197+ ),
198+ "data-slot": "card-panel",
199+ };
200+
201+ return useRender({
202+ defaultTagName: "div",
203+ props: mergeProps<"div">(defaultProps, props),
204+ render,
205+ });
206+}
207+
208+function CardFooter({
209+ className,
210+ render,
211+ ...props
212+}: useRender.ComponentProps<"div">) {
213+ const defaultProps = {
214+ className: cn(
215+ "flex items-center p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pt-4",
216+ className,
217+ ),
218+ "data-slot": "card-footer",
219+ };
220+
221+ return useRender({
222+ defaultTagName: "div",
223+ props: mergeProps<"div">(defaultProps, props),
224+ render,
225+ });
226+}
227+
228+export {
229+ Card,
230+ CardFrame,
231+ CardFrameHeader,
232+ CardFrameTitle,
233+ CardFrameDescription,
234+ CardFrameFooter,
235+ CardAction,
236+ CardDescription,
237+ CardFooter,
238+ CardHeader,
239+ CardPanel,
240+ CardPanel as CardContent,
241+ CardTitle,
242+};
apps/renderer/src/components/ui/checkbox-group.tsx+14−0
@@ -0,0 +1,14 @@
1+import { CheckboxGroup as CheckboxGroupPrimitive } from "@base-ui/react/checkbox-group";
2+
3+import { cn } from "#/lib/utils";
4+
5+function CheckboxGroup({ className, ...props }: CheckboxGroupPrimitive.Props) {
6+ return (
7+ <CheckboxGroupPrimitive
8+ className={cn("flex flex-col items-start gap-3", className)}
9+ {...props}
10+ />
11+ );
12+}
13+
14+export { CheckboxGroup };
apps/renderer/src/components/ui/checkbox.tsx+58−0
@@ -0,0 +1,58 @@
1+import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
2+
3+import { cn } from "#/lib/utils";
4+
5+function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
6+ return (
7+ <CheckboxPrimitive.Root
8+ className={cn(
9+ "relative inline-flex size-4.5 shrink-0 items-center justify-center rounded-[4px] border border-input bg-background not-dark:bg-clip-padding shadow-xs/5 outline-none ring-ring transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[3px] not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-background aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/48 data-disabled:opacity-64 sm:size-4 dark:not-data-checked:bg-input/32 dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)] [[data-disabled],[data-checked],[aria-invalid]]:shadow-none",
10+ className,
11+ )}
12+ data-slot="checkbox"
13+ {...props}
14+ >
15+ <CheckboxPrimitive.Indicator
16+ className="-inset-px absolute flex items-center justify-center rounded-[4px] text-primary-foreground data-unchecked:hidden data-checked:bg-primary data-indeterminate:text-foreground"
17+ data-slot="checkbox-indicator"
18+ render={(props, state) => (
19+ <span {...props}>
20+ {state.indeterminate ? (
21+ <svg
22+ className="size-3.5 sm:size-3"
23+ fill="none"
24+ height="24"
25+ stroke="currentColor"
26+ strokeLinecap="round"
27+ strokeLinejoin="round"
28+ strokeWidth="3"
29+ viewBox="0 0 24 24"
30+ width="24"
31+ xmlns="http://www.w3.org/2000/svg"
32+ >
33+ <path d="M5.252 12h13.496" />
34+ </svg>
35+ ) : (
36+ <svg
37+ className="size-3.5 sm:size-3"
38+ fill="none"
39+ height="24"
40+ stroke="currentColor"
41+ strokeLinecap="round"
42+ strokeLinejoin="round"
43+ strokeWidth="3"
44+ viewBox="0 0 24 24"
45+ width="24"
46+ xmlns="http://www.w3.org/2000/svg"
47+ >
48+ <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
49+ </svg>
50+ )}
51+ </span>
52+ )}
53+ />
54+ </CheckboxPrimitive.Root>
55+ );
56+}
57+
58+export { Checkbox };
apps/renderer/src/components/ui/collapsible.tsx+43−0
@@ -0,0 +1,43 @@
1+import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
2+
3+import { cn } from "#/lib/utils";
4+
5+function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
6+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
7+}
8+
9+function CollapsibleTrigger({
10+ className,
11+ ...props
12+}: CollapsiblePrimitive.Trigger.Props) {
13+ return (
14+ <CollapsiblePrimitive.Trigger
15+ className={cn("cursor-pointer", className)}
16+ data-slot="collapsible-trigger"
17+ {...props}
18+ />
19+ );
20+}
21+
22+function CollapsiblePanel({
23+ className,
24+ ...props
25+}: CollapsiblePrimitive.Panel.Props) {
26+ return (
27+ <CollapsiblePrimitive.Panel
28+ className={cn(
29+ "h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-200 data-ending-style:h-0 data-starting-style:h-0",
30+ className,
31+ )}
32+ data-slot="collapsible-panel"
33+ {...props}
34+ />
35+ );
36+}
37+
38+export {
39+ Collapsible,
40+ CollapsibleTrigger,
41+ CollapsiblePanel,
42+ CollapsiblePanel as CollapsibleContent,
43+};
apps/renderer/src/components/ui/combobox.tsx+422−0
@@ -0,0 +1,422 @@
1+import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox";
2+import { ChevronsUpDownIcon, XIcon } from "lucide-react";
3+import * as React from "react";
4+
5+import { Input } from "#/components/ui/input";
6+import { ScrollArea } from "#/components/ui/scroll-area";
7+import { cn } from "#/lib/utils";
8+
9+const ComboboxContext = React.createContext<{
10+ chipsRef: React.RefObject<Element | null> | null;
11+ multiple: boolean;
12+}>({
13+ chipsRef: null,
14+ multiple: false,
15+});
16+
17+function Combobox<Value, Multiple extends boolean | undefined = false>(
18+ props: ComboboxPrimitive.Root.Props<Value, Multiple>,
19+): React.JSX.Element {
20+ const chipsRef = React.useRef<Element | null>(null);
21+ return (
22+ <ComboboxContext.Provider value={{ chipsRef, multiple: !!props.multiple }}>
23+ <ComboboxPrimitive.Root {...props} />
24+ </ComboboxContext.Provider>
25+ );
26+}
27+
28+function ComboboxChipsInput({
29+ className,
30+ size,
31+ ...props
32+}: Omit<ComboboxPrimitive.Input.Props, "size"> & {
33+ size?: "sm" | "default" | "lg" | number;
34+ ref?: React.Ref<HTMLInputElement>;
35+}) {
36+ const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;
37+
38+ return (
39+ <ComboboxPrimitive.Input
40+ className={cn(
41+ "min-w-12 flex-1 text-base outline-none sm:text-sm [[data-slot=combobox-chip]+&]:ps-0.5",
42+ sizeValue === "sm" ? "ps-1.5" : "ps-2",
43+ className,
44+ )}
45+ data-size={typeof sizeValue === "string" ? sizeValue : undefined}
46+ data-slot="combobox-chips-input"
47+ size={typeof sizeValue === "number" ? sizeValue : undefined}
48+ {...props}
49+ />
50+ );
51+}
52+
53+function ComboboxInput({
54+ className,
55+ showTrigger = true,
56+ showClear = false,
57+ startAddon,
58+ size,
59+ ...props
60+}: Omit<ComboboxPrimitive.Input.Props, "size"> & {
61+ showTrigger?: boolean;
62+ showClear?: boolean;
63+ startAddon?: React.ReactNode;
64+ size?: "sm" | "default" | "lg" | number;
65+ ref?: React.Ref<HTMLInputElement>;
66+}) {
67+ const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;
68+
69+ return (
70+ <div className="relative not-has-[>*.w-full]:w-fit w-full text-foreground has-disabled:opacity-64">
71+ {startAddon && (
72+ <div
73+ aria-hidden="true"
74+ className="[&_svg]:-mx-0.5 pointer-events-none absolute inset-y-0 start-px z-10 flex items-center ps-[calc(--spacing(3)-1px)] opacity-80 has-[+[data-size=sm]]:ps-[calc(--spacing(2.5)-1px)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4"
75+ data-slot="combobox-start-addon"
76+ >
77+ {startAddon}
78+ </div>
79+ )}
80+ <ComboboxPrimitive.Input
81+ className={cn(
82+ startAddon &&
83+ "data-[size=sm]:*:data-[slot=combobox-input]:ps-[calc(--spacing(7.5)-1px)] *:data-[slot=combobox-input]:ps-[calc(--spacing(8.5)-1px)] sm:data-[size=sm]:*:data-[slot=combobox-input]:ps-[calc(--spacing(7)-1px)] sm:*:data-[slot=combobox-input]:ps-[calc(--spacing(8)-1px)]",
84+ sizeValue === "sm"
85+ ? "has-[+[data-slot=combobox-trigger],+[data-slot=combobox-clear]]:*:data-[slot=combobox-input]:pe-6.5"
86+ : "has-[+[data-slot=combobox-trigger],+[data-slot=combobox-clear]]:*:data-[slot=combobox-input]:pe-7",
87+ className,
88+ )}
89+ data-slot="combobox-input"
90+ render={
91+ <Input
92+ className="has-disabled:opacity-100"
93+ nativeInput
94+ size={sizeValue}
95+ />
96+ }
97+ {...props}
98+ />
99+ {showTrigger && (
100+ <ComboboxTrigger
101+ className={cn(
102+ "-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-opacity pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=combobox-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
103+ sizeValue === "sm" ? "end-0" : "end-0.5",
104+ )}
105+ >
106+ <ChevronsUpDownIcon />
107+ </ComboboxTrigger>
108+ )}
109+ {showClear && (
110+ <ComboboxClear
111+ className={cn(
112+ "-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-opacity pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=combobox-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
113+ sizeValue === "sm" ? "end-0" : "end-0.5",
114+ )}
115+ >
116+ <XIcon />
117+ </ComboboxClear>
118+ )}
119+ </div>
120+ );
121+}
122+
123+function ComboboxTrigger({
124+ className,
125+ ...props
126+}: ComboboxPrimitive.Trigger.Props) {
127+ return (
128+ <ComboboxPrimitive.Trigger
129+ className={className}
130+ data-slot="combobox-trigger"
131+ {...props}
132+ />
133+ );
134+}
135+
136+function ComboboxPopup({
137+ className,
138+ children,
139+ side = "bottom",
140+ sideOffset = 4,
141+ alignOffset,
142+ align = "start",
143+ ...props
144+}: ComboboxPrimitive.Popup.Props & {
145+ align?: ComboboxPrimitive.Positioner.Props["align"];
146+ sideOffset?: ComboboxPrimitive.Positioner.Props["sideOffset"];
147+ alignOffset?: ComboboxPrimitive.Positioner.Props["alignOffset"];
148+ side?: ComboboxPrimitive.Positioner.Props["side"];
149+}) {
150+ const { chipsRef } = React.useContext(ComboboxContext);
151+
152+ return (
153+ <ComboboxPrimitive.Portal>
154+ <ComboboxPrimitive.Positioner
155+ align={align}
156+ alignOffset={alignOffset}
157+ anchor={chipsRef}
158+ className="z-50 select-none"
159+ data-slot="combobox-positioner"
160+ side={side}
161+ sideOffset={sideOffset}
162+ >
163+ <span
164+ className={cn(
165+ "relative flex max-h-full min-w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
166+ className,
167+ )}
168+ >
169+ <ComboboxPrimitive.Popup
170+ className="flex max-h-[min(var(--available-height),23rem)] flex-1 flex-col text-foreground"
171+ data-slot="combobox-popup"
172+ {...props}
173+ >
174+ {children}
175+ </ComboboxPrimitive.Popup>
176+ </span>
177+ </ComboboxPrimitive.Positioner>
178+ </ComboboxPrimitive.Portal>
179+ );
180+}
181+
182+function ComboboxItem({
183+ className,
184+ children,
185+ ...props
186+}: ComboboxPrimitive.Item.Props) {
187+ return (
188+ <ComboboxPrimitive.Item
189+ className={cn(
190+ "grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
191+ className,
192+ )}
193+ data-slot="combobox-item"
194+ {...props}
195+ >
196+ <ComboboxPrimitive.ItemIndicator className="col-start-1">
197+ <svg
198+ fill="none"
199+ height="24"
200+ stroke="currentColor"
201+ strokeLinecap="round"
202+ strokeLinejoin="round"
203+ strokeWidth="2"
204+ viewBox="0 0 24 24"
205+ width="24"
206+ xmlns="http://www.w3.org/1500/svg"
207+ >
208+ <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
209+ </svg>
210+ </ComboboxPrimitive.ItemIndicator>
211+ <div className="col-start-2">{children}</div>
212+ </ComboboxPrimitive.Item>
213+ );
214+}
215+
216+function ComboboxSeparator({
217+ className,
218+ ...props
219+}: ComboboxPrimitive.Separator.Props) {
220+ return (
221+ <ComboboxPrimitive.Separator
222+ className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}
223+ data-slot="combobox-separator"
224+ {...props}
225+ />
226+ );
227+}
228+
229+function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
230+ return (
231+ <ComboboxPrimitive.Group
232+ className={cn("[[role=group]+&]:mt-1.5", className)}
233+ data-slot="combobox-group"
234+ {...props}
235+ />
236+ );
237+}
238+
239+function ComboboxGroupLabel({
240+ className,
241+ ...props
242+}: ComboboxPrimitive.GroupLabel.Props) {
243+ return (
244+ <ComboboxPrimitive.GroupLabel
245+ className={cn(
246+ "px-2 py-1.5 font-medium text-muted-foreground text-xs",
247+ className,
248+ )}
249+ data-slot="combobox-group-label"
250+ {...props}
251+ />
252+ );
253+}
254+
255+function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
256+ return (
257+ <ComboboxPrimitive.Empty
258+ className={cn(
259+ "not-empty:p-2 text-center text-base text-muted-foreground sm:text-sm",
260+ className,
261+ )}
262+ data-slot="combobox-empty"
263+ {...props}
264+ />
265+ );
266+}
267+
268+function ComboboxRow({ className, ...props }: ComboboxPrimitive.Row.Props) {
269+ return (
270+ <ComboboxPrimitive.Row
271+ className={className}
272+ data-slot="combobox-row"
273+ {...props}
274+ />
275+ );
276+}
277+
278+function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
279+ return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
280+}
281+
282+function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
283+ return (
284+ <ScrollArea scrollbarGutter scrollFade>
285+ <ComboboxPrimitive.List
286+ className={cn(
287+ "not-empty:scroll-py-1 not-empty:px-1 not-empty:py-1 in-data-has-overflow-y:pe-3",
288+ className,
289+ )}
290+ data-slot="combobox-list"
291+ {...props}
292+ />
293+ </ScrollArea>
294+ );
295+}
296+
297+function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
298+ return (
299+ <ComboboxPrimitive.Clear
300+ className={className}
301+ data-slot="combobox-clear"
302+ {...props}
303+ />
304+ );
305+}
306+
307+function ComboboxStatus({
308+ className,
309+ ...props
310+}: ComboboxPrimitive.Status.Props) {
311+ return (
312+ <ComboboxPrimitive.Status
313+ className={cn(
314+ "px-3 py-2 font-medium text-muted-foreground text-xs empty:m-0 empty:p-0",
315+ className,
316+ )}
317+ data-slot="combobox-status"
318+ {...props}
319+ />
320+ );
321+}
322+
323+function ComboboxCollection(props: ComboboxPrimitive.Collection.Props) {
324+ return (
325+ <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
326+ );
327+}
328+
329+function ComboboxChips({
330+ className,
331+ children,
332+ startAddon,
333+ ...props
334+}: ComboboxPrimitive.Chips.Props & {
335+ startAddon?: React.ReactNode;
336+}) {
337+ const { chipsRef } = React.useContext(ComboboxContext);
338+
339+ return (
340+ <ComboboxPrimitive.Chips
341+ className={cn(
342+ "relative inline-flex min-h-9 w-full flex-wrap gap-1 rounded-lg border border-input bg-background not-dark:bg-clip-padding p-[calc(--spacing(1)-1px)] text-base shadow-xs/5 outline-none ring-ring/24 transition-shadow *:min-h-7 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] focus-within:border-ring focus-within:ring-[3px] has-disabled:pointer-events-none has-data-[size=lg]:min-h-10 has-data-[size=sm]:min-h-8 has-aria-invalid:border-destructive/36 has-autofill:bg-foreground/4 has-disabled:opacity-64 has-[:disabled,:focus-within,[aria-invalid]]:shadow-none focus-within:has-aria-invalid:border-destructive/64 focus-within:has-aria-invalid:ring-destructive/16 has-data-[size=lg]:*:min-h-8 has-data-[size=sm]:*:min-h-6 sm:min-h-8 sm:text-sm sm:has-data-[size=lg]:min-h-9 sm:has-data-[size=sm]:min-h-7 sm:*:min-h-6 sm:has-data-[size=lg]:*:min-h-7 sm:has-data-[size=sm]:*:min-h-5 dark:not-has-disabled:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]",
343+ className,
344+ )}
345+ data-slot="combobox-chips"
346+ onMouseDown={(e) => {
347+ const target = e.target as HTMLElement;
348+ const isChip = target.closest('[data-slot="combobox-chip"]');
349+ if (isChip || !chipsRef?.current) return;
350+ e.preventDefault();
351+ const input: HTMLInputElement | null =
352+ chipsRef.current.querySelector("input");
353+ if (input && !chipsRef.current.querySelector("input:focus")) {
354+ input.focus();
355+ }
356+ }}
357+ ref={chipsRef as React.Ref<HTMLDivElement> | null}
358+ {...props}
359+ >
360+ {startAddon && (
361+ <div
362+ aria-hidden="true"
363+ className="[&_svg]:-ms-0.5 [&_svg]:-me-1.5 flex shrink-0 items-center ps-2 opacity-80 has-[~[data-size=sm]]:has-[+[data-slot=combobox-chip]]:pe-1.5 has-[~[data-size=sm]]:ps-1.5 has-[+[data-slot=combobox-chip]]:pe-2 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none"
364+ data-slot="combobox-start-addon"
365+ >
366+ {startAddon}
367+ </div>
368+ )}
369+ {children}
370+ </ComboboxPrimitive.Chips>
371+ );
372+}
373+
374+function ComboboxChip({ children, ...props }: ComboboxPrimitive.Chip.Props) {
375+ return (
376+ <ComboboxPrimitive.Chip
377+ className="flex items-center rounded-[calc(var(--radius-md)-1px)] bg-accent ps-2 font-medium text-accent-foreground text-sm outline-none sm:text-xs/(--text-xs--line-height) [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5"
378+ data-slot="combobox-chip"
379+ {...props}
380+ >
381+ {children}
382+ <ComboboxChipRemove />
383+ </ComboboxPrimitive.Chip>
384+ );
385+}
386+
387+function ComboboxChipRemove(props: ComboboxPrimitive.ChipRemove.Props) {
388+ return (
389+ <ComboboxPrimitive.ChipRemove
390+ aria-label="Remove"
391+ className="h-full shrink-0 cursor-pointer px-1.5 opacity-80 hover:opacity-100 [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5"
392+ data-slot="combobox-chip-remove"
393+ {...props}
394+ >
395+ <XIcon />
396+ </ComboboxPrimitive.ChipRemove>
397+ );
398+}
399+
400+const useComboboxFilter = ComboboxPrimitive.useFilter;
401+
402+export {
403+ Combobox,
404+ ComboboxChipsInput,
405+ ComboboxInput,
406+ ComboboxTrigger,
407+ ComboboxPopup,
408+ ComboboxItem,
409+ ComboboxSeparator,
410+ ComboboxGroup,
411+ ComboboxGroupLabel,
412+ ComboboxEmpty,
413+ ComboboxValue,
414+ ComboboxList,
415+ ComboboxClear,
416+ ComboboxStatus,
417+ ComboboxRow,
418+ ComboboxCollection,
419+ ComboboxChips,
420+ ComboboxChip,
421+ useComboboxFilter,
422+};
apps/renderer/src/components/ui/command.tsx+262−0
@@ -0,0 +1,262 @@
1+import { Dialog as CommandDialogPrimitive } from "@base-ui/react/dialog";
2+import { SearchIcon } from "lucide-react";
3+import type * as React from "react";
4+import {
5+ Autocomplete,
6+ AutocompleteCollection,
7+ AutocompleteEmpty,
8+ AutocompleteGroup,
9+ AutocompleteGroupLabel,
10+ AutocompleteInput,
11+ AutocompleteItem,
12+ AutocompleteList,
13+ AutocompleteSeparator,
14+} from "#/components/ui/autocomplete";
15+import { cn } from "#/lib/utils";
16+
17+const CommandDialog = CommandDialogPrimitive.Root;
18+
19+const CommandDialogPortal = CommandDialogPrimitive.Portal;
20+
21+const CommandCreateHandle = CommandDialogPrimitive.createHandle;
22+
23+function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) {
24+ return (
25+ <CommandDialogPrimitive.Trigger
26+ data-slot="command-dialog-trigger"
27+ {...props}
28+ />
29+ );
30+}
31+
32+function CommandDialogBackdrop({
33+ className,
34+ ...props
35+}: CommandDialogPrimitive.Backdrop.Props) {
36+ return (
37+ <CommandDialogPrimitive.Backdrop
38+ className={cn(
39+ "fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
40+ className,
41+ )}
42+ data-slot="command-dialog-backdrop"
43+ {...props}
44+ />
45+ );
46+}
47+
48+function CommandDialogViewport({
49+ className,
50+ ...props
51+}: CommandDialogPrimitive.Viewport.Props) {
52+ return (
53+ <CommandDialogPrimitive.Viewport
54+ className={cn(
55+ "fixed inset-0 z-50 flex flex-col items-center px-4 py-[max(--spacing(4),4vh)] sm:py-[10vh]",
56+ className,
57+ )}
58+ data-slot="command-dialog-viewport"
59+ {...props}
60+ />
61+ );
62+}
63+
64+function CommandDialogPopup({
65+ className,
66+ children,
67+ ...props
68+}: CommandDialogPrimitive.Popup.Props) {
69+ return (
70+ <CommandDialogPortal>
71+ <CommandDialogBackdrop />
72+ <CommandDialogViewport>
73+ <CommandDialogPrimitive.Popup
74+ className={cn(
75+ "-translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-105 min-h-0 w-full min-w-0 max-w-xl scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border bg-popover not-dark:bg-clip-padding text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 outline-none transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:bg-muted/72 before:shadow-[0_1px_--theme(--color-black/4%)] data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 **:data-[slot=scroll-area-viewport]:data-has-overflow-y:pe-1 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
76+ className,
77+ )}
78+ data-slot="command-dialog-popup"
79+ {...props}
80+ >
81+ {children}
82+ </CommandDialogPrimitive.Popup>
83+ </CommandDialogViewport>
84+ </CommandDialogPortal>
85+ );
86+}
87+
88+function Command({
89+ autoHighlight = "always",
90+ keepHighlight = true,
91+ ...props
92+}: React.ComponentProps<typeof Autocomplete>) {
93+ return (
94+ <Autocomplete
95+ autoHighlight={autoHighlight}
96+ inline
97+ keepHighlight={keepHighlight}
98+ open
99+ {...props}
100+ />
101+ );
102+}
103+
104+function CommandInput({
105+ className,
106+ placeholder = undefined,
107+ ...props
108+}: React.ComponentProps<typeof AutocompleteInput>) {
109+ return (
110+ <div className="px-2.5 py-1.5">
111+ <AutocompleteInput
112+ autoFocus
113+ className={cn(
114+ "border-transparent! bg-transparent! shadow-none before:hidden has-focus-visible:ring-0",
115+ className,
116+ )}
117+ placeholder={placeholder}
118+ size="lg"
119+ startAddon={<SearchIcon />}
120+ {...props}
121+ />
122+ </div>
123+ );
124+}
125+
126+function CommandList({
127+ className,
128+ ...props
129+}: React.ComponentProps<typeof AutocompleteList>) {
130+ return (
131+ <AutocompleteList
132+ className={cn("not-empty:scroll-py-2 not-empty:p-2", className)}
133+ data-slot="command-list"
134+ {...props}
135+ />
136+ );
137+}
138+
139+function CommandEmpty({
140+ className,
141+ ...props
142+}: React.ComponentProps<typeof AutocompleteEmpty>) {
143+ return (
144+ <AutocompleteEmpty
145+ className={cn("not-empty:py-6", className)}
146+ data-slot="command-empty"
147+ {...props}
148+ />
149+ );
150+}
151+
152+function CommandPanel({ className, ...props }: React.ComponentProps<"div">) {
153+ return (
154+ <div
155+ className="-mx-px not-has-[+[data-slot=command-footer]]:-mb-px relative min-h-0 rounded-t-xl not-has-[+[data-slot=command-footer]]:rounded-b-2xl border border-b-0 bg-popover bg-clip-padding shadow-xs/5 [clip-path:inset(0_1px)] not-has-[+[data-slot=command-footer]]:[clip-path:inset(0_1px_1px_1px_round_0_0_calc(var(--radius-2xl)-1px)_calc(var(--radius-2xl)-1px))] before:pointer-events-none before:absolute before:inset-0 before:rounded-t-[calc(var(--radius-xl)-1px)] **:data-[slot=scroll-area-scrollbar]:mt-2"
156+ {...props}
157+ />
158+ );
159+}
160+
161+function CommandGroup({
162+ className,
163+ ...props
164+}: React.ComponentProps<typeof AutocompleteGroup>) {
165+ return (
166+ <AutocompleteGroup
167+ className={className}
168+ data-slot="command-group"
169+ {...props}
170+ />
171+ );
172+}
173+
174+function CommandGroupLabel({
175+ className,
176+ ...props
177+}: React.ComponentProps<typeof AutocompleteGroupLabel>) {
178+ return (
179+ <AutocompleteGroupLabel
180+ className={className}
181+ data-slot="command-group-label"
182+ {...props}
183+ />
184+ );
185+}
186+
187+function CommandCollection({
188+ ...props
189+}: React.ComponentProps<typeof AutocompleteCollection>) {
190+ return <AutocompleteCollection data-slot="command-collection" {...props} />;
191+}
192+
193+function CommandItem({
194+ className,
195+ ...props
196+}: React.ComponentProps<typeof AutocompleteItem>) {
197+ return (
198+ <AutocompleteItem
199+ className={cn("py-1.5", className)}
200+ data-slot="command-item"
201+ {...props}
202+ />
203+ );
204+}
205+
206+function CommandSeparator({
207+ className,
208+ ...props
209+}: React.ComponentProps<typeof AutocompleteSeparator>) {
210+ return (
211+ <AutocompleteSeparator
212+ className={cn("my-2", className)}
213+ data-slot="command-separator"
214+ {...props}
215+ />
216+ );
217+}
218+
219+function CommandShortcut({ className, ...props }: React.ComponentProps<"kbd">) {
220+ return (
221+ <kbd
222+ className={cn(
223+ "ms-auto font-medium font-sans text-muted-foreground/72 text-xs tracking-widest",
224+ className,
225+ )}
226+ data-slot="command-shortcut"
227+ {...props}
228+ />
229+ );
230+}
231+
232+function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {
233+ return (
234+ <div
235+ className={cn(
236+ "flex items-center justify-between gap-2 rounded-b-[calc(var(--radius-2xl)-1px)] border-t px-5 py-3 text-muted-foreground text-xs",
237+ className,
238+ )}
239+ data-slot="command-footer"
240+ {...props}
241+ />
242+ );
243+}
244+
245+export {
246+ CommandCreateHandle,
247+ Command,
248+ CommandCollection,
249+ CommandDialog,
250+ CommandDialogPopup,
251+ CommandDialogTrigger,
252+ CommandEmpty,
253+ CommandFooter,
254+ CommandGroup,
255+ CommandGroupLabel,
256+ CommandInput,
257+ CommandItem,
258+ CommandList,
259+ CommandPanel,
260+ CommandSeparator,
261+ CommandShortcut,
262+};
apps/renderer/src/components/ui/dialog.tsx+194−0
@@ -0,0 +1,194 @@
1+import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
2+import { XIcon } from "lucide-react";
3+import { Button } from "#/components/ui/button";
4+import { ScrollArea } from "#/components/ui/scroll-area";
5+import { cn } from "#/lib/utils";
6+
7+const DialogCreateHandle = DialogPrimitive.createHandle;
8+
9+const Dialog = DialogPrimitive.Root;
10+
11+const DialogPortal = DialogPrimitive.Portal;
12+
13+function DialogTrigger(props: DialogPrimitive.Trigger.Props) {
14+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
15+}
16+
17+function DialogClose(props: DialogPrimitive.Close.Props) {
18+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
19+}
20+
21+function DialogBackdrop({
22+ className,
23+ ...props
24+}: DialogPrimitive.Backdrop.Props) {
25+ return (
26+ <DialogPrimitive.Backdrop
27+ className={cn(
28+ "fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
29+ className,
30+ )}
31+ data-slot="dialog-backdrop"
32+ {...props}
33+ />
34+ );
35+}
36+
37+function DialogViewport({
38+ className,
39+ ...props
40+}: DialogPrimitive.Viewport.Props) {
41+ return (
42+ <DialogPrimitive.Viewport
43+ className={cn(
44+ "fixed inset-0 z-50 grid grid-rows-[1fr_auto_3fr] justify-items-center p-4",
45+ className,
46+ )}
47+ data-slot="dialog-viewport"
48+ {...props}
49+ />
50+ );
51+}
52+
53+function DialogPopup({
54+ className,
55+ children,
56+ showCloseButton = true,
57+ bottomStickOnMobile = true,
58+ ...props
59+}: DialogPrimitive.Popup.Props & {
60+ showCloseButton?: boolean;
61+ bottomStickOnMobile?: boolean;
62+}) {
63+ return (
64+ <DialogPortal>
65+ <DialogBackdrop />
66+ <DialogViewport
67+ className={cn(
68+ bottomStickOnMobile &&
69+ "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12",
70+ )}
71+ >
72+ <DialogPrimitive.Popup
73+ className={cn(
74+ "-translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 max-w-lg scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border bg-popover not-dark:bg-clip-padding text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
75+ bottomStickOnMobile &&
76+ "max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4 max-sm:before:hidden max-sm:before:rounded-none",
77+ className,
78+ )}
79+ data-slot="dialog-popup"
80+ {...props}
81+ >
82+ {children}
83+ {showCloseButton && (
84+ <DialogPrimitive.Close
85+ aria-label="Close"
86+ className="absolute end-2 top-2"
87+ render={<Button size="icon" variant="ghost" />}
88+ >
89+ <XIcon />
90+ </DialogPrimitive.Close>
91+ )}
92+ </DialogPrimitive.Popup>
93+ </DialogViewport>
94+ </DialogPortal>
95+ );
96+}
97+
98+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
99+ return (
100+ <div
101+ className={cn(
102+ "flex flex-col gap-2 p-6 in-[[data-slot=dialog-popup]:has([data-slot=dialog-panel])]:pb-3 max-sm:pb-4",
103+ className,
104+ )}
105+ data-slot="dialog-header"
106+ {...props}
107+ />
108+ );
109+}
110+
111+function DialogFooter({
112+ className,
113+ variant = "default",
114+ ...props
115+}: React.ComponentProps<"div"> & {
116+ variant?: "default" | "bare";
117+}) {
118+ return (
119+ <div
120+ className={cn(
121+ "flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end sm:rounded-b-[calc(var(--radius-2xl)-1px)]",
122+ variant === "default" && "border-t bg-muted/72 py-4",
123+ variant === "bare" &&
124+ "in-[[data-slot=dialog-popup]:has([data-slot=dialog-panel])]:pt-3 pt-4 pb-6",
125+ className,
126+ )}
127+ data-slot="dialog-footer"
128+ {...props}
129+ />
130+ );
131+}
132+
133+function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
134+ return (
135+ <DialogPrimitive.Title
136+ className={cn(
137+ "font-heading font-semibold text-xl leading-none",
138+ className,
139+ )}
140+ data-slot="dialog-title"
141+ {...props}
142+ />
143+ );
144+}
145+
146+function DialogDescription({
147+ className,
148+ ...props
149+}: DialogPrimitive.Description.Props) {
150+ return (
151+ <DialogPrimitive.Description
152+ className={cn("text-muted-foreground text-sm", className)}
153+ data-slot="dialog-description"
154+ {...props}
155+ />
156+ );
157+}
158+
159+function DialogPanel({
160+ className,
161+ scrollFade = true,
162+ ...props
163+}: React.ComponentProps<"div"> & { scrollFade?: boolean }) {
164+ return (
165+ <ScrollArea scrollFade={scrollFade}>
166+ <div
167+ className={cn(
168+ "p-6 in-[[data-slot=dialog-popup]:has([data-slot=dialog-header])]:pt-1 in-[[data-slot=dialog-popup]:has([data-slot=dialog-footer]:not(.border-t))]:pb-1",
169+ className,
170+ )}
171+ data-slot="dialog-panel"
172+ {...props}
173+ />
174+ </ScrollArea>
175+ );
176+}
177+
178+export {
179+ DialogCreateHandle,
180+ Dialog,
181+ DialogTrigger,
182+ DialogPortal,
183+ DialogClose,
184+ DialogBackdrop,
185+ DialogBackdrop as DialogOverlay,
186+ DialogPopup,
187+ DialogPopup as DialogContent,
188+ DialogHeader,
189+ DialogFooter,
190+ DialogTitle,
191+ DialogDescription,
192+ DialogPanel,
193+ DialogViewport,
194+};
apps/renderer/src/components/ui/empty.tsx+127−0
@@ -0,0 +1,127 @@
1+import { type VariantProps, cva } from "class-variance-authority";
2+
3+import { cn } from "#/lib/utils";
4+
5+function Empty({ className, ...props }: React.ComponentProps<"div">) {
6+ return (
7+ <div
8+ className={cn(
9+ "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 text-balance p-6 text-center md:p-12",
10+ className,
11+ )}
12+ data-slot="empty"
13+ {...props}
14+ />
15+ );
16+}
17+
18+function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
19+ return (
20+ <div
21+ className={cn(
22+ "flex max-w-sm flex-col items-center text-center",
23+ className,
24+ )}
25+ data-slot="empty-header"
26+ {...props}
27+ />
28+ );
29+}
30+
31+const emptyMediaVariants = cva(
32+ "flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
33+ {
34+ defaultVariants: {
35+ variant: "default",
36+ },
37+ variants: {
38+ variant: {
39+ default: "bg-transparent",
40+ icon: "relative flex size-9 shrink-0 items-center justify-center rounded-md border bg-card not-dark:bg-clip-padding text-foreground shadow-sm/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-md)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='size-'])]:size-4.5",
41+ },
42+ },
43+ },
44+);
45+
46+function EmptyMedia({
47+ className,
48+ variant = "default",
49+ ...props
50+}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
51+ return (
52+ <div
53+ className={cn("relative mb-6", className)}
54+ data-slot="empty-media"
55+ data-variant={variant}
56+ {...props}
57+ >
58+ {variant === "icon" && (
59+ <>
60+ <div
61+ aria-hidden="true"
62+ className={cn(
63+ emptyMediaVariants({ className, variant }),
64+ "-translate-x-0.5 -rotate-10 pointer-events-none absolute bottom-px origin-bottom-left scale-84 shadow-none",
65+ )}
66+ />
67+ <div
68+ aria-hidden="true"
69+ className={cn(
70+ emptyMediaVariants({ className, variant }),
71+ "pointer-events-none absolute bottom-px origin-bottom-right translate-x-0.5 rotate-10 scale-84 shadow-none",
72+ )}
73+ />
74+ </>
75+ )}
76+ <div
77+ className={cn(emptyMediaVariants({ className, variant }))}
78+ {...props}
79+ />
80+ </div>
81+ );
82+}
83+
84+function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
85+ return (
86+ <div
87+ className={cn("font-heading font-semibold text-xl", className)}
88+ data-slot="empty-title"
89+ {...props}
90+ />
91+ );
92+}
93+
94+function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
95+ return (
96+ <div
97+ className={cn(
98+ "text-muted-foreground text-sm [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4 [[data-slot=empty-title]+&]:mt-1",
99+ className,
100+ )}
101+ data-slot="empty-description"
102+ {...props}
103+ />
104+ );
105+}
106+
107+function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
108+ return (
109+ <div
110+ className={cn(
111+ "flex w-full min-w-0 max-w-sm flex-col items-center gap-4 text-balance text-sm",
112+ className,
113+ )}
114+ data-slot="empty-content"
115+ {...props}
116+ />
117+ );
118+}
119+
120+export {
121+ Empty,
122+ EmptyHeader,
123+ EmptyTitle,
124+ EmptyDescription,
125+ EmptyContent,
126+ EmptyMedia,
127+};
apps/renderer/src/components/ui/field.tsx+72−0
@@ -0,0 +1,72 @@
1+import { Field as FieldPrimitive } from "@base-ui/react/field";
2+
3+import { cn } from "#/lib/utils";
4+
5+function Field({ className, ...props }: FieldPrimitive.Root.Props) {
6+ return (
7+ <FieldPrimitive.Root
8+ className={cn("flex flex-col items-start gap-2", className)}
9+ data-slot="field"
10+ {...props}
11+ />
12+ );
13+}
14+
15+function FieldLabel({ className, ...props }: FieldPrimitive.Label.Props) {
16+ return (
17+ <FieldPrimitive.Label
18+ className={cn(
19+ "inline-flex items-center gap-2 font-medium text-base/4.5 text-foreground sm:text-sm/4",
20+ className,
21+ )}
22+ data-slot="field-label"
23+ {...props}
24+ />
25+ );
26+}
27+
28+function FieldItem({ className, ...props }: FieldPrimitive.Item.Props) {
29+ return (
30+ <FieldPrimitive.Item
31+ className={cn("flex", className)}
32+ data-slot="field-item"
33+ {...props}
34+ />
35+ );
36+}
37+
38+function FieldDescription({
39+ className,
40+ ...props
41+}: FieldPrimitive.Description.Props) {
42+ return (
43+ <FieldPrimitive.Description
44+ className={cn("text-muted-foreground text-xs", className)}
45+ data-slot="field-description"
46+ {...props}
47+ />
48+ );
49+}
50+
51+function FieldError({ className, ...props }: FieldPrimitive.Error.Props) {
52+ return (
53+ <FieldPrimitive.Error
54+ className={cn("text-destructive-foreground text-xs", className)}
55+ data-slot="field-error"
56+ {...props}
57+ />
58+ );
59+}
60+
61+const FieldControl = FieldPrimitive.Control;
62+const FieldValidity = FieldPrimitive.Validity;
63+
64+export {
65+ Field,
66+ FieldLabel,
67+ FieldDescription,
68+ FieldError,
69+ FieldControl,
70+ FieldItem,
71+ FieldValidity,
72+};
apps/renderer/src/components/ui/fieldset.tsx+27−0
@@ -0,0 +1,27 @@
1+import { Fieldset as FieldsetPrimitive } from "@base-ui/react/fieldset";
2+
3+import { cn } from "#/lib/utils";
4+
5+function Fieldset({ className, ...props }: FieldsetPrimitive.Root.Props) {
6+ return (
7+ <FieldsetPrimitive.Root
8+ className={cn("flex w-full max-w-64 flex-col gap-6", className)}
9+ data-slot="fieldset"
10+ {...props}
11+ />
12+ );
13+}
14+function FieldsetLegend({
15+ className,
16+ ...props
17+}: FieldsetPrimitive.Legend.Props) {
18+ return (
19+ <FieldsetPrimitive.Legend
20+ className={cn("font-semibold text-foreground", className)}
21+ data-slot="fieldset-legend"
22+ {...props}
23+ />
24+ );
25+}
26+
27+export { Fieldset, FieldsetLegend };
apps/renderer/src/components/ui/form.tsx+15−0
@@ -0,0 +1,15 @@
1+import { Form as FormPrimitive } from "@base-ui/react/form";
2+
3+import { cn } from "#/lib/utils";
4+
5+function Form({ className, ...props }: FormPrimitive.Props) {
6+ return (
7+ <FormPrimitive
8+ className={cn("flex w-full flex-col gap-4", className)}
9+ data-slot="form"
10+ {...props}
11+ />
12+ );
13+}
14+
15+export { Form };
apps/renderer/src/components/ui/input-group.tsx+99−0
@@ -0,0 +1,99 @@
1+import { type VariantProps, cva } from "class-variance-authority";
2+import type * as React from "react";
3+
4+import { Input, type InputProps } from "#/components/ui/input";
5+import { Textarea, type TextareaProps } from "#/components/ui/textarea";
6+import { cn } from "#/lib/utils";
7+
8+function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
9+ return (
10+ <div
11+ className={cn(
12+ "relative inline-flex w-full min-w-0 items-center rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/4%)] has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-autofill:bg-foreground/4 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/6%)] has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--radius-md)-1px)]",
13+ className,
14+ )}
15+ data-slot="input-group"
16+ role="group"
17+ {...props}
18+ />
19+ );
20+}
21+
22+const inputGroupAddonVariants = cva(
23+ "[&_svg]:-mx-0.5 flex h-auto cursor-text select-none items-center justify-center gap-2 leading-none [&>kbd]:rounded-[calc(var(--radius)-5px)] in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 not-has-[button]:**:[svg:not([class*='opacity-'])]:opacity-80",
24+ {
25+ defaultVariants: {
26+ align: "inline-start",
27+ },
28+ variants: {
29+ align: {
30+ "block-end":
31+ "order-last w-full justify-start px-[calc(--spacing(3)-1px)] pb-[calc(--spacing(3)-1px)] [.border-t]:pt-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
32+ "block-start":
33+ "order-first w-full justify-start px-[calc(--spacing(3)-1px)] pt-[calc(--spacing(3)-1px)] [.border-b]:pb-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]",
34+ "inline-end":
35+ "has-[>:last-child[data-slot=badge]]:-me-1.5 has-[>button]:-me-2 order-last pe-[calc(--spacing(3)-1px)] has-[>kbd:last-child]:me-[-0.35rem] [[data-size=sm]+&]:pe-[calc(--spacing(2.5)-1px)]",
36+ "inline-start":
37+ "has-[>:last-child[data-slot=badge]]:-ms-1.5 has-[>button]:-ms-2 order-first ps-[calc(--spacing(3)-1px)] has-[>kbd:last-child]:ms-[-0.35rem] [[data-size=sm]+&]:ps-[calc(--spacing(2.5)-1px)]",
38+ },
39+ },
40+ },
41+);
42+
43+function InputGroupAddon({
44+ className,
45+ align = "inline-start",
46+ ...props
47+}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
48+ return (
49+ <div
50+ className={cn(inputGroupAddonVariants({ align }), className)}
51+ data-align={align}
52+ data-slot="input-group-addon"
53+ onMouseDown={(e) => {
54+ const target = e.target as HTMLElement;
55+ const isInteractive = target.closest(
56+ "button, a, input, select, textarea, [role='button'], [role='combobox'], [role='listbox'], [data-slot='select-trigger']",
57+ );
58+ if (isInteractive) return;
59+ e.preventDefault();
60+ const parent = e.currentTarget.parentElement;
61+ const input = parent?.querySelector<
62+ HTMLInputElement | HTMLTextAreaElement
63+ >("input, textarea");
64+ if (input && !parent?.querySelector("input:focus, textarea:focus")) {
65+ input.focus();
66+ }
67+ }}
68+ {...props}
69+ />
70+ );
71+}
72+
73+function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
74+ return (
75+ <span
76+ className={cn(
77+ "[&_svg]:-mx-0.5 line-clamp-1 flex items-center gap-2 text-muted-foreground leading-none in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
78+ className,
79+ )}
80+ {...props}
81+ />
82+ );
83+}
84+
85+function InputGroupInput({ className, ...props }: InputProps) {
86+ return <Input className={className} unstyled {...props} />;
87+}
88+
89+function InputGroupTextarea({ className, ...props }: TextareaProps) {
90+ return <Textarea className={className} unstyled {...props} />;
91+}
92+
93+export {
94+ InputGroup,
95+ InputGroupAddon,
96+ InputGroupText,
97+ InputGroupInput,
98+ InputGroupTextarea,
99+};
apps/renderer/src/components/ui/input.tsx+64−0
@@ -0,0 +1,64 @@
1+import { Input as InputPrimitive } from "@base-ui/react/input";
2+import type * as React from "react";
3+
4+import { cn } from "#/lib/utils";
5+
6+type InputProps = Omit<
7+ InputPrimitive.Props & React.RefAttributes<HTMLInputElement>,
8+ "size"
9+> & {
10+ size?: "sm" | "default" | "lg" | number;
11+ unstyled?: boolean;
12+ nativeInput?: boolean;
13+};
14+
15+function Input({
16+ className,
17+ size = "default",
18+ unstyled = false,
19+ nativeInput = false,
20+ ...props
21+}: InputProps) {
22+ const inputClassName = cn(
23+ "h-8.5 w-full min-w-0 rounded-[inherit] px-[calc(--spacing(3)-1px)] leading-8.5 outline-none placeholder:text-muted-foreground/72 sm:h-7.5 sm:leading-7.5 [transition:background-color_5000000s_ease-in-out_0s]",
24+ size === "sm" &&
25+ "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5",
26+ size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5",
27+ props.type === "search" &&
28+ "[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none",
29+ props.type === "file" &&
30+ "text-muted-foreground file:me-3 file:bg-transparent file:font-medium file:text-foreground file:text-sm",
31+ );
32+
33+ return (
34+ <span
35+ className={
36+ cn(
37+ !unstyled &&
38+ "relative inline-flex w-full rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-autofill:bg-foreground/4 has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]",
39+ className,
40+ ) || undefined
41+ }
42+ data-size={size}
43+ data-slot="input-control"
44+ >
45+ {nativeInput ? (
46+ <input
47+ className={inputClassName}
48+ data-slot="input"
49+ size={typeof size === "number" ? size : undefined}
50+ {...props}
51+ />
52+ ) : (
53+ <InputPrimitive
54+ className={inputClassName}
55+ data-slot="input"
56+ size={typeof size === "number" ? size : undefined}
57+ {...props}
58+ />
59+ )}
60+ </span>
61+ );
62+}
63+
64+export { Input, type InputProps };
apps/renderer/src/components/ui/kbd.tsx+28−0
@@ -0,0 +1,28 @@
1+import type * as React from "react";
2+
3+import { cn } from "#/lib/utils";
4+
5+function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
6+ return (
7+ <kbd
8+ className={cn(
9+ "pointer-events-none inline-flex h-5 min-w-5 select-none items-center justify-center gap-1 rounded bg-muted px-1 font-medium font-sans text-muted-foreground text-xs [&_svg:not([class*='size-'])]:size-3",
10+ className,
11+ )}
12+ data-slot="kbd"
13+ {...props}
14+ />
15+ );
16+}
17+
18+function KbdGroup({ className, ...props }: React.ComponentProps<"kbd">) {
19+ return (
20+ <kbd
21+ className={cn("inline-flex items-center gap-1", className)}
22+ data-slot="kbd-group"
23+ {...props}
24+ />
25+ );
26+}
27+
28+export { Kbd, KbdGroup };
apps/renderer/src/components/ui/label.tsx+26−0
@@ -0,0 +1,26 @@
1+import { mergeProps } from "@base-ui/react/merge-props";
2+import { useRender } from "@base-ui/react/use-render";
3+
4+import { cn } from "#/lib/utils";
5+
6+function Label({
7+ className,
8+ render,
9+ ...props
10+}: useRender.ComponentProps<"label">) {
11+ const defaultProps = {
12+ className: cn(
13+ "inline-flex items-center gap-2 text-base/4.5 sm:text-sm/4 font-medium text-foreground",
14+ className,
15+ ),
16+ "data-slot": "label",
17+ };
18+
19+ return useRender({
20+ defaultTagName: "label",
21+ props: mergeProps<"label">(defaultProps, props),
22+ render,
23+ });
24+}
25+
26+export { Label };
apps/renderer/src/components/ui/menu.tsx+308−0
@@ -0,0 +1,308 @@
1+import { Menu as MenuPrimitive } from "@base-ui/react/menu";
2+import { ChevronRightIcon } from "lucide-react";
3+import type * as React from "react";
4+import { cn } from "#/lib/utils";
5+
6+const MenuCreateHandle = MenuPrimitive.createHandle;
7+
8+const Menu = MenuPrimitive.Root;
9+
10+const MenuPortal = MenuPrimitive.Portal;
11+
12+function MenuTrigger(props: MenuPrimitive.Trigger.Props) {
13+ return <MenuPrimitive.Trigger data-slot="menu-trigger" {...props} />;
14+}
15+
16+function MenuPopup({
17+ children,
18+ className,
19+ sideOffset = 4,
20+ align = "center",
21+ alignOffset,
22+ side = "bottom",
23+ ...props
24+}: MenuPrimitive.Popup.Props & {
25+ align?: MenuPrimitive.Positioner.Props["align"];
26+ sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"];
27+ alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"];
28+ side?: MenuPrimitive.Positioner.Props["side"];
29+}) {
30+ return (
31+ <MenuPrimitive.Portal>
32+ <MenuPrimitive.Positioner
33+ align={align}
34+ alignOffset={alignOffset}
35+ className="z-50"
36+ data-slot="menu-positioner"
37+ side={side}
38+ sideOffset={sideOffset}
39+ >
40+ <MenuPrimitive.Popup
41+ className={cn(
42+ "relative flex not-[class*='w-']:min-w-32 origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 outline-none before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] focus:outline-none dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
43+ className,
44+ )}
45+ data-slot="menu-popup"
46+ {...props}
47+ >
48+ <div className="max-h-(--available-height) w-full overflow-y-auto p-1">
49+ {children}
50+ </div>
51+ </MenuPrimitive.Popup>
52+ </MenuPrimitive.Positioner>
53+ </MenuPrimitive.Portal>
54+ );
55+}
56+
57+function MenuGroup(props: MenuPrimitive.Group.Props) {
58+ return <MenuPrimitive.Group data-slot="menu-group" {...props} />;
59+}
60+
61+function MenuItem({
62+ className,
63+ inset,
64+ variant = "default",
65+ ...props
66+}: MenuPrimitive.Item.Props & {
67+ inset?: boolean;
68+ variant?: "default" | "destructive";
69+}) {
70+ return (
71+ <MenuPrimitive.Item
72+ className={cn(
73+ "[&>svg]:-mx-0.5 flex min-h-8 cursor-default select-none items-center gap-2 rounded-sm px-2 py-1 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-inset:ps-8 data-[variant=destructive]:text-destructive-foreground data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&>svg:not([class*='opacity-'])]:opacity-80 [&>svg:not([class*='size-'])]:size-4.5 sm:[&>svg:not([class*='size-'])]:size-4 [&>svg]:pointer-events-none [&>svg]:shrink-0",
74+ className,
75+ )}
76+ data-inset={inset}
77+ data-slot="menu-item"
78+ data-variant={variant}
79+ {...props}
80+ />
81+ );
82+}
83+
84+function MenuCheckboxItem({
85+ className,
86+ children,
87+ checked,
88+ variant = "default",
89+ ...props
90+}: MenuPrimitive.CheckboxItem.Props & {
91+ variant?: "default" | "switch";
92+}) {
93+ return (
94+ <MenuPrimitive.CheckboxItem
95+ checked={checked}
96+ className={cn(
97+ "grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default items-center gap-2 rounded-sm py-1 ps-2 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
98+ variant === "switch"
99+ ? "grid-cols-[1fr_auto] gap-4 pe-1.5"
100+ : "grid-cols-[1rem_1fr] pe-4",
101+ className,
102+ )}
103+ data-slot="menu-checkbox-item"
104+ {...props}
105+ >
106+ {variant === "switch" ? (
107+ <>
108+ <span className="col-start-1">{children}</span>
109+ <MenuPrimitive.CheckboxItemIndicator
110+ className="inset-shadow-[0_1px_--theme(--color-black/4%)] inline-flex h-[calc(var(--thumb-size)+2px)] w-[calc(var(--thumb-size)*2-2px)] shrink-0 items-center rounded-full p-px outline-none transition-[background-color,box-shadow] duration-200 [--thumb-size:--spacing(4)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-checked:bg-primary data-unchecked:bg-input data-disabled:opacity-64 sm:[--thumb-size:--spacing(3)]"
111+ keepMounted
112+ >
113+ <span className="pointer-events-none block aspect-square h-full in-[[data-slot=menu-checkbox-item][data-checked]]:origin-[var(--thumb-size)_50%] origin-left in-[[data-slot=menu-checkbox-item][data-checked]]:translate-x-[calc(var(--thumb-size)-4px)] in-[[data-slot=menu-checkbox-item]:active]:not-data-disabled:scale-x-110 in-[[data-slot=menu-checkbox-item]:active]:rounded-[var(--thumb-size)/calc(var(--thumb-size)*1.10)] rounded-(--thumb-size) bg-background shadow-sm/5 will-change-transform [transition:translate_.15s,border-radius_.15s,scale_.1s_.1s,transform-origin_.15s]" />
114+ </MenuPrimitive.CheckboxItemIndicator>
115+ </>
116+ ) : (
117+ <>
118+ <MenuPrimitive.CheckboxItemIndicator className="col-start-1">
119+ <svg
120+ fill="none"
121+ height="24"
122+ stroke="currentColor"
123+ strokeLinecap="round"
124+ strokeLinejoin="round"
125+ strokeWidth="2"
126+ viewBox="0 0 24 24"
127+ width="24"
128+ xmlns="http://www.w3.org/2000/svg"
129+ >
130+ <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
131+ </svg>
132+ </MenuPrimitive.CheckboxItemIndicator>
133+ <span className="col-start-2">{children}</span>
134+ </>
135+ )}
136+ </MenuPrimitive.CheckboxItem>
137+ );
138+}
139+
140+function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {
141+ return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />;
142+}
143+
144+function MenuRadioItem({
145+ className,
146+ children,
147+ ...props
148+}: MenuPrimitive.RadioItem.Props) {
149+ return (
150+ <MenuPrimitive.RadioItem
151+ className={cn(
152+ "grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
153+ className,
154+ )}
155+ data-slot="menu-radio-item"
156+ {...props}
157+ >
158+ <MenuPrimitive.RadioItemIndicator className="col-start-1">
159+ <svg
160+ fill="none"
161+ height="24"
162+ stroke="currentColor"
163+ strokeLinecap="round"
164+ strokeLinejoin="round"
165+ strokeWidth="2"
166+ viewBox="0 0 24 24"
167+ width="24"
168+ xmlns="http://www.w3.org/2000/svg"
169+ >
170+ <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
171+ </svg>
172+ </MenuPrimitive.RadioItemIndicator>
173+ <span className="col-start-2">{children}</span>
174+ </MenuPrimitive.RadioItem>
175+ );
176+}
177+
178+function MenuGroupLabel({
179+ className,
180+ inset,
181+ ...props
182+}: MenuPrimitive.GroupLabel.Props & {
183+ inset?: boolean;
184+}) {
185+ return (
186+ <MenuPrimitive.GroupLabel
187+ className={cn(
188+ "px-2 py-1.5 font-medium text-muted-foreground text-xs data-inset:ps-9 sm:data-inset:ps-8",
189+ className,
190+ )}
191+ data-inset={inset}
192+ data-slot="menu-label"
193+ {...props}
194+ />
195+ );
196+}
197+
198+function MenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {
199+ return (
200+ <MenuPrimitive.Separator
201+ className={cn("mx-2 my-1 h-px bg-border", className)}
202+ data-slot="menu-separator"
203+ {...props}
204+ />
205+ );
206+}
207+
208+function MenuShortcut({ className, ...props }: React.ComponentProps<"kbd">) {
209+ return (
210+ <kbd
211+ className={cn(
212+ "ms-auto font-medium font-sans text-muted-foreground/72 text-xs tracking-widest",
213+ className,
214+ )}
215+ data-slot="menu-shortcut"
216+ {...props}
217+ />
218+ );
219+}
220+
221+function MenuSub(props: MenuPrimitive.SubmenuRoot.Props) {
222+ return <MenuPrimitive.SubmenuRoot data-slot="menu-sub" {...props} />;
223+}
224+
225+function MenuSubTrigger({
226+ className,
227+ inset,
228+ children,
229+ ...props
230+}: MenuPrimitive.SubmenuTrigger.Props & {
231+ inset?: boolean;
232+}) {
233+ return (
234+ <MenuPrimitive.SubmenuTrigger
235+ className={cn(
236+ "flex min-h-8 items-center gap-2 rounded-sm px-2 py-1 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-popup-open:bg-accent data-inset:ps-8 data-highlighted:text-accent-foreground data-popup-open:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
237+ className,
238+ )}
239+ data-inset={inset}
240+ data-slot="menu-sub-trigger"
241+ {...props}
242+ >
243+ {children}
244+ <ChevronRightIcon className="-me-0.5 ms-auto opacity-80" />
245+ </MenuPrimitive.SubmenuTrigger>
246+ );
247+}
248+
249+function MenuSubPopup({
250+ className,
251+ sideOffset = 0,
252+ alignOffset,
253+ align = "start",
254+ ...props
255+}: MenuPrimitive.Popup.Props & {
256+ align?: MenuPrimitive.Positioner.Props["align"];
257+ sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"];
258+ alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"];
259+}) {
260+ const defaultAlignOffset = align !== "center" ? -5 : undefined;
261+
262+ return (
263+ <MenuPopup
264+ align={align}
265+ alignOffset={alignOffset ?? defaultAlignOffset}
266+ className={className}
267+ data-slot="menu-sub-content"
268+ side="inline-end"
269+ sideOffset={sideOffset}
270+ {...props}
271+ />
272+ );
273+}
274+
275+export {
276+ MenuCreateHandle,
277+ MenuCreateHandle as DropdownMenuCreateHandle,
278+ Menu,
279+ Menu as DropdownMenu,
280+ MenuPortal,
281+ MenuPortal as DropdownMenuPortal,
282+ MenuTrigger,
283+ MenuTrigger as DropdownMenuTrigger,
284+ MenuPopup,
285+ MenuPopup as DropdownMenuContent,
286+ MenuGroup,
287+ MenuGroup as DropdownMenuGroup,
288+ MenuItem,
289+ MenuItem as DropdownMenuItem,
290+ MenuCheckboxItem,
291+ MenuCheckboxItem as DropdownMenuCheckboxItem,
292+ MenuRadioGroup,
293+ MenuRadioGroup as DropdownMenuRadioGroup,
294+ MenuRadioItem,
295+ MenuRadioItem as DropdownMenuRadioItem,
296+ MenuGroupLabel,
297+ MenuGroupLabel as DropdownMenuLabel,
298+ MenuSeparator,
299+ MenuSeparator as DropdownMenuSeparator,
300+ MenuShortcut,
301+ MenuShortcut as DropdownMenuShortcut,
302+ MenuSub,
303+ MenuSub as DropdownMenuSub,
304+ MenuSubTrigger,
305+ MenuSubTrigger as DropdownMenuSubTrigger,
306+ MenuSubPopup,
307+ MenuSubPopup as DropdownMenuSubContent,
308+};
apps/renderer/src/components/ui/popover.tsx+102−0
@@ -0,0 +1,102 @@
1+import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
2+
3+import { cn } from "#/lib/utils";
4+
5+const PopoverCreateHandle = PopoverPrimitive.createHandle;
6+
7+const Popover = PopoverPrimitive.Root;
8+
9+function PopoverTrigger(props: PopoverPrimitive.Trigger.Props) {
10+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
11+}
12+
13+function PopoverPopup({
14+ children,
15+ className,
16+ side = "bottom",
17+ align = "center",
18+ sideOffset = 4,
19+ alignOffset = 0,
20+ tooltipStyle = false,
21+ ...props
22+}: PopoverPrimitive.Popup.Props & {
23+ side?: PopoverPrimitive.Positioner.Props["side"];
24+ align?: PopoverPrimitive.Positioner.Props["align"];
25+ sideOffset?: PopoverPrimitive.Positioner.Props["sideOffset"];
26+ alignOffset?: PopoverPrimitive.Positioner.Props["alignOffset"];
27+ tooltipStyle?: boolean;
28+}) {
29+ return (
30+ <PopoverPrimitive.Portal>
31+ <PopoverPrimitive.Positioner
32+ align={align}
33+ alignOffset={alignOffset}
34+ className="z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom,transform] data-instant:transition-none"
35+ data-slot="popover-positioner"
36+ side={side}
37+ sideOffset={sideOffset}
38+ >
39+ <PopoverPrimitive.Popup
40+ className={cn(
41+ "relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 transition-[width,height,scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-starting-style:scale-98 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
42+ tooltipStyle &&
43+ "w-fit text-balance rounded-md text-xs shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]",
44+ className,
45+ )}
46+ data-slot="popover-popup"
47+ {...props}
48+ >
49+ <PopoverPrimitive.Viewport
50+ className={cn(
51+ "relative size-full max-h-(--available-height) overflow-clip px-(--viewport-inline-padding) py-4 outline-none [--viewport-inline-padding:--spacing(4)] data-instant:transition-none **:data-current:data-ending-style:opacity-0 **:data-current:data-starting-style:opacity-0 **:data-previous:data-ending-style:opacity-0 **:data-previous:data-starting-style:opacity-0 **:data-current:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-previous:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-current:opacity-100 **:data-previous:opacity-100 **:data-current:transition-opacity **:data-previous:transition-opacity",
52+ tooltipStyle
53+ ? "py-1 [--viewport-inline-padding:--spacing(2)]"
54+ : "not-data-transitioning:overflow-y-auto",
55+ )}
56+ data-slot="popover-viewport"
57+ >
58+ {children}
59+ </PopoverPrimitive.Viewport>
60+ </PopoverPrimitive.Popup>
61+ </PopoverPrimitive.Positioner>
62+ </PopoverPrimitive.Portal>
63+ );
64+}
65+
66+function PopoverClose({ ...props }: PopoverPrimitive.Close.Props) {
67+ return <PopoverPrimitive.Close data-slot="popover-close" {...props} />;
68+}
69+
70+function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
71+ return (
72+ <PopoverPrimitive.Title
73+ className={cn("font-semibold text-lg leading-none", className)}
74+ data-slot="popover-title"
75+ {...props}
76+ />
77+ );
78+}
79+
80+function PopoverDescription({
81+ className,
82+ ...props
83+}: PopoverPrimitive.Description.Props) {
84+ return (
85+ <PopoverPrimitive.Description
86+ className={cn("text-muted-foreground text-sm", className)}
87+ data-slot="popover-description"
88+ {...props}
89+ />
90+ );
91+}
92+
93+export {
94+ PopoverCreateHandle,
95+ Popover,
96+ PopoverTrigger,
97+ PopoverPopup,
98+ PopoverPopup as PopoverContent,
99+ PopoverTitle,
100+ PopoverDescription,
101+ PopoverClose,
102+};
apps/renderer/src/components/ui/progress.tsx+79−0
@@ -0,0 +1,79 @@
1+import { Progress as ProgressPrimitive } from "@base-ui/react/progress";
2+
3+import { cn } from "#/lib/utils";
4+
5+function Progress({
6+ className,
7+ children,
8+ ...props
9+}: ProgressPrimitive.Root.Props) {
10+ return (
11+ <ProgressPrimitive.Root
12+ className={cn("flex w-full flex-col gap-2", className)}
13+ data-slot="progress"
14+ {...props}
15+ >
16+ {children ? (
17+ children
18+ ) : (
19+ <ProgressTrack>
20+ <ProgressIndicator />
21+ </ProgressTrack>
22+ )}
23+ </ProgressPrimitive.Root>
24+ );
25+}
26+
27+function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
28+ return (
29+ <ProgressPrimitive.Label
30+ className={cn("font-medium text-sm", className)}
31+ data-slot="progress-label"
32+ {...props}
33+ />
34+ );
35+}
36+
37+function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
38+ return (
39+ <ProgressPrimitive.Track
40+ className={cn(
41+ "block h-1.5 w-full overflow-hidden rounded-full bg-input",
42+ className,
43+ )}
44+ data-slot="progress-track"
45+ {...props}
46+ />
47+ );
48+}
49+
50+function ProgressIndicator({
51+ className,
52+ ...props
53+}: ProgressPrimitive.Indicator.Props) {
54+ return (
55+ <ProgressPrimitive.Indicator
56+ className={cn("bg-primary transition-all duration-500", className)}
57+ data-slot="progress-indicator"
58+ {...props}
59+ />
60+ );
61+}
62+
63+function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
64+ return (
65+ <ProgressPrimitive.Value
66+ className={cn("text-sm tabular-nums", className)}
67+ data-slot="progress-value"
68+ {...props}
69+ />
70+ );
71+}
72+
73+export {
74+ Progress,
75+ ProgressLabel,
76+ ProgressTrack,
77+ ProgressIndicator,
78+ ProgressValue,
79+};
apps/renderer/src/components/ui/radio-group.tsx+34−0
@@ -0,0 +1,34 @@
1+import { Radio as RadioPrimitive } from "@base-ui/react/radio";
2+import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
3+
4+import { cn } from "#/lib/utils";
5+
6+function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
7+ return (
8+ <RadioGroupPrimitive
9+ className={cn("flex flex-col gap-3", className)}
10+ data-slot="radio-group"
11+ {...props}
12+ />
13+ );
14+}
15+
16+function Radio({ className, ...props }: RadioPrimitive.Root.Props) {
17+ return (
18+ <RadioPrimitive.Root
19+ className={cn(
20+ "relative inline-flex size-4.5 shrink-0 items-center justify-center rounded-full border border-input bg-background not-dark:bg-clip-padding shadow-xs/5 outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-full not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/48 data-disabled:opacity-64 sm:size-4 dark:not-data-checked:bg-input/32 dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)] [[data-disabled],[data-checked],[aria-invalid]]:shadow-none",
21+ className,
22+ )}
23+ data-slot="radio"
24+ {...props}
25+ >
26+ <RadioPrimitive.Indicator
27+ className="-inset-px absolute flex size-4.5 items-center justify-center rounded-full before:size-2 before:rounded-full before:bg-primary-foreground data-unchecked:hidden data-checked:bg-primary sm:size-4 sm:before:size-1.5"
28+ data-slot="radio-indicator"
29+ />
30+ </RadioPrimitive.Root>
31+ );
32+}
33+
34+export { RadioGroup, Radio, Radio as RadioGroupItem };
apps/renderer/src/components/ui/scroll-area.tsx+62−0
@@ -0,0 +1,62 @@
1+import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
2+
3+import { cn } from "#/lib/utils";
4+
5+function ScrollArea({
6+ className,
7+ children,
8+ scrollFade = false,
9+ scrollbarGutter = false,
10+ ...props
11+}: ScrollAreaPrimitive.Root.Props & {
12+ scrollFade?: boolean;
13+ scrollbarGutter?: boolean;
14+}) {
15+ return (
16+ <ScrollAreaPrimitive.Root
17+ className={cn("size-full min-h-0", className)}
18+ {...props}
19+ >
20+ <ScrollAreaPrimitive.Viewport
21+ className={cn(
22+ "h-full overscroll-contain rounded-[inherit] outline-none transition-shadows focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-has-overflow-x:overscroll-x-contain",
23+ scrollFade &&
24+ "mask-t-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-start)))] mask-b-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-end)))] mask-l-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-start)))] mask-r-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-end)))] [--fade-size:1.5rem]",
25+ scrollbarGutter &&
26+ "data-has-overflow-y:pe-2.5 data-has-overflow-x:pb-2.5",
27+ )}
28+ data-slot="scroll-area-viewport"
29+ >
30+ {children}
31+ </ScrollAreaPrimitive.Viewport>
32+ <ScrollBar orientation="vertical" />
33+ <ScrollBar orientation="horizontal" />
34+ <ScrollAreaPrimitive.Corner data-slot="scroll-area-corner" />
35+ </ScrollAreaPrimitive.Root>
36+ );
37+}
38+
39+function ScrollBar({
40+ className,
41+ orientation = "vertical",
42+ ...props
43+}: ScrollAreaPrimitive.Scrollbar.Props) {
44+ return (
45+ <ScrollAreaPrimitive.Scrollbar
46+ className={cn(
47+ "m-1 flex opacity-0 transition-opacity delay-300 data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:w-1.5 data-[orientation=horizontal]:flex-col data-hovering:opacity-100 data-scrolling:opacity-100 data-hovering:delay-0 data-scrolling:delay-0 data-hovering:duration-100 data-scrolling:duration-100",
48+ className,
49+ )}
50+ data-slot="scroll-area-scrollbar"
51+ orientation={orientation}
52+ {...props}
53+ >
54+ <ScrollAreaPrimitive.Thumb
55+ className="relative flex-1 rounded-full bg-foreground/20"
56+ data-slot="scroll-area-thumb"
57+ />
58+ </ScrollAreaPrimitive.Scrollbar>
59+ );
60+}
61+
62+export { ScrollArea, ScrollBar };
apps/renderer/src/components/ui/select.tsx+187−0
@@ -0,0 +1,187 @@
1+import { Select as SelectPrimitive } from "@base-ui/react/select";
2+import {
3+ ChevronDownIcon,
4+ ChevronUpIcon,
5+ ChevronsUpDownIcon,
6+} from "lucide-react";
7+
8+import { cn } from "#/lib/utils";
9+
10+const Select = SelectPrimitive.Root;
11+
12+function SelectTrigger({
13+ className,
14+ size = "default",
15+ children,
16+ ...props
17+}: SelectPrimitive.Trigger.Props & {
18+ size?: "sm" | "default" | "lg";
19+}) {
20+ return (
21+ <SelectPrimitive.Trigger
22+ className={cn(
23+ "relative inline-flex min-h-9 w-full min-w-36 select-none items-center justify-center gap-2 rounded-lg border border-input bg-background not-dark:bg-clip-padding px-[calc(--spacing(3)-1px)] text-left text-base text-foreground shadow-xs/5 outline-none ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 focus-visible:border-ring focus-visible:ring-[3px] aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/16 data-disabled:pointer-events-none data-disabled:opacity-64 sm:min-h-8 sm:text-sm dark:bg-input/32 dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 [[data-disabled],:focus-visible,[aria-invalid],[data-pressed]]:shadow-none",
24+ size === "sm" &&
25+ "min-h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:min-h-7",
26+ size === "lg" && "min-h-10 sm:min-h-9",
27+ className,
28+ )}
29+ data-slot="select-trigger"
30+ {...props}
31+ >
32+ {children}
33+ <SelectPrimitive.Icon data-slot="select-icon">
34+ <ChevronsUpDownIcon className="-me-1 size-4.5 opacity-80 sm:size-4" />
35+ </SelectPrimitive.Icon>
36+ </SelectPrimitive.Trigger>
37+ );
38+}
39+
40+function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
41+ return (
42+ <SelectPrimitive.Value
43+ className={cn(
44+ "flex-1 truncate data-placeholder:text-muted-foreground",
45+ className,
46+ )}
47+ data-slot="select-value"
48+ {...props}
49+ />
50+ );
51+}
52+
53+function SelectPopup({
54+ className,
55+ children,
56+ side = "bottom",
57+ sideOffset = 4,
58+ align = "start",
59+ alignOffset = 0,
60+ alignItemWithTrigger = true,
61+ ...props
62+}: SelectPrimitive.Popup.Props & {
63+ side?: SelectPrimitive.Positioner.Props["side"];
64+ sideOffset?: SelectPrimitive.Positioner.Props["sideOffset"];
65+ align?: SelectPrimitive.Positioner.Props["align"];
66+ alignOffset?: SelectPrimitive.Positioner.Props["alignOffset"];
67+ alignItemWithTrigger?: SelectPrimitive.Positioner.Props["alignItemWithTrigger"];
68+}) {
69+ return (
70+ <SelectPrimitive.Portal>
71+ <SelectPrimitive.Positioner
72+ align={align}
73+ alignItemWithTrigger={alignItemWithTrigger}
74+ alignOffset={alignOffset}
75+ className="z-50 select-none"
76+ data-slot="select-positioner"
77+ side={side}
78+ sideOffset={sideOffset}
79+ >
80+ <SelectPrimitive.Popup
81+ className="origin-(--transform-origin) text-foreground"
82+ data-slot="select-popup"
83+ {...props}
84+ >
85+ <SelectPrimitive.ScrollUpArrow
86+ className="top-0 z-50 flex h-6 w-full cursor-default items-center justify-center before:pointer-events-none before:absolute before:inset-x-px before:top-px before:h-[200%] before:rounded-t-[calc(var(--radius-lg)-1px)] before:bg-linear-to-b before:from-50% before:from-popover"
87+ data-slot="select-scroll-up-arrow"
88+ >
89+ <ChevronUpIcon className="relative size-4.5 sm:size-4" />
90+ </SelectPrimitive.ScrollUpArrow>
91+ <div className="relative h-full min-w-(--anchor-width) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]">
92+ <SelectPrimitive.List
93+ className={cn(
94+ "max-h-(--available-height) overflow-y-auto p-1",
95+ className,
96+ )}
97+ data-slot="select-list"
98+ >
99+ {children}
100+ </SelectPrimitive.List>
101+ </div>
102+ <SelectPrimitive.ScrollDownArrow
103+ className="bottom-0 z-50 flex h-6 w-full cursor-default items-center justify-center before:pointer-events-none before:absolute before:inset-x-px before:bottom-px before:h-[200%] before:rounded-b-[calc(var(--radius-lg)-1px)] before:bg-linear-to-t before:from-50% before:from-popover"
104+ data-slot="select-scroll-down-arrow"
105+ >
106+ <ChevronDownIcon className="relative size-4.5 sm:size-4" />
107+ </SelectPrimitive.ScrollDownArrow>
108+ </SelectPrimitive.Popup>
109+ </SelectPrimitive.Positioner>
110+ </SelectPrimitive.Portal>
111+ );
112+}
113+
114+function SelectItem({
115+ className,
116+ children,
117+ ...props
118+}: SelectPrimitive.Item.Props) {
119+ return (
120+ <SelectPrimitive.Item
121+ className={cn(
122+ "grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
123+ className,
124+ )}
125+ data-slot="select-item"
126+ {...props}
127+ >
128+ <SelectPrimitive.ItemIndicator className="col-start-1">
129+ <svg
130+ fill="none"
131+ height="24"
132+ stroke="currentColor"
133+ strokeLinecap="round"
134+ strokeLinejoin="round"
135+ strokeWidth="2"
136+ viewBox="0 0 24 24"
137+ width="24"
138+ xmlns="http://www.w3.org/1500/svg"
139+ >
140+ <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
141+ </svg>
142+ </SelectPrimitive.ItemIndicator>
143+ <SelectPrimitive.ItemText className="col-start-2 min-w-0">
144+ {children}
145+ </SelectPrimitive.ItemText>
146+ </SelectPrimitive.Item>
147+ );
148+}
149+
150+function SelectSeparator({
151+ className,
152+ ...props
153+}: SelectPrimitive.Separator.Props) {
154+ return (
155+ <SelectPrimitive.Separator
156+ className={cn("mx-2 my-1 h-px bg-border", className)}
157+ data-slot="select-separator"
158+ {...props}
159+ />
160+ );
161+}
162+
163+function SelectGroup(props: SelectPrimitive.Group.Props) {
164+ return <SelectPrimitive.Group data-slot="select-group" {...props} />;
165+}
166+
167+function SelectGroupLabel(props: SelectPrimitive.GroupLabel.Props) {
168+ return (
169+ <SelectPrimitive.GroupLabel
170+ className="px-2 py-1.5 font-medium text-muted-foreground text-xs"
171+ data-slot="select-group-label"
172+ {...props}
173+ />
174+ );
175+}
176+
177+export {
178+ Select,
179+ SelectTrigger,
180+ SelectValue,
181+ SelectPopup,
182+ SelectPopup as SelectContent,
183+ SelectItem,
184+ SelectSeparator,
185+ SelectGroup,
186+ SelectGroupLabel,
187+};
apps/renderer/src/components/ui/separator.tsx+23−0
@@ -0,0 +1,23 @@
1+import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
2+
3+import { cn } from "#/lib/utils";
4+
5+function Separator({
6+ className,
7+ orientation = "horizontal",
8+ ...props
9+}: SeparatorPrimitive.Props) {
10+ return (
11+ <SeparatorPrimitive
12+ className={cn(
13+ "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:not-[[class^='h-']]:not-[[class*='_h-']]:self-stretch",
14+ className,
15+ )}
16+ data-slot="separator"
17+ orientation={orientation}
18+ {...props}
19+ />
20+ );
21+}
22+
23+export { Separator };
apps/renderer/src/components/ui/sheet.tsx+201−0
@@ -0,0 +1,201 @@
1+import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
2+import { XIcon } from "lucide-react";
3+import { Button } from "#/components/ui/button";
4+import { ScrollArea } from "#/components/ui/scroll-area";
5+import { cn } from "#/lib/utils";
6+
7+const Sheet = SheetPrimitive.Root;
8+
9+const SheetPortal = SheetPrimitive.Portal;
10+
11+function SheetTrigger(props: SheetPrimitive.Trigger.Props) {
12+ return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
13+}
14+
15+function SheetClose(props: SheetPrimitive.Close.Props) {
16+ return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
17+}
18+
19+function SheetBackdrop({ className, ...props }: SheetPrimitive.Backdrop.Props) {
20+ return (
21+ <SheetPrimitive.Backdrop
22+ className={cn(
23+ "fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
24+ className,
25+ )}
26+ data-slot="sheet-backdrop"
27+ {...props}
28+ />
29+ );
30+}
31+
32+function SheetViewport({
33+ className,
34+ side,
35+ variant = "default",
36+ ...props
37+}: SheetPrimitive.Viewport.Props & {
38+ side?: "right" | "left" | "top" | "bottom";
39+ variant?: "default" | "inset";
40+}) {
41+ return (
42+ <SheetPrimitive.Viewport
43+ className={cn(
44+ "fixed inset-0 z-50 grid",
45+ side === "bottom" && "grid grid-rows-[1fr_auto] pt-12",
46+ side === "top" && "grid grid-rows-[auto_1fr] pb-12",
47+ side === "left" && "flex justify-start",
48+ side === "right" && "flex justify-end",
49+ variant === "inset" && "sm:p-4",
50+ )}
51+ data-slot="sheet-viewport"
52+ {...props}
53+ />
54+ );
55+}
56+
57+function SheetPopup({
58+ className,
59+ children,
60+ showCloseButton = true,
61+ side = "right",
62+ variant = "default",
63+ ...props
64+}: SheetPrimitive.Popup.Props & {
65+ showCloseButton?: boolean;
66+ side?: "right" | "left" | "top" | "bottom";
67+ variant?: "default" | "inset";
68+}) {
69+ return (
70+ <SheetPortal>
71+ <SheetBackdrop />
72+ <SheetViewport side={side} variant={variant}>
73+ <SheetPrimitive.Popup
74+ className={cn(
75+ "relative flex max-h-full min-h-0 w-full min-w-0 flex-col bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 transition-[opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:opacity-0 data-starting-style:opacity-0 max-sm:before:hidden dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
76+ side === "bottom" &&
77+ "row-start-2 border-t data-ending-style:translate-y-8 data-starting-style:translate-y-8",
78+ side === "top" &&
79+ "data-ending-style:-translate-y-8 data-starting-style:-translate-y-8 border-b",
80+ side === "left" &&
81+ "data-ending-style:-translate-x-8 data-starting-style:-translate-x-8 w-[calc(100%-(--spacing(12)))] max-w-md border-e",
82+ side === "right" &&
83+ "col-start-2 w-[calc(100%-(--spacing(12)))] max-w-md border-s data-ending-style:translate-x-8 data-starting-style:translate-x-8",
84+ variant === "inset" &&
85+ "before:hidden sm:rounded-2xl sm:border sm:before:rounded-[calc(var(--radius-2xl)-1px)] sm:**:data-[slot=sheet-footer]:rounded-b-[calc(var(--radius-2xl)-1px)]",
86+ className,
87+ )}
88+ data-slot="sheet-popup"
89+ {...props}
90+ >
91+ {children}
92+ {showCloseButton && (
93+ <SheetPrimitive.Close
94+ aria-label="Close"
95+ className="absolute end-2 top-2"
96+ render={<Button size="icon" variant="ghost" />}
97+ >
98+ <XIcon />
99+ </SheetPrimitive.Close>
100+ )}
101+ </SheetPrimitive.Popup>
102+ </SheetViewport>
103+ </SheetPortal>
104+ );
105+}
106+
107+function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
108+ return (
109+ <div
110+ className={cn(
111+ "flex flex-col gap-2 p-6 in-[[data-slot=sheet-popup]:has([data-slot=sheet-panel])]:pb-3 max-sm:pb-4",
112+ className,
113+ )}
114+ data-slot="sheet-header"
115+ {...props}
116+ />
117+ );
118+}
119+
120+function SheetFooter({
121+ className,
122+ variant = "default",
123+ ...props
124+}: React.ComponentProps<"div"> & {
125+ variant?: "default" | "bare";
126+}) {
127+ return (
128+ <div
129+ className={cn(
130+ "flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end",
131+ variant === "default" && "border-t bg-muted/72 py-4",
132+ variant === "bare" &&
133+ "in-[[data-slot=sheet-popup]:has([data-slot=sheet-panel])]:pt-3 pt-4 pb-6",
134+ className,
135+ )}
136+ data-slot="sheet-footer"
137+ {...props}
138+ />
139+ );
140+}
141+
142+function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
143+ return (
144+ <SheetPrimitive.Title
145+ className={cn(
146+ "font-heading font-semibold text-xl leading-none",
147+ className,
148+ )}
149+ data-slot="sheet-title"
150+ {...props}
151+ />
152+ );
153+}
154+
155+function SheetDescription({
156+ className,
157+ ...props
158+}: SheetPrimitive.Description.Props) {
159+ return (
160+ <SheetPrimitive.Description
161+ className={cn("text-muted-foreground text-sm", className)}
162+ data-slot="sheet-description"
163+ {...props}
164+ />
165+ );
166+}
167+
168+function SheetPanel({
169+ className,
170+ scrollFade = true,
171+ ...props
172+}: React.ComponentProps<"div"> & { scrollFade?: boolean }) {
173+ return (
174+ <ScrollArea scrollFade={scrollFade}>
175+ <div
176+ className={cn(
177+ "p-6 in-[[data-slot=sheet-popup]:has([data-slot=sheet-header])]:pt-1 in-[[data-slot=sheet-popup]:has([data-slot=sheet-footer]:not(.border-t))]:pb-1",
178+ className,
179+ )}
180+ data-slot="sheet-panel"
181+ {...props}
182+ />
183+ </ScrollArea>
184+ );
185+}
186+
187+export {
188+ Sheet,
189+ SheetTrigger,
190+ SheetPortal,
191+ SheetClose,
192+ SheetBackdrop,
193+ SheetBackdrop as SheetOverlay,
194+ SheetPopup,
195+ SheetPopup as SheetContent,
196+ SheetHeader,
197+ SheetFooter,
198+ SheetTitle,
199+ SheetDescription,
200+ SheetPanel,
201+};
apps/renderer/src/components/ui/skeleton.tsx+16−0
@@ -0,0 +1,16 @@
1+import { cn } from "#/lib/utils";
2+
3+function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4+ return (
5+ <div
6+ className={cn(
7+ "animate-skeleton rounded-sm [--skeleton-highlight:--alpha(var(--color-white)/64%)] [background:linear-gradient(120deg,transparent_40%,var(--skeleton-highlight),transparent_60%)_var(--color-muted)_0_0/200%_100%_fixed] dark:[--skeleton-highlight:--alpha(var(--color-white)/4%)]",
8+ className,
9+ )}
10+ data-slot="skeleton"
11+ {...props}
12+ />
13+ );
14+}
15+
16+export { Skeleton };
apps/renderer/src/components/ui/spinner.tsx+18−0
@@ -0,0 +1,18 @@
1+import { Loader2Icon } from "lucide-react";
2+import { cn } from "#/lib/utils";
3+
4+function Spinner({
5+ className,
6+ ...props
7+}: React.ComponentProps<typeof Loader2Icon>) {
8+ return (
9+ <Loader2Icon
10+ aria-label="Loading"
11+ className={cn("animate-spin", className)}
12+ role="status"
13+ {...props}
14+ />
15+ );
16+}
17+
18+export { Spinner };
apps/renderer/src/components/ui/switch.tsx+25−0
@@ -0,0 +1,25 @@
1+import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
2+
3+import { cn } from "#/lib/utils";
4+
5+function Switch({ className, ...props }: SwitchPrimitive.Root.Props) {
6+ return (
7+ <SwitchPrimitive.Root
8+ className={cn(
9+ "inline-flex h-[calc(var(--thumb-size)+2px)] w-[calc(var(--thumb-size)*2-2px)] shrink-0 items-center rounded-full p-px outline-none transition-[background-color,box-shadow] duration-200 [--thumb-size:--spacing(5)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-checked:bg-primary data-unchecked:bg-input data-disabled:opacity-64 sm:[--thumb-size:--spacing(4)]",
10+ className,
11+ )}
12+ data-slot="switch"
13+ {...props}
14+ >
15+ <SwitchPrimitive.Thumb
16+ className={cn(
17+ "pointer-events-none block aspect-square h-full origin-left in-[[role=switch]:active,[data-slot=label]:active]:not-data-disabled:scale-x-110 in-[[role=switch]:active,[data-slot=label]:active]:rounded-[var(--thumb-size)/calc(var(--thumb-size)*1.1)] rounded-(--thumb-size) bg-background shadow-sm/5 will-change-transform [transition:translate_.15s,border-radius_.15s,scale_.1s_.1s,transform-origin_.15s] data-checked:origin-[var(--thumb-size)_50%] data-checked:translate-x-[calc(var(--thumb-size)-4px)]",
18+ )}
19+ data-slot="switch-thumb"
20+ />
21+ </SwitchPrimitive.Root>
22+ );
23+}
24+
25+export { Switch };
apps/renderer/src/components/ui/table.tsx+126−0
@@ -0,0 +1,126 @@
1+import type * as React from "react";
2+
3+import { cn } from "#/lib/utils";
4+
5+function Table({ className, ...props }: React.ComponentProps<"table">) {
6+ return (
7+ <div
8+ className="relative w-full overflow-x-auto"
9+ data-slot="table-container"
10+ >
11+ <table
12+ className={cn(
13+ "w-full caption-bottom in-data-[slot=frame]:border-separate in-data-[slot=frame]:border-spacing-0 text-sm",
14+ className,
15+ )}
16+ data-slot="table"
17+ {...props}
18+ />
19+ </div>
20+ );
21+}
22+
23+function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
24+ return (
25+ <thead
26+ className={cn(
27+ "[&_tr]:border-b in-data-[slot=frame]:**:[th]:h-9 in-data-[slot=frame]:*:[tr]:border-none in-data-[slot=frame]:*:[tr]:hover:bg-transparent",
28+ className,
29+ )}
30+ data-slot="table-header"
31+ {...props}
32+ />
33+ );
34+}
35+
36+function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
37+ return (
38+ <tbody
39+ className={cn(
40+ "relative in-data-[slot=frame]:rounded-xl in-data-[slot=frame]:shadow-xs/5 before:pointer-events-none before:absolute before:inset-px not-in-data-[slot=frame]:before:hidden before:rounded-[calc(var(--radius-xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/8%)] [&_tr:last-child]:border-0 in-data-[slot=frame]:*:[tr]:border-0 in-data-[slot=frame]:*:[tr]:*:[td]:border-b in-data-[slot=frame]:*:[tr]:*:[td]:bg-background in-data-[slot=frame]:*:[tr]:*:[td]:bg-clip-padding in-data-[slot=frame]:*:[tr]:first:*:[td]:first:rounded-ss-xl in-data-[slot=frame]:*:[tr]:*:[td]:first:border-s in-data-[slot=frame]:*:[tr]:first:*:[td]:border-t in-data-[slot=frame]:*:[tr]:last:*:[td]:last:rounded-ee-xl in-data-[slot=frame]:*:[tr]:*:[td]:last:border-e in-data-[slot=frame]:*:[tr]:first:*:[td]:last:rounded-se-xl in-data-[slot=frame]:*:[tr]:last:*:[td]:first:rounded-es-xl in-data-[slot=frame]:*:[tr]:hover:*:[td]:bg-transparent in-data-[slot=frame]:*:[tr]:data-[state=selected]:*:[td]:bg-muted/72",
41+ className,
42+ )}
43+ data-slot="table-body"
44+ {...props}
45+ />
46+ );
47+}
48+
49+function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
50+ return (
51+ <tfoot
52+ className={cn(
53+ "border-t in-data-[slot=frame]:border-none bg-muted/72 in-data-[slot=frame]:bg-transparent font-medium [&>tr]:last:border-b-0 in-data-[slot=frame]:*:[tr]:hover:bg-transparent",
54+ className,
55+ )}
56+ data-slot="table-footer"
57+ {...props}
58+ />
59+ );
60+}
61+
62+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
63+ return (
64+ <tr
65+ className={cn(
66+ "border-b transition-colors hover:bg-muted/72 in-data-[slot=frame]:hover:bg-transparent data-[state=selected]:bg-muted/72 in-data-[slot=frame]:data-[state=selected]:bg-transparent",
67+ className,
68+ )}
69+ data-slot="table-row"
70+ {...props}
71+ />
72+ );
73+}
74+
75+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
76+ return (
77+ <th
78+ className={cn(
79+ "h-10 whitespace-nowrap px-2.5 text-left align-middle font-medium text-muted-foreground leading-none has-[[role=checkbox]]:w-px has-[[role=checkbox]]:pe-0",
80+ className,
81+ )}
82+ data-slot="table-head"
83+ {...props}
84+ />
85+ );
86+}
87+
88+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
89+ return (
90+ <td
91+ className={cn(
92+ "whitespace-nowrap p-2.5 align-middle leading-none in-data-[slot=frame]:first:p-[calc(--spacing(2.5)-1px)] in-data-[slot=frame]:last:p-[calc(--spacing(2.5)-1px)] has-[[role=checkbox]]:pe-0",
93+ className,
94+ )}
95+ data-slot="table-cell"
96+ {...props}
97+ />
98+ );
99+}
100+
101+function TableCaption({
102+ className,
103+ ...props
104+}: React.ComponentProps<"caption">) {
105+ return (
106+ <caption
107+ className={cn(
108+ "in-data-[slot=frame]:my-4 mt-4 text-muted-foreground text-sm",
109+ className,
110+ )}
111+ data-slot="table-caption"
112+ {...props}
113+ />
114+ );
115+}
116+
117+export {
118+ Table,
119+ TableHeader,
120+ TableBody,
121+ TableFooter,
122+ TableHead,
123+ TableRow,
124+ TableCell,
125+ TableCaption,
126+};
apps/renderer/src/components/ui/tabs.tsx+85−0
@@ -0,0 +1,85 @@
1+import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
2+
3+import { cn } from "#/lib/utils";
4+
5+type TabsVariant = "default" | "underline";
6+
7+function Tabs({ className, ...props }: TabsPrimitive.Root.Props) {
8+ return (
9+ <TabsPrimitive.Root
10+ className={cn(
11+ "flex flex-col gap-2 data-[orientation=vertical]:flex-row",
12+ className,
13+ )}
14+ data-slot="tabs"
15+ {...props}
16+ />
17+ );
18+}
19+
20+function TabsList({
21+ variant = "default",
22+ className,
23+ children,
24+ ...props
25+}: TabsPrimitive.List.Props & {
26+ variant?: TabsVariant;
27+}) {
28+ return (
29+ <TabsPrimitive.List
30+ className={cn(
31+ "relative z-0 flex w-fit items-center justify-center gap-x-0.5 text-muted-foreground",
32+ "data-[orientation=vertical]:flex-col",
33+ variant === "default"
34+ ? "rounded-lg bg-muted p-0.5 text-muted-foreground/72"
35+ : "data-[orientation=vertical]:px-1 data-[orientation=horizontal]:py-1 *:data-[slot=tabs-tab]:hover:bg-accent",
36+ className,
37+ )}
38+ data-slot="tabs-list"
39+ {...props}
40+ >
41+ {children}
42+ <TabsPrimitive.Indicator
43+ className={cn(
44+ "-translate-y-(--active-tab-bottom) absolute bottom-0 left-0 h-(--active-tab-height) w-(--active-tab-width) translate-x-(--active-tab-left) transition-[width,translate] duration-200 ease-in-out",
45+ variant === "underline"
46+ ? "data-[orientation=vertical]:-translate-x-px z-10 bg-primary data-[orientation=horizontal]:h-0.5 data-[orientation=vertical]:w-0.5 data-[orientation=horizontal]:translate-y-px"
47+ : "-z-1 rounded-md bg-background shadow-sm/5 dark:bg-input",
48+ )}
49+ data-slot="tab-indicator"
50+ />
51+ </TabsPrimitive.List>
52+ );
53+}
54+
55+function TabsTab({ className, ...props }: TabsPrimitive.Tab.Props) {
56+ return (
57+ <TabsPrimitive.Tab
58+ className={cn(
59+ "[&_svg]:-mx-0.5 relative flex h-9 shrink-0 grow cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-[calc(--spacing(2.5)-1px)] font-medium text-base outline-none transition-[color,background-color,box-shadow] hover:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring data-disabled:pointer-events-none data-[orientation=vertical]:w-full data-[orientation=vertical]:justify-start data-active:text-foreground data-disabled:opacity-64 sm:h-8 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
60+ className,
61+ )}
62+ data-slot="tabs-tab"
63+ {...props}
64+ />
65+ );
66+}
67+
68+function TabsPanel({ className, ...props }: TabsPrimitive.Panel.Props) {
69+ return (
70+ <TabsPrimitive.Panel
71+ className={cn("flex-1 outline-none", className)}
72+ data-slot="tabs-content"
73+ {...props}
74+ />
75+ );
76+}
77+
78+export {
79+ Tabs,
80+ TabsList,
81+ TabsTab,
82+ TabsTab as TabsTrigger,
83+ TabsPanel,
84+ TabsPanel as TabsContent,
85+};
apps/renderer/src/components/ui/textarea.tsx+49−0
@@ -0,0 +1,49 @@
1+import { Field as FieldPrimitive } from "@base-ui/react/field";
2+import { mergeProps } from "@base-ui/react/merge-props";
3+import type * as React from "react";
4+
5+import { cn } from "#/lib/utils";
6+
7+type TextareaProps = React.ComponentProps<"textarea"> & {
8+ size?: "sm" | "default" | "lg" | number;
9+ unstyled?: boolean;
10+};
11+
12+function Textarea({
13+ className,
14+ size = "default",
15+ unstyled = false,
16+ ...props
17+}: TextareaProps) {
18+ return (
19+ <span
20+ className={
21+ cn(
22+ !unstyled &&
23+ "relative inline-flex w-full rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] sm:text-sm dark:bg-input/32 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]",
24+ className,
25+ ) || undefined
26+ }
27+ data-size={size}
28+ data-slot="textarea-control"
29+ >
30+ <FieldPrimitive.Control
31+ render={(defaultProps) => (
32+ <textarea
33+ className={cn(
34+ "field-sizing-content min-h-17.5 w-full rounded-[inherit] px-[calc(--spacing(3)-1px)] py-[calc(--spacing(1.5)-1px)] outline-none max-sm:min-h-20.5",
35+ size === "sm" &&
36+ "min-h-16.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)] max-sm:min-h-19.5",
37+ size === "lg" &&
38+ "min-h-18.5 py-[calc(--spacing(2)-1px)] max-sm:min-h-21.5",
39+ )}
40+ data-slot="textarea"
41+ {...mergeProps(defaultProps, props)}
42+ />
43+ )}
44+ />
45+ </span>
46+ );
47+}
48+
49+export { Textarea, type TextareaProps };
apps/renderer/src/components/ui/toast.tsx+267−0
@@ -0,0 +1,267 @@
1+import { Toast } from "@base-ui/react/toast";
2+import {
3+ CircleAlertIcon,
4+ CircleCheckIcon,
5+ InfoIcon,
6+ LoaderCircleIcon,
7+ TriangleAlertIcon,
8+} from "lucide-react";
9+
10+import { buttonVariants } from "#/components/ui/button";
11+import { cn } from "#/lib/utils";
12+
13+const toastManager = Toast.createToastManager();
14+const anchoredToastManager = Toast.createToastManager();
15+
16+const TOAST_ICONS = {
17+ error: CircleAlertIcon,
18+ info: InfoIcon,
19+ loading: LoaderCircleIcon,
20+ success: CircleCheckIcon,
21+ warning: TriangleAlertIcon,
22+} as const;
23+
24+type ToastPosition =
25+ | "top-left"
26+ | "top-center"
27+ | "top-right"
28+ | "bottom-left"
29+ | "bottom-center"
30+ | "bottom-right";
31+
32+interface ToastProviderProps extends Toast.Provider.Props {
33+ position?: ToastPosition;
34+}
35+
36+function ToastProvider({
37+ children,
38+ position = "bottom-right",
39+ ...props
40+}: ToastProviderProps) {
41+ return (
42+ <Toast.Provider toastManager={toastManager} {...props}>
43+ {children}
44+ <Toasts position={position} />
45+ </Toast.Provider>
46+ );
47+}
48+
49+function Toasts({ position = "bottom-right" }: { position: ToastPosition }) {
50+ const { toasts } = Toast.useToastManager();
51+ const isTop = position.startsWith("top");
52+
53+ return (
54+ <Toast.Portal data-slot="toast-portal">
55+ <Toast.Viewport
56+ className={cn(
57+ "fixed z-50 mx-auto flex w-[calc(100%-var(--toast-inset)*2)] max-w-90 [--toast-inset:--spacing(4)] sm:[--toast-inset:--spacing(8)]",
58+ // Vertical positioning
59+ "data-[position*=top]:top-(--toast-inset)",
60+ "data-[position*=bottom]:bottom-(--toast-inset)",
61+ // Horizontal positioning
62+ "data-[position*=left]:left-(--toast-inset)",
63+ "data-[position*=right]:right-(--toast-inset)",
64+ "data-[position*=center]:-translate-x-1/2 data-[position*=center]:left-1/2",
65+ )}
66+ data-position={position}
67+ data-slot="toast-viewport"
68+ >
69+ {toasts.map((toast) => {
70+ const Icon = toast.type
71+ ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
72+ : null;
73+
74+ return (
75+ <Toast.Root
76+ className={cn(
77+ "absolute z-[calc(9999-var(--toast-index))] h-(--toast-calc-height) w-full select-none rounded-lg border bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 [transition:transform_.5s_cubic-bezier(.22,1,.36,1),opacity_.5s,height_.15s] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
78+ // Base positioning using data-position
79+ "data-[position*=right]:right-0 data-[position*=right]:left-auto",
80+ "data-[position*=left]:right-auto data-[position*=left]:left-0",
81+ "data-[position*=center]:right-0 data-[position*=center]:left-0",
82+ "data-[position*=top]:top-0 data-[position*=top]:bottom-auto data-[position*=top]:origin-top",
83+ "data-[position*=bottom]:top-auto data-[position*=bottom]:bottom-0 data-[position*=bottom]:origin-bottom",
84+ // Gap fill for hover
85+ "after:absolute after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full",
86+ "data-[position*=top]:after:top-full",
87+ "data-[position*=bottom]:after:bottom-full",
88+ // Define some variables
89+ "[--toast-calc-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:--spacing(3)] [--toast-peek:--spacing(3)] [--toast-scale:calc(max(0,1-(var(--toast-index)*.1)))] [--toast-shrink:calc(1-var(--toast-scale))]",
90+ // Define offset-y variable
91+ "data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+var(--toast-index)*var(--toast-gap)+var(--toast-swipe-movement-y))]",
92+ "data-[position*=bottom]:[--toast-calc-offset-y:calc(var(--toast-offset-y)*-1+var(--toast-index)*var(--toast-gap)*-1+var(--toast-swipe-movement-y))]",
93+ // Default state transform
94+ "data-[position*=top]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]",
95+ "data-[position*=bottom]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]",
96+ // Limited state
97+ "data-limited:opacity-0",
98+ // Expanded state
99+ "data-expanded:h-(--toast-height)",
100+ "data-position:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-calc-offset-y))]",
101+ // Starting and ending animations
102+ "data-[position*=top]:data-starting-style:transform-[translateY(calc(-100%-var(--toast-inset)))]",
103+ "data-[position*=bottom]:data-starting-style:transform-[translateY(calc(100%+var(--toast-inset)))]",
104+ "data-ending-style:opacity-0",
105+ // Ending animations (direction-aware)
106+ "data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateY(calc(100%+var(--toast-inset)))]",
107+ "data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
108+ "data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
109+ "data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
110+ "data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
111+ // Ending animations (expanded)
112+ "data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
113+ "data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
114+ "data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
115+ "data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
116+ )}
117+ data-position={position}
118+ key={toast.id}
119+ swipeDirection={
120+ position.includes("center")
121+ ? [isTop ? "up" : "down"]
122+ : position.includes("left")
123+ ? ["left", isTop ? "up" : "down"]
124+ : ["right", isTop ? "up" : "down"]
125+ }
126+ toast={toast}
127+ >
128+ <Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm transition-opacity duration-250 data-behind:not-data-expanded:pointer-events-none data-behind:opacity-0 data-expanded:opacity-100">
129+ <div className="flex gap-2">
130+ {Icon && (
131+ <div
132+ className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
133+ data-slot="toast-icon"
134+ >
135+ <Icon className="in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80" />
136+ </div>
137+ )}
138+
139+ <div className="flex flex-col gap-0.5">
140+ <Toast.Title
141+ className="font-medium"
142+ data-slot="toast-title"
143+ />
144+ <Toast.Description
145+ className="text-muted-foreground"
146+ data-slot="toast-description"
147+ />
148+ </div>
149+ </div>
150+ {toast.actionProps && (
151+ <Toast.Action
152+ className={buttonVariants({ size: "xs" })}
153+ data-slot="toast-action"
154+ >
155+ {toast.actionProps.children}
156+ </Toast.Action>
157+ )}
158+ </Toast.Content>
159+ </Toast.Root>
160+ );
161+ })}
162+ </Toast.Viewport>
163+ </Toast.Portal>
164+ );
165+}
166+
167+function AnchoredToastProvider({ children, ...props }: Toast.Provider.Props) {
168+ return (
169+ <Toast.Provider toastManager={anchoredToastManager} {...props}>
170+ {children}
171+ <AnchoredToasts />
172+ </Toast.Provider>
173+ );
174+}
175+
176+function AnchoredToasts() {
177+ const { toasts } = Toast.useToastManager();
178+
179+ return (
180+ <Toast.Portal data-slot="toast-portal-anchored">
181+ <Toast.Viewport
182+ className="outline-none"
183+ data-slot="toast-viewport-anchored"
184+ >
185+ {toasts.map((toast) => {
186+ const Icon = toast.type
187+ ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
188+ : null;
189+ const tooltipStyle =
190+ (toast.data as { tooltipStyle?: boolean })?.tooltipStyle ?? false;
191+ const positionerProps = toast.positionerProps;
192+
193+ if (!positionerProps?.anchor) {
194+ return null;
195+ }
196+
197+ return (
198+ <Toast.Positioner
199+ className="z-50 max-w-[min(--spacing(64),var(--available-width))]"
200+ data-slot="toast-positioner"
201+ key={toast.id}
202+ sideOffset={positionerProps.sideOffset ?? 4}
203+ toast={toast}
204+ >
205+ <Toast.Root
206+ className={cn(
207+ "relative text-balance border bg-popover not-dark:bg-clip-padding text-popover-foreground text-xs transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
208+ tooltipStyle
209+ ? "rounded-md shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]"
210+ : "rounded-lg shadow-lg/5 before:rounded-[calc(var(--radius-lg)-1px)]",
211+ )}
212+ data-slot="toast-popup"
213+ toast={toast}
214+ >
215+ {tooltipStyle ? (
216+ <Toast.Content className="pointer-events-auto px-2 py-1">
217+ <Toast.Title data-slot="toast-title" />
218+ </Toast.Content>
219+ ) : (
220+ <Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm">
221+ <div className="flex gap-2">
222+ {Icon && (
223+ <div
224+ className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
225+ data-slot="toast-icon"
226+ >
227+ <Icon className="in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80" />
228+ </div>
229+ )}
230+
231+ <div className="flex flex-col gap-0.5">
232+ <Toast.Title
233+ className="font-medium"
234+ data-slot="toast-title"
235+ />
236+ <Toast.Description
237+ className="text-muted-foreground"
238+ data-slot="toast-description"
239+ />
240+ </div>
241+ </div>
242+ {toast.actionProps && (
243+ <Toast.Action
244+ className={buttonVariants({ size: "xs" })}
245+ data-slot="toast-action"
246+ >
247+ {toast.actionProps.children}
248+ </Toast.Action>
249+ )}
250+ </Toast.Content>
251+ )}
252+ </Toast.Root>
253+ </Toast.Positioner>
254+ );
255+ })}
256+ </Toast.Viewport>
257+ </Toast.Portal>
258+ );
259+}
260+
261+export {
262+ ToastProvider,
263+ type ToastPosition,
264+ toastManager,
265+ AnchoredToastProvider,
266+ anchoredToastManager,
267+};
apps/renderer/src/hooks/useTheme.ts+90−0
@@ -0,0 +1,90 @@
1+import { useCallback, useEffect, useSyncExternalStore } from "react";
2+
3+type Theme = "light" | "dark" | "system";
4+
5+const STORAGE_KEY = "codething:theme";
6+const MEDIA_QUERY = "(prefers-color-scheme: dark)";
7+
8+let listeners: Array<() => void> = [];
9+function emitChange() {
10+ for (const listener of listeners) listener();
11+}
12+
13+function getSystemDark(): boolean {
14+ return window.matchMedia(MEDIA_QUERY).matches;
15+}
16+
17+function getStored(): Theme {
18+ const raw = localStorage.getItem(STORAGE_KEY);
19+ if (raw === "light" || raw === "dark" || raw === "system") return raw;
20+ return "system";
21+}
22+
23+function applyTheme(theme: Theme, suppressTransitions = false) {
24+ if (suppressTransitions) {
25+ document.documentElement.classList.add("no-transitions");
26+ }
27+ const isDark = theme === "dark" || (theme === "system" && getSystemDark());
28+ document.documentElement.classList.toggle("dark", isDark);
29+ if (suppressTransitions) {
30+ // Force a reflow so the no-transitions class takes effect before removal
31+ document.documentElement.offsetHeight;
32+ requestAnimationFrame(() => {
33+ document.documentElement.classList.remove("no-transitions");
34+ });
35+ }
36+}
37+
38+// Apply immediately on module load to prevent flash
39+applyTheme(getStored());
40+
41+function getSnapshot(): Theme {
42+ return getStored();
43+}
44+
45+function subscribe(listener: () => void): () => void {
46+ listeners.push(listener);
47+
48+ // Listen for system preference changes
49+ const mq = window.matchMedia(MEDIA_QUERY);
50+ const handleChange = () => {
51+ if (getStored() === "system") applyTheme("system", true);
52+ emitChange();
53+ };
54+ mq.addEventListener("change", handleChange);
55+
56+ // Listen for storage changes from other tabs
57+ const handleStorage = (e: StorageEvent) => {
58+ if (e.key === STORAGE_KEY) {
59+ applyTheme(getStored(), true);
60+ emitChange();
61+ }
62+ };
63+ window.addEventListener("storage", handleStorage);
64+
65+ return () => {
66+ listeners = listeners.filter((l) => l !== listener);
67+ mq.removeEventListener("change", handleChange);
68+ window.removeEventListener("storage", handleStorage);
69+ };
70+}
71+
72+export function useTheme() {
73+ const theme = useSyncExternalStore(subscribe, getSnapshot);
74+
75+ const resolvedTheme: "light" | "dark" =
76+ theme === "system" ? (getSystemDark() ? "dark" : "light") : theme;
77+
78+ const setTheme = useCallback((next: Theme) => {
79+ localStorage.setItem(STORAGE_KEY, next);
80+ applyTheme(next, true);
81+ emitChange();
82+ }, []);
83+
84+ // Keep DOM in sync on mount/change
85+ useEffect(() => {
86+ applyTheme(theme);
87+ }, [theme]);
88+
89+ return { theme, setTheme, resolvedTheme } as const;
90+}
apps/renderer/src/index.css+194−26
@@ -1,7 +1,178 @@
11 @import "tailwindcss";
22
3+@custom-variant dark (&:is(.dark, .dark *));
4+
5+@theme inline {
6+ --animate-skeleton: skeleton 2s -1s infinite linear;
7+ --color-warning-foreground: var(--warning-foreground);
8+ --color-warning: var(--warning);
9+ --color-success-foreground: var(--success-foreground);
10+ --color-success: var(--success);
11+ --color-info-foreground: var(--info-foreground);
12+ --color-info: var(--info);
13+ --color-destructive-foreground: var(--destructive-foreground);
14+ --color-sidebar-ring: var(--sidebar-ring);
15+ --color-sidebar-border: var(--sidebar-border);
16+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
17+ --color-sidebar-accent: var(--sidebar-accent);
18+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
19+ --color-sidebar-primary: var(--sidebar-primary);
20+ --color-sidebar-foreground: var(--sidebar-foreground);
21+ --color-sidebar: var(--sidebar);
22+ --color-chart-5: var(--chart-5);
23+ --color-chart-4: var(--chart-4);
24+ --color-chart-3: var(--chart-3);
25+ --color-chart-2: var(--chart-2);
26+ --color-chart-1: var(--chart-1);
27+ --color-ring: var(--ring);
28+ --color-input: var(--input);
29+ --color-border: var(--border);
30+ --color-destructive: var(--destructive);
31+ --color-accent-foreground: var(--accent-foreground);
32+ --color-accent: var(--accent);
33+ --color-muted-foreground: var(--muted-foreground);
34+ --color-muted: var(--muted);
35+ --color-secondary-foreground: var(--secondary-foreground);
36+ --color-secondary: var(--secondary);
37+ --color-primary-foreground: var(--primary-foreground);
38+ --color-primary: var(--primary);
39+ --color-popover-foreground: var(--popover-foreground);
40+ --color-popover: var(--popover);
41+ --color-card-foreground: var(--card-foreground);
42+ --color-card: var(--card);
43+ --color-foreground: var(--foreground);
44+ --color-background: var(--background);
45+ --radius-sm: calc(var(--radius) - 4px);
46+ --radius-md: calc(var(--radius) - 2px);
47+ --radius-lg: var(--radius);
48+ --radius-xl: calc(var(--radius) + 4px);
49+ --radius-2xl: calc(var(--radius) + 8px);
50+ --radius-3xl: calc(var(--radius) + 12px);
51+ --radius-4xl: calc(var(--radius) + 16px);
52+ @keyframes skeleton {
53+ to {
54+ background-position: -200% 0;
55+ }
56+ }
57+}
58+
59+@layer base {
60+ * {
61+ @apply border-border outline-ring/50;
62+ }
63+ body {
64+ @apply bg-background text-foreground;
65+ }
66+}
67+
68+/* Suppress all transitions during theme changes */
69+.no-transitions,
70+.no-transitions *,
71+.no-transitions *::before,
72+.no-transitions *::after {
73+ transition-duration: 0s !important;
74+ animation-duration: 0s !important;
75+}
76+
377 :root {
4− color-scheme: dark;
78+ color-scheme: light;
79+ --radius: 0.625rem;
80+ --background: var(--color-white);
81+ --foreground: var(--color-neutral-800);
82+ --card: var(--color-white);
83+ --card-foreground: var(--color-neutral-800);
84+ --popover: var(--color-white);
85+ --popover-foreground: var(--color-neutral-800);
86+ --primary: var(--color-neutral-800);
87+ --primary-foreground: var(--color-neutral-50);
88+ --secondary: --alpha(var(--color-black) / 4%);
89+ --secondary-foreground: var(--color-neutral-800);
90+ --muted: --alpha(var(--color-black) / 4%);
91+ --muted-foreground: color-mix(
92+ in srgb,
93+ var(--color-neutral-500) 90%,
94+ var(--color-black)
95+ );
96+ --accent: --alpha(var(--color-black) / 4%);
97+ --accent-foreground: var(--color-neutral-800);
98+ --destructive: var(--color-red-500);
99+ --border: --alpha(var(--color-black) / 8%);
100+ --input: --alpha(var(--color-black) / 10%);
101+ --ring: var(--color-neutral-400);
102+ --chart-1: oklch(0.646 0.222 41.116);
103+ --chart-2: oklch(0.6 0.118 184.704);
104+ --chart-3: oklch(0.398 0.07 227.392);
105+ --chart-4: oklch(0.828 0.189 84.429);
106+ --chart-5: oklch(0.769 0.188 70.08);
107+ --sidebar: oklch(0.985 0 0);
108+ --sidebar-foreground: oklch(0.141 0.005 285.823);
109+ --sidebar-primary: oklch(0.21 0.006 285.885);
110+ --sidebar-primary-foreground: oklch(0.985 0 0);
111+ --sidebar-accent: oklch(0.967 0.001 286.375);
112+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
113+ --sidebar-border: oklch(0.92 0.004 286.32);
114+ --sidebar-ring: oklch(0.705 0.015 286.067);
115+ --destructive-foreground: var(--color-red-700);
116+ --info: var(--color-blue-500);
117+ --info-foreground: var(--color-blue-700);
118+ --success: var(--color-emerald-500);
119+ --success-foreground: var(--color-emerald-700);
120+ --warning: var(--color-amber-500);
121+ --warning-foreground: var(--color-amber-700);
122+
123+ @variant dark {
124+ color-scheme: dark;
125+ --background: color-mix(
126+ in srgb,
127+ var(--color-neutral-950) 95%,
128+ var(--color-white)
129+ );
130+ --foreground: var(--color-neutral-100);
131+ --card: color-mix(in srgb, var(--background) 98%, var(--color-white));
132+ --card-foreground: var(--color-neutral-100);
133+ --popover: color-mix(in srgb, var(--background) 98%, var(--color-white));
134+ --popover-foreground: var(--color-neutral-100);
135+ --primary: var(--color-neutral-100);
136+ --primary-foreground: var(--color-neutral-800);
137+ --secondary: --alpha(var(--color-white) / 4%);
138+ --secondary-foreground: var(--color-neutral-100);
139+ --muted: --alpha(var(--color-white) / 4%);
140+ --muted-foreground: color-mix(
141+ in srgb,
142+ var(--color-neutral-500) 90%,
143+ var(--color-white)
144+ );
145+ --accent: --alpha(var(--color-white) / 4%);
146+ --accent-foreground: var(--color-neutral-100);
147+ --destructive: color-mix(
148+ in srgb,
149+ var(--color-red-500) 90%,
150+ var(--color-white)
151+ );
152+ --border: --alpha(var(--color-white) / 6%);
153+ --input: --alpha(var(--color-white) / 8%);
154+ --ring: var(--color-neutral-500);
155+ --chart-1: oklch(0.488 0.243 264.376);
156+ --chart-2: oklch(0.696 0.17 162.48);
157+ --chart-3: oklch(0.769 0.188 70.08);
158+ --chart-4: oklch(0.627 0.265 303.9);
159+ --chart-5: oklch(0.645 0.246 16.439);
160+ --sidebar: oklch(0.21 0.006 285.885);
161+ --sidebar-foreground: oklch(0.985 0 0);
162+ --sidebar-primary: oklch(0.488 0.243 264.376);
163+ --sidebar-primary-foreground: oklch(0.985 0 0);
164+ --sidebar-accent: oklch(0.274 0.006 286.033);
165+ --sidebar-accent-foreground: oklch(0.985 0 0);
166+ --sidebar-border: oklch(1 0 0 / 10%);
167+ --sidebar-ring: oklch(0.552 0.016 285.938);
168+ --destructive-foreground: var(--color-red-400);
169+ --info: var(--color-blue-500);
170+ --info-foreground: var(--color-blue-400);
171+ --success: var(--color-emerald-500);
172+ --success-foreground: var(--color-emerald-400);
173+ --warning: var(--color-amber-500);
174+ --warning-foreground: var(--color-amber-400);
175+ }
5176 }
6177
7178 body {
@@ -10,8 +181,6 @@ body { · 2 unmodified lines
10181 margin: 0;
11182 padding: 0;
12183 min-height: 100vh;
13− color: #e0e0e0;
14− background: #0c0c0c;
15184 }
16185
17186 body::after {
@@ -55,25 +224,20 @@ input { · 37 unmodified lines
55224 }
56225
57226 ::-webkit-scrollbar-thumb {
58− background: rgba(255, 255, 255, 0.1);
227+ background: rgba(0, 0, 0, 0.15);
59228 border-radius: 3px;
60229 }
61230
62231 ::-webkit-scrollbar-thumb:hover {
63− background: rgba(255, 255, 255, 0.18);
232+ background: rgba(0, 0, 0, 0.25);
64233 }
65234
66−/* Reasoning select — clickable label surface */
67−label:has(> select#reasoning-effort) {
68− position: relative;
235+.dark ::-webkit-scrollbar-thumb {
236+ background: rgba(255, 255, 255, 0.1);
69237 }
70−label:has(> select#reasoning-effort) select {
71− position: absolute;
72− inset: 0;
73− opacity: 0;
74− cursor: pointer;
75− width: 100%;
76− height: 100%;
238+
239+.dark ::-webkit-scrollbar-thumb:hover {
240+ background: rgba(255, 255, 255, 0.18);
77241 }
78242
79243 /* Chat markdown rendering */
@@ -108,34 +272,38 @@ label:has(> select#reasoning-effort) select { · 28 unmodified lines
108272 }
109273
110274 .chat-markdown a {
111− color: #93c5fd;
275+ color: var(--info-foreground);
112276 text-decoration: underline;
113− text-decoration-color: rgba(147, 197, 253, 0.4);
277+ text-decoration-color: color-mix(
278+ in srgb,
279+ var(--info-foreground) 40%,
280+ transparent
281+ );
114282 }
115283
116284 .chat-markdown a:hover {
117− color: #bfdbfe;
285+ opacity: 0.8;
118286 }
119287
120288 .chat-markdown blockquote {
121− border-left: 2px solid rgba(255, 255, 255, 0.2);
289+ border-left: 2px solid var(--border);
122290 padding-left: 0.8rem;
123− color: rgba(224, 224, 224, 0.75);
291+ color: var(--muted-foreground);
124292 }
125293
126294 .chat-markdown :not(pre) > code {
127− border: 1px solid rgba(255, 255, 255, 0.12);
295+ border: 1px solid var(--border);
128296 border-radius: 0.375rem;
129− background: rgba(255, 255, 255, 0.07);
297+ background: var(--muted);
130298 padding: 0.1rem 0.35rem;
131− color: #f4f4f5;
299+ color: var(--foreground);
132300 }
133301
134302 .chat-markdown pre {
135303 overflow-x: auto;
136− border: 1px solid rgba(255, 255, 255, 0.12);
304+ border: 1px solid var(--border);
137305 border-radius: 0.75rem;
138− background: rgba(0, 0, 0, 0.3);
306+ background: var(--muted);
139307 padding: 0.8rem 0.9rem;
140308 }
141309
@@ -153,7 +321,7 @@ label:has(> select#reasoning-effort) select { · 11 unmodified lines
153321
154322 .chat-markdown th,
155323 .chat-markdown td {
156− border: 1px solid rgba(255, 255, 255, 0.12);
324+ border: 1px solid var(--border);
157325 padding: 0.35rem 0.45rem;
158326 text-align: left;
159327 }
apps/renderer/src/lib/utils.ts+6−0
@@ -0,0 +1,6 @@
1+import { type ClassValue, clsx } from "clsx";
2+import { twMerge } from "tailwind-merge";
3+
4+export function cn(...inputs: ClassValue[]) {
5+ return twMerge(clsx(inputs));
6+}
apps/renderer/tsconfig.json+5−1
@@ -4,7 +4,11 @@ · 3 unmodified lines
44 "composite": true,
55 "jsx": "react-jsx",
66 "lib": ["ES2023", "DOM", "DOM.Iterable"],
7− "types": ["vite/client"]
7+ "exactOptionalPropertyTypes": false,
8+ "types": ["vite/client"],
9+ "paths": {
10+ "#/*": ["./src/*"]
11+ }
812 },
913 "include": ["src", "vite.config.ts"]
1014 }
apps/renderer/vite.config.ts+6−0
@@ -1,3 +1,4 @@
1+import path from "node:path";
12 import tailwindcss from "@tailwindcss/vite";
23 import react from "@vitejs/plugin-react";
34 import { defineConfig } from "vite";
@@ -11,6 +12,11 @@ export default defineConfig({ · 7 unmodified lines
1112 }),
1213 tailwindcss(),
1314 ],
15+ resolve: {
16+ alias: {
17+ "#": path.resolve(__dirname, "src"),
18+ },
19+ },
1420 server: {
1521 port: Number(process.env.ELECTRON_RENDERER_PORT ?? 5173),
1622 strictPort: true,
bun.lock+96−1
@@ -34,12 +34,17 @@ · 33 unmodified lines
3434 "version": "0.0.0",
3535 "dependencies": {
3636 "@acme/contracts": "workspace:*",
37+ "@base-ui/react": "^1.1.0",
38+ "class-variance-authority": "^0.7.1",
39+ "clsx": "^2.1.1",
3740 "highlight.js": "^11.11.1",
41+ "lucide-react": "^0.563.0",
3842 "react": "^19.0.0",
3943 "react-dom": "^19.0.0",
4044 "react-markdown": "^10.1.0",
4145 "rehype-highlight": "^7.0.2",
4246 "remark-gfm": "^4.0.1",
47+ "tailwind-merge": "^3.4.0",
4348 "zod": "^3.24.1",
4449 },
4550 "devDependencies": {
@@ -104,12 +109,18 @@ · 58 unmodified lines
104109
105110 "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
106111
112+ "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
113+
107114 "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
108115
109116 "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
110117
111118 "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
112119
120+ "@base-ui/react": ["@base-ui/react@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@base-ui/utils": "0.2.4", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-ikcJRNj1mOiF2HZ5jQHrXoVoHcNHdBU5ejJljcBl+VTLoYXR6FidjTN86GjO6hyshi6TZFuNvv0dEOgaOFv6Lw=="],
121+
122+ "@base-ui/utils": ["@base-ui/utils@0.2.4", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-smZwpMhjO29v+jrZusBSc5T+IJ3vBb9cjIiBjtKcvWmRj9Z4DWGVR3efr1eHR56/bqY5a4qyY9ElkOY5ljo3ng=="],
123+
113124 "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="],
114125
115126 "@electron/node-gyp": ["@electron/node-gyp@github:electron/node-gyp#06b29aa", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": "./bin/node-gyp.js" }, "electron-node-gyp-06b29aa"],
@@ -168,6 +179,14 @@ · 52 unmodified lines
168179
169180 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
170181
182+ "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
183+
184+ "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
185+
186+ "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="],
187+
188+ "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
189+
171190 "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
172191
173192 "@hapi/address": ["@hapi/address@5.1.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA=="],
@@ -182,6 +201,16 @@ · 8 unmodified lines
182201
183202 "@hapi/topo": ["@hapi/topo@6.0.2", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg=="],
184203
204+ "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="],
205+
206+ "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="],
207+
208+ "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
209+
210+ "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="],
211+
212+ "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
213+
185214 "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
186215
187216 "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -194,10 +223,18 @@ · 6 unmodified lines
194223
195224 "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="],
196225
226+ "@mswjs/interceptors": ["@mswjs/interceptors@0.41.2", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-7G0Uf0yK3f2bjElBLGHIQzgRgMESczOMyYVasq1XK8P5HaXtlW4eQhz9MBL+TQILZLaruq+ClGId+hH0w4jvWw=="],
227+
197228 "@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="],
198229
199230 "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="],
200231
232+ "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="],
233+
234+ "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="],
235+
236+ "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="],
237+
201238 "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jmUfF7cNJPw57bEK7sMIqrYRgn4LH428tSgtgLTCtjuGuu1ShREyrkeB7y8HtkXRfhBs4lVY+HMLhqElJvZ6ww=="],
202239
203240 "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-S6vlV8S7jbjzJOSjfVg2CimUC0r7/aHDLdUm/3+/B/SU/s1jV7ivqWkMv1/8EB43d1BBwT9JQ60ZMTkBqeXSFA=="],
@@ -358,6 +395,8 @@ · 154 unmodified lines
358395
359396 "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
360397
398+ "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="],
399+
361400 "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
362401
363402 "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
@@ -460,18 +499,24 @@ · 96 unmodified lines
460499
461500 "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
462501
502+ "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
503+
463504 "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
464505
465506 "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
466507
467508 "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
468509
510+ "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
511+
469512 "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
470513
471514 "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
472515
473516 "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
474517
518+ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
519+
475520 "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
476521
477522 "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
@@ -490,6 +535,8 @@ · 12 unmodified lines
490535
491536 "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
492537
538+ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
539+
493540 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
494541
495542 "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
@@ -614,6 +661,8 @@ · 118 unmodified lines
614661
615662 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
616663
664+ "graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="],
665+
617666 "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
618667
619668 "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
@@ -632,6 +681,8 @@ · 12 unmodified lines
632681
633682 "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
634683
684+ "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],
685+
635686 "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
636687
637688 "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
@@ -680,6 +731,8 @@ · 42 unmodified lines
680731
681732 "is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="],
682733
734+ "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="],
735+
683736 "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
684737
685738 "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
@@ -754,6 +807,8 @@ · 68 unmodified lines
754807
755808 "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
756809
810+ "lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
811+
757812 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
758813
759814 "make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="],
@@ -884,6 +939,10 @@ · 124 unmodified lines
884939
885940 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
886941
942+ "msw": ["msw@2.12.9", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-NYbi51C6M3dujGmcmuGemu68jy12KqQPoVWGeroKToLGsBgrwG5ErM8WctoIIg49/EV49SEvYM9WSqO4G7kNeQ=="],
943+
944+ "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
945+
887946 "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
888947
889948 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
@@ -916,6 +975,8 @@ · 26 unmodified lines
916975
917976 "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
918977
978+ "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="],
979+
919980 "oxfmt": ["oxfmt@0.28.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.28.0", "@oxfmt/darwin-x64": "0.28.0", "@oxfmt/linux-arm64-gnu": "0.28.0", "@oxfmt/linux-arm64-musl": "0.28.0", "@oxfmt/linux-x64-gnu": "0.28.0", "@oxfmt/linux-x64-musl": "0.28.0", "@oxfmt/win32-arm64": "0.28.0", "@oxfmt/win32-x64": "0.28.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-3+hhBqPE6Kp22KfJmnstrZbl+KdOVSEu1V0ABaFIg1rYLtrMgrupx9znnHgHLqKxAVHebjTdiCJDk30CXOt6cw=="],
920981
921982 "oxlint": ["oxlint@1.43.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.43.0", "@oxlint/darwin-x64": "1.43.0", "@oxlint/linux-arm64-gnu": "1.43.0", "@oxlint/linux-arm64-musl": "1.43.0", "@oxlint/linux-x64-gnu": "1.43.0", "@oxlint/linux-x64-musl": "1.43.0", "@oxlint/win32-arm64": "1.43.0", "@oxlint/win32-x64": "1.43.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.11.2" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-xiqTCsKZch+R61DPCjyqUVP2MhkQlRRYxLRBeBDi+dtQJ90MOgdcjIktvDCgXz0bgtx94EQzHEndsizZjMX2OA=="],
@@ -930,6 +991,8 @@ · 8 unmodified lines
930991
931992 "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
932993
994+ "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
995+
933996 "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
934997
935998 "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
@@ -994,6 +1057,8 @@ · 58 unmodified lines
9941057
9951058 "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
9961059
1060+ "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
1061+
9971062 "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
9981063
9991064 "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
@@ -1004,6 +1069,8 @@ · 4 unmodified lines
10041069
10051070 "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
10061071
1072+ "rettime": ["rettime@0.10.1", "", {}, "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw=="],
1073+
10071074 "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
10081075
10091076 "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="],
@@ -1052,8 +1119,12 @@ · 42 unmodified lines
10521119
10531120 "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
10541121
1122+ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
1123+
10551124 "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
10561125
1126+ "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="],
1127+
10571128 "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
10581129
10591130 "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
@@ -1074,6 +1145,12 @@ · 14 unmodified lines
10741145
10751146 "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
10761147
1148+ "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
1149+
1150+ "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
1151+
1152+ "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
1153+
10771154 "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
10781155
10791156 "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
@@ -1096,8 +1173,14 @@ · 16 unmodified lines
10961173
10971174 "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
10981175
1176+ "tldts": ["tldts@7.0.23", "", { "dependencies": { "tldts-core": "^7.0.23" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw=="],
1177+
1178+ "tldts-core": ["tldts-core@7.0.23", "", {}, "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ=="],
1179+
10991180 "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
11001181
1182+ "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="],
1183+
11011184 "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
11021185
11031186 "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
@@ -1124,7 +1207,7 @@ · 20 unmodified lines
11241207
11251208 "turbo-windows-arm64": ["turbo-windows-arm64@2.8.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-afTUGKBRmOJU1smQSBnFGcbq0iabAPwh1uXu2BVk7BREg30/1gMnJh9DFEQTah+UD3n3ru8V55J83RQNFfqoyw=="],
11261209
1127− "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
1210+ "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="],
11281211
11291212 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
11301213
@@ -1154,8 +1237,12 @@ · 23 unmodified lines
11541237
11551238 "unixify": ["unixify@1.0.0", "", { "dependencies": { "normalize-path": "^2.1.1" } }, "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg=="],
11561239
1240+ "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="],
1241+
11571242 "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
11581243
1244+ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
1245+
11591246 "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
11601247
11611248 "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
@@ -1192,6 +1279,8 @@ · 30 unmodified lines
11921279
11931280 "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
11941281
1282+ "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
1283+
11951284 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
11961285
11971286 "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
@@ -1208,6 +1297,10 @@ · 10 unmodified lines
12081297
12091298 "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
12101299
1300+ "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
1301+
1302+ "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
1303+
12111304 "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
12121305
12131306 "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
@@ -1254,6 +1347,8 @@ · 40 unmodified lines
12541347
12551348 "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
12561349
1350+ "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
1351+
12571352 "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
12581353
12591354 "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],