authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-09 01:13:42-08:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-02-09 01:13:42-08:00
logc51f252ff2007f7dbb0abd9a6b0af37476bbca9d
tree688952c9109aabe023f738f2b015f022c64b74ef
parent70a9a043eecc0da4d11f30416add232b4bf6e0f1
signature Signed by PGP key B5690EEEBB952194

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> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

52 files changed, 4872 insertions(+), 918 deletions(-)

apps/desktop/scripts/smoke-test.mjs+2-2
......@@ -13,7 +13,7 @@
1313 * the first render). For production mode (no React DevTools message), we
1414 * instead check that no fatal errors appeared.
1515 */
16import { spawn, execSync } from "node:child_process";
16import { execSync, spawn } from "node:child_process";
1717import { dirname, resolve } from "node:path";
1818import { fileURLToPath } from "node:url";
1919
......@@ -75,7 +75,7 @@ child.on("exit", () => {
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 created+25
......@@ -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 @@
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
......@@ -12,12 +12,17 @@
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() {
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() {
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 = {
1515
1616export 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 @@
1import type { ProviderApprovalDecision, ProviderEvent } from "@acme/contracts";
1import { ArrowUpIcon, LoaderCircleIcon, SquareIcon } from "lucide-react";
22import {
33 type FormEvent,
4 Fragment,
54 type KeyboardEvent,
65 useEffect,
76 useMemo,
......@@ -9,7 +8,6 @@ import {
98 useState,
109} from "react";
1110
12import { EDITORS, type EditorId } from "@acme/contracts";
1311import {
1412 DEFAULT_MODEL,
1513 DEFAULT_REASONING,
......@@ -21,100 +19,44 @@ import {
2119 derivePhase,
2220 deriveTimelineEntries,
2321 deriveWorkLogEntries,
24 formatDuration,
2522 formatElapsed,
2623 formatTimestamp,
2724 readNativeApi,
2825} from "../session-logic";
2926import { useStore } from "../store";
3027import ChatMarkdown from "./ChatMarkdown";
28import { Button } from "./ui/button";
29import {
30 InputGroup,
31 InputGroupAddon,
32 InputGroupText,
33 InputGroupTextarea,
34} from "./ui/input-group";
35import {
36 Select,
37 SelectItem,
38 SelectPopup,
39 SelectTrigger,
40 SelectValue,
41} from "./ui/select";
3142
3243function formatMessageMeta(createdAt: string, duration: string | null): string {
3344 if (!duration) return formatTimestamp(createdAt);
3445 return `${formatTimestamp(createdAt)} • ${duration}`;
3546}
3647
37const FILE_MANAGER_LABEL = navigator.platform.includes("Mac")
38 ? "Finder"
39 : navigator.platform.includes("Win")
40 ? "Explorer"
41 : "Files";
42
43function editorLabel(editor: (typeof EDITORS)[number]): string {
44 return editor.command ? editor.label : FILE_MANAGER_LABEL;
48function 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
47const LAST_EDITOR_KEY = "codething:last-editor";
48
4955function 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
56interface PendingApprovalCard {
57 requestId: string;
58 requestKind: "command" | "file-change";
59 createdAt: string;
60 detail?: string;
61}
62
63function asRecord(value: unknown): Record<string, unknown> | undefined {
64 if (!value || typeof value !== "object") return undefined;
65 return value as Record<string, unknown>;
66}
67
68function asString(value: unknown): string | undefined {
69 return typeof value === "string" ? value : undefined;
70}
71
72function 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
79function 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
12062export default function ChatView() {
......@@ -123,46 +65,30 @@ export default function ChatView() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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 @@
1import { useCallback, useEffect, useMemo, useState } from "react";
2import { DEFAULT_MODEL } from "../model-logic";
1import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
2import { useMemo, useState } from "react";
3import { useTheme } from "../hooks/useTheme";
4import { DEFAULT_MODEL, MODEL_OPTIONS, resolveModelSlug } from "../model-logic";
35import { readNativeApi } from "../session-logic";
46import { useStore } from "../store";
57import type { Project } from "../types";
68
7
89function 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 {
1516 return `${Math.floor(hours / 24)}d ago`;
1617}
1718
18function 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}
19const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const;
2520
2621export 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() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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() {
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 created+67
......@@ -0,0 +1,67 @@
1import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
2import { ChevronDownIcon } from "lucide-react";
3
4import { cn } from "#/lib/utils";
5
6function Accordion(props: AccordionPrimitive.Root.Props) {
7 return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
8}
9
10function 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
20function 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
45function 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
61export {
62 Accordion,
63 AccordionItem,
64 AccordionTrigger,
65 AccordionPanel,
66 AccordionPanel as AccordionContent,
67};
apps/renderer/src/components/ui/alert-dialog.tsx created+167
......@@ -0,0 +1,167 @@
1import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
2
3import { cn } from "#/lib/utils";
4
5const AlertDialogCreateHandle = AlertDialogPrimitive.createHandle;
6
7const AlertDialog = AlertDialogPrimitive.Root;
8
9const AlertDialogPortal = AlertDialogPrimitive.Portal;
10
11function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) {
12 return (
13 <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
14 );
15}
16
17function 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
33function 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
49function 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
80function 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
96function 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
117function 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
133function 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
146function AlertDialogClose(props: AlertDialogPrimitive.Close.Props) {
147 return (
148 <AlertDialogPrimitive.Close data-slot="alert-dialog-close" {...props} />
149 );
150}
151
152export {
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 created+80
......@@ -0,0 +1,80 @@
1import { type VariantProps, cva } from "class-variance-authority";
2import type * as React from "react";
3
4import { cn } from "#/lib/utils";
5
6const 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
26function 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
41function 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
51function 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
67function 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
80export { Alert, AlertTitle, AlertDescription, AlertAction };
apps/renderer/src/components/ui/autocomplete.tsx created+308
......@@ -0,0 +1,308 @@
1import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete";
2import { ChevronsUpDownIcon, XIcon } from "lucide-react";
3
4import { Input } from "#/components/ui/input";
5import { ScrollArea } from "#/components/ui/scroll-area";
6import { cn } from "#/lib/utils";
7
8const Autocomplete = AutocompletePrimitive.Root;
9
10function 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
74function 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
117function 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
136function 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
149function 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
162function 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
178function 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
194function 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
207function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) {
208 return (
209 <AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />
210 );
211}
212
213function 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
231function 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
249function 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
265function AutocompleteCollection({
266 ...props
267}: AutocompletePrimitive.Collection.Props) {
268 return (
269 <AutocompletePrimitive.Collection
270 data-slot="autocomplete-collection"
271 {...props}
272 />
273 );
274}
275
276function 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
289const useAutocompleteFilter = AutocompletePrimitive.useFilter;
290
291export {
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 created+44
......@@ -0,0 +1,44 @@
1import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar";
2
3import { cn } from "#/lib/utils";
4
5function 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
18function 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
28function 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
44export { Avatar, AvatarImage, AvatarFallback };
apps/renderer/src/components/ui/badge.tsx created+58
......@@ -0,0 +1,58 @@
1import { mergeProps } from "@base-ui/react/merge-props";
2import { useRender } from "@base-ui/react/use-render";
3import { type VariantProps, cva } from "class-variance-authority";
4
5import { cn } from "#/lib/utils";
6
7const 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
40interface BadgeProps extends useRender.ComponentProps<"span"> {
41 variant?: VariantProps<typeof badgeVariants>["variant"];
42 size?: VariantProps<typeof badgeVariants>["size"];
43}
44
45function 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
58export { Badge, badgeVariants };
apps/renderer/src/components/ui/button.tsx created+71
......@@ -0,0 +1,71 @@
1import { mergeProps } from "@base-ui/react/merge-props";
2import { useRender } from "@base-ui/react/use-render";
3import { type VariantProps, cva } from "class-variance-authority";
4import type * as React from "react";
5
6import { cn } from "#/lib/utils";
7
8const 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
49interface ButtonProps extends useRender.ComponentProps<"button"> {
50 variant?: VariantProps<typeof buttonVariants>["variant"];
51 size?: VariantProps<typeof buttonVariants>["size"];
52}
53
54function 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
71export { Button, buttonVariants };
apps/renderer/src/components/ui/card.tsx created+242
......@@ -0,0 +1,242 @@
1import { mergeProps } from "@base-ui/react/merge-props";
2import { useRender } from "@base-ui/react/use-render";
3
4import { cn } from "#/lib/utils";
5
6function 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
26function 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
46function 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
63function 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
80function 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
97function 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
114function 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
134function 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
151function 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
168function 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
188function 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
208function 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
228export {
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 created+14
......@@ -0,0 +1,14 @@
1import { CheckboxGroup as CheckboxGroupPrimitive } from "@base-ui/react/checkbox-group";
2
3import { cn } from "#/lib/utils";
4
5function 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
14export { CheckboxGroup };
apps/renderer/src/components/ui/checkbox.tsx created+58
......@@ -0,0 +1,58 @@
1import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
2
3import { cn } from "#/lib/utils";
4
5function 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
58export { Checkbox };
apps/renderer/src/components/ui/collapsible.tsx created+43
......@@ -0,0 +1,43 @@
1import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
2
3import { cn } from "#/lib/utils";
4
5function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
6 return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
7}
8
9function 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
22function 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
38export {
39 Collapsible,
40 CollapsibleTrigger,
41 CollapsiblePanel,
42 CollapsiblePanel as CollapsibleContent,
43};
apps/renderer/src/components/ui/combobox.tsx created+422
......@@ -0,0 +1,422 @@
1import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox";
2import { ChevronsUpDownIcon, XIcon } from "lucide-react";
3import * as React from "react";
4
5import { Input } from "#/components/ui/input";
6import { ScrollArea } from "#/components/ui/scroll-area";
7import { cn } from "#/lib/utils";
8
9const ComboboxContext = React.createContext<{
10 chipsRef: React.RefObject<Element | null> | null;
11 multiple: boolean;
12}>({
13 chipsRef: null,
14 multiple: false,
15});
16
17function 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
28function 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
53function 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
123function 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
136function 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
182function 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
216function 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
229function 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
239function 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
255function 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
268function 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
278function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
279 return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
280}
281
282function 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
297function 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
307function 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
323function ComboboxCollection(props: ComboboxPrimitive.Collection.Props) {
324 return (
325 <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
326 );
327}
328
329function 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
374function 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
387function 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
400const useComboboxFilter = ComboboxPrimitive.useFilter;
401
402export {
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 created+262
......@@ -0,0 +1,262 @@
1import { Dialog as CommandDialogPrimitive } from "@base-ui/react/dialog";
2import { SearchIcon } from "lucide-react";
3import type * as React from "react";
4import {
5 Autocomplete,
6 AutocompleteCollection,
7 AutocompleteEmpty,
8 AutocompleteGroup,
9 AutocompleteGroupLabel,
10 AutocompleteInput,
11 AutocompleteItem,
12 AutocompleteList,
13 AutocompleteSeparator,
14} from "#/components/ui/autocomplete";
15import { cn } from "#/lib/utils";
16
17const CommandDialog = CommandDialogPrimitive.Root;
18
19const CommandDialogPortal = CommandDialogPrimitive.Portal;
20
21const CommandCreateHandle = CommandDialogPrimitive.createHandle;
22
23function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) {
24 return (
25 <CommandDialogPrimitive.Trigger
26 data-slot="command-dialog-trigger"
27 {...props}
28 />
29 );
30}
31
32function 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
48function 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
64function 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
88function 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
104function 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
126function 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
139function 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
152function 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
161function 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
174function 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
187function CommandCollection({
188 ...props
189}: React.ComponentProps<typeof AutocompleteCollection>) {
190 return <AutocompleteCollection data-slot="command-collection" {...props} />;
191}
192
193function 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
206function 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
219function 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
232function 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
245export {
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 created+194
......@@ -0,0 +1,194 @@
1import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
2import { XIcon } from "lucide-react";
3import { Button } from "#/components/ui/button";
4import { ScrollArea } from "#/components/ui/scroll-area";
5import { cn } from "#/lib/utils";
6
7const DialogCreateHandle = DialogPrimitive.createHandle;
8
9const Dialog = DialogPrimitive.Root;
10
11const DialogPortal = DialogPrimitive.Portal;
12
13function DialogTrigger(props: DialogPrimitive.Trigger.Props) {
14 return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
15}
16
17function DialogClose(props: DialogPrimitive.Close.Props) {
18 return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
19}
20
21function 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
37function 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
53function 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
98function 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
111function 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
133function 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
146function 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
159function 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
178export {
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 created+127
......@@ -0,0 +1,127 @@
1import { type VariantProps, cva } from "class-variance-authority";
2
3import { cn } from "#/lib/utils";
4
5function 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
18function 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
31const 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
46function 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
84function 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
94function 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
107function 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
120export {
121 Empty,
122 EmptyHeader,
123 EmptyTitle,
124 EmptyDescription,
125 EmptyContent,
126 EmptyMedia,
127};
apps/renderer/src/components/ui/field.tsx created+72
......@@ -0,0 +1,72 @@
1import { Field as FieldPrimitive } from "@base-ui/react/field";
2
3import { cn } from "#/lib/utils";
4
5function 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
15function 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
28function 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
38function 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
51function 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
61const FieldControl = FieldPrimitive.Control;
62const FieldValidity = FieldPrimitive.Validity;
63
64export {
65 Field,
66 FieldLabel,
67 FieldDescription,
68 FieldError,
69 FieldControl,
70 FieldItem,
71 FieldValidity,
72};
apps/renderer/src/components/ui/fieldset.tsx created+27
......@@ -0,0 +1,27 @@
1import { Fieldset as FieldsetPrimitive } from "@base-ui/react/fieldset";
2
3import { cn } from "#/lib/utils";
4
5function 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}
14function 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
27export { Fieldset, FieldsetLegend };
apps/renderer/src/components/ui/form.tsx created+15
......@@ -0,0 +1,15 @@
1import { Form as FormPrimitive } from "@base-ui/react/form";
2
3import { cn } from "#/lib/utils";
4
5function 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
15export { Form };
apps/renderer/src/components/ui/input-group.tsx created+99
......@@ -0,0 +1,99 @@
1import { type VariantProps, cva } from "class-variance-authority";
2import type * as React from "react";
3
4import { Input, type InputProps } from "#/components/ui/input";
5import { Textarea, type TextareaProps } from "#/components/ui/textarea";
6import { cn } from "#/lib/utils";
7
8function 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
22const 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
43function 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
73function 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
85function InputGroupInput({ className, ...props }: InputProps) {
86 return <Input className={className} unstyled {...props} />;
87}
88
89function InputGroupTextarea({ className, ...props }: TextareaProps) {
90 return <Textarea className={className} unstyled {...props} />;
91}
92
93export {
94 InputGroup,
95 InputGroupAddon,
96 InputGroupText,
97 InputGroupInput,
98 InputGroupTextarea,
99};
apps/renderer/src/components/ui/input.tsx created+64
......@@ -0,0 +1,64 @@
1import { Input as InputPrimitive } from "@base-ui/react/input";
2import type * as React from "react";
3
4import { cn } from "#/lib/utils";
5
6type 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
15function 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
64export { Input, type InputProps };
apps/renderer/src/components/ui/kbd.tsx created+28
......@@ -0,0 +1,28 @@
1import type * as React from "react";
2
3import { cn } from "#/lib/utils";
4
5function 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
18function 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
28export { Kbd, KbdGroup };
apps/renderer/src/components/ui/label.tsx created+26
......@@ -0,0 +1,26 @@
1import { mergeProps } from "@base-ui/react/merge-props";
2import { useRender } from "@base-ui/react/use-render";
3
4import { cn } from "#/lib/utils";
5
6function 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
26export { Label };
apps/renderer/src/components/ui/menu.tsx created+308
......@@ -0,0 +1,308 @@
1import { Menu as MenuPrimitive } from "@base-ui/react/menu";
2import { ChevronRightIcon } from "lucide-react";
3import type * as React from "react";
4import { cn } from "#/lib/utils";
5
6const MenuCreateHandle = MenuPrimitive.createHandle;
7
8const Menu = MenuPrimitive.Root;
9
10const MenuPortal = MenuPrimitive.Portal;
11
12function MenuTrigger(props: MenuPrimitive.Trigger.Props) {
13 return <MenuPrimitive.Trigger data-slot="menu-trigger" {...props} />;
14}
15
16function 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
57function MenuGroup(props: MenuPrimitive.Group.Props) {
58 return <MenuPrimitive.Group data-slot="menu-group" {...props} />;
59}
60
61function 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
84function 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
140function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {
141 return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />;
142}
143
144function 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
178function 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
198function 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
208function 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
221function MenuSub(props: MenuPrimitive.SubmenuRoot.Props) {
222 return <MenuPrimitive.SubmenuRoot data-slot="menu-sub" {...props} />;
223}
224
225function 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
249function 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
275export {
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 created+102
......@@ -0,0 +1,102 @@
1import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
2
3import { cn } from "#/lib/utils";
4
5const PopoverCreateHandle = PopoverPrimitive.createHandle;
6
7const Popover = PopoverPrimitive.Root;
8
9function PopoverTrigger(props: PopoverPrimitive.Trigger.Props) {
10 return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
11}
12
13function 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
66function PopoverClose({ ...props }: PopoverPrimitive.Close.Props) {
67 return <PopoverPrimitive.Close data-slot="popover-close" {...props} />;
68}
69
70function 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
80function 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
93export {
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 created+79
......@@ -0,0 +1,79 @@
1import { Progress as ProgressPrimitive } from "@base-ui/react/progress";
2
3import { cn } from "#/lib/utils";
4
5function 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
27function 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
37function 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
50function 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
63function 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
73export {
74 Progress,
75 ProgressLabel,
76 ProgressTrack,
77 ProgressIndicator,
78 ProgressValue,
79};
apps/renderer/src/components/ui/radio-group.tsx created+34
......@@ -0,0 +1,34 @@
1import { Radio as RadioPrimitive } from "@base-ui/react/radio";
2import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
3
4import { cn } from "#/lib/utils";
5
6function 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
16function 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
34export { RadioGroup, Radio, Radio as RadioGroupItem };
apps/renderer/src/components/ui/scroll-area.tsx created+62
......@@ -0,0 +1,62 @@
1import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
2
3import { cn } from "#/lib/utils";
4
5function 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
39function 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
62export { ScrollArea, ScrollBar };
apps/renderer/src/components/ui/select.tsx created+187
......@@ -0,0 +1,187 @@
1import { Select as SelectPrimitive } from "@base-ui/react/select";
2import {
3 ChevronDownIcon,
4 ChevronUpIcon,
5 ChevronsUpDownIcon,
6} from "lucide-react";
7
8import { cn } from "#/lib/utils";
9
10const Select = SelectPrimitive.Root;
11
12function 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
40function 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
53function 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
114function 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
150function 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
163function SelectGroup(props: SelectPrimitive.Group.Props) {
164 return <SelectPrimitive.Group data-slot="select-group" {...props} />;
165}
166
167function 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
177export {
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 created+23
......@@ -0,0 +1,23 @@
1import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
2
3import { cn } from "#/lib/utils";
4
5function 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
23export { Separator };
apps/renderer/src/components/ui/sheet.tsx created+201
......@@ -0,0 +1,201 @@
1import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
2import { XIcon } from "lucide-react";
3import { Button } from "#/components/ui/button";
4import { ScrollArea } from "#/components/ui/scroll-area";
5import { cn } from "#/lib/utils";
6
7const Sheet = SheetPrimitive.Root;
8
9const SheetPortal = SheetPrimitive.Portal;
10
11function SheetTrigger(props: SheetPrimitive.Trigger.Props) {
12 return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
13}
14
15function SheetClose(props: SheetPrimitive.Close.Props) {
16 return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
17}
18
19function 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
32function 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
57function 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
107function 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
120function 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
142function 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
155function 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
168function 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
187export {
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 created+16
......@@ -0,0 +1,16 @@
1import { cn } from "#/lib/utils";
2
3function 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
16export { Skeleton };
apps/renderer/src/components/ui/spinner.tsx created+18
......@@ -0,0 +1,18 @@
1import { Loader2Icon } from "lucide-react";
2import { cn } from "#/lib/utils";
3
4function 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
18export { Spinner };
apps/renderer/src/components/ui/switch.tsx created+25
......@@ -0,0 +1,25 @@
1import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
2
3import { cn } from "#/lib/utils";
4
5function 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
25export { Switch };
apps/renderer/src/components/ui/table.tsx created+126
......@@ -0,0 +1,126 @@
1import type * as React from "react";
2
3import { cn } from "#/lib/utils";
4
5function 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
23function 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
36function 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
49function 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
62function 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
75function 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
88function 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
101function 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
117export {
118 Table,
119 TableHeader,
120 TableBody,
121 TableFooter,
122 TableHead,
123 TableRow,
124 TableCell,
125 TableCaption,
126};
apps/renderer/src/components/ui/tabs.tsx created+85
......@@ -0,0 +1,85 @@
1import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
2
3import { cn } from "#/lib/utils";
4
5type TabsVariant = "default" | "underline";
6
7function 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
20function 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
55function 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
68function 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
78export {
79 Tabs,
80 TabsList,
81 TabsTab,
82 TabsTab as TabsTrigger,
83 TabsPanel,
84 TabsPanel as TabsContent,
85};
apps/renderer/src/components/ui/textarea.tsx created+49
......@@ -0,0 +1,49 @@
1import { Field as FieldPrimitive } from "@base-ui/react/field";
2import { mergeProps } from "@base-ui/react/merge-props";
3import type * as React from "react";
4
5import { cn } from "#/lib/utils";
6
7type TextareaProps = React.ComponentProps<"textarea"> & {
8 size?: "sm" | "default" | "lg" | number;
9 unstyled?: boolean;
10};
11
12function 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
49export { Textarea, type TextareaProps };
apps/renderer/src/components/ui/toast.tsx created+267
......@@ -0,0 +1,267 @@
1import { Toast } from "@base-ui/react/toast";
2import {
3 CircleAlertIcon,
4 CircleCheckIcon,
5 InfoIcon,
6 LoaderCircleIcon,
7 TriangleAlertIcon,
8} from "lucide-react";
9
10import { buttonVariants } from "#/components/ui/button";
11import { cn } from "#/lib/utils";
12
13const toastManager = Toast.createToastManager();
14const anchoredToastManager = Toast.createToastManager();
15
16const TOAST_ICONS = {
17 error: CircleAlertIcon,
18 info: InfoIcon,
19 loading: LoaderCircleIcon,
20 success: CircleCheckIcon,
21 warning: TriangleAlertIcon,
22} as const;
23
24type ToastPosition =
25 | "top-left"
26 | "top-center"
27 | "top-right"
28 | "bottom-left"
29 | "bottom-center"
30 | "bottom-right";
31
32interface ToastProviderProps extends Toast.Provider.Props {
33 position?: ToastPosition;
34}
35
36function 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
49function 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
167function AnchoredToastProvider({ children, ...props }: Toast.Provider.Props) {
168 return (
169 <Toast.Provider toastManager={anchoredToastManager} {...props}>
170 {children}
171 <AnchoredToasts />
172 </Toast.Provider>
173 );
174}
175
176function 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
261export {
262 ToastProvider,
263 type ToastPosition,
264 toastManager,
265 AnchoredToastProvider,
266 anchoredToastManager,
267};
apps/renderer/src/hooks/useTheme.ts created+90
......@@ -0,0 +1,90 @@
1import { useCallback, useEffect, useSyncExternalStore } from "react";
2
3type Theme = "light" | "dark" | "system";
4
5const STORAGE_KEY = "codething:theme";
6const MEDIA_QUERY = "(prefers-color-scheme: dark)";
7
8let listeners: Array<() => void> = [];
9function emitChange() {
10 for (const listener of listeners) listener();
11}
12
13function getSystemDark(): boolean {
14 return window.matchMedia(MEDIA_QUERY).matches;
15}
16
17function getStored(): Theme {
18 const raw = localStorage.getItem(STORAGE_KEY);
19 if (raw === "light" || raw === "dark" || raw === "system") return raw;
20 return "system";
21}
22
23function 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
39applyTheme(getStored());
40
41function getSnapshot(): Theme {
42 return getStored();
43}
44
45function 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
72export 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
7178body {
......@@ -10,8 +181,6 @@ body {
10181 margin: 0;
11182 padding: 0;
12183 min-height: 100vh;
13 color: #e0e0e0;
14 background: #0c0c0c;
15184}
16185
17186body::after {
......@@ -55,25 +224,20 @@ input {
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 */
67label:has(> select#reasoning-effort) {
68 position: relative;
235.dark ::-webkit-scrollbar-thumb {
236 background: rgba(255, 255, 255, 0.1);
69237}
70label: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 {
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 {
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 created+6
......@@ -0,0 +1,6 @@
1import { type ClassValue, clsx } from "clsx";
2import { twMerge } from "tailwind-merge";
3
4export function cn(...inputs: ClassValue[]) {
5 return twMerge(clsx(inputs));
6}
apps/renderer/tsconfig.json+5-1
......@@ -4,7 +4,11 @@
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
......@@ -1,3 +1,4 @@
1import path from "node:path";
12import tailwindcss from "@tailwindcss/vite";
23import react from "@vitejs/plugin-react";
34import { defineConfig } from "vite";
......@@ -11,6 +12,11 @@ export default defineConfig({
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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=="],