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 @@...@@ -13,7 +13,7 @@
13 * the first render). For production mode (no React DevTools message), we13 * the first render). For production mode (no React DevTools message), we
14 * instead check that no fatal errors appeared.14 * instead check that no fatal errors appeared.
15 */15 */
16import { spawn, execSync } from "node:child_process";16import { execSync, spawn } from "node:child_process";
17import { dirname, resolve } from "node:path";17import { dirname, resolve } from "node:path";
18import { fileURLToPath } from "node:url";18import { fileURLToPath } from "node:url";
1919
...@@ -75,7 +75,7 @@ child.on("exit", () => {...@@ -75,7 +75,7 @@ child.on("exit", () => {
75 for (const f of failures) {75 for (const f of failures) {
76 console.error(` • ${f}`);76 console.error(` • ${f}`);
77 }77 }
78 console.error("\nFull output:\n" + output);78 console.error(`\nFull output:\n${output}`);
79 process.exit(1);79 process.exit(1);
80 }80 }
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 @@...@@ -5,7 +5,7 @@
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>Electron Todo</title>6 <title>Electron Todo</title>
7 </head>7 </head>
8 <body class="bg-slate-100">8 <body>
9 <div id="root"></div>9 <div id="root"></div>
10 <script type="module" src="/src/main.tsx"></script>10 <script type="module" src="/src/main.tsx"></script>
11 </body>11 </body>
apps/renderer/package.json+5
...@@ -12,12 +12,17 @@...@@ -12,12 +12,17 @@
12 },12 },
13 "dependencies": {13 "dependencies": {
14 "@acme/contracts": "workspace:*",14 "@acme/contracts": "workspace:*",
15 "@base-ui/react": "^1.1.0",
16 "class-variance-authority": "^0.7.1",
17 "clsx": "^2.1.1",
15 "highlight.js": "^11.11.1",18 "highlight.js": "^11.11.1",
19 "lucide-react": "^0.563.0",
16 "react": "^19.0.0",20 "react": "^19.0.0",
17 "react-dom": "^19.0.0",21 "react-dom": "^19.0.0",
18 "react-markdown": "^10.1.0",22 "react-markdown": "^10.1.0",
19 "rehype-highlight": "^7.0.2",23 "rehype-highlight": "^7.0.2",
20 "remark-gfm": "^4.0.1",24 "remark-gfm": "^4.0.1",
25 "tailwind-merge": "^3.4.0",
21 "zod": "^3.24.1"26 "zod": "^3.24.1"
22 },27 },
23 "devDependencies": {28 "devDependencies": {
apps/renderer/src/App.tsx+3-3
...@@ -31,10 +31,10 @@ function Layout() {...@@ -31,10 +31,10 @@ function Layout() {
3131
32 if (!api) {32 if (!api) {
33 return (33 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">
35 <div className="drag-region h-[52px] shrink-0" />35 <div className="drag-region h-[52px] shrink-0" />
36 <div className="flex flex-1 items-center justify-center">36 <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">
38 Native bridge unavailable. Launch through Electron.38 Native bridge unavailable. Launch through Electron.
39 </p>39 </p>
40 </div>40 </div>
...@@ -43,7 +43,7 @@ function Layout() {...@@ -43,7 +43,7 @@ function Layout() {
43 }43 }
4444
45 return (45 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">
47 <EventRouter />47 <EventRouter />
48 <Sidebar />48 <Sidebar />
49 <ChatView />49 <ChatView />
apps/renderer/src/components/ChatMarkdown.tsx+1-1
...@@ -15,7 +15,7 @@ const markdownComponents: Components = {...@@ -15,7 +15,7 @@ const markdownComponents: Components = {
1515
16export default function ChatMarkdown({ text }: ChatMarkdownProps) {16export default function ChatMarkdown({ text }: ChatMarkdownProps) {
17 return (17 return (
18 <div className="chat-markdown text-sm leading-relaxed text-[#d0d0d0]">18 <div className="chat-markdown text-sm leading-relaxed text-foreground/80">
19 <ReactMarkdown19 <ReactMarkdown
20 remarkPlugins={[remarkGfm]}20 remarkPlugins={[remarkGfm]}
21 rehypePlugins={[[rehypeHighlight, { detect: true, ignoreMissing: true }]]}21 rehypePlugins={[[rehypeHighlight, { detect: true, ignoreMissing: true }]]}
apps/renderer/src/components/ChatView.tsx+255-754
...@@ -1,7 +1,6 @@...@@ -1,7 +1,6 @@
1import type { ProviderApprovalDecision, ProviderEvent } from "@acme/contracts";1import { ArrowUpIcon, LoaderCircleIcon, SquareIcon } from "lucide-react";
2import {2import {
3 type FormEvent,3 type FormEvent,
4 Fragment,
5 type KeyboardEvent,4 type KeyboardEvent,
6 useEffect,5 useEffect,
7 useMemo,6 useMemo,
...@@ -9,7 +8,6 @@ import {...@@ -9,7 +8,6 @@ import {
9 useState,8 useState,
10} from "react";9} from "react";
1110
12import { EDITORS, type EditorId } from "@acme/contracts";
13import {11import {
14 DEFAULT_MODEL,12 DEFAULT_MODEL,
15 DEFAULT_REASONING,13 DEFAULT_REASONING,
...@@ -21,100 +19,44 @@ import {...@@ -21,100 +19,44 @@ import {
21 derivePhase,19 derivePhase,
22 deriveTimelineEntries,20 deriveTimelineEntries,
23 deriveWorkLogEntries,21 deriveWorkLogEntries,
24 formatDuration,
25 formatElapsed,22 formatElapsed,
26 formatTimestamp,23 formatTimestamp,
27 readNativeApi,24 readNativeApi,
28} from "../session-logic";25} from "../session-logic";
29import { useStore } from "../store";26import { useStore } from "../store";
30import ChatMarkdown from "./ChatMarkdown";27import 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
32function formatMessageMeta(createdAt: string, duration: string | null): string {43function formatMessageMeta(createdAt: string, duration: string | null): string {
33 if (!duration) return formatTimestamp(createdAt);44 if (!duration) return formatTimestamp(createdAt);
34 return `${formatTimestamp(createdAt)} • ${duration}`;45 return `${formatTimestamp(createdAt)} • ${duration}`;
35}46}
3647
37const FILE_MANAGER_LABEL = navigator.platform.includes("Mac")48function statusLabel(phase: string): string {
38 ? "Finder"49 if (phase === "running") return "Thinking / working";
39 : navigator.platform.includes("Win")50 if (phase === "connecting") return "Connecting";
40 ? "Explorer"51 if (phase === "ready") return "Ready";
41 : "Files";52 return "Disconnected";
42
43function editorLabel(editor: (typeof EDITORS)[number]): string {
44 return editor.command ? editor.label : FILE_MANAGER_LABEL;
45}53}
4654
47const LAST_EDITOR_KEY = "codething:last-editor";
48
49function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {55function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {
50 if (tone === "error") return "text-rose-300/50";56 if (tone === "error") return "text-destructive/50";
51 if (tone === "tool") return "text-[#8a8a8a]";57 if (tone === "tool") return "text-muted-foreground";
52 if (tone === "thinking") return "text-[#707070]";58 if (tone === "thinking") return "text-muted-foreground/70";
53 return "text-[#606060]";59 return "text-muted-foreground/60";
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());
118}60}
11961
120export default function ChatView() {62export default function ChatView() {
...@@ -123,46 +65,30 @@ export default function ChatView() {...@@ -123,46 +65,30 @@ export default function ChatView() {
123 const [prompt, setPrompt] = useState("");65 const [prompt, setPrompt] = useState("");
124 const [isSending, setIsSending] = useState(false);66 const [isSending, setIsSending] = useState(false);
125 const [isConnecting, setIsConnecting] = useState(false);67 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 });
134 const [selectedEffort, setSelectedEffort] =68 const [selectedEffort, setSelectedEffort] =
135 useState<string>(DEFAULT_REASONING);69 useState<string>(DEFAULT_REASONING);
136 const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false);
137 const [respondingRequestIds, setRespondingRequestIds] = useState<string[]>(
138 [],
139 );
140 const [nowTick, setNowTick] = useState(() => Date.now());70 const [nowTick, setNowTick] = useState(() => Date.now());
141 const messagesEndRef = useRef<HTMLDivElement>(null);71 const messagesEndRef = useRef<HTMLDivElement>(null);
142 const textareaRef = useRef<HTMLTextAreaElement>(null);
143 const modelMenuRef = useRef<HTMLDivElement>(null);
144 const editorMenuRef = useRef<HTMLDivElement>(null);
14572
146 const activeThread = state.threads.find((t) => t.id === state.activeThreadId);73 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 );
148 const selectedModel = resolveModelSlug(77 const selectedModel = resolveModelSlug(
149 activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL,78 activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL,
150 );79 );
151 const phase = derivePhase(activeThread?.session ?? null);80 const phase = derivePhase(activeThread?.session ?? null);
152 const isWorking = phase === "running" || isSending || isConnecting;81 const isWorking = phase === "running" || isSending || isConnecting;
82 const activeTurnId = activeThread?.session?.activeTurnId;
153 const nowIso = new Date(nowTick).toISOString();83 const nowIso = new Date(nowTick).toISOString();
154 const modelOptions = MODEL_OPTIONS;84 const modelOptions = MODEL_OPTIONS;
155 const workLogEntries = useMemo(85 const workLogEntries = useMemo(
156 () => deriveWorkLogEntries(activeThread?.events ?? [], undefined),86 () => deriveWorkLogEntries(activeThread?.events ?? [], activeTurnId),
157 [activeThread?.events],87 [activeThread?.events, activeTurnId],
158 );
159 const pendingApprovals = useMemo(
160 () => derivePendingApprovals(activeThread?.events ?? []),
161 [activeThread?.events],
162 );88 );
163 const assistantCompletionByItemId = useMemo(() => {89 const assistantCompletionByItemId = useMemo(() => {
164 const map = new Map<string, string>();90 const map = new Map<string, string>();
165 const ordered = [...(activeThread?.events ?? [])].toReversed();91 const ordered = [...(activeThread?.events ?? [])].reverse();
166 for (const event of ordered) {92 for (const event of ordered) {
167 if (event.method !== "item/completed") continue;93 if (event.method !== "item/completed") continue;
168 if (!event.itemId) continue;94 if (!event.itemId) continue;
...@@ -171,113 +97,27 @@ export default function ChatView() {...@@ -171,113 +97,27 @@ export default function ChatView() {
171 return map;97 return map;
172 }, [activeThread?.events]);98 }, [activeThread?.events]);
173 const timelineEntries = useMemo(99 const timelineEntries = useMemo(
174 () => deriveTimelineEntries(activeThread?.messages ?? [], workLogEntries),100 () =>
175 [activeThread?.messages, workLogEntries],101 deriveTimelineEntries(
102 activeThread?.messages ?? [],
103 isWorking ? workLogEntries : [],
104 ),
105 [activeThread?.messages, isWorking, workLogEntries],
176 );106 );
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
262 // Auto-scroll on new messages108 // Auto-scroll on new messages
263 const messageCount = activeThread?.messages.length ?? 0;109 const messageCount = activeThread?.messages.length ?? 0;
264 const workLogCount = workLogEntries.length;110 const workLogCount = workLogEntries.length;
111 // biome-ignore lint/correctness/useExhaustiveDependencies: trigger on message count change
265 useEffect(() => {112 useEffect(() => {
266 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });113 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
267 }, [messageCount]);114 }, [messageCount]);
115 // biome-ignore lint/correctness/useExhaustiveDependencies: auto-scroll while active work-log events stream in
268 useEffect(() => {116 useEffect(() => {
269 if (phase !== "running") return;117 if (phase !== "running") return;
270 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });118 messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
271 }, [phase, workLogCount]);119 }, [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
281 useEffect(() => {121 useEffect(() => {
282 if (phase !== "running") return;122 if (phase !== "running") return;
283 const timer = window.setInterval(() => {123 const timer = window.setInterval(() => {
...@@ -288,63 +128,6 @@ export default function ChatView() {...@@ -288,63 +128,6 @@ export default function ChatView() {
288 };128 };
289 }, [phase]);129 }, [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
348 const ensureSession = async (): Promise<string | null> => {131 const ensureSession = async (): Promise<string | null> => {
349 if (!api || !activeThread || !activeProject) return null;132 if (!api || !activeThread || !activeProject) return null;
350 if (activeThread.session && activeThread.session.status !== "closed") {133 if (activeThread.session && activeThread.session.status !== "closed") {
...@@ -357,8 +140,6 @@ export default function ChatView() {...@@ -357,8 +140,6 @@ export default function ChatView() {
357 provider: "codex",140 provider: "codex",
358 cwd: activeProject.cwd || undefined,141 cwd: activeProject.cwd || undefined,
359 model: selectedModel || undefined,142 model: selectedModel || undefined,
360 approvalPolicy: runtimeSessionConfig.approvalPolicy,
361 sandboxMode: runtimeSessionConfig.sandboxMode,
362 });143 });
363 dispatch({144 dispatch({
364 type: "UPDATE_SESSION",145 type: "UPDATE_SESSION",
...@@ -386,7 +167,8 @@ export default function ChatView() {...@@ -386,7 +167,8 @@ export default function ChatView() {
386167
387 // Auto-title from first message168 // Auto-title from first message
388 if (activeThread.messages.length === 0) {169 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;
390 dispatch({172 dispatch({
391 type: "SET_THREAD_TITLE",173 type: "SET_THREAD_TITLE",
392 threadId: activeThread.id,174 threadId: activeThread.id,
...@@ -437,37 +219,6 @@ export default function ChatView() {...@@ -437,37 +219,6 @@ export default function ChatView() {
437 });219 });
438 };220 };
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
471 const onModelSelect = (model: string) => {222 const onModelSelect = (model: string) => {
472 if (!activeThread) return;223 if (!activeThread) return;
473 dispatch({224 dispatch({
...@@ -475,7 +226,6 @@ export default function ChatView() {...@@ -475,7 +226,6 @@ export default function ChatView() {
475 threadId: activeThread.id,226 threadId: activeThread.id,
476 model: resolveModelSlug(model),227 model: resolveModelSlug(model),
477 });228 });
478 setIsModelMenuOpen(false);
479 };229 };
480230
481 const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {231 const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
...@@ -488,11 +238,13 @@ export default function ChatView() {...@@ -488,11 +238,13 @@ export default function ChatView() {
488 // Empty state: no active thread238 // Empty state: no active thread
489 if (!activeThread) {239 if (!activeThread) {
490 return (240 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">
492 <div className="drag-region h-[52px] shrink-0" />242 <div className="drag-region h-[52px] shrink-0" />
493 <div className="flex flex-1 items-center justify-center">243 <div className="flex flex-1 items-center justify-center">
494 <div className="text-center">244 <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>
496 </div>248 </div>
497 </div>249 </div>
498 </div>250 </div>
...@@ -500,55 +252,61 @@ export default function ChatView() {...@@ -500,55 +252,61 @@ export default function ChatView() {
500 }252 }
501253
502 return (254 return (
503 <div className="flex flex-1 flex-col bg-[#0c0c0c]">255 <div className="flex flex-1 flex-col bg-background">
504 {/* Top bar */}256 {/* 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">
506 <div className="flex items-center gap-3">258 <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">
508 {activeThread.title}260 {activeThread.title}
509 </h2>261 </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 )}
510 </div>267 </div>
511 <div className="flex items-center gap-3">268 <div className="flex items-center gap-3">
512 {/* Open in editor */}269 {/* Status indicator */}
513 {activeProject && (270 <div
514 <div className="relative" ref={editorMenuRef}>271 className={`inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-[10px] ${
515 <button272 phase === "running"
516 type="button"273 ? "border-sky-400/35 bg-sky-500/8 text-sky-700 dark:text-sky-100"
517 className="rounded-md px-2 py-1 text-[10px] text-[#a0a0a0]/40 transition-colors duration-150 hover:text-[#a0a0a0]/60"274 : phase === "connecting"
518 onClick={() => setIsEditorMenuOpen((v) => !v)}275 ? "border-amber-400/35 bg-amber-500/8 text-amber-700 dark:text-amber-100"
519 >276 : phase === "ready"
520 Open in&hellip;277 ? "border-emerald-400/35 bg-emerald-500/8 text-emerald-700 dark:text-emerald-100"
521 </button>278 : "border-border bg-secondary text-muted-foreground"
522 {isEditorMenuOpen && (279 }`}
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">280 >
524 {EDITORS.map((editor) => (281 <span className="relative inline-flex h-2.5 w-2.5">
525 <button282 {(phase === "running" || phase === "connecting") && (
526 key={editor.id}283 <span
527 type="button"284 className={`absolute inline-flex h-full w-full animate-ping rounded-full ${
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]"285 phase === "running" ? "bg-sky-300/70" : "bg-amber-300/70"
529 onClick={() => openInEditor(editor.id)}286 }`}
530 >287 />
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>
542 )}288 )}
543 </div>289 <span
544 )}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>
545 {/* Diff toggle */}303 {/* Diff toggle */}
546 <button304 <button
547 type="button"305 type="button"
548 className={`rounded-md px-2 py-1 text-[10px] transition-colors duration-150 ${306 className={`rounded-md px-2 py-1 text-[10px] transition-colors duration-150 ${
549 state.diffOpen307 state.diffOpen
550 ? "bg-white/10 text-white"308 ? "bg-accent text-accent-foreground"
551 : "text-[#a0a0a0]/40 hover:text-[#a0a0a0]/60"309 : "text-muted-foreground/60 hover:text-muted-foreground"
552 }`}310 }`}
553 onClick={() => dispatch({ type: "TOGGLE_DIFF" })}311 onClick={() => dispatch({ type: "TOGGLE_DIFF" })}
554 >312 >
...@@ -559,192 +317,105 @@ export default function ChatView() {...@@ -559,192 +317,105 @@ export default function ChatView() {
559317
560 {/* Error banner */}318 {/* Error banner */}
561 {activeThread.error && (319 {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">
563 {activeThread.error}321 {activeThread.error}
564 </div>322 </div>
565 )}323 )}
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
642 {/* Messages */}325 {/* Messages */}
643 <div className="flex-1 overflow-y-auto px-5 py-4">326 <div className="flex-1 overflow-y-auto px-5 py-4">
644 {activeThread.messages.length === 0 && !isWorking ? (327 {activeThread.messages.length === 0 && !isWorking ? (
645 <div className="flex h-full items-center justify-center">328 <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>
647 </div>332 </div>
648 ) : (333 ) : (
649 <div className="mx-auto max-w-3xl space-y-4">334 <div className="mx-auto max-w-3xl space-y-4">
650 {timelineEntries.map((timelineEntry, index) => (335 {timelineEntries.map((timelineEntry) =>
651 <Fragment key={timelineEntry.id}>336 timelineEntry.kind === "work" ? (
652 {timelineEntry.kind === "message" &&337 <div
653 timelineEntry.message.role === "assistant" &&338 key={timelineEntry.id}
654 (completionDividerBeforeEntryId === timelineEntry.id ||339 className="border-l-2 border-border/60 pl-4 py-1"
655 timelineEntries[index - 1]?.kind === "work") && (340 >
656 <div className="my-3 flex items-center gap-3">341 <p
657 <span className="h-px flex-1 bg-white/[0.1]" />342 className={`py-[2px] text-[12px] leading-relaxed ${workToneClass(timelineEntry.entry.tone)}`}
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">343 >
659 {completionSummary344 {timelineEntry.entry.detail ? (
660 ? `Response • ${completionSummary}`345 <>
661 : "Response"}346 {timelineEntry.entry.label}
662 </span>347 <span className="ml-1.5 font-mono text-[11px] opacity-60">
663 <span className="h-px flex-1 bg-white/[0.1]" />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>
664 </div>368 </div>
665 )}369 ) : (
666 {timelineEntry.kind === "work" ? (370 <div className="border-l-2 border-border pl-4">
667 <div className="flex items-start gap-2 py-0.5 pl-1.5">371 <ChatMarkdown
668 <span className="mt-[7px] h-1.5 w-1.5 shrink-0 rounded-full bg-white/[0.18]" />372 text={
669 <p373 timelineEntry.message.text ||
670 className={`py-[2px] text-[11px] leading-relaxed ${workToneClass(timelineEntry.entry.tone)}`}374 (timelineEntry.message.streaming
671 >375 ? ""
672 {timelineEntry.entry.detail ? (376 : "(empty response)")
673 <>377 }
674 {timelineEntry.entry.label}378 />
675 <span379 {timelineEntry.message.streaming && (
676 className="ml-1.5 inline-block max-w-[70ch] truncate align-bottom font-mono text-[11px] opacity-60"380 <div className="pt-1.5">
677 title={timelineEntry.entry.detail}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">
678 >382 <span className="inline-flex gap-1">
679 {timelineEntry.entry.detail}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>
680 </span>388 </span>
681 </>389 </div>
682 ) : (
683 timelineEntry.entry.label
684 )}390 )}
685 </p>391 <p className="mt-1.5 text-[10px] text-muted-foreground/40">
686 </div>392 {formatMessageMeta(
687 ) : timelineEntry.message.role === "user" ? (393 timelineEntry.message.createdAt,
688 <div className="flex justify-end">394 timelineEntry.message.streaming
689 <div className="max-w-[80%] rounded-2xl rounded-br-sm border border-white/[0.08] bg-white/[0.05] px-4 py-3">395 ? formatElapsed(
690 <pre className="whitespace-pre-wrap break-words font-mono text-sm leading-relaxed text-[#e0e0e0]">396 timelineEntry.message.createdAt,
691 {timelineEntry.message.text}397 nowIso,
692 </pre>398 )
693 <p className="mt-1.5 text-right text-[10px] text-[#a0a0a0]/30">399 : formatElapsed(
694 {formatTimestamp(timelineEntry.message.createdAt)}400 timelineEntry.message.createdAt,
401 assistantCompletionByItemId.get(
402 timelineEntry.message.id,
403 ),
404 ),
405 )}
695 </p>406 </p>
696 </div>407 </div>
697 </div>408 )}
698 ) : (409 </div>
699 <div className="px-1 py-0.5">410 ),
700 <ChatMarkdown411 )}
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 ))}
740 {isWorking && (412 {isWorking && (
741 <div className="flex items-center gap-2 py-0.5 pl-1.5">413 <div className="border-l-2 border-border/60 pl-4 py-1">
742 <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-white/[0.18]" />
743 <div className="flex items-center pt-1">414 <div className="flex items-center pt-1">
744 <span className="inline-flex items-center gap-[3px]">415 <span className="inline-flex items-center gap-[3px]">
745 <span className="h-1 w-1 rounded-full bg-white/20 animate-pulse" />416 <span className="h-1 w-1 rounded-full bg-foreground/20 animate-pulse" />
746 <span className="h-1 w-1 rounded-full bg-white/20 animate-pulse [animation-delay:200ms]" />417 <span className="h-1 w-1 rounded-full bg-foreground/20 animate-pulse [animation-delay:200ms]" />
747 <span className="h-1 w-1 rounded-full bg-white/20 animate-pulse [animation-delay:400ms]" />418 <span className="h-1 w-1 rounded-full bg-foreground/20 animate-pulse [animation-delay:400ms]" />
748 </span>419 </span>
749 </div>420 </div>
750 </div>421 </div>
...@@ -757,271 +428,101 @@ export default function ChatView() {...@@ -757,271 +428,101 @@ export default function ChatView() {
757 {/* Input bar */}428 {/* Input bar */}
758 <div className="px-5 pb-4 pt-2">429 <div className="px-5 pb-4 pt-2">
759 <form onSubmit={onSend} className="mx-auto max-w-3xl">430 <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">431 <InputGroup className="rounded-[20px] before:rounded-[19px] **:[textarea]:max-h-[200px]">
761 {/* Textarea area */}432 <InputGroupTextarea
762 <div className="px-4 pt-4 pb-2">433 value={prompt}
763 <textarea434 onChange={(e) => setPrompt(e.target.value)}
764 ref={textareaRef}435 onKeyDown={onKeyDown}
765 className="w-full resize-none bg-transparent text-[14px] leading-relaxed text-[#e0e0e0] placeholder:text-[#a0a0a0]/35 focus:outline-none"436 placeholder={
766 rows={2}437 phase === "disconnected"
767 value={prompt}438 ? "Ask for follow-up changes"
768 onChange={(e) => setPrompt(e.target.value)}439 : "Ask anything..."
769 onKeyDown={onKeyDown}440 }
770 placeholder={441 disabled={isSending || isConnecting}
771 phase === "disconnected" ? "Ask for follow-up changes" : "Ask anything..."442 rows={2}
772 }443 className="font-sans!"
773 disabled={isSending || isConnecting}444 />
774 />445 <InputGroupAddon align="block-end">
775 </div>446 {/* Model picker */}
776447 <Select
777 {/* Bottom toolbar */}448 value={selectedModel}
778 <div className="flex items-center justify-between px-3 pb-3">449 onValueChange={(val) => val && onModelSelect(val)}
779 <div className="flex items-center gap-1">450 >
780 {/* Model picker */}451 <SelectTrigger
781 <div className="relative" ref={modelMenuRef}>452 size="sm"
782 <button453 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"
783 type="button"454 >
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]"455 <SelectValue />
785 onClick={() => setIsModelMenuOpen((open) => !open)}456 </SelectTrigger>
786 >457 <SelectPopup side="top" align="start">
787 <span className="max-w-[180px] truncate">{selectedModel}</span>458 {modelOptions.map((model) => (
788 <svg459 <SelectItem key={model} value={model}>
789 width="10"460 {model}
790 height="10"461 </SelectItem>
791 viewBox="0 0 10 10"462 ))}
792 fill="none"463 </SelectPopup>
793 className="opacity-50"464 </Select>
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" />
840465
841 {/* Reasoning effort */}466 {/* Reasoning effort */}
842 <label467 <Select
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]"468 value={selectedEffort}
844 htmlFor="reasoning-effort"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"
845 >474 >
846 <span>{selectedEffort.charAt(0).toUpperCase() + selectedEffort.slice(1)}</span>475 <SelectValue />
847 <select476 </SelectTrigger>
848 id="reasoning-effort"477 <SelectPopup side="top" align="start">
849 className="absolute opacity-0 w-0 h-0"478 {REASONING_OPTIONS.map((effort) => (
850 value={selectedEffort}479 <SelectItem key={effort} value={effort}>
851 onChange={(event) => setSelectedEffort(event.target.value)}480 {effort.charAt(0).toUpperCase() + effort.slice(1)}
852 >481 {effort === DEFAULT_REASONING ? " (default)" : ""}
853 {REASONING_OPTIONS.map((effort) => (482 </SelectItem>
854 <option key={effort} value={effort} className="bg-[#1b1b1d]">483 ))}
855 {effort}484 </SelectPopup>
856 {effort === DEFAULT_REASONING ? " (default)" : ""}485 </Select>
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>
877486
878 {/* Divider */}487 {activeProject && (
879 <div className="mx-0.5 h-4 w-px bg-white/[0.08]" />488 <InputGroupText className="ml-auto text-muted-foreground/40">
489 {activeProject.name}
490 </InputGroupText>
491 )}
880492
881 {/* Runtime mode toggle */}493 {phase === "running" ? (
882 <button494 <Button
883 type="button"495 variant="destructive"
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]"496 size="icon-sm"
885 disabled={isSwitchingRuntimeMode}497 className="rounded-full"
886 onClick={() =>498 onClick={() => void onInterrupt()}
887 void handleRuntimeModeChange(499 aria-label="Stop generation"
888 state.runtimeMode === "full-access"500 >
889 ? "approval-required"501 <SquareIcon className="size-3" />
890 : "full-access",502 </Button>
891 )503 ) : (
892 }504 <Button
893 title={505 type="submit"
894 state.runtimeMode === "full-access"506 size="icon-sm"
895 ? "Full access — click to require approvals"507 className="rounded-full"
896 : "Approval required — click for full access"508 disabled={isSending || isConnecting || !prompt.trim()}
509 aria-label={
510 isConnecting
511 ? "Connecting"
512 : isSending
513 ? "Sending"
514 : "Send message"
897 }515 }
898 >516 >
899 {state.runtimeMode === "full-access" ? (517 {isConnecting || isSending ? (
900 <svg518 <LoaderCircleIcon className="animate-spin" />
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>
923 ) : (519 ) : (
924 <svg520 <ArrowUpIcon />
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>
947 )}521 )}
948 <span>522 </Button>
949 {state.runtimeMode === "full-access"523 )}
950 ? "Full access"524 </InputGroupAddon>
951 : "Supervised"}525 </InputGroup>
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>
1025 </form>526 </form>
1026 </div>527 </div>
1027 </div>528 </div>
apps/renderer/src/components/DiffPanel.tsx+18-10
...@@ -4,13 +4,15 @@ export default function DiffPanel() {...@@ -4,13 +4,15 @@ export default function DiffPanel() {
4 const { dispatch } = useStore();4 const { dispatch } = useStore();
55
6 return (6 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">
8 {/* Header */}8 {/* Header */}
9 <div className="drag-region flex items-center justify-between border-b border-white/[0.08] px-4 pt-[28px] pb-3">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-[#e0e0e0]">Uncommitted changes</h3>10 <h3 className="text-xs font-medium text-foreground">
11 Uncommitted changes
12 </h3>
11 <button13 <button
12 type="button"14 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"
14 onClick={() => dispatch({ type: "TOGGLE_DIFF" })}16 onClick={() => dispatch({ type: "TOGGLE_DIFF" })}
15 >17 >
16 <span className="text-sm">&times;</span>18 <span className="text-sm">&times;</span>
...@@ -20,30 +22,36 @@ export default function DiffPanel() {...@@ -20,30 +22,36 @@ export default function DiffPanel() {
20 {/* Placeholder content */}22 {/* Placeholder content */}
21 <div className="flex-1 overflow-y-auto p-4">23 <div className="flex-1 overflow-y-auto p-4">
22 <div className="space-y-3">24 <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">
24 <div className="mb-2 flex items-center gap-2">26 <div className="mb-2 flex items-center gap-2">
25 <span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[9px] font-medium text-emerald-400">27 <span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[9px] font-medium text-emerald-400">
26 M28 M
27 </span>29 </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>
29 </div>33 </div>
30 <div className="space-y-0.5 font-mono text-[11px]">34 <div className="space-y-0.5 font-mono text-[11px]">
31 <div className="text-red-400/60">- const oldValue = "hello";</div>35 <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>
33 </div>39 </div>
34 </div>40 </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">
37 <div className="flex items-center gap-2">43 <div className="flex items-center gap-2">
38 <span className="rounded bg-blue-500/20 px-1.5 py-0.5 text-[9px] font-medium text-blue-400">44 <span className="rounded bg-blue-500/20 px-1.5 py-0.5 text-[9px] font-medium text-blue-400">
39 A45 A
40 </span>46 </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>
42 </div>50 </div>
43 </div>51 </div>
44 </div>52 </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">
47 Diff integration coming soon55 Diff integration coming soon
48 </p>56 </p>
49 </div>57 </div>
apps/renderer/src/components/Sidebar.tsx+95-119
...@@ -1,10 +1,11 @@...@@ -1,10 +1,11 @@
1import { useCallback, useEffect, useMemo, useState } from "react";1import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
2import { DEFAULT_MODEL } from "../model-logic";2import { useMemo, useState } from "react";
3import { useTheme } from "../hooks/useTheme";
4import { DEFAULT_MODEL, MODEL_OPTIONS, resolveModelSlug } from "../model-logic";
3import { readNativeApi } from "../session-logic";5import { readNativeApi } from "../session-logic";
4import { useStore } from "../store";6import { useStore } from "../store";
5import type { Project } from "../types";7import type { Project } from "../types";
68
7
8function formatRelativeTime(iso: string): string {9function formatRelativeTime(iso: string): string {
9 const diff = Date.now() - new Date(iso).getTime();10 const diff = Date.now() - new Date(iso).getTime();
10 const minutes = Math.floor(diff / 60_000);11 const minutes = Math.floor(diff / 60_000);
...@@ -15,58 +16,53 @@ function formatRelativeTime(iso: string): string {...@@ -15,58 +16,53 @@ function formatRelativeTime(iso: string): string {
15 return `${Math.floor(hours / 24)}d ago`;16 return `${Math.floor(hours / 24)}d ago`;
16}17}
1718
18function threadStatusLabel(19const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const;
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}
2520
26export default function Sidebar() {21export default function Sidebar() {
27 const { state, dispatch } = useStore();22 const { state, dispatch } = useStore();
28 const api = useMemo(() => readNativeApi(), []);23 const api = useMemo(() => readNativeApi(), []);
24 const { theme, setTheme } = useTheme();
29 const [addingProject, setAddingProject] = useState(false);25 const [addingProject, setAddingProject] = useState(false);
30 const [newCwd, setNewCwd] = useState("");26 const [newCwd, setNewCwd] = useState("");
27 const [newModel, setNewModel] = useState(DEFAULT_MODEL);
31 const [isPickingFolder, setIsPickingFolder] = useState(false);28 const [isPickingFolder, setIsPickingFolder] = useState(false);
3229
33 const handleAddProject = () => {30 const handleAddProject = () => {
34 const cwd = newCwd.trim();31 const cwd = newCwd.trim();
35 if (!cwd) return;32 if (!cwd) return;
36 const name = cwd.split("/").filter(Boolean).pop() ?? "project";33 const name = cwd.split("/").filter(Boolean).pop() ?? "project";
34 const normalizedModel = resolveModelSlug(newModel);
37 const project: Project = {35 const project: Project = {
38 id: crypto.randomUUID(),36 id: crypto.randomUUID(),
39 name,37 name,
40 cwd,38 cwd,
41 model: DEFAULT_MODEL,39 model: normalizedModel,
42 expanded: true,40 expanded: true,
43 };41 };
44 dispatch({ type: "ADD_PROJECT", project });42 dispatch({ type: "ADD_PROJECT", project });
45 setNewCwd("");43 setNewCwd("");
44 setNewModel(DEFAULT_MODEL);
46 setAddingProject(false);45 setAddingProject(false);
47 };46 };
4847
49 const handleNewThread = useCallback(48 const handleNewThread = (projectId: string) => {
50 (projectId: string) => {49 dispatch({
51 dispatch({50 type: "ADD_THREAD",
52 type: "ADD_THREAD",51 thread: {
53 thread: {52 id: crypto.randomUUID(),
54 id: crypto.randomUUID(),53 projectId,
55 projectId,54 title: "New thread",
56 title: "New thread",55 model:
57 model:56 state.projects.find((p) => p.id === projectId)?.model ??
58 state.projects.find((p) => p.id === projectId)?.model ??57 DEFAULT_MODEL,
59 DEFAULT_MODEL,58 session: null,
60 session: null,59 messages: [],
61 messages: [],60 events: [],
62 events: [],61 error: null,
63 error: null,62 createdAt: new Date().toISOString(),
64 createdAt: new Date().toISOString(),63 },
65 },64 });
66 });65 };
67 },
68 [dispatch, state.projects],
69 );
7066
71 const handlePickFolder = async () => {67 const handlePickFolder = async () => {
72 if (!api || isPickingFolder) return;68 if (!api || isPickingFolder) return;
...@@ -80,62 +76,40 @@ export default function Sidebar() {...@@ -80,62 +76,40 @@ export default function Sidebar() {
80 }76 }
81 };77 };
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
122 return (79 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">
124 {/* Drag region / traffic light space */}81 {/* Drag region / traffic light space */}
125 <div className="drag-region h-[52px] shrink-0" />82 <div className="drag-region h-[52px] shrink-0" />
126 {/* Branding */}83 {/* Branding */}
127 <div className="flex items-center gap-2 px-4 pb-4">84 <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">
129 CT86 CT
130 </div>87 </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>
132 </div>106 </div>
133107
134 {/* New thread (global) */}108 {/* New thread (global) */}
135 <div className="px-3 pb-3">109 <div className="px-3 pb-3">
136 <button110 <button
137 type="button"111 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"
139 onClick={() => {113 onClick={() => {
140 if (state.projects.length === 0) {114 if (state.projects.length === 0) {
141 setAddingProject(true);115 setAddingProject(true);
...@@ -145,7 +119,7 @@ export default function Sidebar() {...@@ -145,7 +119,7 @@ export default function Sidebar() {
145 if (firstProject) handleNewThread(firstProject.id);119 if (firstProject) handleNewThread(firstProject.id);
146 }}120 }}
147 >121 >
148 <span className="text-white">+</span>122 <span className="text-foreground">+</span>
149 New thread123 New thread
150 </button>124 </button>
151 </div>125 </div>
...@@ -153,46 +127,43 @@ export default function Sidebar() {...@@ -153,46 +127,43 @@ export default function Sidebar() {
153 {/* Project list */}127 {/* Project list */}
154 <nav className="flex-1 overflow-y-auto px-2">128 <nav className="flex-1 overflow-y-auto px-2">
155 {state.projects.map((project) => {129 {state.projects.map((project) => {
156 const threads = state.threads130 const threads = state.threads.filter(
157 .filter((t) => t.projectId === project.id)131 (t) => t.projectId === project.id,
158 .sort((a, b) => {132 );
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 });
165 return (133 return (
166 <div key={project.id} className="mb-1">134 <div key={project.id} className="mb-1">
167 {/* Project header */}135 {/* Project header */}
168 <button136 <button
169 type="button"137 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"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"
171 onClick={() => dispatch({ type: "TOGGLE_PROJECT", projectId: project.id })}139 onClick={() =>
140 dispatch({ type: "TOGGLE_PROJECT", projectId: project.id })
141 }
172 >142 >
173 <span className="text-[10px] text-[#a0a0a0]/50">143 <span className="text-[10px] text-muted-foreground/70">
174 {project.expanded ? "â–¼" : "â–¶"}144 {project.expanded ? "â–¼" : "â–¶"}
175 </span>145 </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">
177 {project.name}147 {project.name}
178 </span>148 </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>
180 </button>152 </button>
181153
182 {/* Threads */}154 {/* Threads */}
183 {project.expanded && (155 {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">
185 {threads.map((thread) => {157 {threads.map((thread) => {
186 const isActive = state.activeThreadId === thread.id;158 const isActive = state.activeThreadId === thread.id;
187 const threadStatus = threadStatusLabel(
188 thread.session?.status,
189 );
190 return (159 return (
191 <button160 <button
192 key={thread.id}161 key={thread.id}
193 type="button"162 type="button"
194 className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 ${163 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"
196 }`}167 }`}
197 onClick={() =>168 onClick={() =>
198 dispatch({169 dispatch({
...@@ -201,20 +172,10 @@ export default function Sidebar() {...@@ -201,20 +172,10 @@ export default function Sidebar() {
201 })172 })
202 }173 }
203 >174 >
204 <div className="flex min-w-0 flex-1 items-center gap-1.5">175 <span className="flex-1 truncate text-xs">
205 {threadStatus && (176 {thread.title}
206 <span className="inline-flex items-center gap-1 text-[10px] text-[#7ed8ff]/80">177 </span>
207 <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-300/80" />178 <span className="shrink-0 text-[10px] text-muted-foreground/40">
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">
218 {formatRelativeTime(thread.createdAt)}179 {formatRelativeTime(thread.createdAt)}
219 </span>180 </span>
220 </button>181 </button>
...@@ -224,7 +185,7 @@ export default function Sidebar() {...@@ -224,7 +185,7 @@ export default function Sidebar() {
224 {/* New thread within project */}185 {/* New thread within project */}
225 <button186 <button
226 type="button"187 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"
228 onClick={() => handleNewThread(project.id)}189 onClick={() => handleNewThread(project.id)}
229 >190 >
230 <span>+</span> New thread191 <span>+</span> New thread
...@@ -236,7 +197,7 @@ export default function Sidebar() {...@@ -236,7 +197,7 @@ export default function Sidebar() {
236 })}197 })}
237198
238 {state.projects.length === 0 && !addingProject && (199 {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">
240 No projects yet.201 No projects yet.
241 <br />202 <br />
242 Add one to get started.203 Add one to get started.
...@@ -246,12 +207,12 @@ export default function Sidebar() {...@@ -246,12 +207,12 @@ export default function Sidebar() {
246207
247 {/* Add project form */}208 {/* Add project form */}
248 {addingProject ? (209 {addingProject ? (
249 <div className="border-t border-white/8 p-3">210 <div className="border-t border-border p-3">
250 <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-[#a0a0a0]/50">211 <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70">
251 Add project212 Add project
252 </p>213 </p>
253 <input214 <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"
255 placeholder="/path/to/project"216 placeholder="/path/to/project"
256 value={newCwd}217 value={newCwd}
257 onChange={(e) => setNewCwd(e.target.value)}218 onChange={(e) => setNewCwd(e.target.value)}
...@@ -263,24 +224,39 @@ export default function Sidebar() {...@@ -263,24 +224,39 @@ export default function Sidebar() {
263 {api && (224 {api && (
264 <button225 <button
265 type="button"226 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"
267 onClick={() => void handlePickFolder()}228 onClick={() => void handlePickFolder()}
268 disabled={isPickingFolder}229 disabled={isPickingFolder}
269 >230 >
270 {isPickingFolder ? "Picking folder..." : "Browse for folder"}231 {isPickingFolder ? "Picking folder..." : "Browse for folder"}
271 </button>232 </button>
272 )}233 )}
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>
273 <div className="flex gap-2">249 <div className="flex gap-2">
274 <button250 <button
275 type="button"251 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"
277 onClick={handleAddProject}253 onClick={handleAddProject}
278 >254 >
279 Add255 Add
280 </button>256 </button>
281 <button257 <button
282 type="button"258 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"
284 onClick={() => setAddingProject(false)}260 onClick={() => setAddingProject(false)}
285 >261 >
286 Cancel262 Cancel
...@@ -288,10 +264,10 @@ export default function Sidebar() {...@@ -288,10 +264,10 @@ export default function Sidebar() {
288 </div>264 </div>
289 </div>265 </div>
290 ) : (266 ) : (
291 <div className="border-t border-white/8 p-3">267 <div className="border-t border-border p-3">
292 <button268 <button
293 type="button"269 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"
295 onClick={() => setAddingProject(true)}271 onClick={() => setAddingProject(true)}
296 >272 >
297 + Add project273 + 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 @@...@@ -1,7 +1,178 @@
1@import "tailwindcss";1@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
3:root {77: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 }
5}176}
6177
7body {178body {
...@@ -10,8 +181,6 @@ body {...@@ -10,8 +181,6 @@ body {
10 margin: 0;181 margin: 0;
11 padding: 0;182 padding: 0;
12 min-height: 100vh;183 min-height: 100vh;
13 color: #e0e0e0;
14 background: #0c0c0c;
15}184}
16185
17body::after {186body::after {
...@@ -55,25 +224,20 @@ input {...@@ -55,25 +224,20 @@ input {
55}224}
56225
57::-webkit-scrollbar-thumb {226::-webkit-scrollbar-thumb {
58 background: rgba(255, 255, 255, 0.1);227 background: rgba(0, 0, 0, 0.15);
59 border-radius: 3px;228 border-radius: 3px;
60}229}
61230
62::-webkit-scrollbar-thumb:hover {231::-webkit-scrollbar-thumb:hover {
63 background: rgba(255, 255, 255, 0.18);232 background: rgba(0, 0, 0, 0.25);
64}233}
65234
66/* Reasoning select — clickable label surface */235.dark ::-webkit-scrollbar-thumb {
67label:has(> select#reasoning-effort) {236 background: rgba(255, 255, 255, 0.1);
68 position: relative;
69}237}
70label:has(> select#reasoning-effort) select {238
71 position: absolute;239.dark ::-webkit-scrollbar-thumb:hover {
72 inset: 0;240 background: rgba(255, 255, 255, 0.18);
73 opacity: 0;
74 cursor: pointer;
75 width: 100%;
76 height: 100%;
77}241}
78242
79/* Chat markdown rendering */243/* Chat markdown rendering */
...@@ -108,34 +272,38 @@ label:has(> select#reasoning-effort) select {...@@ -108,34 +272,38 @@ label:has(> select#reasoning-effort) select {
108}272}
109273
110.chat-markdown a {274.chat-markdown a {
111 color: #93c5fd;275 color: var(--info-foreground);
112 text-decoration: underline;276 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 );
114}282}
115283
116.chat-markdown a:hover {284.chat-markdown a:hover {
117 color: #bfdbfe;285 opacity: 0.8;
118}286}
119287
120.chat-markdown blockquote {288.chat-markdown blockquote {
121 border-left: 2px solid rgba(255, 255, 255, 0.2);289 border-left: 2px solid var(--border);
122 padding-left: 0.8rem;290 padding-left: 0.8rem;
123 color: rgba(224, 224, 224, 0.75);291 color: var(--muted-foreground);
124}292}
125293
126.chat-markdown :not(pre) > code {294.chat-markdown :not(pre) > code {
127 border: 1px solid rgba(255, 255, 255, 0.12);295 border: 1px solid var(--border);
128 border-radius: 0.375rem;296 border-radius: 0.375rem;
129 background: rgba(255, 255, 255, 0.07);297 background: var(--muted);
130 padding: 0.1rem 0.35rem;298 padding: 0.1rem 0.35rem;
131 color: #f4f4f5;299 color: var(--foreground);
132}300}
133301
134.chat-markdown pre {302.chat-markdown pre {
135 overflow-x: auto;303 overflow-x: auto;
136 border: 1px solid rgba(255, 255, 255, 0.12);304 border: 1px solid var(--border);
137 border-radius: 0.75rem;305 border-radius: 0.75rem;
138 background: rgba(0, 0, 0, 0.3);306 background: var(--muted);
139 padding: 0.8rem 0.9rem;307 padding: 0.8rem 0.9rem;
140}308}
141309
...@@ -153,7 +321,7 @@ label:has(> select#reasoning-effort) select {...@@ -153,7 +321,7 @@ label:has(> select#reasoning-effort) select {
153321
154.chat-markdown th,322.chat-markdown th,
155.chat-markdown td {323.chat-markdown td {
156 border: 1px solid rgba(255, 255, 255, 0.12);324 border: 1px solid var(--border);
157 padding: 0.35rem 0.45rem;325 padding: 0.35rem 0.45rem;
158 text-align: left;326 text-align: left;
159}327}
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 @@...@@ -4,7 +4,11 @@
4 "composite": true,4 "composite": true,
5 "jsx": "react-jsx",5 "jsx": "react-jsx",
6 "lib": ["ES2023", "DOM", "DOM.Iterable"],6 "lib": ["ES2023", "DOM", "DOM.Iterable"],
7 "types": ["vite/client"]7 "exactOptionalPropertyTypes": false,
8 "types": ["vite/client"],
9 "paths": {
10 "#/*": ["./src/*"]
11 }
8 },12 },
9 "include": ["src", "vite.config.ts"]13 "include": ["src", "vite.config.ts"]
10}14}
apps/renderer/vite.config.ts+6
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1import path from "node:path";
1import tailwindcss from "@tailwindcss/vite";2import tailwindcss from "@tailwindcss/vite";
2import react from "@vitejs/plugin-react";3import react from "@vitejs/plugin-react";
3import { defineConfig } from "vite";4import { defineConfig } from "vite";
...@@ -11,6 +12,11 @@ export default defineConfig({...@@ -11,6 +12,11 @@ export default defineConfig({
11 }),12 }),
12 tailwindcss(),13 tailwindcss(),
13 ],14 ],
15 resolve: {
16 alias: {
17 "#": path.resolve(__dirname, "src"),
18 },
19 },
14 server: {20 server: {
15 port: Number(process.env.ELECTRON_RENDERER_PORT ?? 5173),21 port: Number(process.env.ELECTRON_RENDERER_PORT ?? 5173),
16 strictPort: true,22 strictPort: true,
bun.lock+96-1
...@@ -34,12 +34,17 @@...@@ -34,12 +34,17 @@
34 "version": "0.0.0",34 "version": "0.0.0",
35 "dependencies": {35 "dependencies": {
36 "@acme/contracts": "workspace:*",36 "@acme/contracts": "workspace:*",
37 "@base-ui/react": "^1.1.0",
38 "class-variance-authority": "^0.7.1",
39 "clsx": "^2.1.1",
37 "highlight.js": "^11.11.1",40 "highlight.js": "^11.11.1",
41 "lucide-react": "^0.563.0",
38 "react": "^19.0.0",42 "react": "^19.0.0",
39 "react-dom": "^19.0.0",43 "react-dom": "^19.0.0",
40 "react-markdown": "^10.1.0",44 "react-markdown": "^10.1.0",
41 "rehype-highlight": "^7.0.2",45 "rehype-highlight": "^7.0.2",
42 "remark-gfm": "^4.0.1",46 "remark-gfm": "^4.0.1",
47 "tailwind-merge": "^3.4.0",
43 "zod": "^3.24.1",48 "zod": "^3.24.1",
44 },49 },
45 "devDependencies": {50 "devDependencies": {
...@@ -104,12 +109,18 @@...@@ -104,12 +109,18 @@
104109
105 "@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=="],110 "@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
107 "@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=="],114 "@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
109 "@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=="],116 "@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
111 "@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=="],118 "@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
113 "@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=="],124 "@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
115 "@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"],126 "@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 @@...@@ -168,6 +179,14 @@
168179
169 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],180 "@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
171 "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],190 "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
172191
173 "@hapi/address": ["@hapi/address@5.1.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA=="],192 "@hapi/address": ["@hapi/address@5.1.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA=="],
...@@ -182,6 +201,16 @@...@@ -182,6 +201,16 @@
182201
183 "@hapi/topo": ["@hapi/topo@6.0.2", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg=="],202 "@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
185 "@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=="],214 "@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
187 "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],216 "@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 @@...@@ -194,10 +223,18 @@
194223
195 "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="],224 "@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
197 "@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="],228 "@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="],
198229
199 "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="],230 "@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
201 "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jmUfF7cNJPw57bEK7sMIqrYRgn4LH428tSgtgLTCtjuGuu1ShREyrkeB7y8HtkXRfhBs4lVY+HMLhqElJvZ6ww=="],238 "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jmUfF7cNJPw57bEK7sMIqrYRgn4LH428tSgtgLTCtjuGuu1ShREyrkeB7y8HtkXRfhBs4lVY+HMLhqElJvZ6ww=="],
202239
203 "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-S6vlV8S7jbjzJOSjfVg2CimUC0r7/aHDLdUm/3+/B/SU/s1jV7ivqWkMv1/8EB43d1BBwT9JQ60ZMTkBqeXSFA=="],240 "@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 @@...@@ -358,6 +395,8 @@
358395
359 "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],396 "@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
361 "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],400 "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
362401
363 "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],402 "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
...@@ -460,18 +499,24 @@...@@ -460,18 +499,24 @@
460499
461 "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],500 "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
463 "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],504 "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
464505
465 "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],506 "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="],
466507
467 "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],508 "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
468509
510 "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
511
469 "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=="],512 "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
471 "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],514 "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
472515
473 "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],516 "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
475 "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],520 "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
476521
477 "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],522 "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
...@@ -490,6 +535,8 @@...@@ -490,6 +535,8 @@
490535
491 "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],536 "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
493 "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],540 "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
495 "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],542 "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
...@@ -614,6 +661,8 @@...@@ -614,6 +661,8 @@
614661
615 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],662 "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
617 "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],666 "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
618667
619 "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],668 "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
...@@ -632,6 +681,8 @@...@@ -632,6 +681,8 @@
632681
633 "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],682 "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
635 "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],686 "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
636687
637 "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],688 "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
...@@ -680,6 +731,8 @@...@@ -680,6 +731,8 @@
680731
681 "is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="],732 "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
683 "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],736 "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
684737
685 "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],738 "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
...@@ -754,6 +807,8 @@...@@ -754,6 +807,8 @@
754807
755 "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],808 "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
757 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],812 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
758813
759 "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=="],814 "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 @@...@@ -884,6 +939,10 @@
884939
885 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],940 "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
887 "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=="],946 "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
889 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],948 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
...@@ -916,6 +975,8 @@...@@ -916,6 +975,8 @@
916975
917 "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=="],976 "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
919 "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=="],980 "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
921 "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=="],982 "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 @@...@@ -930,6 +991,8 @@
930991
931 "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],992 "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
933 "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],996 "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
934997
935 "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],998 "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
...@@ -994,6 +1057,8 @@...@@ -994,6 +1057,8 @@
9941057
995 "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],1058 "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
997 "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],1062 "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
9981063
999 "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],1064 "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
...@@ -1004,6 +1069,8 @@...@@ -1004,6 +1069,8 @@
10041069
1005 "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],1070 "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
10061071
1072 "rettime": ["rettime@0.10.1", "", {}, "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw=="],
1073
1007 "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],1074 "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
10081075
1009 "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=="],1076 "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 @@...@@ -1052,8 +1119,12 @@
10521119
1053 "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],1120 "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
10541121
1122 "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
1123
1055 "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],1124 "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
10561125
1126 "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="],
1127
1057 "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=="],1128 "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
1059 "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],1130 "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
...@@ -1074,6 +1145,12 @@...@@ -1074,6 +1145,12 @@
10741145
1075 "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],1146 "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
1077 "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],1154 "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
10781155
1079 "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],1156 "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
...@@ -1096,8 +1173,14 @@...@@ -1096,8 +1173,14 @@
10961173
1097 "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],1174 "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
1099 "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],1180 "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
1101 "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],1184 "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
11021185
1103 "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],1186 "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
...@@ -1124,7 +1207,7 @@...@@ -1124,7 +1207,7 @@
11241207
1125 "turbo-windows-arm64": ["turbo-windows-arm64@2.8.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-afTUGKBRmOJU1smQSBnFGcbq0iabAPwh1uXu2BVk7BREg30/1gMnJh9DFEQTah+UD3n3ru8V55J83RQNFfqoyw=="],1208 "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
1129 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],1212 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
11301213
...@@ -1154,8 +1237,12 @@...@@ -1154,8 +1237,12 @@
11541237
1155 "unixify": ["unixify@1.0.0", "", { "dependencies": { "normalize-path": "^2.1.1" } }, "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg=="],1238 "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
1157 "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=="],1242 "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
1159 "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],1246 "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
11601247
1161 "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],1248 "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 @@...@@ -1192,6 +1279,8 @@
11921279
1193 "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],1280 "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
1195 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],1284 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
11961285
1197 "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],1286 "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
...@@ -1208,6 +1297,10 @@...@@ -1208,6 +1297,10 @@
12081297
1209 "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],1298 "@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
1211 "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],1304 "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
12121305
1213 "@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=="],1306 "@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 @@...@@ -1254,6 +1347,8 @@
12541347
1255 "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=="],1348 "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
1257 "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],1352 "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
12581353
1259 "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=="],1354 "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=="],