authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-14 10:16:31-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-14 23:35:48-08:00
logd3c93752e5c89b2a77b15630a629e70295878582
tree8baabdb3705c77f73fd8f6477a8997d7cd96eae7
parent4b4abcdd7d197647b8b21b18d2344e4e12ae4c34

Adopt Base UI components and improve editor open behavior

- migrate web UI to shared Base UI primitives (alerts, buttons, combobox, toast, etc.) - refactor chat and branch toolbars to use new components and cleaner interactions - add cross-platform file-manager fallback in `openInEditor` server handling - update model contracts/tests and web dependencies to support new UI flow

48 files changed, 4540 insertions(+), 1202 deletions(-)

apps/server/src/wsServer.ts+24-3
...@@ -324,11 +324,32 @@ export function createServer(options: ServerOptions) {...@@ -324,11 +324,32 @@ export function createServer(options: ServerOptions) {
324 if (!params?.cwd) throw new Error("cwd is required");324 if (!params?.cwd) throw new Error("cwd is required");
325 const editorDef = EDITORS.find((e) => e.id === params.editor);325 const editorDef = EDITORS.find((e) => e.id === params.editor);
326 if (!editorDef) throw new Error(`Unknown editor: ${params.editor}`);326 if (!editorDef) throw new Error(`Unknown editor: ${params.editor}`);
327 if (!editorDef.command) {327
328 // Can't open file manager programmatically from server, just succeed silently328 let command: string;
329 let args: string[];
330
331 if (editorDef.command) {
332 command = editorDef.command;
333 args = [params.cwd];
334 } else if (editorDef.id === "file-manager") {
335 // Use platform-specific file manager command
336 switch (process.platform) {
337 case "darwin":
338 command = "open";
339 break;
340 case "win32":
341 command = "explorer";
342 break;
343 default:
344 command = "xdg-open";
345 break;
346 }
347 args = [params.cwd];
348 } else {
329 return undefined;349 return undefined;
330 }350 }
331 const child = spawn(editorDef.command, [params.cwd], {351
352 const child = spawn(command, args, {
332 detached: true,353 detached: true,
333 stdio: "ignore",354 stdio: "ignore",
334 });355 });
apps/web/components.json created+27
...@@ -0,0 +1,27 @@
1{
2 "$schema": "https://ui.shadcn.com/schema.json",
3 "style": "base-mira",
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 "menuColor": "default",
16 "menuAccent": "bold",
17 "aliases": {
18 "components": "~/components",
19 "utils": "~/lib/utils",
20 "ui": "~/components/ui",
21 "lib": "~/lib",
22 "hooks": "~/hooks"
23 },
24 "registries": {
25 "@coss": "https://coss.com/ui/r/{name}.json"
26 }
27}
apps/web/package.json+6-3
...@@ -11,17 +11,20 @@...@@ -11,17 +11,20 @@
11 "test": "vitest run --passWithNoTests"11 "test": "vitest run --passWithNoTests"
12 },12 },
13 "dependencies": {13 "dependencies": {
14 "@xterm/addon-fit": "^0.11.0",14 "@base-ui/react": "^1.2.0",
15 "@xterm/xterm": "^6.0.0",
16 "@t3tools/contracts": "workspace:*",15 "@t3tools/contracts": "workspace:*",
17 "@tanstack/react-query": "^5.90.0",16 "@tanstack/react-query": "^5.90.0",
17 "@xterm/addon-fit": "^0.11.0",
18 "@xterm/xterm": "^6.0.0",
19 "class-variance-authority": "^0.7.1",
18 "highlight.js": "^11.11.1",20 "highlight.js": "^11.11.1",
19 "lucide-react": "^0.563.0",21 "lucide-react": "^0.564.0",
20 "react": "^19.0.0",22 "react": "^19.0.0",
21 "react-dom": "^19.0.0",23 "react-dom": "^19.0.0",
22 "react-markdown": "^10.1.0",24 "react-markdown": "^10.1.0",
23 "rehype-highlight": "^7.0.2",25 "rehype-highlight": "^7.0.2",
24 "remark-gfm": "^4.0.1",26 "remark-gfm": "^4.0.1",
27 "tailwind-merge": "^3.4.0",
25 "zod": "^3.24.1"28 "zod": "^3.24.1"
26 },29 },
27 "devDependencies": {30 "devDependencies": {
apps/web/src/App.tsx+7-2
...@@ -10,6 +10,7 @@ import { StoreProvider, useStore } from "./store";...@@ -10,6 +10,7 @@ import { StoreProvider, useStore } from "./store";
10import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID } from "./types";10import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID } from "./types";
11import { onServerWelcome } from "./wsNativeApi";11import { onServerWelcome } from "./wsNativeApi";
12import { useNativeApi } from "./hooks/useNativeApi";12import { useNativeApi } from "./hooks/useNativeApi";
13import { AnchoredToastProvider, ToastProvider } from "./components/ui/toast";
1314
14function EventRouter() {15function EventRouter() {
15 const api = useNativeApi();16 const api = useNativeApi();
...@@ -181,7 +182,7 @@ function Layout() {...@@ -181,7 +182,7 @@ function Layout() {
181 }182 }
182183
183 return (184 return (
184 <div className="flex h-screen overflow-hidden bg-background text-foreground">185 <div className="flex h-screen overflow-hidden bg-background text-foreground isolate">
185 <EventRouter />186 <EventRouter />
186 <AutoProjectBootstrap />187 <AutoProjectBootstrap />
187 <DesktopProjectBootstrap />188 <DesktopProjectBootstrap />
...@@ -198,7 +199,11 @@ export default function App() {...@@ -198,7 +199,11 @@ export default function App() {
198 return (199 return (
199 <QueryClientProvider client={queryClient}>200 <QueryClientProvider client={queryClient}>
200 <StoreProvider>201 <StoreProvider>
201 <Layout />202 <ToastProvider>
203 <AnchoredToastProvider>
204 <Layout />
205 </AnchoredToastProvider>
206 </ToastProvider>
202 </StoreProvider>207 </StoreProvider>
203 </QueryClientProvider>208 </QueryClientProvider>
204 );209 );
apps/web/src/components/BranchToolbar.tsx+117-109
...@@ -1,9 +1,21 @@...@@ -1,9 +1,21 @@
1import type { GitBranch } from "@t3tools/contracts";1import type { GitBranch } from "@t3tools/contracts";
2import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";2import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
3import { useEffect, useRef, useState } from "react";3import { useEffect, useState } from "react";
44
5import { useStore } from "../store";
6import { useNativeApi } from "../hooks/useNativeApi";5import { useNativeApi } from "../hooks/useNativeApi";
6import { useStore } from "../store";
7import { Button } from "./ui/button";
8import {
9 Combobox,
10 ComboboxEmpty,
11 ComboboxInput,
12 ComboboxItem,
13 ComboboxList,
14 ComboboxPopup,
15 ComboboxTrigger,
16} from "./ui/combobox";
17import { ChevronDownIcon, PlusIcon } from "lucide-react";
18import { InputGroup, InputGroupAddon, InputGroupInput } from "./ui/input-group";
719
8interface BranchToolbarProps {20interface BranchToolbarProps {
9 envMode: "local" | "worktree";21 envMode: "local" | "worktree";
...@@ -19,7 +31,6 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B...@@ -19,7 +31,6 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B
19 const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false);31 const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false);
20 const [isCreatingBranch, setIsCreatingBranch] = useState(false);32 const [isCreatingBranch, setIsCreatingBranch] = useState(false);
21 const [newBranchName, setNewBranchName] = useState("");33 const [newBranchName, setNewBranchName] = useState("");
22 const branchMenuRef = useRef<HTMLDivElement>(null);
2334
24 const activeThread = state.threads.find((thread) => thread.id === state.activeThreadId);35 const activeThread = state.threads.find((thread) => thread.id === state.activeThreadId);
25 const activeProject = state.projects.find((project) => project.id === activeThread?.projectId);36 const activeProject = state.projects.find((project) => project.id === activeThread?.projectId);
...@@ -37,6 +48,8 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B...@@ -37,6 +48,8 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B
37 });48 });
3849
39 const branches = branchesQuery.data?.branches ?? [];50 const branches = branchesQuery.data?.branches ?? [];
51 const branchNames = branches.map((branch) => branch.name);
52 const branchByName = new Map(branches.map((branch) => [branch.name, branch]));
40 // Default to true while loading — showing "Initialize git" during a fetch is wrong,53 // Default to true while loading — showing "Initialize git" during a fetch is wrong,
41 // and worktrees are inherently git repos.54 // and worktrees are inherently git repos.
42 const isRepo = branchesQuery.data?.isRepo ?? !branchesQuery.isLoading;55 const isRepo = branchesQuery.data?.isRepo ?? !branchesQuery.isLoading;
...@@ -46,8 +59,10 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B...@@ -46,8 +59,10 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B
46 const checkoutMutation = useMutation({59 const checkoutMutation = useMutation({
47 mutationFn: (branch: string) => api!.git.checkout({ cwd: branchCwd!, branch }),60 mutationFn: (branch: string) => api!.git.checkout({ cwd: branchCwd!, branch }),
48 onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }),61 onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }),
49 onError: (error) =>62 onError: (error) => {
50 setThreadError(error instanceof Error ? error.message : "Failed to checkout branch."),63 setThreadError(error instanceof Error ? error.message : "Failed to checkout branch.");
64 setIsBranchMenuOpen(true);
65 },
51 });66 });
5267
53 const createBranchMutation = useMutation({68 const createBranchMutation = useMutation({
...@@ -88,17 +103,9 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B...@@ -88,17 +103,9 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B
88 }, [activeThreadId, activeWorktreePath, activeThreadBranch, queryBranches, dispatch]);103 }, [activeThreadId, activeWorktreePath, activeThreadBranch, queryBranches, dispatch]);
89104
90 useEffect(() => {105 useEffect(() => {
91 if (!isBranchMenuOpen) return;106 if (isBranchMenuOpen) return;
92 const handleClickOutside = (event: MouseEvent) => {107 setIsCreatingBranch(false);
93 if (!branchMenuRef.current) return;108 setNewBranchName("");
94 if (event.target instanceof Node && !branchMenuRef.current.contains(event.target)) {
95 setIsBranchMenuOpen(false);
96 }
97 };
98 window.addEventListener("mousedown", handleClickOutside);
99 return () => {
100 window.removeEventListener("mousedown", handleClickOutside);
101 };
102 }, [isBranchMenuOpen]);109 }, [isBranchMenuOpen]);
103110
104 // ── Helpers ───────────────────────────────────────────────────────────111 // ── Helpers ───────────────────────────────────────────────────────────
...@@ -171,21 +178,19 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B...@@ -171,21 +178,19 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B
171 <div className="mx-auto flex w-full max-w-3xl items-center justify-between px-5 pb-3 pt-1">178 <div className="mx-auto flex w-full max-w-3xl items-center justify-between px-5 pb-3 pt-1">
172 <div className="flex items-center gap-2">179 <div className="flex items-center gap-2">
173 {envLocked || activeWorktreePath ? (180 {envLocked || activeWorktreePath ? (
174 <span className="inline-flex items-center gap-1.5 px-1 text-[12px] text-muted-foreground/55">181 <span className="border border-transparent px-[calc(--spacing(2)-1px)] text-sm font-medium text-muted-foreground/70 sm:text-xs">
175 {activeWorktreePath ? "Worktree" : "Local"}182 {activeWorktreePath ? "Worktree" : "Local"}
176 </span>183 </span>
177 ) : (184 ) : (
178 <button185 <Button
179 type="button"186 type="button"
180 className={`inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-[12px] transition-colors duration-150 ${187 variant="ghost"
181 envMode === "worktree"188 className="text-muted-foreground/70 hover:text-foreground/80"
182 ? "bg-accent text-foreground/80"189 size="xs"
183 : "text-muted-foreground/60 hover:bg-accent/50 hover:text-muted-foreground/80"
184 }`}
185 onClick={() => onEnvModeChange(envMode === "local" ? "worktree" : "local")}190 onClick={() => onEnvModeChange(envMode === "local" ? "worktree" : "local")}
186 >191 >
187 {envMode === "worktree" ? "New worktree" : "Local"}192 {envMode === "worktree" ? "New worktree" : "Local"}
188 </button>193 </Button>
189 )}194 )}
190 </div>195 </div>
191196
...@@ -199,59 +204,56 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B...@@ -199,59 +204,56 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B
199 {initMutation.isPending ? "Initializing\u2026" : "Initialize git"}204 {initMutation.isPending ? "Initializing\u2026" : "Initialize git"}
200 </button>205 </button>
201 ) : (206 ) : (
202 <div className="relative" ref={branchMenuRef}>207 <Combobox
203 <button208 items={branchNames}
204 type="button"209 autoHighlight
205 className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-[12px] text-muted-foreground/60 transition-colors duration-150 hover:bg-accent/50 hover:text-muted-foreground/80"210 onOpenChange={(open) => setIsBranchMenuOpen(open)}
206 onClick={() => setIsBranchMenuOpen((open) => !open)}211 open={isBranchMenuOpen}
212 value={activeThread.branch}
213 >
214 <ComboboxTrigger
215 render={<Button variant="ghost" size="xs" />}
216 className="text-muted-foreground/70 hover:text-foreground/80"
207 disabled={branchesQuery.isLoading}217 disabled={branchesQuery.isLoading}
208 >218 >
209 <span className="max-w-[240px] truncate font-mono">219 <span className="max-w-[240px] truncate">
210 {activeThread.branch220 {activeThread.branch
211 ? envMode === "worktree" && !activeWorktreePath221 ? envMode === "worktree" && !activeWorktreePath
212 ? `From ${activeThread.branch}`222 ? `From ${activeThread.branch}`
213 : activeThread.branch223 : activeThread.branch
214 : "Select branch"}224 : "Select branch"}
215 </span>225 </span>
216 <svg226 <ChevronDownIcon />
217 width="10"227 </ComboboxTrigger>
218 height="10"228 <ComboboxPopup align="end" side="top" className="w-64">
219 viewBox="0 0 10 10"229 <div className="border-b">
220 fill="none"230 <ComboboxInput
221 className="opacity-40"231 className="rounded-b-none before:rounded-b-none [&_input]:font-sans"
222 aria-hidden="true"232 placeholder="Search branches..."
223 >233 showClear
224 <path234 showTrigger={false}
225 d="M2.5 4L5 6.5L7.5 4"235 size="sm"
226 stroke="currentColor"
227 strokeWidth="1.2"
228 strokeLinecap="round"
229 strokeLinejoin="round"
230 />236 />
231 </svg>237 </div>
232 </button>238 <ComboboxEmpty>No branches found.</ComboboxEmpty>
233 {isBranchMenuOpen && (239
234 <div className="absolute bottom-full right-0 z-20 mb-2 w-[320px] rounded-2xl border border-border bg-popover/95 p-2 shadow-[0_16px_40px_rgba(0,0,0,0.55)] backdrop-blur">240 <ComboboxList className="max-h-56">
235 <p className="px-2 py-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/55">241 {(branchName) => {
236 Branch242 const branch = branchByName.get(branchName);
237 </p>243 if (!branch) return null;
238 <div className="max-h-64 overflow-y-auto">244
239 {branches.map((branch) => {245 const hasSecondaryWorktree =
240 const isSelected = branch.name === activeThread.branch;246 branch.worktreePath && branch.worktreePath !== activeProject.cwd;
241 const hasSecondaryWorktree =247 return (
242 branch.worktreePath && branch.worktreePath !== activeProject.cwd;248 <ComboboxItem
243 return (249 hideIndicator
244 <button250 key={branchName}
245 key={branch.name}251 value={branchName}
246 type="button"252 className={branchName === activeThread.branch ? "bg-accent text-foreground" : undefined}
247 className={`mb-0.5 flex w-full items-center justify-between gap-2 rounded-xl px-2 py-1.5 text-left font-mono text-xs transition-colors duration-150 ${253 onClick={() => selectBranch(branch)}
248 isSelected254 >
249 ? "bg-accent text-foreground"255 <div className="flex w-full items-center justify-between gap-2">
250 : "text-foreground/90 hover:bg-accent/50"256 <span className="truncate">{branchName}</span>
251 }`}
252 onClick={() => selectBranch(branch)}
253 >
254 <span className="truncate">{branch.name}</span>
255 {(branch.current || branch.isDefault || hasSecondaryWorktree) && (257 {(branch.current || branch.isDefault || hasSecondaryWorktree) && (
256 <span className="shrink-0 text-[10px] text-muted-foreground/45">258 <span className="shrink-0 text-[10px] text-muted-foreground/45">
257 {branch.current259 {branch.current
...@@ -261,58 +263,64 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B...@@ -261,58 +263,64 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B
261 : "default"}263 : "default"}
262 </span>264 </span>
263 )}265 )}
264 </button>266 </div>
265 );267 </ComboboxItem>
266 })}268 );
267 </div>269 }}
268 {envMode === "local" && (270 </ComboboxList>
269 <>271 {envMode === "local" && (
270 <div className="mx-1 my-1 h-px bg-border" />272 <>
271 {isCreatingBranch ? (273 {isCreatingBranch ? (
272 <form274 <form
273 className="flex items-center gap-1 px-1"275 onSubmit={(event) => {
274 onSubmit={(event) => {276 event.preventDefault();
275 event.preventDefault();277 createBranch();
276 createBranch();278 }}
277 }}279 >
278 >280 <InputGroup className="rounded-t-none before:rounded-t-none">
279 <input281 <InputGroupInput
280 type="text"282 className="[&_input]:font-sans"
281 className="flex-1 rounded-lg border border-border bg-background px-2 py-1.5 font-mono text-xs text-foreground placeholder:text-muted-foreground/30 focus:border-ring focus:outline-none"283 size="sm"
282 placeholder="branch-name"284 placeholder="branch-name"
285 type="text"
283 value={newBranchName}286 value={newBranchName}
284 onChange={(event) => setNewBranchName(event.target.value)}287 onChange={(event) => setNewBranchName(event.target.value)}
285 onKeyDown={(event) => {288 onKeyDown={(event) => {
286 if (event.key === "Escape") {289 if (event.key === "Escape") {
290 event.stopPropagation();
287 setIsCreatingBranch(false);291 setIsCreatingBranch(false);
288 setNewBranchName("");292 setNewBranchName("");
289 }293 }
290 }}294 }}
291 // biome-ignore lint/a11y/noAutofocus: branch name input should focus when shown
292 autoFocus295 autoFocus
293 />296 />
294 <button297 <InputGroupAddon align="inline-end">
295 type="submit"298 <Button
296 disabled={!newBranchName.trim() || createBranchMutation.isPending}299 size="xs"
297 className="rounded-lg px-2 py-1.5 text-xs text-muted-foreground/70 transition-colors hover:bg-accent hover:text-foreground/80 disabled:opacity-30"300 variant="secondary"
298 >301 type="submit"
299 Create302 disabled={!newBranchName.trim() || createBranchMutation.isPending}
300 </button>303 >
301 </form>304 Create
302 ) : (305 </Button>
303 <button306 </InputGroupAddon>
304 type="button"307 </InputGroup>
305 className="w-full rounded-lg px-2 py-1.5 text-left text-xs text-muted-foreground/70 transition-colors duration-150 hover:bg-accent hover:text-foreground/80"308 </form>
306 onClick={() => setIsCreatingBranch(true)}309 ) : (
307 >310 <Button
308 + New branch311 size="sm"
309 </button>312 className="w-full justify-start h-9 rounded-t-none rounded-b-lg before:rounded-t-none"
310 )}313 variant="outline"
311 </>314 onClick={() => setIsCreatingBranch(true)}
312 )}315 >
313 </div>316 <PlusIcon />
314 )}317 New branch
315 </div>318 </Button>
319 )}
320 </>
321 )}
322 </ComboboxPopup>
323 </Combobox>
316 )}324 )}
317 </div>325 </div>
318 );326 );
apps/web/src/components/ChatView.tsx+274-353
...@@ -4,6 +4,7 @@ import {...@@ -4,6 +4,7 @@ import {
4 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,4 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
5 type ProviderSendTurnAttachmentInput,5 type ProviderSendTurnAttachmentInput,
6 type ProviderApprovalDecision,6 type ProviderApprovalDecision,
7 ModelSlug,
7} from "@t3tools/contracts";8} from "@t3tools/contracts";
8import {9import {
9 type ClipboardEvent,10 type ClipboardEvent,
...@@ -27,6 +28,7 @@ import {...@@ -27,6 +28,7 @@ import {
27 DEFAULT_REASONING,28 DEFAULT_REASONING,
28 MODEL_OPTIONS,29 MODEL_OPTIONS,
29 REASONING_OPTIONS,30 REASONING_OPTIONS,
31 ReasoningEffort,
30 resolveModelSlug,32 resolveModelSlug,
31} from "../model-logic";33} from "../model-logic";
32import {34import {
...@@ -46,22 +48,30 @@ import { isTerminalToggleShortcut } from "../terminal-shortcuts";...@@ -46,22 +48,30 @@ import { isTerminalToggleShortcut } from "../terminal-shortcuts";
46import ChatMarkdown from "./ChatMarkdown";48import ChatMarkdown from "./ChatMarkdown";
47import ThreadTerminalDrawer from "./ThreadTerminalDrawer";49import ThreadTerminalDrawer from "./ThreadTerminalDrawer";
48import { useNativeApi } from "../hooks/useNativeApi";50import { useNativeApi } from "../hooks/useNativeApi";
51import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert";
52import {
53 ChevronDownIcon,
54 CircleAlertIcon,
55 FolderClosedIcon,
56 InfoIcon,
57 LockIcon,
58 LockOpenIcon,
59 XIcon,
60} from "lucide-react";
61import { Button } from "./ui/button";
62import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select";
63import { Separator } from "./ui/separator";
64import { Group, GroupSeparator } from "./ui/group";
65import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "./ui/menu";
66import { CursorIcon, Icon } from "./Icons";
67import { cn } from "~/lib/utils";
68import { Badge } from "./ui/badge";
4969
50function formatMessageMeta(createdAt: string, duration: string | null): string {70function formatMessageMeta(createdAt: string, duration: string | null): string {
51 if (!duration) return formatTimestamp(createdAt);71 if (!duration) return formatTimestamp(createdAt);
52 return `${formatTimestamp(createdAt)} • ${duration}`;72 return `${formatTimestamp(createdAt)} • ${duration}`;
53}73}
5474
55const FILE_MANAGER_LABEL = navigator.platform.includes("Mac")
56 ? "Finder"
57 : navigator.platform.includes("Win")
58 ? "Explorer"
59 : "Files";
60
61function editorLabel(editor: (typeof EDITORS)[number]): string {
62 return editor.command ? editor.label : FILE_MANAGER_LABEL;
63}
64
65const LAST_EDITOR_KEY = "t3code:last-editor";75const LAST_EDITOR_KEY = "t3code:last-editor";
66const MAX_VISIBLE_WORK_LOG_ENTRIES = 6;76const MAX_VISIBLE_WORK_LOG_ENTRIES = 6;
67const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`;77const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`;
...@@ -119,13 +129,7 @@ export default function ChatView() {...@@ -119,13 +129,7 @@ export default function ChatView() {
119 const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null);129 const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null);
120 const [isSending, setIsSending] = useState(false);130 const [isSending, setIsSending] = useState(false);
121 const [isConnecting, setIsConnecting] = useState(false);131 const [isConnecting, setIsConnecting] = useState(false);
122 const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);132 const [selectedEffort, setSelectedEffort] = useState(DEFAULT_REASONING);
123 const [isEditorMenuOpen, setIsEditorMenuOpen] = useState(false);
124 const [lastEditor, setLastEditor] = useState<EditorId>(() => {
125 const stored = localStorage.getItem(LAST_EDITOR_KEY);
126 return EDITORS.some((e) => e.id === stored) ? (stored as EditorId) : EDITORS[0].id;
127 });
128 const [selectedEffort, setSelectedEffort] = useState<string>(DEFAULT_REASONING);
129 const [envMode, setEnvMode] = useState<"local" | "worktree">("local");133 const [envMode, setEnvMode] = useState<"local" | "worktree">("local");
130 const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false);134 const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false);
131 const [respondingRequestIds, setRespondingRequestIds] = useState<string[]>([]);135 const [respondingRequestIds, setRespondingRequestIds] = useState<string[]>([]);
...@@ -137,8 +141,6 @@ export default function ChatView() {...@@ -137,8 +141,6 @@ export default function ChatView() {
137 const textareaRef = useRef<HTMLTextAreaElement>(null);141 const textareaRef = useRef<HTMLTextAreaElement>(null);
138 const composerImagesRef = useRef<ComposerImageAttachment[]>([]);142 const composerImagesRef = useRef<ComposerImageAttachment[]>([]);
139 const dragDepthRef = useRef(0);143 const dragDepthRef = useRef(0);
140 const modelMenuRef = useRef<HTMLDivElement>(null);
141 const editorMenuRef = useRef<HTMLDivElement>(null);
142 const terminalOpenByThreadRef = useRef<Record<string, boolean>>({});144 const terminalOpenByThreadRef = useRef<Record<string, boolean>>({});
143145
144 const activeThread = state.threads.find((t) => t.id === state.activeThreadId);146 const activeThread = state.threads.find((t) => t.id === state.activeThreadId);
...@@ -150,7 +152,6 @@ export default function ChatView() {...@@ -150,7 +152,6 @@ export default function ChatView() {
150 const phase = derivePhase(activeThread?.session ?? null);152 const phase = derivePhase(activeThread?.session ?? null);
151 const isWorking = phase === "running" || isSending || isConnecting;153 const isWorking = phase === "running" || isSending || isConnecting;
152 const nowIso = new Date(nowTick).toISOString();154 const nowIso = new Date(nowTick).toISOString();
153 const modelOptions = MODEL_OPTIONS;
154 const workLogEntries = useMemo(155 const workLogEntries = useMemo(
155 () => deriveWorkLogEntries(activeThread?.events ?? [], undefined),156 () => deriveWorkLogEntries(activeThread?.events ?? [], undefined),
156 [activeThread?.events],157 [activeThread?.events],
...@@ -304,7 +305,9 @@ export default function ChatView() {...@@ -304,7 +305,9 @@ export default function ChatView() {
304 (terminalId: string) => {305 (terminalId: string) => {
305 if (!activeThreadId || !api) return;306 if (!activeThreadId || !api) return;
306 const fallbackExitWrite = () =>307 const fallbackExitWrite = () =>
307 api.terminal.write({ threadId: activeThreadId, terminalId, data: "exit\n" }).catch(() => undefined);308 api.terminal
309 .write({ threadId: activeThreadId, terminalId, data: "exit\n" })
310 .catch(() => undefined);
308 if ("close" in api.terminal && typeof api.terminal.close === "function") {311 if ("close" in api.terminal && typeof api.terminal.close === "function") {
309 void api.terminal312 void api.terminal
310 .close({ threadId: activeThreadId, terminalId })313 .close({ threadId: activeThreadId, terminalId })
...@@ -395,22 +398,21 @@ export default function ChatView() {...@@ -395,22 +398,21 @@ export default function ChatView() {
395 };398 };
396 }, [revokePreviewUrls]);399 }, [revokePreviewUrls]);
397400
401 /**
402 * Close expanded image on Escape key
403 */
398 useEffect(() => {404 useEffect(() => {
399 if (!expandedImage) {405 if (!expandedImage) {
400 return;406 return;
401 }407 }
402408
403 const onKeyDown = (event: globalThis.KeyboardEvent) => {409 const onKeyDown = (event: globalThis.KeyboardEvent) => {
404 if (event.key !== "Escape") {410 if (event.key !== "Escape") return;
405 return;
406 }
407 setExpandedImage(null);411 setExpandedImage(null);
408 };412 };
409413
410 window.addEventListener("keydown", onKeyDown);414 window.addEventListener("keydown", onKeyDown);
411 return () => {415 return () => window.removeEventListener("keydown", onKeyDown);
412 window.removeEventListener("keydown", onKeyDown);
413 };
414 }, [expandedImage]);416 }, [expandedImage]);
415417
416 const activeWorktreePath = activeThread?.worktreePath;418 const activeWorktreePath = activeThread?.worktreePath;
...@@ -438,53 +440,6 @@ export default function ChatView() {...@@ -438,53 +440,6 @@ export default function ChatView() {
438 };440 };
439 }, [phase]);441 }, [phase]);
440442
441 useEffect(() => {
442 if (!isModelMenuOpen) return;
443
444 const handleClickOutside = (event: MouseEvent) => {
445 if (!modelMenuRef.current) return;
446 if (event.target instanceof Node && !modelMenuRef.current.contains(event.target)) {
447 setIsModelMenuOpen(false);
448 }
449 };
450
451 window.addEventListener("mousedown", handleClickOutside);
452 return () => {
453 window.removeEventListener("mousedown", handleClickOutside);
454 };
455 }, [isModelMenuOpen]);
456
457 useEffect(() => {
458 if (!isEditorMenuOpen) return;
459
460 const handleClickOutside = (event: MouseEvent) => {
461 if (!editorMenuRef.current) return;
462 if (event.target instanceof Node && !editorMenuRef.current.contains(event.target)) {
463 setIsEditorMenuOpen(false);
464 }
465 };
466
467 window.addEventListener("mousedown", handleClickOutside);
468 return () => {
469 window.removeEventListener("mousedown", handleClickOutside);
470 };
471 }, [isEditorMenuOpen]);
472
473 // Cmd+O / Ctrl+O to open in last-used editor
474 useEffect(() => {
475 const handler = (e: globalThis.KeyboardEvent) => {
476 if (e.key === "o" && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
477 if (api && activeProject) {
478 e.preventDefault();
479 const cwd = activeThread?.worktreePath ?? activeProject.cwd;
480 void api.shell.openInEditor(cwd, lastEditor);
481 }
482 }
483 };
484 window.addEventListener("keydown", handler);
485 return () => window.removeEventListener("keydown", handler);
486 }, [api, activeProject, activeThread, lastEditor]);
487
488 useEffect(() => {443 useEffect(() => {
489 if (!activeThreadId) return;444 if (!activeThreadId) return;
490 const previous = terminalOpenByThreadRef.current[activeThreadId] ?? false;445 const previous = terminalOpenByThreadRef.current[activeThreadId] ?? false;
...@@ -516,15 +471,6 @@ export default function ChatView() {...@@ -516,15 +471,6 @@ export default function ChatView() {
516 return () => window.removeEventListener("keydown", handler);471 return () => window.removeEventListener("keydown", handler);
517 }, [activeThreadId, toggleTerminalVisibility]);472 }, [activeThreadId, toggleTerminalVisibility]);
518473
519 const openInEditor = (editorId: EditorId) => {
520 if (!api || !activeProject) return;
521 const cwd = activeThread?.worktreePath ?? activeProject.cwd;
522 void api.shell.openInEditor(cwd, editorId);
523 setLastEditor(editorId);
524 localStorage.setItem(LAST_EDITOR_KEY, editorId);
525 setIsEditorMenuOpen(false);
526 };
527
528 const setThreadError = (threadId: string | null, error: string | null) => {474 const setThreadError = (threadId: string | null, error: string | null) => {
529 if (!threadId) return;475 if (!threadId) return;
530 dispatch({476 dispatch({
...@@ -579,13 +525,6 @@ export default function ChatView() {...@@ -579,13 +525,6 @@ export default function ChatView() {
579 });525 });
580 };526 };
581527
582 const openImageDialog = (src: string | undefined, name: string) => {
583 if (!src) {
584 return;
585 }
586 setExpandedImage({ src, name });
587 };
588
589 const onComposerPaste = (event: ClipboardEvent<HTMLTextAreaElement>) => {528 const onComposerPaste = (event: ClipboardEvent<HTMLTextAreaElement>) => {
590 const files = Array.from(event.clipboardData.files);529 const files = Array.from(event.clipboardData.files);
591 if (files.length === 0) {530 if (files.length === 0) {
...@@ -782,13 +721,15 @@ export default function ChatView() {...@@ -782,13 +721,15 @@ export default function ChatView() {
782 setIsSending(true);721 setIsSending(true);
783 try {722 try {
784 const turnAttachments = await Promise.all(723 const turnAttachments = await Promise.all(
785 composerImagesSnapshot.map(async (image): Promise<ProviderSendTurnAttachmentInput> => ({724 composerImagesSnapshot.map(
786 type: "image",725 async (image): Promise<ProviderSendTurnAttachmentInput> => ({
787 name: image.name,726 type: "image",
788 mimeType: image.mimeType,727 name: image.name,
789 sizeBytes: image.sizeBytes,728 mimeType: image.mimeType,
790 dataUrl: await readFileAsDataUrl(image.file),729 sizeBytes: image.sizeBytes,
791 })),730 dataUrl: await readFileAsDataUrl(image.file),
731 }),
732 ),
792 );733 );
793 const shouldBootstrap =734 const shouldBootstrap =
794 previousMessages.length > 0 &&735 previousMessages.length > 0 &&
...@@ -800,7 +741,7 @@ export default function ChatView() {...@@ -800,7 +741,7 @@ export default function ChatView() {
800 latestPromptForBootstrap,741 latestPromptForBootstrap,
801 PROVIDER_SEND_TURN_MAX_INPUT_CHARS,742 PROVIDER_SEND_TURN_MAX_INPUT_CHARS,
802 ).text743 ).text
803 : (trimmed || undefined);744 : trimmed || undefined;
804 await api.providers.sendTurn({745 await api.providers.sendTurn({
805 sessionId: sessionInfo.sessionId,746 sessionId: sessionInfo.sessionId,
806 ...(input ? { input } : {}),747 ...(input ? { input } : {}),
...@@ -849,14 +790,13 @@ export default function ChatView() {...@@ -849,14 +790,13 @@ export default function ChatView() {
849 }790 }
850 };791 };
851792
852 const onModelSelect = (model: string) => {793 const onModelSelect = (model: ModelSlug) => {
853 if (!activeThread) return;794 if (!activeThread) return;
854 dispatch({795 dispatch({
855 type: "SET_THREAD_MODEL",796 type: "SET_THREAD_MODEL",
856 threadId: activeThread.id,797 threadId: activeThread.id,
857 model: resolveModelSlug(model),798 model: resolveModelSlug(model),
858 });799 });
859 setIsModelMenuOpen(false);
860 };800 };
861801
862 const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {802 const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
...@@ -892,132 +832,97 @@ export default function ChatView() {...@@ -892,132 +832,97 @@ export default function ChatView() {
892 >832 >
893 <div className="flex items-center gap-3">833 <div className="flex items-center gap-3">
894 <h2 className="text-sm font-medium text-foreground">{activeThread.title}</h2>834 <h2 className="text-sm font-medium text-foreground">{activeThread.title}</h2>
895 {activeProject && (835 {activeProject && <Badge variant="outline">{activeProject.name}</Badge>}
896 <span className="rounded-full bg-accent px-2 py-0.5 text-[10px] text-muted-foreground/50">
897 {activeProject.name}
898 </span>
899 )}
900 </div>836 </div>
901 <div className="flex items-center gap-3">837 <div className="flex items-center gap-3">
902 {/* Open in editor */}838 {/* Open in editor */}
903 {activeProject && (839 {activeProject && <OpenInPicker />}
904 <div className="relative" ref={editorMenuRef}>
905 <button
906 type="button"
907 className="rounded-md px-2 py-1 text-[10px] text-muted-foreground/40 transition-colors duration-150 hover:text-muted-foreground/60"
908 onClick={() => setIsEditorMenuOpen((v) => !v)}
909 >
910 Open in&hellip;
911 </button>
912 {isEditorMenuOpen && (
913 <div className="absolute right-0 top-full z-50 mt-1 min-w-[120px] rounded-md border border-border bg-popover py-1 shadow-xl">
914 {EDITORS.map((editor) => (
915 <button
916 key={editor.id}
917 type="button"
918 className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[11px] text-foreground hover:bg-accent"
919 onClick={() => openInEditor(editor.id)}
920 >
921 {editorLabel(editor)}
922 {editor.id === lastEditor && (
923 <kbd className="ml-auto text-[9px] text-muted-foreground/40">
924 {navigator.platform.includes("Mac") ? "\u2318O" : "Ctrl+O"}
925 </kbd>
926 )}
927 </button>
928 ))}
929 </div>
930 )}
931 </div>
932 )}
933 {/* Git actions */}840 {/* Git actions */}
934 {activeProject && <GitActionsControl api={api} gitCwd={gitCwd} />}841 {activeProject && <GitActionsControl api={api} gitCwd={gitCwd} />}
935842
936 {/* Diff toggle */}843 {/* Diff toggle */}
937 <button844 <Button
938 type="button"845 size="xs"
939 className={`rounded-md px-2 py-1 text-[10px] transition-colors duration-150 ${846 variant="ghost"
940 state.diffOpen847 className={cn(
941 ? "bg-accent text-foreground"848 "text-muted-foreground/70 hover:text-foreground/80",
942 : "text-muted-foreground/40 hover:text-muted-foreground/60"849 state.diffOpen && "bg-accent text-accent-foreground",
943 }`}850 )}
944 onClick={() => dispatch({ type: "TOGGLE_DIFF" })}851 onClick={() => dispatch({ type: "TOGGLE_DIFF" })}
945 >852 >
946 Diff853 Diff
947 </button>854 </Button>
948 </div>855 </div>
949 </header>856 </header>
950857
951 {/* Error banner */}858 {/* Error banner */}
952 {activeThread.error && (859 {activeThread.error && (
953 <div className="mx-4 mt-3 rounded-lg border border-destructive/20 bg-destructive/10 px-3 py-2 text-xs text-destructive">860 <div className="pt-3 mx-auto max-w-3xl">
954 {activeThread.error}861 <Alert variant="error">
862 <CircleAlertIcon />
863 <AlertDescription className="line-clamp-3" title={activeThread.error}>
864 {activeThread.error}
865 </AlertDescription>
866 </Alert>
955 </div>867 </div>
956 )}868 )}
957869
958 {pendingApprovals.length > 0 && (870 {pendingApprovals.length > 0 && (
959 <div className="px-5 pt-3">871 <div className="pt-3 mx-auto max-w-3xl space-y-2">
960 <div className="mx-auto max-w-3xl space-y-2">872 {pendingApprovals.map((approval) => {
961 {pendingApprovals.map((approval) => {873 const isResponding = respondingRequestIds.includes(approval.requestId);
962 const isResponding = respondingRequestIds.includes(approval.requestId);874
963 return (875 return (
964 <div876 <Alert variant="warning" key={approval.requestId}>
965 key={approval.requestId}877 <InfoIcon />
966 className="rounded-lg border border-amber-300/40 bg-amber-50 px-3 py-2 dark:border-amber-300/20 dark:bg-amber-500/[0.07]"878 <AlertTitle className="text-xs">
879 {approval.requestKind === "command"
880 ? "Command approval requested"
881 : "File-change approval requested"}
882 </AlertTitle>
883 <AlertDescription
884 className="truncate block font-mono text-[11px]"
885 title={approval.detail}
967 >886 >
968 <p className="text-xs font-medium text-amber-900 dark:text-amber-100">887 {approval.detail}
969 {approval.requestKind === "command"888 </AlertDescription>
970 ? "Command approval requested"889 <AlertAction className="col-start-2! -col-end-1! mt-1.5 sm:row-start-auto sm:row-end-auto">
971 : "File-change approval requested"}890 <Button
972 </p>891 size="xs"
973 {approval.detail && (892 variant="default"
974 <p893 disabled={isResponding}
975 className="mt-1 truncate font-mono text-[11px] text-amber-900/80 dark:text-amber-100/75"894 onClick={() => void onRespondToApproval(approval.requestId, "accept")}
976 title={approval.detail}895 >
977 >896 Approve once
978 {approval.detail}897 </Button>
979 </p>898 <Button
980 )}899 size="xs"
981 <div className="mt-2 flex flex-wrap gap-1.5">900 variant="outline"
982 <button901 disabled={isResponding}
983 type="button"902 onClick={() => void onRespondToApproval(approval.requestId, "acceptForSession")}
984 className="rounded-md border border-border bg-accent px-2 py-1 text-[11px] text-foreground transition-colors duration-150 hover:bg-accent/80 disabled:cursor-not-allowed disabled:opacity-50"903 >
985 disabled={isResponding}904 Always allow this session
986 onClick={() => void onRespondToApproval(approval.requestId, "accept")}905 </Button>
987 >906 <Button
988 Approve once907 size="xs"
989 </button>908 variant="destructive-outline"
990 <button909 disabled={isResponding}
991 type="button"910 onClick={() => void onRespondToApproval(approval.requestId, "decline")}
992 className="rounded-md border border-sky-300/70 bg-sky-100 px-2 py-1 text-[11px] text-sky-800 transition-colors duration-150 hover:bg-sky-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-sky-300/30 dark:bg-sky-500/15 dark:text-sky-100 dark:hover:bg-sky-500/22"911 >
993 disabled={isResponding}912 Decline
994 onClick={() =>913 </Button>
995 void onRespondToApproval(approval.requestId, "acceptForSession")914 <Button
996 }915 size="xs"
997 >916 variant="ghost"
998 Always allow this session917 disabled={isResponding}
999 </button>918 onClick={() => void onRespondToApproval(approval.requestId, "cancel")}
1000 <button919 >
1001 type="button"920 Cancel turn
1002 className="rounded-md border border-border px-2 py-1 text-[11px] text-foreground/90 transition-colors duration-150 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"921 </Button>
1003 disabled={isResponding}922 </AlertAction>
1004 onClick={() => void onRespondToApproval(approval.requestId, "decline")}923 </Alert>
1005 >924 );
1006 Decline925 })}
1007 </button>
1008 <button
1009 type="button"
1010 className="rounded-md border border-rose-300/70 bg-rose-100 px-2 py-1 text-[11px] text-rose-800 transition-colors duration-150 hover:bg-rose-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-rose-300/30 dark:bg-rose-500/12 dark:text-rose-100 dark:hover:bg-rose-500/20"
1011 disabled={isResponding}
1012 onClick={() => void onRespondToApproval(approval.requestId, "cancel")}
1013 >
1014 Cancel turn
1015 </button>
1016 </div>
1017 </div>
1018 );
1019 })}
1020 </div>
1021 </div>926 </div>
1022 )}927 )}
1023928
...@@ -1140,7 +1045,9 @@ export default function ChatView() {...@@ -1140,7 +1045,9 @@ export default function ChatView() {
1140 src={image.previewUrl}1045 src={image.previewUrl}
1141 alt={image.name}1046 alt={image.name}
1142 className="h-full max-h-[220px] w-full cursor-zoom-in object-cover"1047 className="h-full max-h-[220px] w-full cursor-zoom-in object-cover"
1143 onClick={() => openImageDialog(image.previewUrl, image.name)}1048 onClick={() =>
1049 setExpandedImage({ src: image.previewUrl!, name: image.name })
1050 }
1144 />1051 />
1145 ) : (1052 ) : (
1146 <div className="flex min-h-[72px] items-center justify-center px-2 py-3 text-center text-[11px] text-muted-foreground/70">1053 <div className="flex min-h-[72px] items-center justify-center px-2 py-3 text-center text-[11px] text-muted-foreground/70">
...@@ -1152,7 +1059,7 @@ export default function ChatView() {...@@ -1152,7 +1059,7 @@ export default function ChatView() {
1152 </div>1059 </div>
1153 )}1060 )}
1154 {timelineEntry.message.text && (1061 {timelineEntry.message.text && (
1155 <pre className="whitespace-pre-wrap break-words font-mono text-sm leading-relaxed text-foreground">1062 <pre className="whitespace-pre-wrap wrap-break-word font-mono text-sm leading-relaxed text-foreground">
1156 {timelineEntry.message.text}1063 {timelineEntry.message.text}
1157 </pre>1064 </pre>
1158 )}1065 )}
...@@ -1241,21 +1148,24 @@ export default function ChatView() {...@@ -1241,21 +1148,24 @@ export default function ChatView() {
1241 src={image.previewUrl}1148 src={image.previewUrl}
1242 alt={image.name}1149 alt={image.name}
1243 className="h-full w-full cursor-zoom-in object-cover"1150 className="h-full w-full cursor-zoom-in object-cover"
1244 onClick={() => openImageDialog(image.previewUrl, image.name)}1151 onClick={() =>
1152 setExpandedImage({ src: image.previewUrl, name: image.name })
1153 }
1245 />1154 />
1246 ) : (1155 ) : (
1247 <div className="flex h-full w-full items-center justify-center px-1 text-center text-[10px] text-muted-foreground/70">1156 <div className="flex h-full w-full items-center justify-center px-1 text-center text-[10px] text-muted-foreground/70">
1248 {image.name}1157 {image.name}
1249 </div>1158 </div>
1250 )}1159 )}
1251 <button1160 <Button
1252 type="button"1161 variant="ghost"
1253 className="absolute right-1 top-1 flex h-4 w-4 items-center justify-center rounded-full bg-background/80 text-[11px] leading-none text-foreground transition-colors duration-150 hover:bg-background"1162 size="icon-xs"
1163 className="absolute right-1 top-1 bg-background/80 hover:bg-background/90"
1254 onClick={() => removeComposerImage(image.id)}1164 onClick={() => removeComposerImage(image.id)}
1255 aria-label={`Remove ${image.name}`}1165 aria-label={`Remove ${image.name}`}
1256 >1166 >
1257 ×1167 <XIcon />
1258 </button>1168 </Button>
1259 </div>1169 </div>
1260 ))}1170 ))}
1261 </div>1171 </div>
...@@ -1281,110 +1191,23 @@ export default function ChatView() {...@@ -1281,110 +1191,23 @@ export default function ChatView() {
1281 <div className="flex items-center justify-between px-3 pb-3">1191 <div className="flex items-center justify-between px-3 pb-3">
1282 <div className="flex items-center gap-1">1192 <div className="flex items-center gap-1">
1283 {/* Model picker */}1193 {/* Model picker */}
1284 <div className="relative" ref={modelMenuRef}>1194 <ModelPicker model={selectedModel} onModelChange={onModelSelect} />
1285 <button
1286 type="button"
1287 className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-muted-foreground/70 transition-colors duration-150 hover:bg-accent hover:text-foreground/80"
1288 onClick={() => setIsModelMenuOpen((open) => !open)}
1289 >
1290 <span className="max-w-[180px] truncate">{selectedModel}</span>
1291 <svg
1292 width="10"
1293 height="10"
1294 viewBox="0 0 10 10"
1295 fill="none"
1296 className="opacity-50"
1297 aria-hidden="true"
1298 >
1299 <path
1300 d="M2.5 4L5 6.5L7.5 4"
1301 stroke="currentColor"
1302 strokeWidth="1.2"
1303 strokeLinecap="round"
1304 strokeLinejoin="round"
1305 />
1306 </svg>
1307 </button>
1308 {isModelMenuOpen && (
1309 <div className="absolute bottom-full left-0 z-20 mb-2 w-[320px] rounded-2xl border border-border bg-popover/95 p-2 shadow-[0_16px_40px_rgba(0,0,0,0.55)] backdrop-blur">
1310 <p className="px-2 py-1 text-[11px] text-muted-foreground/70">Select model</p>
1311 <div className="max-h-72 overflow-y-auto">
1312 {modelOptions.map((model) => {
1313 const isSelected = model === selectedModel;
1314 return (
1315 <button
1316 key={model}
1317 type="button"
1318 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 ${
1319 isSelected
1320 ? "bg-accent text-foreground"
1321 : "text-foreground/90 hover:bg-accent"
1322 }`}
1323 onClick={() => onModelSelect(model)}
1324 >
1325 <span className="truncate">{model}</span>
1326 <span
1327 className={`pt-0.5 text-sm ${
1328 isSelected ? "text-foreground" : "text-transparent"
1329 }`}
1330 >
1331 ✓
1332 </span>
1333 </button>
1334 );
1335 })}
1336 </div>
1337 </div>
1338 )}
1339 </div>
13401195
1341 {/* Divider */}1196 {/* Divider */}
1342 <div className="mx-0.5 h-4 w-px bg-border" />1197 <Separator orientation="vertical" className="mx-0.5 h-4" />
13431198
1344 {/* Reasoning effort */}1199 {/* Reasoning effort */}
1345 <label1200 <ReasoningEffortPicker effort={selectedEffort} onEffortChange={setSelectedEffort} />
1346 className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-muted-foreground/70 transition-colors duration-150 hover:bg-accent hover:text-foreground/80"
1347 htmlFor="reasoning-effort"
1348 >
1349 <span>{selectedEffort.charAt(0).toUpperCase() + selectedEffort.slice(1)}</span>
1350 <select
1351 id="reasoning-effort"
1352 className="absolute opacity-0 w-0 h-0"
1353 value={selectedEffort}
1354 onChange={(event) => setSelectedEffort(event.target.value)}
1355 >
1356 {REASONING_OPTIONS.map((effort) => (
1357 <option key={effort} value={effort} className="bg-popover">
1358 {effort}
1359 {effort === DEFAULT_REASONING ? " (default)" : ""}
1360 </option>
1361 ))}
1362 </select>
1363 <svg
1364 width="10"
1365 height="10"
1366 viewBox="0 0 10 10"
1367 fill="none"
1368 className="opacity-50"
1369 aria-hidden="true"
1370 >
1371 <path
1372 d="M2.5 4L5 6.5L7.5 4"
1373 stroke="currentColor"
1374 strokeWidth="1.2"
1375 strokeLinecap="round"
1376 strokeLinejoin="round"
1377 />
1378 </svg>
1379 </label>
13801201
1381 {/* Divider */}1202 {/* Divider */}
1382 <div className="mx-0.5 h-4 w-px bg-border" />1203 <Separator orientation="vertical" className="mx-0.5 h-4" />
13831204
1384 {/* Runtime mode toggle */}1205 {/* Runtime mode toggle */}
1385 <button1206 <Button
1207 variant="ghost"
1208 className="text-muted-foreground/70 hover:text-foreground/80"
1209 size="sm"
1386 type="button"1210 type="button"
1387 className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-muted-foreground/70 transition-colors duration-150 hover:bg-accent hover:text-foreground/80"
1388 disabled={isSwitchingRuntimeMode}1211 disabled={isSwitchingRuntimeMode}
1389 onClick={() =>1212 onClick={() =>
1390 void handleRuntimeModeChange(1213 void handleRuntimeModeChange(
...@@ -1397,45 +1220,9 @@ export default function ChatView() {...@@ -1397,45 +1220,9 @@ export default function ChatView() {
1397 : "Approval required — click for full access"1220 : "Approval required — click for full access"
1398 }1221 }
1399 >1222 >
1400 {state.runtimeMode === "full-access" ? (1223 {state.runtimeMode === "full-access" ? <LockOpenIcon /> : <LockIcon />}
1401 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">1224 {state.runtimeMode === "full-access" ? "Full access" : "Supervised"}
1402 <rect1225 </Button>
1403 x="2"
1404 y="5.5"
1405 width="10"
1406 height="7"
1407 rx="1.5"
1408 stroke="currentColor"
1409 strokeWidth="1.3"
1410 />
1411 <path
1412 d="M9.5 5.5V4a2.5 2.5 0 0 0-5 0"
1413 stroke="currentColor"
1414 strokeWidth="1.3"
1415 strokeLinecap="round"
1416 />
1417 </svg>
1418 ) : (
1419 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">
1420 <rect
1421 x="2"
1422 y="5.5"
1423 width="10"
1424 height="7"
1425 rx="1.5"
1426 stroke="currentColor"
1427 strokeWidth="1.3"
1428 />
1429 <path
1430 d="M4.5 5.5V4a2.5 2.5 0 0 1 5 0v1.5"
1431 stroke="currentColor"
1432 strokeWidth="1.3"
1433 strokeLinecap="round"
1434 />
1435 </svg>
1436 )}
1437 <span>{state.runtimeMode === "full-access" ? "Full access" : "Supervised"}</span>
1438 </button>
1439 </div>1226 </div>
14401227
1441 {/* Right side: send / stop button */}1228 {/* Right side: send / stop button */}
...@@ -1461,7 +1248,9 @@ export default function ChatView() {...@@ -1461,7 +1248,9 @@ export default function ChatView() {
1461 <button1248 <button
1462 type="submit"1249 type="submit"
1463 className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/90 text-primary-foreground transition-all duration-150 hover:bg-primary hover:scale-105 disabled:opacity-30 disabled:hover:scale-100"1250 className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/90 text-primary-foreground transition-all duration-150 hover:bg-primary hover:scale-105 disabled:opacity-30 disabled:hover:scale-100"
1464 disabled={isSending || isConnecting || (!prompt.trim() && composerImages.length === 0)}1251 disabled={
1252 isSending || isConnecting || (!prompt.trim() && composerImages.length === 0)
1253 }
1465 aria-label={1254 aria-label={
1466 isConnecting ? "Connecting" : isSending ? "Sending" : "Send message"1255 isConnecting ? "Connecting" : isSending ? "Sending" : "Send message"
1467 }1256 }
...@@ -1550,14 +1339,16 @@ export default function ChatView() {...@@ -1550,14 +1339,16 @@ export default function ChatView() {
1550 className="relative isolate max-h-[92vh] max-w-[92vw]"1339 className="relative isolate max-h-[92vh] max-w-[92vw]"
1551 onClick={(event) => event.stopPropagation()}1340 onClick={(event) => event.stopPropagation()}
1552 >1341 >
1553 <button1342 <Button
1554 type="button"1343 type="button"
1555 className="absolute right-2 top-2 z-30 flex h-8 w-8 cursor-pointer items-center justify-center rounded-full bg-black/60 text-xl leading-none text-white transition-colors duration-150 hover:bg-black/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80 [-webkit-app-region:no-drag]"1344 size="icon-xs"
1345 variant="ghost"
1346 className="absolute right-2 top-2"
1556 onClick={() => setExpandedImage(null)}1347 onClick={() => setExpandedImage(null)}
1557 aria-label="Close image preview"1348 aria-label="Close image preview"
1558 >1349 >
1559 <span className="pointer-events-none">×</span>1350 <XIcon />
1560 </button>1351 </Button>
1561 <img1352 <img
1562 src={expandedImage.src}1353 src={expandedImage.src}
1563 alt={expandedImage.name}1354 alt={expandedImage.name}
...@@ -1573,3 +1364,133 @@ export default function ChatView() {...@@ -1573,3 +1364,133 @@ export default function ChatView() {
1573 </div>1364 </div>
1574 );1365 );
1575}1366}
1367
1368function ModelPicker(props: { model: ModelSlug; onModelChange: (model: ModelSlug) => void }) {
1369 return (
1370 <Select
1371 items={MODEL_OPTIONS.map((option) => ({ label: option.name, value: option.slug }))}
1372 value={props.model}
1373 onValueChange={(value) => (value ? props.onModelChange(value) : undefined)}
1374 >
1375 <SelectTrigger size="sm" variant="ghost">
1376 <SelectValue />
1377 </SelectTrigger>
1378 <SelectPopup alignItemWithTrigger={false}>
1379 {MODEL_OPTIONS.map(({ slug, name }) => (
1380 <SelectItem key={slug} value={slug}>
1381 {name}
1382 </SelectItem>
1383 ))}
1384 </SelectPopup>
1385 </Select>
1386 );
1387}
1388
1389function ReasoningEffortPicker(props: {
1390 effort: ReasoningEffort;
1391 onEffortChange: (effort: ReasoningEffort) => void;
1392}) {
1393 return (
1394 <Select
1395 value={props.effort}
1396 onValueChange={(value) => (value ? props.onEffortChange(value) : undefined)}
1397 >
1398 <SelectTrigger variant="ghost" size="sm">
1399 <SelectValue />
1400 </SelectTrigger>
1401 <SelectPopup alignItemWithTrigger={false}>
1402 {REASONING_OPTIONS.map((effort) => (
1403 <SelectItem key={effort} value={effort}>
1404 {effort}
1405 {effort === DEFAULT_REASONING ? " (default)" : ""}
1406 </SelectItem>
1407 ))}
1408 </SelectPopup>
1409 </Select>
1410 );
1411}
1412
1413function OpenInPicker() {
1414 const [lastEditor, setLastEditor] = useState<EditorId>(() => {
1415 const stored = localStorage.getItem(LAST_EDITOR_KEY);
1416 return EDITORS.some((e) => e.id === stored) ? (stored as EditorId) : EDITORS[0].id;
1417 });
1418
1419 const options = [
1420 {
1421 label: "Cursor",
1422 Icon: CursorIcon,
1423 value: "cursor",
1424 },
1425 {
1426 label: navigator.platform.includes("Mac")
1427 ? "Finder"
1428 : navigator.platform.includes("Win")
1429 ? "Explorer"
1430 : "Files",
1431 Icon: FolderClosedIcon,
1432 value: "file-manager",
1433 },
1434 ] satisfies { label: string; Icon: Icon; value: EditorId }[];
1435 const primaryOption = options.find(({ value }) => value === lastEditor);
1436
1437 const api = useNativeApi();
1438 const { state } = useStore();
1439 const activeProject = state.projects.find((p) => p.id === state.activeThreadId);
1440 const activeThread = state.threads.find((t) => t.id === state.activeThreadId);
1441
1442 const openInEditor = useCallback(
1443 (editorId: EditorId | null) => {
1444 if (!api || !activeProject) return;
1445 const editor = editorId ?? lastEditor;
1446 const cwd = activeThread?.worktreePath ?? activeProject.cwd;
1447 void api.shell.openInEditor(cwd, editor);
1448 localStorage.setItem(LAST_EDITOR_KEY, editor);
1449 setLastEditor(editor);
1450 },
1451 [api, activeProject, activeThread, lastEditor, setLastEditor],
1452 );
1453
1454 // Cmd+O / Ctrl+O to open in last-used editor
1455 useEffect(() => {
1456 const handler = (e: globalThis.KeyboardEvent) => {
1457 if (e.key === "o" && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
1458 if (api && activeProject) {
1459 e.preventDefault();
1460 const cwd = activeThread?.worktreePath ?? activeProject.cwd;
1461 void api.shell.openInEditor(cwd, lastEditor);
1462 }
1463 }
1464 };
1465 window.addEventListener("keydown", handler);
1466 return () => window.removeEventListener("keydown", handler);
1467 }, [api, activeProject, activeThread, lastEditor]);
1468
1469 return (
1470 <Group aria-label="Subscription actions">
1471 <Button size="xs" variant="outline" onClick={() => openInEditor(lastEditor)}>
1472 {primaryOption?.Icon && <primaryOption.Icon aria-hidden="true" className="size-3" />}
1473 Open
1474 </Button>
1475 <GroupSeparator />
1476 <Menu>
1477 <MenuTrigger render={<Button aria-label="Copy options" size="icon-xs" variant="outline" />}>
1478 <ChevronDownIcon aria-hidden="true" className="size-4" />
1479 </MenuTrigger>
1480 <MenuPopup align="end">
1481 {options.map(({ label, Icon, value }) => (
1482 <MenuItem key={value} onClick={() => openInEditor(value)}>
1483 <Icon aria-hidden="true" className="text-muted-foreground" />
1484 {label}
1485 {value === lastEditor && (
1486 <MenuShortcut>
1487 {navigator.platform.includes("Mac") ? "\u2318O" : "Ctrl+O"}
1488 </MenuShortcut>
1489 )}
1490 </MenuItem>
1491 ))}
1492 </MenuPopup>
1493 </Menu>
1494 </Group>
1495 );
1496}
apps/web/src/components/DiffPanel.tsx+7-4
...@@ -1,5 +1,7 @@...@@ -1,5 +1,7 @@
1import { XIcon } from "lucide-react";
1import { isElectron } from "../env";2import { isElectron } from "../env";
2import { useStore } from "../store";3import { useStore } from "../store";
4import { Button } from "./ui/button";
35
4export default function DiffPanel() {6export default function DiffPanel() {
5 const { dispatch } = useStore();7 const { dispatch } = useStore();
...@@ -11,13 +13,14 @@ export default function DiffPanel() {...@@ -11,13 +13,14 @@ export default function DiffPanel() {
11 className={`flex items-center justify-between border-b border-border px-4 ${isElectron ? "drag-region h-[52px]" : "py-3"}`}13 className={`flex items-center justify-between border-b border-border px-4 ${isElectron ? "drag-region h-[52px]" : "py-3"}`}
12 >14 >
13 <h3 className="text-xs font-medium text-foreground">Uncommitted changes</h3>15 <h3 className="text-xs font-medium text-foreground">Uncommitted changes</h3>
14 <button16 <Button
15 type="button"17 type="button"
16 className="text-muted-foreground/60 transition-colors duration-150 hover:text-muted-foreground"18 size="icon-xs"
19 variant="ghost"
17 onClick={() => dispatch({ type: "TOGGLE_DIFF" })}20 onClick={() => dispatch({ type: "TOGGLE_DIFF" })}
18 >21 >
19 <span className="text-sm">&times;</span>22 <XIcon />
20 </button>23 </Button>
21 </div>24 </div>
2225
23 {/* Placeholder content */}26 {/* Placeholder content */}
apps/web/src/components/GitActionsControl.tsx+136-670
...@@ -5,15 +5,13 @@ import {...@@ -5,15 +5,13 @@ import {
5 type NativeApi,5 type NativeApi,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import { queryOptions, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";7import { queryOptions, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
8import { useCallback, useEffect, useMemo, useRef, useState } from "react";8import { useCallback, useEffect, useMemo, useState } from "react";
9import {9import { ChevronDownIcon, CloudUploadIcon, GitCommitIcon } from "lucide-react";
10 CheckIcon,
11 CloudUploadIcon,
12 GitCommitIcon,
13 Loader2Icon,
14 XIcon,
15} from "lucide-react";
16import { GitHubIcon } from "./Icons";10import { GitHubIcon } from "./Icons";
11import { Button } from "~/components/ui/button";
12import { Group, GroupSeparator } from "~/components/ui/group";
13import { Menu, MenuItem, MenuPopup, MenuTrigger } from "~/components/ui/menu";
14import { toastManager } from "~/components/ui/toast";
1715
18interface GitActionsControlProps {16interface GitActionsControlProps {
19 api: NativeApi | undefined;17 api: NativeApi | undefined;
...@@ -27,112 +25,38 @@ interface GitActionMenuItem {...@@ -27,112 +25,38 @@ interface GitActionMenuItem {
27 label: string;25 label: string;
28 disabled: boolean;26 disabled: boolean;
29 icon: GitActionIconName;27 icon: GitActionIconName;
30 kind: "open_modal" | "run_action" | "open_pr";28 kind: "run_action" | "open_pr";
31 action?: GitStackedAction;29 action?: GitStackedAction;
32}30}
3331
34interface GitModalActionOption {32function GitActionItemIcon({ icon }: { icon: GitActionIconName }) {
35 action: GitStackedAction;33 if (icon === "commit") return <GitCommitIcon />;
36 label: string;34 if (icon === "push") return <CloudUploadIcon />;
37 disabled: boolean;35 return <GitHubIcon />;
38 icon: GitActionIconName;
39 detail?: string;
40}
41
42type GitProgressStepStatus = "pending" | "running" | "completed" | "skipped" | "failed";
43
44interface GitProgressStep {
45 id: "generate" | "commit" | "push" | "pr";
46 label: string;
47 status: GitProgressStepStatus;
48 detail?: string;
49}
50
51function GitActionIcon(props: { icon: GitActionIconName; disabled: boolean }) {
52 const toneClass = props.disabled ? "text-muted-foreground/45" : "text-foreground/85";
53
54 if (props.icon === "commit") {
55 return <GitCommitIcon className={`h-5 w-5 shrink-0 ${toneClass}`} />;
56 }
57
58 if (props.icon === "push") {
59 return <CloudUploadIcon className={`h-5 w-5 shrink-0 ${toneClass}`} />;
60 }
61
62 return <GitHubIcon className={`h-5 w-5 shrink-0 ${toneClass}`} />;
63}
64
65function runningStepMessage(stepId: GitProgressStep["id"]): string {
66 if (stepId === "generate") return "Generating commit message...";
67 if (stepId === "commit") return "Committing changes...";
68 if (stepId === "push") return "Pushing branch...";
69 return "Preparing pull request...";
70}36}
7137
72function gitActionModalTitle(): string {38function describeGitResult(result: GitRunStackedActionResult): string {
73 return "Commit your changes";39 const parts: string[] = [];
74}
7540
76function initialGitProgressSteps(41 if (result.commit.status === "created") {
77 action: GitStackedAction,42 parts.push(result.commit.subject ?? "Committed changes");
78 commitMessage: string,
79): GitProgressStep[] {
80 const hasCustomMessage = commitMessage.trim().length > 0;
81 const steps: GitProgressStep[] = [];
82
83 if (!hasCustomMessage) {
84 steps.push({
85 id: "generate",
86 label: "Generate commit message",
87 status: "pending",
88 });
89 }43 }
9044
91 steps.push({45 if (result.push.status === "pushed") {
92 id: "commit",46 parts.push(
93 label: "Commit changes",47 result.push.upstreamBranch
94 status: "pending",48 ? `Pushed to ${result.push.upstreamBranch}`
95 });49 : "Pushed",
9650 );
97 if (action !== "commit") {
98 steps.push({
99 id: "push",
100 label: "Push branch",
101 status: "pending",
102 });
103 }51 }
10452
105 if (action === "commit_push_pr") {53 if (result.pr.status === "created") {
106 steps.push({54 parts.push(result.pr.number ? `Created PR #${result.pr.number}` : "Created PR");
107 id: "pr",55 } else if (result.pr.status === "opened_existing") {
108 label: "Create or open PR",56 parts.push(result.pr.number ? `PR #${result.pr.number}` : "Opened existing PR");
109 status: "pending",
110 });
111 }57 }
11258
113 return steps;59 return parts.join(" · ") || "No changes needed.";
114}
115
116function updateProgressStep(
117 steps: GitProgressStep[],
118 id: GitProgressStep["id"],
119 status: GitProgressStepStatus,
120 detail?: string,
121): GitProgressStep[] {
122 return steps.map((step) => {
123 if (step.id !== id) return step;
124 return {
125 ...step,
126 status,
127 ...(detail ? { detail } : {}),
128 };
129 });
130}
131
132function runActionLabel(action: GitStackedAction): string {
133 if (action === "commit") return "Commit changes";
134 if (action === "commit_push") return "Commit and push";
135 return "Commit and create PR";
136}60}
13761
138function buildGitActionMenuItems(62function buildGitActionMenuItems(
...@@ -162,7 +86,8 @@ function buildGitActionMenuItems(...@@ -162,7 +86,8 @@ function buildGitActionMenuItems(
162 label: "Commit",86 label: "Commit",
163 disabled: !canCommit,87 disabled: !canCommit,
164 icon: "commit",88 icon: "commit",
165 kind: "open_modal",89 kind: "run_action",
90 action: "commit",
166 },91 },
167 {92 {
168 id: "push",93 id: "push",
...@@ -191,41 +116,6 @@ function buildGitActionMenuItems(...@@ -191,41 +116,6 @@ function buildGitActionMenuItems(
191 ];116 ];
192}117}
193118
194function buildGitModalActionOptions(
195 gitStatus: GitStatusResult | null,
196 isDisabled: boolean,
197): GitModalActionOption[] {
198 const hasBranch = gitStatus?.branch !== null;
199 const hasOpenPr = gitStatus?.openPr !== null;
200
201 return [
202 {
203 action: "commit",
204 label: "Commit",
205 disabled: isDisabled,
206 icon: "commit",
207 },
208 {
209 action: "commit_push",
210 label: "Commit and push",
211 disabled: isDisabled || !hasBranch,
212 icon: "push",
213 ...(!hasBranch ? { detail: "Requires an attached branch." } : {}),
214 },
215 {
216 action: "commit_push_pr",
217 label: "Commit and create PR",
218 disabled: isDisabled || !hasBranch || hasOpenPr,
219 icon: "pr",
220 ...(!hasBranch
221 ? { detail: "Requires an attached branch." }
222 : hasOpenPr
223 ? { detail: "A PR is already open for this branch." }
224 : {}),
225 },
226 ];
227}
228
229function getGitStatusQueryKey(gitCwd: string | null): readonly ["git", "status", string | null] {119function getGitStatusQueryKey(gitCwd: string | null): readonly ["git", "status", string | null] {
230 return ["git", "status", gitCwd];120 return ["git", "status", gitCwd];
231}121}
...@@ -245,17 +135,7 @@ function gitStatusQueryOptions(api: NativeApi | undefined, gitCwd: string | null...@@ -245,17 +135,7 @@ function gitStatusQueryOptions(api: NativeApi | undefined, gitCwd: string | null
245135
246export default function GitActionsControl({ api, gitCwd }: GitActionsControlProps) {136export default function GitActionsControl({ api, gitCwd }: GitActionsControlProps) {
247 const queryClient = useQueryClient();137 const queryClient = useQueryClient();
248 const [isGitMenuOpen, setIsGitMenuOpen] = useState(false);138 const [selectedGitActionId, setSelectedGitActionId] = useState<GitActionMenuItem["id"]>("commit");
249 const [isGitModalActionRunning, setIsGitModalActionRunning] = useState(false);
250 const [gitActionError, setGitActionError] = useState<string | null>(null);
251 const [isGitModalOpen, setIsGitModalOpen] = useState(false);
252 const [gitModalSelectedAction, setGitModalSelectedAction] = useState<GitStackedAction>("commit");
253 const [gitModalCommitMessage, setGitModalCommitMessage] = useState("");
254 const [gitModalProgress, setGitModalProgress] = useState<GitProgressStep[]>([]);
255 const [gitModalError, setGitModalError] = useState<string | null>(null);
256 const [gitModalResult, setGitModalResult] = useState<GitRunStackedActionResult | null>(null);
257 const [isGitProgressToastVisible, setIsGitProgressToastVisible] = useState(false);
258 const gitMenuRef = useRef<HTMLDivElement>(null);
259139
260 const { data: gitStatus = null, error: gitStatusError } = useQuery(140 const { data: gitStatus = null, error: gitStatusError } = useQuery(
261 gitStatusQueryOptions(api, gitCwd),141 gitStatusQueryOptions(api, gitCwd),
...@@ -268,581 +148,167 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -268,581 +148,167 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
268 }148 }
269 return api.git.runStackedAction({ cwd: gitCwd, action });149 return api.git.runStackedAction({ cwd: gitCwd, action });
270 },150 },
271 onMutate: () => {
272 setIsGitMenuOpen(false);
273 setGitActionError(null);
274 setGitModalError(null);
275 },
276 onSuccess: async () => {151 onSuccess: async () => {
277 await queryClient.invalidateQueries({ queryKey: ["git"] });152 await queryClient.invalidateQueries({ queryKey: ["git"] });
278 },153 },
279 onError: (error) => {
280 setGitActionError(error instanceof Error ? error.message : "Git action failed.");
281 },
282 });154 });
283155
284 const isGitActionRunning = isGitModalActionRunning || runImmediateGitActionMutation.isPending;156 const isGitActionRunning = runImmediateGitActionMutation.isPending;
285157
286 const gitBaseDisabled = !api || !gitCwd || !gitStatus || isGitActionRunning;158 const gitBaseDisabled = !api || !gitCwd || !gitStatus || isGitActionRunning;
287 const gitActionMenuItems = useMemo(159 const gitActionMenuItems = useMemo(
288 () => buildGitActionMenuItems(gitStatus, gitBaseDisabled),160 () => buildGitActionMenuItems(gitStatus, gitBaseDisabled),
289 [gitBaseDisabled, gitStatus],161 [gitBaseDisabled, gitStatus],
290 );162 );
291 const gitModalActionOptions = useMemo(163 const selectedGitActionItem = useMemo(
292 () => buildGitModalActionOptions(gitStatus, !gitStatus || isGitActionRunning),164 () => gitActionMenuItems.find((item) => item.id === selectedGitActionId) ?? null,
293 [gitStatus, isGitActionRunning],165 [gitActionMenuItems, selectedGitActionId],
294 );
295 const selectedGitModalActionOption = useMemo(
296 () => gitModalActionOptions.find((option) => option.action === gitModalSelectedAction) ?? null,
297 [gitModalActionOptions, gitModalSelectedAction],
298 );166 );
299 const visibleGitActionError = gitActionError ?? gitStatusError?.message;
300 const gitProgressToastOpenPrUrl = gitModalResult?.pr.url ?? gitStatus?.openPr?.url ?? null;
301 const showGitProgressToast =
302 isGitProgressToastVisible &&
303 (isGitModalActionRunning ||
304 gitModalProgress.length > 0 ||
305 gitModalError !== null ||
306 gitModalResult !== null);
307 const activeGitProgressStep = useMemo(
308 () => gitModalProgress.find((step) => step.status === "running") ?? null,
309 [gitModalProgress],
310 );
311 const gitProgressToastStatus = isGitModalActionRunning
312 ? "running"
313 : gitModalError
314 ? "failed"
315 : "done";
316 const gitProgressToastMessage = isGitModalActionRunning
317 ? runningStepMessage(activeGitProgressStep?.id ?? "commit")
318 : gitModalError
319 ? "Git action failed."
320 : "Done.";
321167
322 const refreshGitStatus = useCallback(async () => {168 const refreshGitStatus = useCallback(async () => {
323 if (!api || !gitCwd) return;169 if (!api || !gitCwd) return;
324 await queryClient.invalidateQueries({ queryKey: ["git"] });170 await queryClient.invalidateQueries({ queryKey: ["git"] });
325 setGitActionError(null);
326 }, [api, gitCwd, queryClient]);171 }, [api, gitCwd, queryClient]);
327172
328 const openGitActionModal = useCallback(() => {
329 setIsGitMenuOpen(false);
330 setIsGitModalOpen(true);
331 setGitModalSelectedAction("commit");
332 setGitModalCommitMessage("");
333 setIsGitProgressToastVisible(false);
334 setGitModalProgress([]);
335 setGitModalError(null);
336 setGitModalResult(null);
337 setGitActionError(null);
338 }, []);
339
340 const closeGitActionModal = useCallback(() => {
341 if (isGitActionRunning) return;
342 setIsGitModalOpen(false);
343 setGitModalSelectedAction("commit");
344 setGitModalCommitMessage("");
345 }, [isGitActionRunning]);
346
347 const runGitActionImmediately = useCallback(173 const runGitActionImmediately = useCallback(
348 (action: GitStackedAction) => {174 (action: GitStackedAction) => {
349 runImmediateGitActionMutation.mutate(action);175 const promise = runImmediateGitActionMutation.mutateAsync(action);
176 toastManager.promise(promise, {
177 loading: {
178 title: action === "commit_push" ? "Pushing..." : "Creating PR...",
179 },
180 success: (result) => {
181 const prUrl = result.pr.url;
182 return {
183 title: "Done",
184 description: describeGitResult(result),
185 ...(prUrl
186 ? {
187 timeout: 10_000,
188 actionProps: {
189 children: "Open PR",
190 onClick: () => void api?.shell.openExternal(prUrl),
191 },
192 }
193 : {}),
194 };
195 },
196 error: (err) => ({
197 title: "Action failed",
198 description: err instanceof Error ? err.message : "An error occurred.",
199 }),
200 });
201 void promise.catch(() => undefined);
350 },202 },
351 [runImmediateGitActionMutation],203 [api, runImmediateGitActionMutation],
352 );204 );
353205
354 const openExistingPr = useCallback(() => {206 const openExistingPr = useCallback(() => {
355 setIsGitMenuOpen(false);
356 setGitActionError(null);
357 if (!api) {207 if (!api) {
358 setGitActionError("Link opening is unavailable.");208 toastManager.add({ type: "error", title: "Link opening is unavailable." });
359 return;209 return;
360 }210 }
361211
362 const prUrl = gitStatus?.openPr?.url ?? null;212 const prUrl = gitStatus?.openPr?.url ?? null;
363 if (!prUrl) {213 if (!prUrl) {
364 setGitActionError("No open PR found for the current branch.");214 toastManager.add({ type: "error", title: "No open PR found." });
365 return;215 return;
366 }216 }
367217
368 void api.shell.openExternal(prUrl).catch((error) => {218 void api.shell.openExternal(prUrl).catch((error) => {
369 setGitActionError(error instanceof Error ? error.message : "Unable to open PR link.");219 toastManager.add({
220 type: "error",
221 title: "Unable to open PR link",
222 description: error instanceof Error ? error.message : undefined,
223 });
370 });224 });
371 }, [api, gitStatus?.openPr?.url]);225 }, [api, gitStatus?.openPr?.url]);
372226
373 const openPrFromToast = useCallback(() => {227 const runSelectedAction = useCallback(() => {
374 if (!api) {228 if (!selectedGitActionItem || selectedGitActionItem.disabled) return;
375 setGitModalError("Link opening is unavailable.");229 if (selectedGitActionItem.kind === "open_pr") {
230 openExistingPr();
376 return;231 return;
377 }232 }
378233 if (selectedGitActionItem.action) {
379 const prUrl = gitProgressToastOpenPrUrl;234 runGitActionImmediately(selectedGitActionItem.action);
380 if (!prUrl) return;
381
382 void api.shell.openExternal(prUrl).catch((error) => {
383 setGitModalError(error instanceof Error ? error.message : "Unable to open PR link.");
384 });
385 }, [api, gitProgressToastOpenPrUrl]);
386
387 const dismissGitProgressToast = useCallback(() => {
388 if (isGitModalActionRunning) return;
389 setIsGitProgressToastVisible(false);
390 setGitModalProgress([]);
391 setGitModalError(null);
392 setGitModalResult(null);
393 }, [isGitModalActionRunning]);
394
395 const runGitAction = useCallback(async () => {
396 if (!api || !gitCwd) return;
397 if (!selectedGitModalActionOption || selectedGitModalActionOption.disabled) return;
398 const actionCwd = gitCwd;
399 const action = selectedGitModalActionOption.action;
400 const commitMessage = gitModalCommitMessage.trim();
401 const includeGeneratedCommitMessage = commitMessage.length === 0;
402
403 setIsGitMenuOpen(false);
404 setIsGitModalOpen(false);
405 setIsGitProgressToastVisible(true);
406 setIsGitModalActionRunning(true);
407 setGitModalError(null);
408 setGitActionError(null);
409 setGitModalResult(null);
410 setGitModalProgress(initialGitProgressSteps(action, commitMessage));
411
412 let commit: GitRunStackedActionResult["commit"] = {
413 status: "skipped_no_changes",
414 };
415 let push: GitRunStackedActionResult["push"] = {
416 status: "skipped_not_requested",
417 };
418 let pr: GitRunStackedActionResult["pr"] = {
419 status: "skipped_not_requested",
420 };
421
422 const updateStep = (
423 id: GitProgressStep["id"],
424 status: GitProgressStepStatus,
425 detail?: string,
426 ) => {
427 setGitModalProgress((steps) => updateProgressStep(steps, id, status, detail));
428 };
429
430 try {
431 if (includeGeneratedCommitMessage) {
432 updateStep("generate", "running");
433 } else {
434 updateStep("commit", "running");
435 }
436
437 const commitRun = await api.git.runStackedAction({
438 cwd: actionCwd,
439 action: "commit",
440 ...(commitMessage.length > 0 ? { commitMessage } : {}),
441 });
442 commit = commitRun.commit;
443
444 if (includeGeneratedCommitMessage) {
445 if (commitRun.commit.status === "created") {
446 if (commitRun.commit.subject) {
447 setGitModalCommitMessage(commitRun.commit.subject);
448 }
449 updateStep("generate", "completed");
450 } else {
451 updateStep("generate", "skipped", "No local changes to commit.");
452 }
453 }
454
455 if (commitRun.commit.status === "created") {
456 updateStep("commit", "completed", commitRun.commit.subject ?? "Committed local changes.");
457 } else {
458 updateStep("commit", "skipped", "No local changes to commit.");
459 }
460
461 if (action !== "commit") {
462 updateStep("push", "running");
463 const pushRun = await api.git.runStackedAction({
464 cwd: actionCwd,
465 action: "commit_push",
466 });
467 push = pushRun.push;
468 if (pushRun.push.status === "pushed") {
469 updateStep(
470 "push",
471 "completed",
472 pushRun.push.upstreamBranch
473 ? `Pushed to ${pushRun.push.upstreamBranch}.`
474 : "Pushed latest commits.",
475 );
476 } else {
477 updateStep("push", "skipped", "Branch already up to date.");
478 }
479 }
480
481 if (action === "commit_push_pr") {
482 updateStep("pr", "running");
483 const prRun = await api.git.runStackedAction({
484 cwd: actionCwd,
485 action: "commit_push_pr",
486 });
487 pr = prRun.pr;
488 if (prRun.pr.status === "opened_existing") {
489 updateStep(
490 "pr",
491 "completed",
492 prRun.pr.number ? `Opened existing PR #${prRun.pr.number}.` : "Opened existing PR.",
493 );
494 } else if (prRun.pr.status === "created") {
495 updateStep(
496 "pr",
497 "completed",
498 prRun.pr.number ? `Created PR #${prRun.pr.number}.` : "Created PR.",
499 );
500 } else {
501 updateStep("pr", "skipped", "PR step was not requested.");
502 }
503 }
504
505 setGitModalResult({ action, commit, push, pr });
506 } catch (error) {
507 const message = error instanceof Error ? error.message : "Git action failed.";
508 setGitModalError(message);
509 setGitActionError(message);
510 setGitModalProgress((steps) => {
511 const active = steps.find((step) => step.status === "running");
512 if (!active) return steps;
513 return updateProgressStep(steps, active.id, "failed", message);
514 });
515 } finally {
516 setIsGitModalActionRunning(false);
517 await refreshGitStatus().catch(() => undefined);
518 }235 }
519 }, [236 }, [openExistingPr, runGitActionImmediately, selectedGitActionItem]);
520 api,
521 gitCwd,
522 gitModalCommitMessage,
523 refreshGitStatus,
524 selectedGitModalActionOption,
525 ]);
526237
527 useEffect(() => {238 useEffect(() => {
528 setGitActionError(null);239 setSelectedGitActionId("commit");
529 setGitModalError(null);
530 setIsGitModalOpen(false);
531 setIsGitProgressToastVisible(false);
532 setGitModalSelectedAction("commit");
533 setGitModalCommitMessage("");
534 setGitModalProgress([]);
535 setGitModalResult(null);
536 }, [gitCwd]);240 }, [gitCwd]);
537241
538 useEffect(() => {242 useEffect(() => {
539 if (!isGitModalOpen) return;243 if (selectedGitActionItem) return;
540 if (selectedGitModalActionOption && !selectedGitModalActionOption.disabled) return;244 const fallback = gitActionMenuItems.find((item) => !item.disabled) ?? gitActionMenuItems[0];
541
542 const fallback = gitModalActionOptions.find((option) => !option.disabled);
543 if (fallback) {245 if (fallback) {
544 setGitModalSelectedAction(fallback.action);246 setSelectedGitActionId(fallback.id);
545 }247 }
546 }, [gitModalActionOptions, isGitModalOpen, selectedGitModalActionOption]);248 }, [gitActionMenuItems, selectedGitActionItem]);
547
548 useEffect(() => {
549 if (!isGitMenuOpen) return;
550
551 const handleClickOutside = (event: MouseEvent) => {
552 if (!gitMenuRef.current) return;
553 if (event.target instanceof Node && !gitMenuRef.current.contains(event.target)) {
554 setIsGitMenuOpen(false);
555 }
556 };
557
558 window.addEventListener("mousedown", handleClickOutside);
559 return () => {
560 window.removeEventListener("mousedown", handleClickOutside);
561 };
562 }, [isGitMenuOpen]);
563
564 useEffect(() => {
565 if (!isGitModalOpen) return;
566
567 const handleEscape = (event: globalThis.KeyboardEvent) => {
568 if (event.key !== "Escape") return;
569 if (isGitActionRunning) return;
570 closeGitActionModal();
571 };
572
573 window.addEventListener("keydown", handleEscape);
574 return () => {
575 window.removeEventListener("keydown", handleEscape);
576 };
577 }, [closeGitActionModal, isGitActionRunning, isGitModalOpen]);
578249
579 if (!gitCwd) return null;250 if (!gitCwd) return null;
580251
581 return (252 return (
582 <>253 <Group aria-label="Git actions">
583 <div className="relative" ref={gitMenuRef}>254 <Button
584 <button255 variant="ghost"
585 type="button"256 size="xs"
586 className="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px] text-muted-foreground/70 transition-colors duration-150 hover:bg-accent hover:text-foreground/80 disabled:cursor-not-allowed disabled:opacity-60"257 className="text-muted-foreground/70 hover:text-foreground/80"
587 onClick={() => {258 disabled={!selectedGitActionItem || selectedGitActionItem.disabled || isGitActionRunning}
588 if (!isGitMenuOpen) {259 onClick={runSelectedAction}
589 void refreshGitStatus().catch(() => undefined);260 >
590 }261 {isGitActionRunning ? "Running..." : selectedGitActionItem?.label ?? "Git actions"}
591 setIsGitMenuOpen((v) => !v);262 </Button>
592 }}263 <GroupSeparator />
264 <Menu
265 onOpenChange={(open) => {
266 if (open) void refreshGitStatus();
267 }}
268 >
269 <MenuTrigger
270 render={<Button aria-label="Git action options" size="icon-xs" variant="ghost" />}
593 disabled={!gitStatus || isGitActionRunning}271 disabled={!gitStatus || isGitActionRunning}
594 >272 >
595 {isGitActionRunning ? "Running..." : "Git actions"}273 <ChevronDownIcon aria-hidden="true" className="size-4" />
596 <span aria-hidden="true">▾</span>274 </MenuTrigger>
597 </button>275 <MenuPopup align="end" sideOffset={4}>
598 {isGitMenuOpen && (276 {gitActionMenuItems.map((item) => (
599 <div className="absolute right-0 top-full z-50 mt-1 w-[280px] rounded-3xl border border-border bg-popover p-3 shadow-xl">277 <MenuItem
600 <p className="px-3 pb-2 text-[13px] text-muted-foreground/75">Git actions</p>278 key={`${item.id}-${item.label}`}
601 {gitActionMenuItems.map((item) => {279 disabled={item.disabled}
602 return (280 onClick={() => {
603 <button281 setSelectedGitActionId(item.id);
604 key={`${item.id}-${item.label}`}282 if (item.kind === "open_pr") {
605 type="button"283 openExistingPr();
606 className="mb-1.5 flex w-full items-center gap-3 rounded-2xl px-3 py-2.5 text-left text-[14px] text-foreground transition-colors duration-150 hover:bg-accent disabled:cursor-not-allowed disabled:text-muted-foreground/65"284 } else if (item.action) {
607 disabled={item.disabled}285 runGitActionImmediately(item.action);
608 onClick={() => {286 }
609 if (item.kind === "open_modal") {287 }}
610 openGitActionModal();288 >
611 return;289 <GitActionItemIcon icon={item.icon} />
612 }290 {item.label}
613291 </MenuItem>
614 if (item.kind === "open_pr") {292 ))}
615 openExistingPr();293 {gitStatus?.branch === null && (
616 return;294 <p className="px-2 py-1.5 text-xs text-warning">
617 }295 Detached HEAD: push and PR are unavailable.
618296 </p>
619 if (item.action) {297 )}
620 void runGitActionImmediately(item.action);298 {gitStatus &&
621 }299 gitStatus.branch !== null &&
622 }}300 !gitStatus.hasWorkingTreeChanges &&
623 >301 gitStatus.aheadCount === 0 &&
624 <GitActionIcon icon={item.icon} disabled={item.disabled} />302 gitStatus.behindCount > 0 && (
625 <span>{item.label}</span>303 <p className="px-2 py-1.5 text-xs text-warning">
626 </button>304 Behind upstream. Pull/rebase first.
627 );
628 })}
629 {gitStatus?.branch === null && (
630 <p className="px-2 pt-1 text-[10px] text-amber-500 dark:text-amber-300">
631 Detached HEAD: push and PR actions are unavailable.
632 </p>
633 )}
634 {gitStatus &&
635 gitStatus.branch !== null &&
636 !gitStatus.hasWorkingTreeChanges &&
637 gitStatus.aheadCount === 0 &&
638 gitStatus.behindCount > 0 && (
639 <p className="px-3 pt-1 text-[10px] text-amber-500 dark:text-amber-300">
640 Branch is behind upstream. Pull/rebase before opening a PR.
641 </p>
642 )}
643 {visibleGitActionError && (
644 <p className="px-3 pt-2 text-[11px] text-rose-500 dark:text-rose-300">
645 {visibleGitActionError}
646 </p>305 </p>
647 )}306 )}
648 </div>307 {gitStatusError && (
649 )}308 <p className="px-2 py-1.5 text-xs text-destructive">{gitStatusError.message}</p>
650 </div>309 )}
651310 </MenuPopup>
652 {isGitModalOpen && (311 </Menu>
653 <div312 </Group>
654 className="fixed inset-0 z-80 flex items-center justify-center bg-black/35 px-4 py-6"
655 onMouseDown={() => {
656 closeGitActionModal();
657 }}
658 >
659 <div
660 className="w-full max-w-[640px] rounded-3xl border border-border bg-popover p-6 shadow-2xl"
661 role="dialog"
662 aria-modal="true"
663 aria-label="Git action confirmation"
664 onMouseDown={(event) => {
665 event.stopPropagation();
666 }}
667 >
668 <div className="flex items-start justify-between gap-4">
669 <div className="flex items-center gap-3">
670 <div className="rounded-2xl bg-accent p-3">
671 <GitActionIcon
672 icon={selectedGitModalActionOption?.icon ?? "commit"}
673 disabled={false}
674 />
675 </div>
676 <div>
677 <p className="text-xs uppercase tracking-[0.14em] text-muted-foreground/70">
678 Git actions
679 </p>
680 <h3 className="text-3xl font-semibold tracking-tight text-foreground">
681 {gitActionModalTitle()}
682 </h3>
683 </div>
684 </div>
685 <button
686 type="button"
687 className="rounded-md p-1 text-muted-foreground/60 transition-colors duration-150 hover:bg-accent hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
688 onClick={closeGitActionModal}
689 disabled={isGitActionRunning}
690 aria-label="Close git action dialog"
691 >
692 <XIcon className="h-5 w-5" />
693 </button>
694 </div>
695 <div className="mt-6 space-y-2 rounded-2xl border border-border/80 bg-card/40 px-4 py-3">
696 <div className="flex items-center justify-between gap-4 text-sm">
697 <span className="text-muted-foreground/70">Branch</span>
698 <span className="font-mono text-foreground">
699 {gitStatus?.branch ?? "(detached HEAD)"}
700 </span>
701 </div>
702 <div className="flex items-center justify-between gap-4 text-sm">
703 <span className="text-muted-foreground/70">Changes</span>
704 <span className="text-foreground">
705 {gitStatus?.hasWorkingTreeChanges
706 ? "Working tree has changes"
707 : "No local changes"}
708 </span>
709 </div>
710 </div>
711 <div className="mt-6">
712 <label
713 htmlFor="git-commit-message"
714 className="mb-2 block text-sm font-medium text-foreground"
715 >
716 Commit message
717 </label>
718 <textarea
719 id="git-commit-message"
720 rows={3}
721 className="w-full resize-none rounded-2xl border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground/50 focus:border-ring focus:outline-none disabled:cursor-not-allowed disabled:opacity-70"
722 placeholder="Leave blank to autogenerate a commit message"
723 value={gitModalCommitMessage}
724 onChange={(event) => setGitModalCommitMessage(event.target.value)}
725 disabled={isGitActionRunning}
726 />
727 </div>
728 <div className="mt-6">
729 <p className="text-sm font-medium text-foreground">Next steps</p>
730 <div className="mt-2 overflow-hidden rounded-2xl border border-border">
731 {gitModalActionOptions.map((option, index) => {
732 const borderClass =
733 index < gitModalActionOptions.length - 1 ? "border-b border-border/70" : "";
734 const selected = option.action === gitModalSelectedAction && !option.disabled;
735 const disabled = option.disabled || isGitActionRunning;
736
737 return (
738 <button
739 key={option.action}
740 type="button"
741 className={`flex w-full items-start gap-3 px-4 py-3 text-left transition-colors duration-150 ${borderClass} ${
742 selected
743 ? "bg-accent/55"
744 : disabled
745 ? "bg-card/25"
746 : "bg-card/45 hover:bg-accent/35"
747 } disabled:cursor-not-allowed`}
748 disabled={disabled}
749 onClick={() => {
750 setGitModalSelectedAction(option.action);
751 }}
752 >
753 <span className="mt-0.5">
754 <GitActionIcon icon={option.icon} disabled={option.disabled} />
755 </span>
756 <div className="min-w-0 flex-1">
757 <p
758 className={`text-sm ${disabled ? "text-muted-foreground/60" : "text-foreground"}`}
759 >
760 {option.label}
761 </p>
762 {option.detail && (
763 <p
764 className={`mt-0.5 text-xs ${disabled ? "text-muted-foreground/55" : "text-muted-foreground/70"}`}
765 >
766 {option.detail}
767 </p>
768 )}
769 </div>
770 {selected && !disabled ? (
771 <CheckIcon className="mt-0.5 h-4 w-4 shrink-0 text-foreground" />
772 ) : null}
773 </button>
774 );
775 })}
776 </div>
777 </div>
778 {(gitModalError ?? visibleGitActionError) && (
779 <div className="mt-4 rounded-lg border border-rose-300/50 bg-rose-500/10 px-3 py-2 text-xs text-rose-600 dark:text-rose-200">
780 {gitModalError ?? visibleGitActionError}
781 </div>
782 )}
783 <div className="mt-6 flex justify-end gap-2">
784 <button
785 type="button"
786 className="rounded-xl border border-border px-4 py-2 text-sm text-foreground transition-colors duration-150 hover:bg-accent disabled:cursor-not-allowed disabled:opacity-60"
787 onClick={closeGitActionModal}
788 disabled={isGitActionRunning}
789 >
790 Cancel
791 </button>
792 <button
793 type="button"
794 className="rounded-xl bg-foreground px-4 py-2 text-sm font-medium text-background transition-colors duration-150 hover:bg-foreground/90 disabled:cursor-not-allowed disabled:opacity-60"
795 onClick={() => {
796 void runGitAction();
797 }}
798 disabled={isGitActionRunning || !selectedGitModalActionOption}
799 >
800 {runActionLabel(gitModalSelectedAction)}
801 </button>
802 </div>
803 </div>
804 </div>
805 )}
806
807 {showGitProgressToast && (
808 <div
809 className="fixed bottom-4 right-4 z-[90]"
810 role="status"
811 aria-live="polite"
812 >
813 <div className="flex max-w-[calc(100vw-2rem)] items-center gap-2 rounded-xl border border-border bg-popover/95 px-3 py-2 shadow-xl backdrop-blur">
814 <span className="shrink-0">
815 {gitProgressToastStatus === "running" ? (
816 <Loader2Icon className="h-4 w-4 animate-spin text-foreground" />
817 ) : gitProgressToastStatus === "failed" ? (
818 <XIcon className="h-4 w-4 text-rose-500 dark:text-rose-300" />
819 ) : (
820 <CheckIcon className="h-4 w-4 text-emerald-600 dark:text-emerald-300" />
821 )}
822 </span>
823 <span className="min-w-0 truncate text-sm text-foreground">{gitProgressToastMessage}</span>
824 {!isGitModalActionRunning && gitModalResult && gitProgressToastOpenPrUrl && (
825 <button
826 type="button"
827 className="rounded-md bg-foreground px-2.5 py-1 text-xs font-medium text-background transition-colors duration-150 hover:bg-foreground/90 disabled:cursor-not-allowed disabled:opacity-60"
828 onClick={openPrFromToast}
829 disabled={isGitModalActionRunning}
830 >
831 Open PR
832 </button>
833 )}
834 <button
835 type="button"
836 className="rounded p-1 text-muted-foreground/60 transition-colors duration-150 hover:bg-accent hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
837 onClick={dismissGitProgressToast}
838 disabled={isGitModalActionRunning}
839 aria-label="Dismiss git action progress toast"
840 >
841 <XIcon className="h-4 w-4" />
842 </button>
843 </div>
844 </div>
845 )}
846 </>
847 );313 );
848}314}
apps/web/src/components/Icons.tsx+6
...@@ -13,3 +13,9 @@ export const GitHubIcon: Icon = (props) => (...@@ -13,3 +13,9 @@ export const GitHubIcon: Icon = (props) => (
13 />13 />
14 </svg>14 </svg>
15);15);
16
17export const CursorIcon: Icon = (props) => (
18 <svg {...props} viewBox="0 0 466.73 532.09" fill="currentColor">
19 <path d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z" />
20 </svg>
21);
apps/web/src/components/ui/alert-dialog.tsx created+169
...@@ -0,0 +1,169 @@
1"use client";
2
3import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
4
5import { cn } from "~/lib/utils";
6
7const AlertDialogCreateHandle = AlertDialogPrimitive.createHandle;
8
9const AlertDialog = AlertDialogPrimitive.Root;
10
11const AlertDialogPortal = AlertDialogPrimitive.Portal;
12
13function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) {
14 return (
15 <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
16 );
17}
18
19function AlertDialogBackdrop({
20 className,
21 ...props
22}: AlertDialogPrimitive.Backdrop.Props) {
23 return (
24 <AlertDialogPrimitive.Backdrop
25 className={cn(
26 "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",
27 className,
28 )}
29 data-slot="alert-dialog-backdrop"
30 {...props}
31 />
32 );
33}
34
35function AlertDialogViewport({
36 className,
37 ...props
38}: AlertDialogPrimitive.Viewport.Props) {
39 return (
40 <AlertDialogPrimitive.Viewport
41 className={cn(
42 "fixed inset-0 z-50 grid grid-rows-[1fr_auto_3fr] justify-items-center p-4",
43 className,
44 )}
45 data-slot="alert-dialog-viewport"
46 {...props}
47 />
48 );
49}
50
51function AlertDialogPopup({
52 className,
53 bottomStickOnMobile = true,
54 ...props
55}: AlertDialogPrimitive.Popup.Props & {
56 bottomStickOnMobile?: boolean;
57}) {
58 return (
59 <AlertDialogPortal>
60 <AlertDialogBackdrop />
61 <AlertDialogViewport
62 className={cn(
63 bottomStickOnMobile &&
64 "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12",
65 )}
66 >
67 <AlertDialogPrimitive.Popup
68 className={cn(
69 "-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%)]",
70 bottomStickOnMobile &&
71 "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",
72 className,
73 )}
74 data-slot="alert-dialog-popup"
75 {...props}
76 />
77 </AlertDialogViewport>
78 </AlertDialogPortal>
79 );
80}
81
82function AlertDialogHeader({
83 className,
84 ...props
85}: React.ComponentProps<"div">) {
86 return (
87 <div
88 className={cn(
89 "flex flex-col gap-2 p-6 text-center max-sm:pb-4 sm:text-left",
90 className,
91 )}
92 data-slot="alert-dialog-header"
93 {...props}
94 />
95 );
96}
97
98function AlertDialogFooter({
99 className,
100 variant = "default",
101 ...props
102}: React.ComponentProps<"div"> & {
103 variant?: "default" | "bare";
104}) {
105 return (
106 <div
107 className={cn(
108 "flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end sm:rounded-b-[calc(var(--radius-2xl)-1px)]",
109 variant === "default" && "border-t bg-muted/72 py-4",
110 variant === "bare" && "pb-6",
111 className,
112 )}
113 data-slot="alert-dialog-footer"
114 {...props}
115 />
116 );
117}
118
119function AlertDialogTitle({
120 className,
121 ...props
122}: AlertDialogPrimitive.Title.Props) {
123 return (
124 <AlertDialogPrimitive.Title
125 className={cn(
126 "font-heading font-semibold text-xl leading-none",
127 className,
128 )}
129 data-slot="alert-dialog-title"
130 {...props}
131 />
132 );
133}
134
135function AlertDialogDescription({
136 className,
137 ...props
138}: AlertDialogPrimitive.Description.Props) {
139 return (
140 <AlertDialogPrimitive.Description
141 className={cn("text-muted-foreground text-sm", className)}
142 data-slot="alert-dialog-description"
143 {...props}
144 />
145 );
146}
147
148function AlertDialogClose(props: AlertDialogPrimitive.Close.Props) {
149 return (
150 <AlertDialogPrimitive.Close data-slot="alert-dialog-close" {...props} />
151 );
152}
153
154export {
155 AlertDialogCreateHandle,
156 AlertDialog,
157 AlertDialogPortal,
158 AlertDialogBackdrop,
159 AlertDialogBackdrop as AlertDialogOverlay,
160 AlertDialogTrigger,
161 AlertDialogPopup,
162 AlertDialogPopup as AlertDialogContent,
163 AlertDialogHeader,
164 AlertDialogFooter,
165 AlertDialogTitle,
166 AlertDialogDescription,
167 AlertDialogClose,
168 AlertDialogViewport,
169};
apps/web/src/components/ui/alert.tsx created+80
...@@ -0,0 +1,80 @@
1import { cva, type VariantProps } 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/web/src/components/ui/autocomplete.tsx created+318
...@@ -0,0 +1,318 @@
1"use client";
2
3import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete";
4import { ChevronsUpDownIcon, XIcon } from "lucide-react";
5
6import { cn } from "~/lib/utils";
7import { Input } from "~/components/ui/input";
8import { ScrollArea } from "~/components/ui/scroll-area";
9
10const Autocomplete = AutocompletePrimitive.Root;
11
12function AutocompleteInput({
13 className,
14 showTrigger = false,
15 showClear = false,
16 startAddon,
17 size,
18 ...props
19}: Omit<AutocompletePrimitive.Input.Props, "size"> & {
20 showTrigger?: boolean;
21 showClear?: boolean;
22 startAddon?: React.ReactNode;
23 size?: "sm" | "default" | "lg" | number;
24 ref?: React.Ref<HTMLInputElement>;
25}) {
26 const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;
27
28 return (
29 <div className="relative not-has-[>*.w-full]:w-fit w-full text-foreground has-disabled:opacity-64">
30 {startAddon && (
31 <div
32 aria-hidden="true"
33 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"
34 data-slot="autocomplete-start-addon"
35 >
36 {startAddon}
37 </div>
38 )}
39 <AutocompletePrimitive.Input
40 className={cn(
41 startAddon &&
42 "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)]",
43 sizeValue === "sm"
44 ? "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-6.5"
45 : "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-7",
46 className,
47 )}
48 data-slot="autocomplete-input"
49 render={<Input nativeInput size={sizeValue} />}
50 {...props}
51 />
52 {showTrigger && (
53 <AutocompleteTrigger
54 className={cn(
55 "-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",
56 sizeValue === "sm" ? "end-0" : "end-0.5",
57 )}
58 >
59 <AutocompletePrimitive.Icon data-slot="autocomplete-icon">
60 <ChevronsUpDownIcon />
61 </AutocompletePrimitive.Icon>
62 </AutocompleteTrigger>
63 )}
64 {showClear && (
65 <AutocompleteClear
66 className={cn(
67 "-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",
68 sizeValue === "sm" ? "end-0" : "end-0.5",
69 )}
70 >
71 <XIcon />
72 </AutocompleteClear>
73 )}
74 </div>
75 );
76}
77
78function AutocompletePopup({
79 className,
80 children,
81 side = "bottom",
82 sideOffset = 4,
83 alignOffset,
84 align = "start",
85 anchor,
86 ...props
87}: AutocompletePrimitive.Popup.Props & {
88 align?: AutocompletePrimitive.Positioner.Props["align"];
89 sideOffset?: AutocompletePrimitive.Positioner.Props["sideOffset"];
90 alignOffset?: AutocompletePrimitive.Positioner.Props["alignOffset"];
91 side?: AutocompletePrimitive.Positioner.Props["side"];
92 anchor?: AutocompletePrimitive.Positioner.Props["anchor"];
93}) {
94 return (
95 <AutocompletePrimitive.Portal>
96 <AutocompletePrimitive.Positioner
97 align={align}
98 alignOffset={alignOffset}
99 anchor={anchor}
100 className="z-50 select-none"
101 data-slot="autocomplete-positioner"
102 side={side}
103 sideOffset={sideOffset}
104 >
105 <span
106 className={cn(
107 "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%)]",
108 className,
109 )}
110 >
111 <AutocompletePrimitive.Popup
112 className="flex max-h-[min(var(--available-height),23rem)] flex-1 flex-col text-foreground"
113 data-slot="autocomplete-popup"
114 {...props}
115 >
116 {children}
117 </AutocompletePrimitive.Popup>
118 </span>
119 </AutocompletePrimitive.Positioner>
120 </AutocompletePrimitive.Portal>
121 );
122}
123
124function AutocompleteItem({
125 className,
126 children,
127 ...props
128}: AutocompletePrimitive.Item.Props) {
129 return (
130 <AutocompletePrimitive.Item
131 className={cn(
132 "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",
133 className,
134 )}
135 data-slot="autocomplete-item"
136 {...props}
137 >
138 {children}
139 </AutocompletePrimitive.Item>
140 );
141}
142
143function AutocompleteSeparator({
144 className,
145 ...props
146}: AutocompletePrimitive.Separator.Props) {
147 return (
148 <AutocompletePrimitive.Separator
149 className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}
150 data-slot="autocomplete-separator"
151 {...props}
152 />
153 );
154}
155
156function AutocompleteGroup({
157 className,
158 ...props
159}: AutocompletePrimitive.Group.Props) {
160 return (
161 <AutocompletePrimitive.Group
162 className={cn("[[role=group]+&]:mt-1.5", className)}
163 data-slot="autocomplete-group"
164 {...props}
165 />
166 );
167}
168
169function AutocompleteGroupLabel({
170 className,
171 ...props
172}: AutocompletePrimitive.GroupLabel.Props) {
173 return (
174 <AutocompletePrimitive.GroupLabel
175 className={cn(
176 "px-2 py-1.5 font-medium text-muted-foreground text-xs",
177 className,
178 )}
179 data-slot="autocomplete-group-label"
180 {...props}
181 />
182 );
183}
184
185function AutocompleteEmpty({
186 className,
187 ...props
188}: AutocompletePrimitive.Empty.Props) {
189 return (
190 <AutocompletePrimitive.Empty
191 className={cn(
192 "not-empty:p-2 text-center text-base text-muted-foreground sm:text-sm",
193 className,
194 )}
195 data-slot="autocomplete-empty"
196 {...props}
197 />
198 );
199}
200
201function AutocompleteRow({
202 className,
203 ...props
204}: AutocompletePrimitive.Row.Props) {
205 return (
206 <AutocompletePrimitive.Row
207 className={className}
208 data-slot="autocomplete-row"
209 {...props}
210 />
211 );
212}
213
214function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) {
215 return (
216 <AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />
217 );
218}
219
220function AutocompleteList({
221 className,
222 ...props
223}: AutocompletePrimitive.List.Props) {
224 return (
225 <ScrollArea scrollbarGutter scrollFade>
226 <AutocompletePrimitive.List
227 className={cn(
228 "not-empty:scroll-py-1 not-empty:p-1 in-data-has-overflow-y:pe-3",
229 className,
230 )}
231 data-slot="autocomplete-list"
232 {...props}
233 />
234 </ScrollArea>
235 );
236}
237
238function AutocompleteClear({
239 className,
240 ...props
241}: AutocompletePrimitive.Clear.Props) {
242 return (
243 <AutocompletePrimitive.Clear
244 className={cn(
245 "-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",
246 className,
247 )}
248 data-slot="autocomplete-clear"
249 {...props}
250 >
251 <XIcon />
252 </AutocompletePrimitive.Clear>
253 );
254}
255
256function AutocompleteStatus({
257 className,
258 ...props
259}: AutocompletePrimitive.Status.Props) {
260 return (
261 <AutocompletePrimitive.Status
262 className={cn(
263 "px-3 py-2 font-medium text-muted-foreground text-xs empty:m-0 empty:p-0",
264 className,
265 )}
266 data-slot="autocomplete-status"
267 {...props}
268 />
269 );
270}
271
272function AutocompleteCollection({
273 ...props
274}: AutocompletePrimitive.Collection.Props) {
275 return (
276 <AutocompletePrimitive.Collection
277 data-slot="autocomplete-collection"
278 {...props}
279 />
280 );
281}
282
283function AutocompleteTrigger({
284 className,
285 children,
286 ...props
287}: AutocompletePrimitive.Trigger.Props) {
288 return (
289 <AutocompletePrimitive.Trigger
290 className={className}
291 data-slot="autocomplete-trigger"
292 {...props}
293 >
294 {children}
295 </AutocompletePrimitive.Trigger>
296 );
297}
298
299const useAutocompleteFilter = AutocompletePrimitive.useFilter;
300
301export {
302 Autocomplete,
303 AutocompleteInput,
304 AutocompleteTrigger,
305 AutocompletePopup,
306 AutocompleteItem,
307 AutocompleteSeparator,
308 AutocompleteGroup,
309 AutocompleteGroupLabel,
310 AutocompleteEmpty,
311 AutocompleteValue,
312 AutocompleteList,
313 AutocompleteClear,
314 AutocompleteStatus,
315 AutocompleteRow,
316 AutocompleteCollection,
317 useAutocompleteFilter,
318};
apps/web/src/components/ui/badge.tsx created+60
...@@ -0,0 +1,60 @@
1"use client";
2
3import { mergeProps } from "@base-ui/react/merge-props";
4import { useRender } from "@base-ui/react/use-render";
5import { cva, type VariantProps } from "class-variance-authority";
6
7import { cn } from "~/lib/utils";
8
9const badgeVariants = cva(
10 "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",
11 {
12 defaultVariants: {
13 size: "default",
14 variant: "default",
15 },
16 variants: {
17 size: {
18 default:
19 "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",
20 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",
21 sm: "h-5 min-w-5 rounded-[.25rem] px-[calc(--spacing(1)-1px)] text-xs sm:h-4 sm:min-w-4 sm:text-[.625rem]",
22 },
23 variant: {
24 default:
25 "bg-primary text-primary-foreground [button&,a&]:hover:bg-primary/90",
26 destructive:
27 "bg-destructive text-white [button&,a&]:hover:bg-destructive/90",
28 error:
29 "bg-destructive/8 text-destructive-foreground dark:bg-destructive/16",
30 info: "bg-info/8 text-info-foreground dark:bg-info/16",
31 outline:
32 "border-input bg-background text-foreground dark:bg-input/32 [button&,a&]:hover:bg-accent/50 dark:[button&,a&]:hover:bg-input/48",
33 secondary:
34 "bg-secondary text-secondary-foreground [button&,a&]:hover:bg-secondary/90",
35 success: "bg-success/8 text-success-foreground dark:bg-success/16",
36 warning: "bg-warning/8 text-warning-foreground dark:bg-warning/16",
37 },
38 },
39 },
40);
41
42interface BadgeProps extends useRender.ComponentProps<"span"> {
43 variant?: VariantProps<typeof badgeVariants>["variant"];
44 size?: VariantProps<typeof badgeVariants>["size"];
45}
46
47function Badge({ className, variant, size, render, ...props }: BadgeProps) {
48 const defaultProps = {
49 className: cn(badgeVariants({ className, size, variant })),
50 "data-slot": "badge",
51 };
52
53 return useRender({
54 defaultTagName: "span",
55 props: mergeProps<"span">(defaultProps, props),
56 render,
57 });
58}
59
60export { Badge, badgeVariants };
apps/web/src/components/ui/button.tsx created+74
...@@ -0,0 +1,74 @@
1"use client";
2
3import { mergeProps } from "@base-ui/react/merge-props";
4import { useRender } from "@base-ui/react/use-render";
5import { cva, type VariantProps } from "class-variance-authority";
6import type * as React from "react";
7
8import { cn } from "~/lib/utils";
9
10const buttonVariants = cva(
11 "[&_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",
12 {
13 defaultVariants: {
14 size: "default",
15 variant: "default",
16 },
17 variants: {
18 size: {
19 default: "h-9 px-[calc(--spacing(3)-1px)] sm:h-8",
20 icon: "size-9 sm:size-8",
21 "icon-lg": "size-10 sm:size-9",
22 "icon-sm": "size-8 sm:size-7",
23 "icon-xl":
24 "size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",
25 "icon-xs":
26 "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",
27 lg: "h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9",
28 sm: "h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:h-7",
29 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",
30 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",
31 },
32 variant: {
33 default:
34 "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",
35 destructive:
36 "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",
37 "destructive-outline":
38 "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",
39 ghost:
40 "border-transparent text-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent",
41 link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline",
42 outline:
43 "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",
44 secondary:
45 "border-transparent bg-secondary text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80 [:hover,[data-pressed]]:bg-secondary/90",
46 },
47 },
48 },
49);
50
51interface ButtonProps extends useRender.ComponentProps<"button"> {
52 variant?: VariantProps<typeof buttonVariants>["variant"];
53 size?: VariantProps<typeof buttonVariants>["size"];
54}
55
56function Button({ className, variant, size, render, ...props }: ButtonProps) {
57 const typeValue: React.ButtonHTMLAttributes<HTMLButtonElement>["type"] = render
58 ? undefined
59 : "button";
60
61 const defaultProps = {
62 className: cn(buttonVariants({ className, size, variant })),
63 "data-slot": "button",
64 type: typeValue,
65 };
66
67 return useRender({
68 defaultTagName: "button",
69 props: mergeProps<"button">(defaultProps, props),
70 render,
71 });
72}
73
74export { Button, buttonVariants };
apps/web/src/components/ui/card.tsx created+244
...@@ -0,0 +1,244 @@
1"use client";
2
3import { mergeProps } from "@base-ui/react/merge-props";
4import { useRender } from "@base-ui/react/use-render";
5
6import { cn } from "~/lib/utils";
7
8function Card({
9 className,
10 render,
11 ...props
12}: useRender.ComponentProps<"div">) {
13 const defaultProps = {
14 className: cn(
15 "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%)]",
16 className,
17 ),
18 "data-slot": "card",
19 };
20
21 return useRender({
22 defaultTagName: "div",
23 props: mergeProps<"div">(defaultProps, props),
24 render,
25 });
26}
27
28function CardFrame({
29 className,
30 render,
31 ...props
32}: useRender.ComponentProps<"div">) {
33 const defaultProps = {
34 className: cn(
35 "[--clip-top:-1rem] [--clip-bottom:-1rem] *:data-[slot=card]:first:[--clip-top:1px] *:data-[slot=card]:last:[--clip-bottom:1px] 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(var(--clip-top)_1px_var(--clip-bottom)_1px_round_calc(var(--radius-2xl)-1px))] *:data-[slot=card]:shadow-none *:data-[slot=card]:before:hidden *:data-[slot=card]:bg-clip-padding",
36 className,
37 ),
38 "data-slot": "card-frame",
39 };
40
41 return useRender({
42 defaultTagName: "div",
43 props: mergeProps<"div">(defaultProps, props),
44 render,
45 });
46}
47
48function CardFrameHeader({
49 className,
50 render,
51 ...props
52}: useRender.ComponentProps<"div">) {
53 const defaultProps = {
54 className: cn("relative flex flex-col px-6 py-4", className),
55 "data-slot": "card-frame-header",
56 };
57
58 return useRender({
59 defaultTagName: "div",
60 props: mergeProps<"div">(defaultProps, props),
61 render,
62 });
63}
64
65function CardFrameTitle({
66 className,
67 render,
68 ...props
69}: useRender.ComponentProps<"div">) {
70 const defaultProps = {
71 className: cn("font-semibold text-sm", className),
72 "data-slot": "card-frame-title",
73 };
74
75 return useRender({
76 defaultTagName: "div",
77 props: mergeProps<"div">(defaultProps, props),
78 render,
79 });
80}
81
82function CardFrameDescription({
83 className,
84 render,
85 ...props
86}: useRender.ComponentProps<"div">) {
87 const defaultProps = {
88 className: cn("text-muted-foreground text-sm", className),
89 "data-slot": "card-frame-description",
90 };
91
92 return useRender({
93 defaultTagName: "div",
94 props: mergeProps<"div">(defaultProps, props),
95 render,
96 });
97}
98
99function CardFrameFooter({
100 className,
101 render,
102 ...props
103}: useRender.ComponentProps<"div">) {
104 const defaultProps = {
105 className: cn("px-6 py-4", className),
106 "data-slot": "card-frame-footer",
107 };
108
109 return useRender({
110 defaultTagName: "div",
111 props: mergeProps<"div">(defaultProps, props),
112 render,
113 });
114}
115
116function CardHeader({
117 className,
118 render,
119 ...props
120}: useRender.ComponentProps<"div">) {
121 const defaultProps = {
122 className: cn(
123 "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]",
124 className,
125 ),
126 "data-slot": "card-header",
127 };
128
129 return useRender({
130 defaultTagName: "div",
131 props: mergeProps<"div">(defaultProps, props),
132 render,
133 });
134}
135
136function CardTitle({
137 className,
138 render,
139 ...props
140}: useRender.ComponentProps<"div">) {
141 const defaultProps = {
142 className: cn("font-semibold text-lg leading-none", className),
143 "data-slot": "card-title",
144 };
145
146 return useRender({
147 defaultTagName: "div",
148 props: mergeProps<"div">(defaultProps, props),
149 render,
150 });
151}
152
153function CardDescription({
154 className,
155 render,
156 ...props
157}: useRender.ComponentProps<"div">) {
158 const defaultProps = {
159 className: cn("text-muted-foreground text-sm", className),
160 "data-slot": "card-description",
161 };
162
163 return useRender({
164 defaultTagName: "div",
165 props: mergeProps<"div">(defaultProps, props),
166 render,
167 });
168}
169
170function CardAction({
171 className,
172 render,
173 ...props
174}: useRender.ComponentProps<"div">) {
175 const defaultProps = {
176 className: cn(
177 "col-start-2 row-span-2 row-start-1 self-start justify-self-end inline-flex",
178 className,
179 ),
180 "data-slot": "card-action",
181 };
182
183 return useRender({
184 defaultTagName: "div",
185 props: mergeProps<"div">(defaultProps, props),
186 render,
187 });
188}
189
190function CardPanel({
191 className,
192 render,
193 ...props
194}: useRender.ComponentProps<"div">) {
195 const defaultProps = {
196 className: cn(
197 "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",
198 className,
199 ),
200 "data-slot": "card-panel",
201 };
202
203 return useRender({
204 defaultTagName: "div",
205 props: mergeProps<"div">(defaultProps, props),
206 render,
207 });
208}
209
210function CardFooter({
211 className,
212 render,
213 ...props
214}: useRender.ComponentProps<"div">) {
215 const defaultProps = {
216 className: cn(
217 "flex items-center p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pt-4",
218 className,
219 ),
220 "data-slot": "card-footer",
221 };
222
223 return useRender({
224 defaultTagName: "div",
225 props: mergeProps<"div">(defaultProps, props),
226 render,
227 });
228}
229
230export {
231 Card,
232 CardFrame,
233 CardFrameHeader,
234 CardFrameTitle,
235 CardFrameDescription,
236 CardFrameFooter,
237 CardAction,
238 CardDescription,
239 CardFooter,
240 CardHeader,
241 CardPanel,
242 CardPanel as CardContent,
243 CardTitle,
244};
apps/web/src/components/ui/checkbox.tsx created+60
...@@ -0,0 +1,60 @@
1"use client";
2
3import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
4
5import { cn } from "~/lib/utils";
6
7function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
8 return (
9 <CheckboxPrimitive.Root
10 className={cn(
11 "relative inline-flex size-4.5 shrink-0 items-center justify-center rounded-[.25rem] 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",
12 className,
13 )}
14 data-slot="checkbox"
15 {...props}
16 >
17 <CheckboxPrimitive.Indicator
18 className="-inset-px absolute flex items-center justify-center rounded-[.25rem] text-primary-foreground data-unchecked:hidden data-checked:bg-primary data-indeterminate:text-foreground"
19 data-slot="checkbox-indicator"
20 render={(props, state) => (
21 <span {...props}>
22 {state.indeterminate ? (
23 <svg
24 className="size-3.5 sm:size-3"
25 fill="none"
26 height="24"
27 stroke="currentColor"
28 strokeLinecap="round"
29 strokeLinejoin="round"
30 strokeWidth="3"
31 viewBox="0 0 24 24"
32 width="24"
33 xmlns="http://www.w3.org/2000/svg"
34 >
35 <path d="M5.252 12h13.496" />
36 </svg>
37 ) : (
38 <svg
39 className="size-3.5 sm:size-3"
40 fill="none"
41 height="24"
42 stroke="currentColor"
43 strokeLinecap="round"
44 strokeLinejoin="round"
45 strokeWidth="3"
46 viewBox="0 0 24 24"
47 width="24"
48 xmlns="http://www.w3.org/2000/svg"
49 >
50 <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
51 </svg>
52 )}
53 </span>
54 )}
55 />
56 </CheckboxPrimitive.Root>
57 );
58}
59
60export { Checkbox };
apps/web/src/components/ui/combobox.tsx created+372
...@@ -0,0 +1,372 @@
1"use client";
2
3import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox";
4import { CheckIcon, ChevronsUpDownIcon, XIcon } from "lucide-react";
5import * as React from "react";
6
7import { cn } from "~/lib/utils";
8import { Input } from "~/components/ui/input";
9import { ScrollArea } from "~/components/ui/scroll-area";
10
11const ComboboxContext = React.createContext<{
12 chipsRef: React.RefObject<Element | null> | null;
13 multiple: boolean;
14}>({
15 chipsRef: null,
16 multiple: false,
17});
18
19function Combobox<Value, Multiple extends boolean | undefined = false>(
20 props: ComboboxPrimitive.Root.Props<Value, Multiple>,
21) {
22 const chipsRef = React.useRef<Element | null>(null);
23 return (
24 <ComboboxContext.Provider value={{ chipsRef, multiple: !!props.multiple }}>
25 <ComboboxPrimitive.Root {...props} />
26 </ComboboxContext.Provider>
27 );
28}
29
30function ComboboxChipsInput({
31 className,
32 size,
33 ...props
34}: Omit<ComboboxPrimitive.Input.Props, "size"> & {
35 size?: "sm" | "default" | "lg" | number;
36 ref?: React.Ref<HTMLInputElement>;
37}) {
38 const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;
39
40 return (
41 <ComboboxPrimitive.Input
42 className={cn(
43 "min-w-12 flex-1 text-base outline-none sm:text-sm [[data-slot=combobox-chip]+&]:ps-0.5",
44 sizeValue === "sm" ? "ps-1.5" : "ps-2",
45 className,
46 )}
47 data-size={typeof sizeValue === "string" ? sizeValue : undefined}
48 data-slot="combobox-chips-input"
49 size={typeof sizeValue === "number" ? sizeValue : undefined}
50 {...props}
51 />
52 );
53}
54
55function ComboboxInput({
56 className,
57 showTrigger = true,
58 showClear = false,
59 startAddon,
60 size,
61 ...props
62}: Omit<ComboboxPrimitive.Input.Props, "size"> & {
63 showTrigger?: boolean;
64 showClear?: boolean;
65 startAddon?: React.ReactNode;
66 size?: "sm" | "default" | "lg" | number;
67 ref?: React.Ref<HTMLInputElement>;
68}) {
69 const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number;
70
71 return (
72 <div className="relative not-has-[>*.w-full]:w-fit w-full text-foreground has-disabled:opacity-64">
73 {startAddon && (
74 <div
75 aria-hidden="true"
76 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"
77 data-slot="combobox-start-addon"
78 >
79 {startAddon}
80 </div>
81 )}
82 <ComboboxPrimitive.Input
83 className={cn(
84 startAddon &&
85 "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)]",
86 sizeValue === "sm"
87 ? "has-[+[data-slot=combobox-trigger],+[data-slot=combobox-clear]]:*:data-[slot=combobox-input]:pe-6.5"
88 : "has-[+[data-slot=combobox-trigger],+[data-slot=combobox-clear]]:*:data-[slot=combobox-input]:pe-7",
89 className,
90 )}
91 data-slot="combobox-input"
92 render={<Input className="has-disabled:opacity-100" nativeInput size={sizeValue} />}
93 {...props}
94 />
95 {showTrigger && (
96 <ComboboxTrigger
97 className={cn(
98 "-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",
99 sizeValue === "sm" ? "end-0" : "end-0.5",
100 )}
101 >
102 <ComboboxPrimitive.Icon data-slot="combobox-icon">
103 <ChevronsUpDownIcon />
104 </ComboboxPrimitive.Icon>
105 </ComboboxTrigger>
106 )}
107 {showClear && (
108 <ComboboxClear
109 className={cn(
110 "-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",
111 sizeValue === "sm" ? "end-0" : "end-0.5",
112 )}
113 >
114 <XIcon />
115 </ComboboxClear>
116 )}
117 </div>
118 );
119}
120
121function ComboboxTrigger({ className, children, ...props }: ComboboxPrimitive.Trigger.Props) {
122 return (
123 <ComboboxPrimitive.Trigger className={className} data-slot="combobox-trigger" {...props}>
124 {children}
125 </ComboboxPrimitive.Trigger>
126 );
127}
128
129function ComboboxPopup({
130 className,
131 children,
132 side = "bottom",
133 sideOffset = 4,
134 alignOffset,
135 align = "start",
136 anchor: anchorProp,
137 ...props
138}: ComboboxPrimitive.Popup.Props & {
139 align?: ComboboxPrimitive.Positioner.Props["align"];
140 sideOffset?: ComboboxPrimitive.Positioner.Props["sideOffset"];
141 alignOffset?: ComboboxPrimitive.Positioner.Props["alignOffset"];
142 side?: ComboboxPrimitive.Positioner.Props["side"];
143 anchor?: ComboboxPrimitive.Positioner.Props["anchor"];
144}) {
145 const { chipsRef } = React.useContext(ComboboxContext);
146 const anchor = anchorProp ?? chipsRef;
147
148 return (
149 <ComboboxPrimitive.Portal>
150 <ComboboxPrimitive.Positioner
151 align={align}
152 alignOffset={alignOffset}
153 anchor={anchor}
154 className="z-50 select-none"
155 data-slot="combobox-positioner"
156 side={side}
157 sideOffset={sideOffset}
158 >
159 <span
160 className={cn(
161 "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%)]",
162 className,
163 )}
164 >
165 <ComboboxPrimitive.Popup
166 className="flex max-h-[min(var(--available-height),23rem)] flex-1 flex-col text-foreground"
167 data-slot="combobox-popup"
168 {...props}
169 >
170 {children}
171 </ComboboxPrimitive.Popup>
172 </span>
173 </ComboboxPrimitive.Positioner>
174 </ComboboxPrimitive.Portal>
175 );
176}
177
178function ComboboxItem({
179 className,
180 children,
181 hideIndicator = false,
182 ...props
183}: ComboboxPrimitive.Item.Props & {
184 hideIndicator?: boolean;
185}) {
186 return (
187 <ComboboxPrimitive.Item
188 className={cn(
189 "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",
190 className,
191 )}
192 data-slot="combobox-item"
193 {...props}
194 >
195 <ComboboxPrimitive.ItemIndicator className={cn("col-start-1", hideIndicator && "hidden")}>
196 <CheckIcon />
197 </ComboboxPrimitive.ItemIndicator>
198 <div className={hideIndicator ? "col-start-1 col-span-full" : "col-start-2"}>{children}</div>
199 </ComboboxPrimitive.Item>
200 );
201}
202
203function ComboboxSeparator({ className, ...props }: ComboboxPrimitive.Separator.Props) {
204 return (
205 <ComboboxPrimitive.Separator
206 className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}
207 data-slot="combobox-separator"
208 {...props}
209 />
210 );
211}
212
213function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
214 return (
215 <ComboboxPrimitive.Group
216 className={cn("[[role=group]+&]:mt-1.5", className)}
217 data-slot="combobox-group"
218 {...props}
219 />
220 );
221}
222
223function ComboboxGroupLabel({ className, ...props }: ComboboxPrimitive.GroupLabel.Props) {
224 return (
225 <ComboboxPrimitive.GroupLabel
226 className={cn("px-2 py-1.5 font-medium text-muted-foreground text-xs", className)}
227 data-slot="combobox-group-label"
228 {...props}
229 />
230 );
231}
232
233function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
234 return (
235 <ComboboxPrimitive.Empty
236 className={cn(
237 "not-empty:p-2 text-center text-base text-muted-foreground sm:text-sm",
238 className,
239 )}
240 data-slot="combobox-empty"
241 {...props}
242 />
243 );
244}
245
246function ComboboxRow({ className, ...props }: ComboboxPrimitive.Row.Props) {
247 return <ComboboxPrimitive.Row className={className} data-slot="combobox-row" {...props} />;
248}
249
250function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
251 return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
252}
253
254function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
255 return (
256 <ScrollArea scrollbarGutter scrollFade>
257 <ComboboxPrimitive.List
258 className={cn(
259 "not-empty:scroll-py-1 not-empty:px-1 not-empty:py-1 in-data-has-overflow-y:pe-3",
260 className,
261 )}
262 data-slot="combobox-list"
263 {...props}
264 />
265 </ScrollArea>
266 );
267}
268
269function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
270 return <ComboboxPrimitive.Clear className={className} data-slot="combobox-clear" {...props} />;
271}
272
273function ComboboxStatus({ className, ...props }: ComboboxPrimitive.Status.Props) {
274 return (
275 <ComboboxPrimitive.Status
276 className={cn(
277 "px-3 py-2 font-medium text-muted-foreground text-xs empty:m-0 empty:p-0",
278 className,
279 )}
280 data-slot="combobox-status"
281 {...props}
282 />
283 );
284}
285
286function ComboboxCollection(props: ComboboxPrimitive.Collection.Props) {
287 return <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />;
288}
289
290function ComboboxChips({
291 className,
292 children,
293 startAddon,
294 ...props
295}: ComboboxPrimitive.Chips.Props & {
296 startAddon?: React.ReactNode;
297}) {
298 const { chipsRef } = React.useContext(ComboboxContext);
299
300 return (
301 <ComboboxPrimitive.Chips
302 className={cn(
303 "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%)]",
304 className,
305 )}
306 data-slot="combobox-chips"
307 ref={chipsRef as React.Ref<HTMLDivElement> | null}
308 {...props}
309 >
310 {startAddon && (
311 <div
312 aria-hidden="true"
313 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"
314 data-slot="combobox-start-addon"
315 >
316 {startAddon}
317 </div>
318 )}
319 {children}
320 </ComboboxPrimitive.Chips>
321 );
322}
323
324function ComboboxChip({ children, ...props }: ComboboxPrimitive.Chip.Props) {
325 return (
326 <ComboboxPrimitive.Chip
327 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"
328 data-slot="combobox-chip"
329 {...props}
330 >
331 {children}
332 <ComboboxChipRemove />
333 </ComboboxPrimitive.Chip>
334 );
335}
336
337function ComboboxChipRemove(props: ComboboxPrimitive.ChipRemove.Props) {
338 return (
339 <ComboboxPrimitive.ChipRemove
340 aria-label="Remove"
341 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"
342 data-slot="combobox-chip-remove"
343 {...props}
344 >
345 <XIcon />
346 </ComboboxPrimitive.ChipRemove>
347 );
348}
349
350const useComboboxFilter = ComboboxPrimitive.useFilter;
351
352export {
353 Combobox,
354 ComboboxChipsInput,
355 ComboboxInput,
356 ComboboxTrigger,
357 ComboboxPopup,
358 ComboboxItem,
359 ComboboxSeparator,
360 ComboboxGroup,
361 ComboboxGroupLabel,
362 ComboboxEmpty,
363 ComboboxValue,
364 ComboboxList,
365 ComboboxClear,
366 ComboboxStatus,
367 ComboboxRow,
368 ComboboxCollection,
369 ComboboxChips,
370 ComboboxChip,
371 useComboboxFilter,
372};
apps/web/src/components/ui/command.tsx created+264
...@@ -0,0 +1,264 @@
1"use client";
2
3import { Dialog as CommandDialogPrimitive } from "@base-ui/react/dialog";
4import { SearchIcon } from "lucide-react";
5import type * as React from "react";
6import { cn } from "~/lib/utils";
7import {
8 Autocomplete,
9 AutocompleteCollection,
10 AutocompleteEmpty,
11 AutocompleteGroup,
12 AutocompleteGroupLabel,
13 AutocompleteInput,
14 AutocompleteItem,
15 AutocompleteList,
16 AutocompleteSeparator,
17} from "~/components/ui/autocomplete";
18
19const CommandDialog = CommandDialogPrimitive.Root;
20
21const CommandDialogPortal = CommandDialogPrimitive.Portal;
22
23const CommandCreateHandle = CommandDialogPrimitive.createHandle;
24
25function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) {
26 return (
27 <CommandDialogPrimitive.Trigger
28 data-slot="command-dialog-trigger"
29 {...props}
30 />
31 );
32}
33
34function CommandDialogBackdrop({
35 className,
36 ...props
37}: CommandDialogPrimitive.Backdrop.Props) {
38 return (
39 <CommandDialogPrimitive.Backdrop
40 className={cn(
41 "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",
42 className,
43 )}
44 data-slot="command-dialog-backdrop"
45 {...props}
46 />
47 );
48}
49
50function CommandDialogViewport({
51 className,
52 ...props
53}: CommandDialogPrimitive.Viewport.Props) {
54 return (
55 <CommandDialogPrimitive.Viewport
56 className={cn(
57 "fixed inset-0 z-50 flex flex-col items-center px-4 py-[max(--spacing(4),4vh)] sm:py-[10vh]",
58 className,
59 )}
60 data-slot="command-dialog-viewport"
61 {...props}
62 />
63 );
64}
65
66function CommandDialogPopup({
67 className,
68 children,
69 ...props
70}: CommandDialogPrimitive.Popup.Props) {
71 return (
72 <CommandDialogPortal>
73 <CommandDialogBackdrop />
74 <CommandDialogViewport>
75 <CommandDialogPrimitive.Popup
76 className={cn(
77 "-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%)]",
78 className,
79 )}
80 data-slot="command-dialog-popup"
81 {...props}
82 >
83 {children}
84 </CommandDialogPrimitive.Popup>
85 </CommandDialogViewport>
86 </CommandDialogPortal>
87 );
88}
89
90function Command({
91 autoHighlight = "always",
92 keepHighlight = true,
93 ...props
94}: React.ComponentProps<typeof Autocomplete>) {
95 return (
96 <Autocomplete
97 autoHighlight={autoHighlight}
98 inline
99 keepHighlight={keepHighlight}
100 open
101 {...props}
102 />
103 );
104}
105
106function CommandInput({
107 className,
108 placeholder = undefined,
109 ...props
110}: React.ComponentProps<typeof AutocompleteInput>) {
111 return (
112 <div className="px-2.5 py-1.5">
113 <AutocompleteInput
114 autoFocus
115 className={cn(
116 "border-transparent! bg-transparent! shadow-none before:hidden has-focus-visible:ring-0",
117 className,
118 )}
119 placeholder={placeholder}
120 size="lg"
121 startAddon={<SearchIcon />}
122 {...props}
123 />
124 </div>
125 );
126}
127
128function CommandList({
129 className,
130 ...props
131}: React.ComponentProps<typeof AutocompleteList>) {
132 return (
133 <AutocompleteList
134 className={cn("not-empty:scroll-py-2 not-empty:p-2", className)}
135 data-slot="command-list"
136 {...props}
137 />
138 );
139}
140
141function CommandEmpty({
142 className,
143 ...props
144}: React.ComponentProps<typeof AutocompleteEmpty>) {
145 return (
146 <AutocompleteEmpty
147 className={cn("not-empty:py-6", className)}
148 data-slot="command-empty"
149 {...props}
150 />
151 );
152}
153
154function CommandPanel({ className, ...props }: React.ComponentProps<"div">) {
155 return (
156 <div
157 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"
158 {...props}
159 />
160 );
161}
162
163function CommandGroup({
164 className,
165 ...props
166}: React.ComponentProps<typeof AutocompleteGroup>) {
167 return (
168 <AutocompleteGroup
169 className={className}
170 data-slot="command-group"
171 {...props}
172 />
173 );
174}
175
176function CommandGroupLabel({
177 className,
178 ...props
179}: React.ComponentProps<typeof AutocompleteGroupLabel>) {
180 return (
181 <AutocompleteGroupLabel
182 className={className}
183 data-slot="command-group-label"
184 {...props}
185 />
186 );
187}
188
189function CommandCollection({
190 ...props
191}: React.ComponentProps<typeof AutocompleteCollection>) {
192 return <AutocompleteCollection data-slot="command-collection" {...props} />;
193}
194
195function CommandItem({
196 className,
197 ...props
198}: React.ComponentProps<typeof AutocompleteItem>) {
199 return (
200 <AutocompleteItem
201 className={cn("py-1.5", className)}
202 data-slot="command-item"
203 {...props}
204 />
205 );
206}
207
208function CommandSeparator({
209 className,
210 ...props
211}: React.ComponentProps<typeof AutocompleteSeparator>) {
212 return (
213 <AutocompleteSeparator
214 className={cn("my-2", className)}
215 data-slot="command-separator"
216 {...props}
217 />
218 );
219}
220
221function CommandShortcut({ className, ...props }: React.ComponentProps<"kbd">) {
222 return (
223 <kbd
224 className={cn(
225 "ms-auto font-medium font-sans text-muted-foreground/72 text-xs tracking-widest",
226 className,
227 )}
228 data-slot="command-shortcut"
229 {...props}
230 />
231 );
232}
233
234function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {
235 return (
236 <div
237 className={cn(
238 "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",
239 className,
240 )}
241 data-slot="command-footer"
242 {...props}
243 />
244 );
245}
246
247export {
248 CommandCreateHandle,
249 Command,
250 CommandCollection,
251 CommandDialog,
252 CommandDialogPopup,
253 CommandDialogTrigger,
254 CommandEmpty,
255 CommandFooter,
256 CommandGroup,
257 CommandGroupLabel,
258 CommandInput,
259 CommandItem,
260 CommandList,
261 CommandPanel,
262 CommandSeparator,
263 CommandShortcut,
264};
apps/web/src/components/ui/dialog.tsx created+196
...@@ -0,0 +1,196 @@
1"use client";
2
3import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
4import { XIcon } from "lucide-react";
5import { cn } from "~/lib/utils";
6import { Button } from "~/components/ui/button";
7import { ScrollArea } from "~/components/ui/scroll-area";
8
9const DialogCreateHandle = DialogPrimitive.createHandle;
10
11const Dialog = DialogPrimitive.Root;
12
13const DialogPortal = DialogPrimitive.Portal;
14
15function DialogTrigger(props: DialogPrimitive.Trigger.Props) {
16 return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
17}
18
19function DialogClose(props: DialogPrimitive.Close.Props) {
20 return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
21}
22
23function DialogBackdrop({
24 className,
25 ...props
26}: DialogPrimitive.Backdrop.Props) {
27 return (
28 <DialogPrimitive.Backdrop
29 className={cn(
30 "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",
31 className,
32 )}
33 data-slot="dialog-backdrop"
34 {...props}
35 />
36 );
37}
38
39function DialogViewport({
40 className,
41 ...props
42}: DialogPrimitive.Viewport.Props) {
43 return (
44 <DialogPrimitive.Viewport
45 className={cn(
46 "fixed inset-0 z-50 grid grid-rows-[1fr_auto_3fr] justify-items-center p-4",
47 className,
48 )}
49 data-slot="dialog-viewport"
50 {...props}
51 />
52 );
53}
54
55function DialogPopup({
56 className,
57 children,
58 showCloseButton = true,
59 bottomStickOnMobile = true,
60 ...props
61}: DialogPrimitive.Popup.Props & {
62 showCloseButton?: boolean;
63 bottomStickOnMobile?: boolean;
64}) {
65 return (
66 <DialogPortal>
67 <DialogBackdrop />
68 <DialogViewport
69 className={cn(
70 bottomStickOnMobile &&
71 "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12",
72 )}
73 >
74 <DialogPrimitive.Popup
75 className={cn(
76 "-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%)]",
77 bottomStickOnMobile &&
78 "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",
79 className,
80 )}
81 data-slot="dialog-popup"
82 {...props}
83 >
84 {children}
85 {showCloseButton && (
86 <DialogPrimitive.Close
87 aria-label="Close"
88 className="absolute end-2 top-2"
89 render={<Button size="icon" variant="ghost" />}
90 >
91 <XIcon />
92 </DialogPrimitive.Close>
93 )}
94 </DialogPrimitive.Popup>
95 </DialogViewport>
96 </DialogPortal>
97 );
98}
99
100function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
101 return (
102 <div
103 className={cn(
104 "flex flex-col gap-2 p-6 in-[[data-slot=dialog-popup]:has([data-slot=dialog-panel])]:pb-3 max-sm:pb-4",
105 className,
106 )}
107 data-slot="dialog-header"
108 {...props}
109 />
110 );
111}
112
113function DialogFooter({
114 className,
115 variant = "default",
116 ...props
117}: React.ComponentProps<"div"> & {
118 variant?: "default" | "bare";
119}) {
120 return (
121 <div
122 className={cn(
123 "flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end sm:rounded-b-[calc(var(--radius-2xl)-1px)]",
124 variant === "default" && "border-t bg-muted/72 py-4",
125 variant === "bare" &&
126 "in-[[data-slot=dialog-popup]:has([data-slot=dialog-panel])]:pt-3 pt-4 pb-6",
127 className,
128 )}
129 data-slot="dialog-footer"
130 {...props}
131 />
132 );
133}
134
135function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
136 return (
137 <DialogPrimitive.Title
138 className={cn(
139 "font-heading font-semibold text-xl leading-none",
140 className,
141 )}
142 data-slot="dialog-title"
143 {...props}
144 />
145 );
146}
147
148function DialogDescription({
149 className,
150 ...props
151}: DialogPrimitive.Description.Props) {
152 return (
153 <DialogPrimitive.Description
154 className={cn("text-muted-foreground text-sm", className)}
155 data-slot="dialog-description"
156 {...props}
157 />
158 );
159}
160
161function DialogPanel({
162 className,
163 scrollFade = true,
164 ...props
165}: React.ComponentProps<"div"> & { scrollFade?: boolean }) {
166 return (
167 <ScrollArea scrollFade={scrollFade}>
168 <div
169 className={cn(
170 "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",
171 className,
172 )}
173 data-slot="dialog-panel"
174 {...props}
175 />
176 </ScrollArea>
177 );
178}
179
180export {
181 DialogCreateHandle,
182 Dialog,
183 DialogTrigger,
184 DialogPortal,
185 DialogClose,
186 DialogBackdrop,
187 DialogBackdrop as DialogOverlay,
188 DialogPopup,
189 DialogPopup as DialogContent,
190 DialogHeader,
191 DialogFooter,
192 DialogTitle,
193 DialogDescription,
194 DialogPanel,
195 DialogViewport,
196};
apps/web/src/components/ui/empty.tsx created+127
...@@ -0,0 +1,127 @@
1import { cva, type VariantProps } 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/web/src/components/ui/field.tsx created+74
...@@ -0,0 +1,74 @@
1"use client";
2
3import { Field as FieldPrimitive } from "@base-ui/react/field";
4
5import { cn } from "~/lib/utils";
6
7function Field({ className, ...props }: FieldPrimitive.Root.Props) {
8 return (
9 <FieldPrimitive.Root
10 className={cn("flex flex-col items-start gap-2", className)}
11 data-slot="field"
12 {...props}
13 />
14 );
15}
16
17function FieldLabel({ className, ...props }: FieldPrimitive.Label.Props) {
18 return (
19 <FieldPrimitive.Label
20 className={cn(
21 "inline-flex items-center gap-2 font-medium text-base/4.5 text-foreground sm:text-sm/4",
22 className,
23 )}
24 data-slot="field-label"
25 {...props}
26 />
27 );
28}
29
30function FieldItem({ className, ...props }: FieldPrimitive.Item.Props) {
31 return (
32 <FieldPrimitive.Item
33 className={cn("flex", className)}
34 data-slot="field-item"
35 {...props}
36 />
37 );
38}
39
40function FieldDescription({
41 className,
42 ...props
43}: FieldPrimitive.Description.Props) {
44 return (
45 <FieldPrimitive.Description
46 className={cn("text-muted-foreground text-xs", className)}
47 data-slot="field-description"
48 {...props}
49 />
50 );
51}
52
53function FieldError({ className, ...props }: FieldPrimitive.Error.Props) {
54 return (
55 <FieldPrimitive.Error
56 className={cn("text-destructive-foreground text-xs", className)}
57 data-slot="field-error"
58 {...props}
59 />
60 );
61}
62
63const FieldControl = FieldPrimitive.Control;
64const FieldValidity = FieldPrimitive.Validity;
65
66export {
67 Field,
68 FieldLabel,
69 FieldDescription,
70 FieldError,
71 FieldControl,
72 FieldItem,
73 FieldValidity,
74};
apps/web/src/components/ui/fieldset.tsx created+29
...@@ -0,0 +1,29 @@
1"use client";
2
3import { Fieldset as FieldsetPrimitive } from "@base-ui/react/fieldset";
4
5import { cn } from "~/lib/utils";
6
7function Fieldset({ className, ...props }: FieldsetPrimitive.Root.Props) {
8 return (
9 <FieldsetPrimitive.Root
10 className={cn("flex w-full max-w-64 flex-col gap-6", className)}
11 data-slot="fieldset"
12 {...props}
13 />
14 );
15}
16function FieldsetLegend({
17 className,
18 ...props
19}: FieldsetPrimitive.Legend.Props) {
20 return (
21 <FieldsetPrimitive.Legend
22 className={cn("font-semibold text-foreground", className)}
23 data-slot="fieldset-legend"
24 {...props}
25 />
26 );
27}
28
29export { Fieldset, FieldsetLegend };
apps/web/src/components/ui/form.tsx created+17
...@@ -0,0 +1,17 @@
1"use client";
2
3import { Form as FormPrimitive } from "@base-ui/react/form";
4
5import { cn } from "~/lib/utils";
6
7function Form({ className, ...props }: FormPrimitive.Props) {
8 return (
9 <FormPrimitive
10 className={cn("flex w-full flex-col gap-4", className)}
11 data-slot="form"
12 {...props}
13 />
14 );
15}
16
17export { Form };
apps/web/src/components/ui/group.tsx created+93
...@@ -0,0 +1,93 @@
1"use client";
2
3import { mergeProps } from "@base-ui/react/merge-props";
4import { useRender } from "@base-ui/react/use-render";
5import { cva, type VariantProps } from "class-variance-authority";
6import type * as React from "react";
7
8import { cn } from "~/lib/utils";
9import { Separator } from "./separator";
10
11const groupVariants = cva(
12 "flex w-fit *:focus-visible:z-1 has-[>[data-slot=group]]:gap-2 *:has-focus-visible:z-1 dark:*:[[data-slot=button]:hover~[data-slot=separator]:not([data-slot]:hover~[data-slot=separator]~[data-slot=separator]),[data-slot][data-pressed]~[data-slot=separator]:not([data-slot][data-pressed]~[data-slot=separator]~[data-slot=separator])]:before:bg-input/64 dark:*:[[data-slot=separator]:has(~[data-slot=button]:hover):not(:has(~[data-slot=separator]~[data-slot]:hover)),[data-slot=separator]:has(~[data-slot][data-pressed]):not(:has(~[data-slot=separator]~[data-slot][data-pressed]))]:before:bg-input/64",
13 {
14 defaultVariants: {
15 orientation: "horizontal",
16 },
17 variants: {
18 orientation: {
19 horizontal:
20 "*:[[data-slot]~[data-slot]:not([data-slot=separator])]:before:-start-[0.5px] *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:-end-[0.5px] *:pointer-coarse:after:min-w-auto *:data-slot:has-[~[data-slot]]:rounded-e-none *:data-slot:has-[~[data-slot]]:border-e-0 *:data-slot:has-[~[data-slot]]:before:rounded-e-none *:[[data-slot]~[data-slot]]:rounded-s-none *:[[data-slot]~[data-slot]]:border-s-0 *:[[data-slot]~[data-slot]]:before:rounded-s-none",
21 vertical:
22 "*:[[data-slot]~[data-slot]:not([data-slot=separator])]:before:-top-[0.5px] *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:-bottom-[0.5px] flex-col *:pointer-coarse:after:min-h-auto *:data-slot:has-[~[data-slot]]:rounded-b-none *:data-slot:has-[~[data-slot]]:border-b-0 *:data-slot:not-data-[slot=separator]:has-[~[data-slot]]:before:hidden *:data-slot:has-[~[data-slot]]:before:rounded-b-none dark:*:last:before:hidden dark:*:first:before:block *:[[data-slot]~[data-slot]]:rounded-t-none *:[[data-slot]~[data-slot]]:border-t-0 *:[[data-slot]~[data-slot]]:before:rounded-t-none",
23 },
24 },
25 },
26);
27
28function Group({
29 className,
30 orientation,
31 children,
32 ...props
33}: {
34 className?: string;
35 orientation?: VariantProps<typeof groupVariants>["orientation"];
36 children: React.ReactNode;
37} & React.ComponentProps<"div">) {
38 return (
39 <div
40 className={cn(groupVariants({ orientation }), className)}
41 data-orientation={orientation}
42 data-slot="group"
43 role="group"
44 {...props}
45 >
46 {children}
47 </div>
48 );
49}
50
51function GroupText({ className, render, ...props }: useRender.ComponentProps<"div">) {
52 const defaultProps = {
53 className: cn(
54 "relative inline-flex items-center whitespace-nowrap gap-2 rounded-lg border border-input bg-muted not-dark:bg-clip-padding px-[calc(--spacing(3)-1px)] text-muted-foreground text-base sm:text-sm shadow-xs/5 outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] dark:bg-input/64 dark:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 [&_svg]:-mx-0.5",
55 className,
56 ),
57 "data-slot": "group-text",
58 };
59 return useRender({
60 defaultTagName: "div",
61 props: mergeProps(defaultProps, props),
62 render,
63 });
64}
65
66function GroupSeparator({
67 className,
68 orientation = "vertical",
69 ...props
70}: {
71 className?: string;
72} & React.ComponentProps<typeof Separator>) {
73 return (
74 <Separator
75 className={cn(
76 "[[data-slot=input-control]:focus-within+&,[data-slot=input-group]:focus-within+&,[data-slot=select-trigger]:focus-visible+*+&,[data-slot=number-field]:focus-within+input+&]:-translate-x-px pointer-events-none relative z-2 bg-input before:absolute before:inset-0 has-[+[data-slot=input-control]:focus-within,+[data-slot=input-group]:focus-within,+[data-slot=select-trigger]:focus-visible+*,+[data-slot=number-field]:focus-within]:translate-x-px has-[+[data-slot=input-control]:focus-within,+[data-slot=input-group]:focus-within,+[data-slot=select-trigger]:focus-visible+*,+[data-slot=number-field]:focus-within]:bg-ring dark:before:bg-input/32 [[data-slot=input-control]:focus-within+&,[data-slot=input-group]:focus-within+&,[data-slot=select-trigger]:focus-visible+*+&,[data-slot=number-field]:focus-within+&,[data-slot=number-field]:focus-within+input+&]:bg-ring",
77 className,
78 )}
79 orientation={orientation}
80 {...props}
81 />
82 );
83}
84
85export {
86 Group,
87 Group as ButtonGroup,
88 GroupText,
89 GroupText as ButtonGroupText,
90 GroupSeparator,
91 GroupSeparator as ButtonGroupSeparator,
92 groupVariants,
93};
apps/web/src/components/ui/input-group.tsx created+101
...@@ -0,0 +1,101 @@
1"use client";
2
3import { cva, type VariantProps } from "class-variance-authority";
4import type * as React from "react";
5
6import { cn } from "~/lib/utils";
7import { Input, type InputProps } from "~/components/ui/input";
8import { Textarea, type TextareaProps } from "~/components/ui/textarea";
9
10function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
11 return (
12 <div
13 className={cn(
14 "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)]",
15 className,
16 )}
17 data-slot="input-group"
18 role="group"
19 {...props}
20 />
21 );
22}
23
24const inputGroupAddonVariants = cva(
25 "[&_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",
26 {
27 defaultVariants: {
28 align: "inline-start",
29 },
30 variants: {
31 align: {
32 "block-end":
33 "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)]",
34 "block-start":
35 "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)]",
36 "inline-end":
37 "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)]",
38 "inline-start":
39 "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)]",
40 },
41 },
42 },
43);
44
45function InputGroupAddon({
46 className,
47 align = "inline-start",
48 ...props
49}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
50 return (
51 <div
52 className={cn(inputGroupAddonVariants({ align }), className)}
53 data-align={align}
54 data-slot="input-group-addon"
55 onMouseDown={(e) => {
56 const target = e.target as HTMLElement;
57 const isInteractive = target.closest(
58 "button, a, input, select, textarea, [role='button'], [role='combobox'], [role='listbox'], [data-slot='select-trigger']",
59 );
60 if (isInteractive) return;
61 e.preventDefault();
62 const parent = e.currentTarget.parentElement;
63 const input = parent?.querySelector<
64 HTMLInputElement | HTMLTextAreaElement
65 >("input, textarea");
66 if (input && !parent?.querySelector("input:focus, textarea:focus")) {
67 input.focus();
68 }
69 }}
70 {...props}
71 />
72 );
73}
74
75function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
76 return (
77 <span
78 className={cn(
79 "[&_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",
80 className,
81 )}
82 {...props}
83 />
84 );
85}
86
87function InputGroupInput({ className, ...props }: InputProps) {
88 return <Input className={className} unstyled {...props} />;
89}
90
91function InputGroupTextarea({ className, ...props }: TextareaProps) {
92 return <Textarea className={className} unstyled {...props} />;
93}
94
95export {
96 InputGroup,
97 InputGroupAddon,
98 InputGroupText,
99 InputGroupInput,
100 InputGroupTextarea,
101};
apps/web/src/components/ui/input.tsx created+66
...@@ -0,0 +1,66 @@
1"use client";
2
3import { Input as InputPrimitive } from "@base-ui/react/input";
4import type * as React from "react";
5
6import { cn } from "~/lib/utils";
7
8type InputProps = Omit<
9 InputPrimitive.Props & React.RefAttributes<HTMLInputElement>,
10 "size"
11> & {
12 size?: "sm" | "default" | "lg" | number;
13 unstyled?: boolean;
14 nativeInput?: boolean;
15};
16
17function Input({
18 className,
19 size = "default",
20 unstyled = false,
21 nativeInput = false,
22 ...props
23}: InputProps) {
24 const inputClassName = cn(
25 "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]",
26 size === "sm" &&
27 "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5",
28 size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5",
29 props.type === "search" &&
30 "[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none",
31 props.type === "file" &&
32 "text-muted-foreground file:me-3 file:bg-transparent file:font-medium file:text-foreground file:text-sm",
33 );
34
35 return (
36 <span
37 className={
38 cn(
39 !unstyled &&
40 "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%)]",
41 className,
42 ) || undefined
43 }
44 data-size={size}
45 data-slot="input-control"
46 >
47 {nativeInput ? (
48 <input
49 className={inputClassName}
50 data-slot="input"
51 size={typeof size === "number" ? size : undefined}
52 {...props}
53 />
54 ) : (
55 <InputPrimitive
56 className={inputClassName}
57 data-slot="input"
58 size={typeof size === "number" ? size : undefined}
59 {...props}
60 />
61 )}
62 </span>
63 );
64}
65
66export { Input, type InputProps };
apps/web/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/web/src/components/ui/label.tsx created+28
...@@ -0,0 +1,28 @@
1"use client";
2
3import { mergeProps } from "@base-ui/react/merge-props";
4import { useRender } from "@base-ui/react/use-render";
5
6import { cn } from "~/lib/utils";
7
8function Label({
9 className,
10 render,
11 ...props
12}: useRender.ComponentProps<"label">) {
13 const defaultProps = {
14 className: cn(
15 "inline-flex items-center gap-2 text-base/4.5 sm:text-sm/4 font-medium text-foreground",
16 className,
17 ),
18 "data-slot": "label",
19 };
20
21 return useRender({
22 defaultTagName: "label",
23 props: mergeProps<"label">(defaultProps, props),
24 render,
25 });
26}
27
28export { Label };
apps/web/src/components/ui/menu.tsx created+326
...@@ -0,0 +1,326 @@
1"use client";
2
3import { Menu as MenuPrimitive } from "@base-ui/react/menu";
4import { ChevronRightIcon } from "lucide-react";
5import type * as React from "react";
6
7import { cn } from "~/lib/utils";
8
9const MenuCreateHandle = MenuPrimitive.createHandle;
10
11const Menu = MenuPrimitive.Root;
12
13const MenuPortal = MenuPrimitive.Portal;
14
15function MenuTrigger({
16 className,
17 children,
18 ...props
19}: MenuPrimitive.Trigger.Props) {
20 return (
21 <MenuPrimitive.Trigger
22 className={className}
23 data-slot="menu-trigger"
24 {...props}
25 >
26 {children}
27 </MenuPrimitive.Trigger>
28 );
29}
30
31function MenuPopup({
32 children,
33 className,
34 sideOffset = 4,
35 align = "center",
36 alignOffset,
37 side = "bottom",
38 anchor,
39 ...props
40}: MenuPrimitive.Popup.Props & {
41 align?: MenuPrimitive.Positioner.Props["align"];
42 sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"];
43 alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"];
44 side?: MenuPrimitive.Positioner.Props["side"];
45 anchor?: MenuPrimitive.Positioner.Props["anchor"];
46}) {
47 return (
48 <MenuPrimitive.Portal>
49 <MenuPrimitive.Positioner
50 align={align}
51 alignOffset={alignOffset}
52 anchor={anchor}
53 className="z-50"
54 data-slot="menu-positioner"
55 side={side}
56 sideOffset={sideOffset}
57 >
58 <MenuPrimitive.Popup
59 className={cn(
60 "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%)]",
61 className,
62 )}
63 data-slot="menu-popup"
64 {...props}
65 >
66 <div className="max-h-(--available-height) w-full overflow-y-auto p-1">
67 {children}
68 </div>
69 </MenuPrimitive.Popup>
70 </MenuPrimitive.Positioner>
71 </MenuPrimitive.Portal>
72 );
73}
74
75function MenuGroup(props: MenuPrimitive.Group.Props) {
76 return <MenuPrimitive.Group data-slot="menu-group" {...props} />;
77}
78
79function MenuItem({
80 className,
81 inset,
82 variant = "default",
83 ...props
84}: MenuPrimitive.Item.Props & {
85 inset?: boolean;
86 variant?: "default" | "destructive";
87}) {
88 return (
89 <MenuPrimitive.Item
90 className={cn(
91 "[&>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",
92 className,
93 )}
94 data-inset={inset}
95 data-slot="menu-item"
96 data-variant={variant}
97 {...props}
98 />
99 );
100}
101
102function MenuCheckboxItem({
103 className,
104 children,
105 checked,
106 variant = "default",
107 ...props
108}: MenuPrimitive.CheckboxItem.Props & {
109 variant?: "default" | "switch";
110}) {
111 return (
112 <MenuPrimitive.CheckboxItem
113 checked={checked}
114 className={cn(
115 "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",
116 variant === "switch"
117 ? "grid-cols-[1fr_auto] gap-4 pe-1.5"
118 : "grid-cols-[1rem_1fr] pe-4",
119 className,
120 )}
121 data-slot="menu-checkbox-item"
122 {...props}
123 >
124 {variant === "switch" ? (
125 <>
126 <span className="col-start-1">{children}</span>
127 <MenuPrimitive.CheckboxItemIndicator
128 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)]"
129 keepMounted
130 >
131 <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]" />
132 </MenuPrimitive.CheckboxItemIndicator>
133 </>
134 ) : (
135 <>
136 <MenuPrimitive.CheckboxItemIndicator className="col-start-1">
137 <svg
138 fill="none"
139 height="24"
140 stroke="currentColor"
141 strokeLinecap="round"
142 strokeLinejoin="round"
143 strokeWidth="2"
144 viewBox="0 0 24 24"
145 width="24"
146 xmlns="http://www.w3.org/2000/svg"
147 >
148 <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
149 </svg>
150 </MenuPrimitive.CheckboxItemIndicator>
151 <span className="col-start-2">{children}</span>
152 </>
153 )}
154 </MenuPrimitive.CheckboxItem>
155 );
156}
157
158function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) {
159 return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />;
160}
161
162function MenuRadioItem({
163 className,
164 children,
165 ...props
166}: MenuPrimitive.RadioItem.Props) {
167 return (
168 <MenuPrimitive.RadioItem
169 className={cn(
170 "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",
171 className,
172 )}
173 data-slot="menu-radio-item"
174 {...props}
175 >
176 <MenuPrimitive.RadioItemIndicator className="col-start-1">
177 <svg
178 fill="none"
179 height="24"
180 stroke="currentColor"
181 strokeLinecap="round"
182 strokeLinejoin="round"
183 strokeWidth="2"
184 viewBox="0 0 24 24"
185 width="24"
186 xmlns="http://www.w3.org/2000/svg"
187 >
188 <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
189 </svg>
190 </MenuPrimitive.RadioItemIndicator>
191 <span className="col-start-2">{children}</span>
192 </MenuPrimitive.RadioItem>
193 );
194}
195
196function MenuGroupLabel({
197 className,
198 inset,
199 ...props
200}: MenuPrimitive.GroupLabel.Props & {
201 inset?: boolean;
202}) {
203 return (
204 <MenuPrimitive.GroupLabel
205 className={cn(
206 "px-2 py-1.5 font-medium text-muted-foreground text-xs data-inset:ps-9 sm:data-inset:ps-8",
207 className,
208 )}
209 data-inset={inset}
210 data-slot="menu-label"
211 {...props}
212 />
213 );
214}
215
216function MenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {
217 return (
218 <MenuPrimitive.Separator
219 className={cn("mx-2 my-1 h-px bg-border", className)}
220 data-slot="menu-separator"
221 {...props}
222 />
223 );
224}
225
226function MenuShortcut({ className, ...props }: React.ComponentProps<"kbd">) {
227 return (
228 <kbd
229 className={cn(
230 "ms-auto font-medium font-sans text-muted-foreground/72 text-xs tracking-widest",
231 className,
232 )}
233 data-slot="menu-shortcut"
234 {...props}
235 />
236 );
237}
238
239function MenuSub(props: MenuPrimitive.SubmenuRoot.Props) {
240 return <MenuPrimitive.SubmenuRoot data-slot="menu-sub" {...props} />;
241}
242
243function MenuSubTrigger({
244 className,
245 inset,
246 children,
247 ...props
248}: MenuPrimitive.SubmenuTrigger.Props & {
249 inset?: boolean;
250}) {
251 return (
252 <MenuPrimitive.SubmenuTrigger
253 className={cn(
254 "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",
255 className,
256 )}
257 data-inset={inset}
258 data-slot="menu-sub-trigger"
259 {...props}
260 >
261 {children}
262 <ChevronRightIcon className="-me-0.5 ms-auto opacity-80" />
263 </MenuPrimitive.SubmenuTrigger>
264 );
265}
266
267function MenuSubPopup({
268 className,
269 sideOffset = 0,
270 alignOffset,
271 align = "start",
272 ...props
273}: MenuPrimitive.Popup.Props & {
274 align?: MenuPrimitive.Positioner.Props["align"];
275 sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"];
276 alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"];
277}) {
278 const defaultAlignOffset = align !== "center" ? -5 : undefined;
279
280 return (
281 <MenuPopup
282 align={align}
283 alignOffset={alignOffset ?? defaultAlignOffset}
284 className={className}
285 data-slot="menu-sub-content"
286 side="inline-end"
287 sideOffset={sideOffset}
288 {...props}
289 />
290 );
291}
292
293export {
294 MenuCreateHandle,
295 MenuCreateHandle as DropdownMenuCreateHandle,
296 Menu,
297 Menu as DropdownMenu,
298 MenuPortal,
299 MenuPortal as DropdownMenuPortal,
300 MenuTrigger,
301 MenuTrigger as DropdownMenuTrigger,
302 MenuPopup,
303 MenuPopup as DropdownMenuContent,
304 MenuGroup,
305 MenuGroup as DropdownMenuGroup,
306 MenuItem,
307 MenuItem as DropdownMenuItem,
308 MenuCheckboxItem,
309 MenuCheckboxItem as DropdownMenuCheckboxItem,
310 MenuRadioGroup,
311 MenuRadioGroup as DropdownMenuRadioGroup,
312 MenuRadioItem,
313 MenuRadioItem as DropdownMenuRadioItem,
314 MenuGroupLabel,
315 MenuGroupLabel as DropdownMenuLabel,
316 MenuSeparator,
317 MenuSeparator as DropdownMenuSeparator,
318 MenuShortcut,
319 MenuShortcut as DropdownMenuShortcut,
320 MenuSub,
321 MenuSub as DropdownMenuSub,
322 MenuSubTrigger,
323 MenuSubTrigger as DropdownMenuSubTrigger,
324 MenuSubPopup,
325 MenuSubPopup as DropdownMenuSubContent,
326};
apps/web/src/components/ui/popover.tsx created+119
...@@ -0,0 +1,119 @@
1"use client";
2
3import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
4
5import { cn } from "~/lib/utils";
6
7const PopoverCreateHandle = PopoverPrimitive.createHandle;
8
9const Popover = PopoverPrimitive.Root;
10
11function PopoverTrigger({
12 className,
13 children,
14 ...props
15}: PopoverPrimitive.Trigger.Props) {
16 return (
17 <PopoverPrimitive.Trigger
18 className={className}
19 data-slot="popover-trigger"
20 {...props}
21 >
22 {children}
23 </PopoverPrimitive.Trigger>
24 );
25}
26
27function PopoverPopup({
28 children,
29 className,
30 side = "bottom",
31 align = "center",
32 sideOffset = 4,
33 alignOffset = 0,
34 tooltipStyle = false,
35 anchor,
36 ...props
37}: PopoverPrimitive.Popup.Props & {
38 side?: PopoverPrimitive.Positioner.Props["side"];
39 align?: PopoverPrimitive.Positioner.Props["align"];
40 sideOffset?: PopoverPrimitive.Positioner.Props["sideOffset"];
41 alignOffset?: PopoverPrimitive.Positioner.Props["alignOffset"];
42 tooltipStyle?: boolean;
43 anchor?: PopoverPrimitive.Positioner.Props["anchor"];
44}) {
45 return (
46 <PopoverPrimitive.Portal>
47 <PopoverPrimitive.Positioner
48 align={align}
49 alignOffset={alignOffset}
50 anchor={anchor}
51 className="z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom,transform] data-instant:transition-none"
52 data-slot="popover-positioner"
53 side={side}
54 sideOffset={sideOffset}
55 >
56 <PopoverPrimitive.Popup
57 className={cn(
58 "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 outline-none 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%)] has-data-[slot=calendar]:rounded-xl has-data-[slot=calendar]:before:rounded-[calc(var(--radius-xl)-1px)] data-starting-style:scale-98 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
59 tooltipStyle &&
60 "w-fit text-balance rounded-md text-xs shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]",
61 className,
62 )}
63 data-slot="popover-popup"
64 {...props}
65 >
66 <PopoverPrimitive.Viewport
67 className={cn(
68 "relative size-full max-h-(--available-height) overflow-clip px-(--viewport-inline-padding) py-4 [--viewport-inline-padding:--spacing(4)] has-data-[slot=calendar]:p-2 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",
69 tooltipStyle
70 ? "py-1 [--viewport-inline-padding:--spacing(2)]"
71 : "not-data-transitioning:overflow-y-auto",
72 )}
73 data-slot="popover-viewport"
74 >
75 {children}
76 </PopoverPrimitive.Viewport>
77 </PopoverPrimitive.Popup>
78 </PopoverPrimitive.Positioner>
79 </PopoverPrimitive.Portal>
80 );
81}
82
83function PopoverClose({ ...props }: PopoverPrimitive.Close.Props) {
84 return <PopoverPrimitive.Close data-slot="popover-close" {...props} />;
85}
86
87function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
88 return (
89 <PopoverPrimitive.Title
90 className={cn("font-semibold text-lg leading-none", className)}
91 data-slot="popover-title"
92 {...props}
93 />
94 );
95}
96
97function PopoverDescription({
98 className,
99 ...props
100}: PopoverPrimitive.Description.Props) {
101 return (
102 <PopoverPrimitive.Description
103 className={cn("text-muted-foreground text-sm", className)}
104 data-slot="popover-description"
105 {...props}
106 />
107 );
108}
109
110export {
111 PopoverCreateHandle,
112 Popover,
113 PopoverTrigger,
114 PopoverPopup,
115 PopoverPopup as PopoverContent,
116 PopoverTitle,
117 PopoverDescription,
118 PopoverClose,
119};
apps/web/src/components/ui/radio-group.tsx created+36
...@@ -0,0 +1,36 @@
1"use client";
2
3import { Radio as RadioPrimitive } from "@base-ui/react/radio";
4import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
5
6import { cn } from "~/lib/utils";
7
8function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
9 return (
10 <RadioGroupPrimitive
11 className={cn("flex flex-col gap-3", className)}
12 data-slot="radio-group"
13 {...props}
14 />
15 );
16}
17
18function Radio({ className, ...props }: RadioPrimitive.Root.Props) {
19 return (
20 <RadioPrimitive.Root
21 className={cn(
22 "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",
23 className,
24 )}
25 data-slot="radio"
26 {...props}
27 >
28 <RadioPrimitive.Indicator
29 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"
30 data-slot="radio-indicator"
31 />
32 </RadioPrimitive.Root>
33 );
34}
35
36export { RadioGroup, Radio, Radio as RadioGroupItem };
apps/web/src/components/ui/scroll-area.tsx created+64
...@@ -0,0 +1,64 @@
1"use client";
2
3import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
4
5import { cn } from "~/lib/utils";
6
7function ScrollArea({
8 className,
9 children,
10 scrollFade = false,
11 scrollbarGutter = false,
12 ...props
13}: ScrollAreaPrimitive.Root.Props & {
14 scrollFade?: boolean;
15 scrollbarGutter?: boolean;
16}) {
17 return (
18 <ScrollAreaPrimitive.Root
19 className={cn("size-full min-h-0", className)}
20 {...props}
21 >
22 <ScrollAreaPrimitive.Viewport
23 className={cn(
24 "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",
25 scrollFade &&
26 "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]",
27 scrollbarGutter &&
28 "data-has-overflow-y:pe-2.5 data-has-overflow-x:pb-2.5",
29 )}
30 data-slot="scroll-area-viewport"
31 >
32 {children}
33 </ScrollAreaPrimitive.Viewport>
34 <ScrollBar orientation="vertical" />
35 <ScrollBar orientation="horizontal" />
36 <ScrollAreaPrimitive.Corner data-slot="scroll-area-corner" />
37 </ScrollAreaPrimitive.Root>
38 );
39}
40
41function ScrollBar({
42 className,
43 orientation = "vertical",
44 ...props
45}: ScrollAreaPrimitive.Scrollbar.Props) {
46 return (
47 <ScrollAreaPrimitive.Scrollbar
48 className={cn(
49 "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",
50 className,
51 )}
52 data-slot="scroll-area-scrollbar"
53 orientation={orientation}
54 {...props}
55 >
56 <ScrollAreaPrimitive.Thumb
57 className="relative flex-1 rounded-full bg-foreground/20"
58 data-slot="scroll-area-thumb"
59 />
60 </ScrollAreaPrimitive.Scrollbar>
61 );
62}
63
64export { ScrollArea, ScrollBar };
apps/web/src/components/ui/select.tsx created+234
...@@ -0,0 +1,234 @@
1"use client";
2
3import { mergeProps } from "@base-ui/react/merge-props";
4import { Select as SelectPrimitive } from "@base-ui/react/select";
5import { useRender } from "@base-ui/react/use-render";
6import { cva, type VariantProps } from "class-variance-authority";
7import { ChevronDownIcon, ChevronsUpDownIcon, ChevronUpIcon } from "lucide-react";
8import type * as React from "react";
9
10import { cn } from "~/lib/utils";
11
12const Select = SelectPrimitive.Root;
13
14const selectTriggerVariants = cva(
15 "relative inline-flex select-none items-center justify-between gap-2 border rounded-lg text-left text-base outline-none transition-[color,box-shadow,background-color] data-disabled:pointer-events-none data-disabled:opacity-64 sm:text-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4",
16 {
17 defaultVariants: {
18 size: "default",
19 variant: "default",
20 },
21 variants: {
22 variant: {
23 default:
24 "w-full min-w-36 border-input bg-background not-dark:bg-clip-padding text-foreground shadow-xs/5 ring-ring/24 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 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 [[data-disabled],:focus-visible,[aria-invalid],[data-pressed]]:shadow-none",
25 ghost:
26 "border-transparent text-muted-foreground/70 focus-visible:ring-2 focus-visible:ring-ring data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent [:hover,[data-pressed]]:text-foreground/80",
27 },
28 size: {
29 default: "min-h-9 px-[calc(--spacing(3)-1px)] sm:min-h-8",
30 lg: "min-h-10 px-[calc(--spacing(3)-1px)] sm:min-h-9",
31 sm: "min-h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:min-h-7",
32 },
33 },
34 },
35);
36
37const selectTriggerIconClassName = "-me-1 size-4.5 opacity-80 sm:size-4";
38
39interface SelectButtonProps extends useRender.ComponentProps<"button"> {
40 size?: VariantProps<typeof selectTriggerVariants>["size"];
41 variant?: VariantProps<typeof selectTriggerVariants>["variant"];
42}
43
44function SelectButton({ className, size, variant, render, children, ...props }: SelectButtonProps) {
45 const typeValue: React.ButtonHTMLAttributes<HTMLButtonElement>["type"] = render
46 ? undefined
47 : "button";
48
49 const defaultProps = {
50 children: (
51 <>
52 <span className="flex-1 truncate in-data-placeholder:text-muted-foreground/72">
53 {children}
54 </span>
55 {variant === "ghost" ? (
56 <ChevronDownIcon className="size-3 opacity-50" />
57 ) : (
58 <ChevronsUpDownIcon className={selectTriggerIconClassName} />
59 )}
60 </>
61 ),
62 className: cn(selectTriggerVariants({ size, variant }), "min-w-none", className),
63 "data-slot": "select-button",
64 type: typeValue,
65 };
66
67 return useRender({
68 defaultTagName: "button",
69 props: mergeProps<"button">(defaultProps, props),
70 render,
71 });
72}
73
74function SelectTrigger({
75 className,
76 size = "default",
77 variant = "default",
78 children,
79 ...props
80}: SelectPrimitive.Trigger.Props & VariantProps<typeof selectTriggerVariants>) {
81 return (
82 <SelectPrimitive.Trigger
83 className={cn(selectTriggerVariants({ size, variant }), className)}
84 data-slot="select-trigger"
85 {...props}
86 >
87 {children}
88 <SelectPrimitive.Icon data-slot="select-icon">
89 <ChevronDownIcon className="size-3 opacity-50" />
90 </SelectPrimitive.Icon>
91 </SelectPrimitive.Trigger>
92 );
93}
94
95function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
96 return (
97 <SelectPrimitive.Value
98 className={cn("flex-1 truncate data-placeholder:text-muted-foreground", className)}
99 data-slot="select-value"
100 {...props}
101 />
102 );
103}
104
105function SelectPopup({
106 className,
107 children,
108 side = "bottom",
109 sideOffset = 4,
110 align = "start",
111 alignOffset = 0,
112 alignItemWithTrigger = true,
113 anchor,
114 ...props
115}: SelectPrimitive.Popup.Props & {
116 side?: SelectPrimitive.Positioner.Props["side"];
117 sideOffset?: SelectPrimitive.Positioner.Props["sideOffset"];
118 align?: SelectPrimitive.Positioner.Props["align"];
119 alignOffset?: SelectPrimitive.Positioner.Props["alignOffset"];
120 alignItemWithTrigger?: SelectPrimitive.Positioner.Props["alignItemWithTrigger"];
121 anchor?: SelectPrimitive.Positioner.Props["anchor"];
122}) {
123 return (
124 <SelectPrimitive.Portal>
125 <SelectPrimitive.Positioner
126 align={align}
127 alignItemWithTrigger={alignItemWithTrigger}
128 alignOffset={alignOffset}
129 anchor={anchor}
130 className="z-50 select-none"
131 data-slot="select-positioner"
132 side={side}
133 sideOffset={sideOffset}
134 >
135 <SelectPrimitive.Popup
136 className="origin-(--transform-origin) text-foreground"
137 data-slot="select-popup"
138 {...props}
139 >
140 <SelectPrimitive.ScrollUpArrow
141 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"
142 data-slot="select-scroll-up-arrow"
143 >
144 <ChevronUpIcon className="relative size-4.5 sm:size-4" />
145 </SelectPrimitive.ScrollUpArrow>
146 <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%)]">
147 <SelectPrimitive.List
148 className={cn("max-h-(--available-height) overflow-y-auto p-1", className)}
149 data-slot="select-list"
150 >
151 {children}
152 </SelectPrimitive.List>
153 </div>
154 <SelectPrimitive.ScrollDownArrow
155 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"
156 data-slot="select-scroll-down-arrow"
157 >
158 <ChevronDownIcon className="relative size-4.5 sm:size-4" />
159 </SelectPrimitive.ScrollDownArrow>
160 </SelectPrimitive.Popup>
161 </SelectPrimitive.Positioner>
162 </SelectPrimitive.Portal>
163 );
164}
165
166function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) {
167 return (
168 <SelectPrimitive.Item
169 className={cn(
170 "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",
171 className,
172 )}
173 data-slot="select-item"
174 {...props}
175 >
176 <SelectPrimitive.ItemIndicator className="col-start-1">
177 <svg
178 fill="none"
179 height="24"
180 stroke="currentColor"
181 strokeLinecap="round"
182 strokeLinejoin="round"
183 strokeWidth="2"
184 viewBox="0 0 24 24"
185 width="24"
186 xmlns="http://www.w3.org/1500/svg"
187 >
188 <path d="M5.252 12.7 10.2 18.63 18.748 5.37" />
189 </svg>
190 </SelectPrimitive.ItemIndicator>
191 <SelectPrimitive.ItemText className="col-start-2 min-w-0">
192 {children}
193 </SelectPrimitive.ItemText>
194 </SelectPrimitive.Item>
195 );
196}
197
198function SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) {
199 return (
200 <SelectPrimitive.Separator
201 className={cn("mx-2 my-1 h-px bg-border", className)}
202 data-slot="select-separator"
203 {...props}
204 />
205 );
206}
207
208function SelectGroup(props: SelectPrimitive.Group.Props) {
209 return <SelectPrimitive.Group data-slot="select-group" {...props} />;
210}
211
212function SelectGroupLabel(props: SelectPrimitive.GroupLabel.Props) {
213 return (
214 <SelectPrimitive.GroupLabel
215 className="px-2 py-1.5 font-medium text-muted-foreground text-xs"
216 data-slot="select-group-label"
217 {...props}
218 />
219 );
220}
221
222export {
223 Select,
224 SelectTrigger,
225 SelectButton,
226 selectTriggerVariants,
227 SelectValue,
228 SelectPopup,
229 SelectPopup as SelectContent,
230 SelectItem,
231 SelectSeparator,
232 SelectGroup,
233 SelectGroupLabel,
234};
apps/web/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/web/src/components/ui/sheet.tsx created+203
...@@ -0,0 +1,203 @@
1"use client";
2
3import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
4import { XIcon } from "lucide-react";
5import { cn } from "~/lib/utils";
6import { Button } from "~/components/ui/button";
7import { ScrollArea } from "~/components/ui/scroll-area";
8
9const Sheet = SheetPrimitive.Root;
10
11const SheetPortal = SheetPrimitive.Portal;
12
13function SheetTrigger(props: SheetPrimitive.Trigger.Props) {
14 return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
15}
16
17function SheetClose(props: SheetPrimitive.Close.Props) {
18 return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
19}
20
21function SheetBackdrop({ className, ...props }: SheetPrimitive.Backdrop.Props) {
22 return (
23 <SheetPrimitive.Backdrop
24 className={cn(
25 "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",
26 className,
27 )}
28 data-slot="sheet-backdrop"
29 {...props}
30 />
31 );
32}
33
34function SheetViewport({
35 className,
36 side,
37 variant = "default",
38 ...props
39}: SheetPrimitive.Viewport.Props & {
40 side?: "right" | "left" | "top" | "bottom";
41 variant?: "default" | "inset";
42}) {
43 return (
44 <SheetPrimitive.Viewport
45 className={cn(
46 "fixed inset-0 z-50 grid",
47 side === "bottom" && "grid grid-rows-[1fr_auto] pt-12",
48 side === "top" && "grid grid-rows-[auto_1fr] pb-12",
49 side === "left" && "flex justify-start",
50 side === "right" && "flex justify-end",
51 variant === "inset" && "sm:p-4",
52 )}
53 data-slot="sheet-viewport"
54 {...props}
55 />
56 );
57}
58
59function SheetPopup({
60 className,
61 children,
62 showCloseButton = true,
63 side = "right",
64 variant = "default",
65 ...props
66}: SheetPrimitive.Popup.Props & {
67 showCloseButton?: boolean;
68 side?: "right" | "left" | "top" | "bottom";
69 variant?: "default" | "inset";
70}) {
71 return (
72 <SheetPortal>
73 <SheetBackdrop />
74 <SheetViewport side={side} variant={variant}>
75 <SheetPrimitive.Popup
76 className={cn(
77 "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%)]",
78 side === "bottom" &&
79 "row-start-2 border-t data-ending-style:translate-y-8 data-starting-style:translate-y-8",
80 side === "top" &&
81 "data-ending-style:-translate-y-8 data-starting-style:-translate-y-8 border-b",
82 side === "left" &&
83 "data-ending-style:-translate-x-8 data-starting-style:-translate-x-8 w-[calc(100%-(--spacing(12)))] max-w-md border-e",
84 side === "right" &&
85 "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",
86 variant === "inset" &&
87 "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)]",
88 className,
89 )}
90 data-slot="sheet-popup"
91 {...props}
92 >
93 {children}
94 {showCloseButton && (
95 <SheetPrimitive.Close
96 aria-label="Close"
97 className="absolute end-2 top-2"
98 render={<Button size="icon" variant="ghost" />}
99 >
100 <XIcon />
101 </SheetPrimitive.Close>
102 )}
103 </SheetPrimitive.Popup>
104 </SheetViewport>
105 </SheetPortal>
106 );
107}
108
109function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
110 return (
111 <div
112 className={cn(
113 "flex flex-col gap-2 p-6 in-[[data-slot=sheet-popup]:has([data-slot=sheet-panel])]:pb-3 max-sm:pb-4",
114 className,
115 )}
116 data-slot="sheet-header"
117 {...props}
118 />
119 );
120}
121
122function SheetFooter({
123 className,
124 variant = "default",
125 ...props
126}: React.ComponentProps<"div"> & {
127 variant?: "default" | "bare";
128}) {
129 return (
130 <div
131 className={cn(
132 "flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end",
133 variant === "default" && "border-t bg-muted/72 py-4",
134 variant === "bare" &&
135 "in-[[data-slot=sheet-popup]:has([data-slot=sheet-panel])]:pt-3 pt-4 pb-6",
136 className,
137 )}
138 data-slot="sheet-footer"
139 {...props}
140 />
141 );
142}
143
144function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
145 return (
146 <SheetPrimitive.Title
147 className={cn(
148 "font-heading font-semibold text-xl leading-none",
149 className,
150 )}
151 data-slot="sheet-title"
152 {...props}
153 />
154 );
155}
156
157function SheetDescription({
158 className,
159 ...props
160}: SheetPrimitive.Description.Props) {
161 return (
162 <SheetPrimitive.Description
163 className={cn("text-muted-foreground text-sm", className)}
164 data-slot="sheet-description"
165 {...props}
166 />
167 );
168}
169
170function SheetPanel({
171 className,
172 scrollFade = true,
173 ...props
174}: React.ComponentProps<"div"> & { scrollFade?: boolean }) {
175 return (
176 <ScrollArea scrollFade={scrollFade}>
177 <div
178 className={cn(
179 "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",
180 className,
181 )}
182 data-slot="sheet-panel"
183 {...props}
184 />
185 </ScrollArea>
186 );
187}
188
189export {
190 Sheet,
191 SheetTrigger,
192 SheetPortal,
193 SheetClose,
194 SheetBackdrop,
195 SheetBackdrop as SheetOverlay,
196 SheetPopup,
197 SheetPopup as SheetContent,
198 SheetHeader,
199 SheetFooter,
200 SheetTitle,
201 SheetDescription,
202 SheetPanel,
203};
apps/web/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/web/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/web/src/components/ui/switch.tsx created+27
...@@ -0,0 +1,27 @@
1"use client";
2
3import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
4
5import { cn } from "~/lib/utils";
6
7function Switch({ className, ...props }: SwitchPrimitive.Root.Props) {
8 return (
9 <SwitchPrimitive.Root
10 className={cn(
11 "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)]",
12 className,
13 )}
14 data-slot="switch"
15 {...props}
16 >
17 <SwitchPrimitive.Thumb
18 className={cn(
19 "pointer-events-none block aspect-square h-full origin-left in-[[role=switch]:active,[data-slot=label]:active,[data-slot=field-label]:active]:not-data-disabled:scale-x-110 in-[[role=switch]:active,[data-slot=label]:active,[data-slot=field-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)]",
20 )}
21 data-slot="switch-thumb"
22 />
23 </SwitchPrimitive.Root>
24 );
25}
26
27export { Switch };
apps/web/src/components/ui/textarea.tsx created+51
...@@ -0,0 +1,51 @@
1"use client";
2
3import { Field as FieldPrimitive } from "@base-ui/react/field";
4import { mergeProps } from "@base-ui/react/merge-props";
5import type * as React from "react";
6
7import { cn } from "~/lib/utils";
8
9type TextareaProps = React.ComponentProps<"textarea"> & {
10 size?: "sm" | "default" | "lg" | number;
11 unstyled?: boolean;
12};
13
14function Textarea({
15 className,
16 size = "default",
17 unstyled = false,
18 ...props
19}: TextareaProps) {
20 return (
21 <span
22 className={
23 cn(
24 !unstyled &&
25 "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%)]",
26 className,
27 ) || undefined
28 }
29 data-size={size}
30 data-slot="textarea-control"
31 >
32 <FieldPrimitive.Control
33 render={(defaultProps) => (
34 <textarea
35 className={cn(
36 "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",
37 size === "sm" &&
38 "min-h-16.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)] max-sm:min-h-19.5",
39 size === "lg" &&
40 "min-h-18.5 py-[calc(--spacing(2)-1px)] max-sm:min-h-21.5",
41 )}
42 data-slot="textarea"
43 {...mergeProps(defaultProps, props)}
44 />
45 )}
46 />
47 </span>
48 );
49}
50
51export { Textarea, type TextareaProps };
apps/web/src/components/ui/toast.tsx created+269
...@@ -0,0 +1,269 @@
1"use client";
2
3import { Toast } from "@base-ui/react/toast";
4import {
5 CircleAlertIcon,
6 CircleCheckIcon,
7 InfoIcon,
8 LoaderCircleIcon,
9 TriangleAlertIcon,
10} from "lucide-react";
11
12import { cn } from "~/lib/utils";
13import { buttonVariants } from "~/components/ui/button";
14
15const toastManager = Toast.createToastManager();
16const anchoredToastManager = Toast.createToastManager();
17
18const TOAST_ICONS = {
19 error: CircleAlertIcon,
20 info: InfoIcon,
21 loading: LoaderCircleIcon,
22 success: CircleCheckIcon,
23 warning: TriangleAlertIcon,
24} as const;
25
26type ToastPosition =
27 | "top-left"
28 | "top-center"
29 | "top-right"
30 | "bottom-left"
31 | "bottom-center"
32 | "bottom-right";
33
34interface ToastProviderProps extends Toast.Provider.Props {
35 position?: ToastPosition;
36}
37
38function ToastProvider({
39 children,
40 position = "bottom-right",
41 ...props
42}: ToastProviderProps) {
43 return (
44 <Toast.Provider toastManager={toastManager} {...props}>
45 {children}
46 <Toasts position={position} />
47 </Toast.Provider>
48 );
49}
50
51function Toasts({ position = "bottom-right" }: { position: ToastPosition }) {
52 const { toasts } = Toast.useToastManager();
53 const isTop = position.startsWith("top");
54
55 return (
56 <Toast.Portal data-slot="toast-portal">
57 <Toast.Viewport
58 className={cn(
59 "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)]",
60 // Vertical positioning
61 "data-[position*=top]:top-(--toast-inset)",
62 "data-[position*=bottom]:bottom-(--toast-inset)",
63 // Horizontal positioning
64 "data-[position*=left]:left-(--toast-inset)",
65 "data-[position*=right]:right-(--toast-inset)",
66 "data-[position*=center]:-translate-x-1/2 data-[position*=center]:left-1/2",
67 )}
68 data-position={position}
69 data-slot="toast-viewport"
70 >
71 {toasts.map((toast) => {
72 const Icon = toast.type
73 ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
74 : null;
75
76 return (
77 <Toast.Root
78 className={cn(
79 "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%)]",
80 // Base positioning using data-position
81 "data-[position*=right]:right-0 data-[position*=right]:left-auto",
82 "data-[position*=left]:right-auto data-[position*=left]:left-0",
83 "data-[position*=center]:right-0 data-[position*=center]:left-0",
84 "data-[position*=top]:top-0 data-[position*=top]:bottom-auto data-[position*=top]:origin-top",
85 "data-[position*=bottom]:top-auto data-[position*=bottom]:bottom-0 data-[position*=bottom]:origin-bottom",
86 // Gap fill for hover
87 "after:absolute after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full",
88 "data-[position*=top]:after:top-full",
89 "data-[position*=bottom]:after:bottom-full",
90 // Define some variables
91 "[--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))]",
92 // Define offset-y variable
93 "data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+var(--toast-index)*var(--toast-gap)+var(--toast-swipe-movement-y))]",
94 "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))]",
95 // Default state transform
96 "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))]",
97 "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))]",
98 // Limited state
99 "data-limited:opacity-0",
100 // Expanded state
101 "data-expanded:h-(--toast-height)",
102 "data-position:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-calc-offset-y))]",
103 // Starting and ending animations
104 "data-[position*=top]:data-starting-style:transform-[translateY(calc(-100%-var(--toast-inset)))]",
105 "data-[position*=bottom]:data-starting-style:transform-[translateY(calc(100%+var(--toast-inset)))]",
106 "data-ending-style:opacity-0",
107 // Ending animations (direction-aware)
108 "data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateY(calc(100%+var(--toast-inset)))]",
109 "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))]",
110 "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))]",
111 "data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
112 "data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
113 // Ending animations (expanded)
114 "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))]",
115 "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))]",
116 "data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
117 "data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
118 )}
119 data-position={position}
120 key={toast.id}
121 swipeDirection={
122 position.includes("center")
123 ? [isTop ? "up" : "down"]
124 : position.includes("left")
125 ? ["left", isTop ? "up" : "down"]
126 : ["right", isTop ? "up" : "down"]
127 }
128 toast={toast}
129 >
130 <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">
131 <div className="flex gap-2">
132 {Icon && (
133 <div
134 className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
135 data-slot="toast-icon"
136 >
137 <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" />
138 </div>
139 )}
140
141 <div className="flex flex-col gap-0.5">
142 <Toast.Title
143 className="font-medium"
144 data-slot="toast-title"
145 />
146 <Toast.Description
147 className="text-muted-foreground"
148 data-slot="toast-description"
149 />
150 </div>
151 </div>
152 {toast.actionProps && (
153 <Toast.Action
154 className={buttonVariants({ size: "xs" })}
155 data-slot="toast-action"
156 >
157 {toast.actionProps.children}
158 </Toast.Action>
159 )}
160 </Toast.Content>
161 </Toast.Root>
162 );
163 })}
164 </Toast.Viewport>
165 </Toast.Portal>
166 );
167}
168
169function AnchoredToastProvider({ children, ...props }: Toast.Provider.Props) {
170 return (
171 <Toast.Provider toastManager={anchoredToastManager} {...props}>
172 {children}
173 <AnchoredToasts />
174 </Toast.Provider>
175 );
176}
177
178function AnchoredToasts() {
179 const { toasts } = Toast.useToastManager();
180
181 return (
182 <Toast.Portal data-slot="toast-portal-anchored">
183 <Toast.Viewport
184 className="outline-none"
185 data-slot="toast-viewport-anchored"
186 >
187 {toasts.map((toast) => {
188 const Icon = toast.type
189 ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
190 : null;
191 const tooltipStyle =
192 (toast.data as { tooltipStyle?: boolean })?.tooltipStyle ?? false;
193 const positionerProps = toast.positionerProps;
194
195 if (!positionerProps?.anchor) {
196 return null;
197 }
198
199 return (
200 <Toast.Positioner
201 className="z-50 max-w-[min(--spacing(64),var(--available-width))]"
202 data-slot="toast-positioner"
203 key={toast.id}
204 sideOffset={positionerProps.sideOffset ?? 4}
205 toast={toast}
206 >
207 <Toast.Root
208 className={cn(
209 "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%)]",
210 tooltipStyle
211 ? "rounded-md shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]"
212 : "rounded-lg shadow-lg/5 before:rounded-[calc(var(--radius-lg)-1px)]",
213 )}
214 data-slot="toast-popup"
215 toast={toast}
216 >
217 {tooltipStyle ? (
218 <Toast.Content className="pointer-events-auto px-2 py-1">
219 <Toast.Title data-slot="toast-title" />
220 </Toast.Content>
221 ) : (
222 <Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm">
223 <div className="flex gap-2">
224 {Icon && (
225 <div
226 className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
227 data-slot="toast-icon"
228 >
229 <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" />
230 </div>
231 )}
232
233 <div className="flex flex-col gap-0.5">
234 <Toast.Title
235 className="font-medium"
236 data-slot="toast-title"
237 />
238 <Toast.Description
239 className="text-muted-foreground"
240 data-slot="toast-description"
241 />
242 </div>
243 </div>
244 {toast.actionProps && (
245 <Toast.Action
246 className={buttonVariants({ size: "xs" })}
247 data-slot="toast-action"
248 >
249 {toast.actionProps.children}
250 </Toast.Action>
251 )}
252 </Toast.Content>
253 )}
254 </Toast.Root>
255 </Toast.Positioner>
256 );
257 })}
258 </Toast.Viewport>
259 </Toast.Portal>
260 );
261}
262
263export {
264 ToastProvider,
265 type ToastPosition,
266 toastManager,
267 AnchoredToastProvider,
268 anchoredToastManager,
269};
apps/web/src/index.css+2-41
...@@ -11,19 +11,6 @@...@@ -11,19 +11,6 @@
11 --color-info-foreground: var(--info-foreground);11 --color-info-foreground: var(--info-foreground);
12 --color-info: var(--info);12 --color-info: var(--info);
13 --color-destructive-foreground: var(--destructive-foreground);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);14 --color-ring: var(--ring);
28 --color-input: var(--input);15 --color-input: var(--input);
29 --color-border: var(--border);16 --color-border: var(--border);
...@@ -61,7 +48,7 @@...@@ -61,7 +48,7 @@
61 @apply border-border outline-ring/50;48 @apply border-border outline-ring/50;
62 }49 }
63 body {50 body {
64 @apply bg-background text-foreground;51 @apply bg-background text-foreground relative;
65 }52 }
66}53}
6754
...@@ -89,25 +76,12 @@...@@ -89,25 +76,12 @@
89 --secondary-foreground: var(--color-neutral-800);76 --secondary-foreground: var(--color-neutral-800);
90 --muted: --alpha(var(--color-black) / 4%);77 --muted: --alpha(var(--color-black) / 4%);
91 --muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-black));78 --muted-foreground: color-mix(in srgb, var(--color-neutral-500) 90%, var(--color-black));
92 --accent: --alpha(var(--color-black) / 5%);79 --accent: --alpha(var(--color-black) / 4%);
93 --accent-foreground: var(--color-neutral-800);80 --accent-foreground: var(--color-neutral-800);
94 --destructive: var(--color-red-500);81 --destructive: var(--color-red-500);
95 --border: --alpha(var(--color-black) / 8%);82 --border: --alpha(var(--color-black) / 8%);
96 --input: --alpha(var(--color-black) / 10%);83 --input: --alpha(var(--color-black) / 10%);
97 --ring: oklch(0.488 0.217 264);84 --ring: oklch(0.488 0.217 264);
98 --chart-1: oklch(0.646 0.222 41.116);
99 --chart-2: oklch(0.6 0.118 184.704);
100 --chart-3: oklch(0.398 0.07 227.392);
101 --chart-4: oklch(0.828 0.189 84.429);
102 --chart-5: oklch(0.769 0.188 70.08);
103 --sidebar: oklch(0.985 0 0);
104 --sidebar-foreground: oklch(0.141 0.005 285.823);
105 --sidebar-primary: oklch(0.21 0.006 285.885);
106 --sidebar-primary-foreground: oklch(0.985 0 0);
107 --sidebar-accent: oklch(0.967 0.001 286.375);
108 --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
109 --sidebar-border: oklch(0.92 0.004 286.32);
110 --sidebar-ring: oklch(0.705 0.015 286.067);
111 --destructive-foreground: var(--color-red-700);85 --destructive-foreground: var(--color-red-700);
112 --info: var(--color-blue-500);86 --info: var(--color-blue-500);
113 --info-foreground: var(--color-blue-700);87 --info-foreground: var(--color-blue-700);
...@@ -136,19 +110,6 @@...@@ -136,19 +110,6 @@
136 --border: --alpha(var(--color-white) / 6%);110 --border: --alpha(var(--color-white) / 6%);
137 --input: --alpha(var(--color-white) / 8%);111 --input: --alpha(var(--color-white) / 8%);
138 --ring: oklch(0.588 0.217 264);112 --ring: oklch(0.588 0.217 264);
139 --chart-1: oklch(0.488 0.243 264.376);
140 --chart-2: oklch(0.696 0.17 162.48);
141 --chart-3: oklch(0.769 0.188 70.08);
142 --chart-4: oklch(0.627 0.265 303.9);
143 --chart-5: oklch(0.645 0.246 16.439);
144 --sidebar: oklch(0.21 0.006 285.885);
145 --sidebar-foreground: oklch(0.985 0 0);
146 --sidebar-primary: oklch(0.488 0.243 264.376);
147 --sidebar-primary-foreground: oklch(0.985 0 0);
148 --sidebar-accent: oklch(0.274 0.006 286.033);
149 --sidebar-accent-foreground: oklch(0.985 0 0);
150 --sidebar-border: oklch(1 0 0 / 10%);
151 --sidebar-ring: oklch(0.552 0.016 285.938);
152 --destructive-foreground: var(--color-red-400);113 --destructive-foreground: var(--color-red-400);
153 --info: var(--color-blue-500);114 --info: var(--color-blue-500);
154 --info-foreground: var(--color-blue-400);115 --info-foreground: var(--color-blue-400);
apps/web/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/web/src/model-logic.ts+2-1
...@@ -6,4 +6,5 @@ export {...@@ -6,4 +6,5 @@ export {
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
77
8export const REASONING_OPTIONS = ["xhigh", "high", "medium", "low"] as const;8export const REASONING_OPTIONS = ["xhigh", "high", "medium", "low"] as const;
9export const DEFAULT_REASONING = "high";9export type ReasoningEffort = (typeof REASONING_OPTIONS)[number];
10export const DEFAULT_REASONING: ReasoningEffort = "high";
apps/web/tsconfig.json+4-1
...@@ -4,7 +4,10 @@...@@ -4,7 +4,10 @@
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 "types": ["vite/client"],
8 "paths": {
9 "~/*": ["./src/*"]
10 }
8 },11 },
9 "include": ["src", "vite.config.ts"]12 "include": ["src", "vite.config.ts"]
10}13}
apps/web/vite.config.ts+6
...@@ -17,6 +17,12 @@ export default defineConfig({...@@ -17,6 +17,12 @@ export default defineConfig({
17 // In dev mode, tell the web app where the WebSocket server lives17 // In dev mode, tell the web app where the WebSocket server lives
18 "import.meta.env.VITE_WS_URL": JSON.stringify(process.env.VITE_WS_URL ?? ""),18 "import.meta.env.VITE_WS_URL": JSON.stringify(process.env.VITE_WS_URL ?? ""),
19 },19 },
20 experimental: {
21 enableNativePlugin: true,
22 },
23 resolve: {
24 tsconfigPaths: true,
25 },
20 server: {26 server: {
21 port,27 port,
22 strictPort: true,28 strictPort: true,
bun.lock+118-3
...@@ -51,17 +51,20 @@...@@ -51,17 +51,20 @@
51 "name": "@t3tools/web",51 "name": "@t3tools/web",
52 "version": "0.0.0",52 "version": "0.0.0",
53 "dependencies": {53 "dependencies": {
54 "@base-ui/react": "^1.2.0",
54 "@t3tools/contracts": "workspace:*",55 "@t3tools/contracts": "workspace:*",
55 "@tanstack/react-query": "^5.90.0",56 "@tanstack/react-query": "^5.90.0",
56 "@xterm/addon-fit": "^0.11.0",57 "@xterm/addon-fit": "^0.11.0",
57 "@xterm/xterm": "^6.0.0",58 "@xterm/xterm": "^6.0.0",
59 "class-variance-authority": "^0.7.1",
58 "highlight.js": "^11.11.1",60 "highlight.js": "^11.11.1",
59 "lucide-react": "^0.563.0",61 "lucide-react": "^0.564.0",
60 "react": "^19.0.0",62 "react": "^19.0.0",
61 "react-dom": "^19.0.0",63 "react-dom": "^19.0.0",
62 "react-markdown": "^10.1.0",64 "react-markdown": "^10.1.0",
63 "rehype-highlight": "^7.0.2",65 "rehype-highlight": "^7.0.2",
64 "remark-gfm": "^4.0.1",66 "remark-gfm": "^4.0.1",
67 "tailwind-merge": "^3.4.0",
65 "zod": "^3.24.1",68 "zod": "^3.24.1",
66 },69 },
67 "devDependencies": {70 "devDependencies": {
...@@ -120,12 +123,18 @@...@@ -120,12 +123,18 @@
120123
121 "@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=="],124 "@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=="],
122125
126 "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
127
123 "@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=="],128 "@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=="],
124129
125 "@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=="],130 "@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=="],
126131
127 "@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=="],132 "@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=="],
128133
134 "@base-ui/react": ["@base-ui/react@1.2.0", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@base-ui/utils": "0.2.5", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "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-O6aEQHcm+QyGTFY28xuwRD3SEJGZOBDpyjN2WvpfWYFVhg+3zfXPysAILqtM0C1kWC82MccOE/v1j+GHXE4qIw=="],
135
136 "@base-ui/utils": ["@base-ui/utils@0.2.5", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@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-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw=="],
137
129 "@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=="],138 "@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=="],
130139
131 "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],140 "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
...@@ -186,6 +195,14 @@...@@ -186,6 +195,14 @@
186195
187 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],196 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
188197
198 "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
199
200 "@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=="],
201
202 "@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=="],
203
204 "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
205
189 "@hapi/address": ["@hapi/address@5.1.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA=="],206 "@hapi/address": ["@hapi/address@5.1.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA=="],
190207
191 "@hapi/formula": ["@hapi/formula@3.0.2", "", {}, "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw=="],208 "@hapi/formula": ["@hapi/formula@3.0.2", "", {}, "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw=="],
...@@ -198,6 +215,16 @@...@@ -198,6 +215,16 @@
198215
199 "@hapi/topo": ["@hapi/topo@6.0.2", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg=="],216 "@hapi/topo": ["@hapi/topo@6.0.2", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg=="],
200217
218 "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="],
219
220 "@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=="],
221
222 "@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=="],
223
224 "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="],
225
226 "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
227
201 "@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=="],228 "@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=="],
202229
203 "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],230 "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
...@@ -208,8 +235,16 @@...@@ -208,8 +235,16 @@
208235
209 "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],236 "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
210237
238 "@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "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-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="],
239
211 "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],240 "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
212241
242 "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="],
243
244 "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="],
245
246 "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="],
247
213 "@oxc-project/runtime": ["@oxc-project/runtime@0.112.0", "", {}, "sha512-4vYtWXMnXM6EaweCxbJ6bISAhkNHeN33SihvuX3wrpqaSJA4ZEoW35i9mSvE74+GDf1yTeVE+aEHA+WBpjDk/g=="],248 "@oxc-project/runtime": ["@oxc-project/runtime@0.112.0", "", {}, "sha512-4vYtWXMnXM6EaweCxbJ6bISAhkNHeN33SihvuX3wrpqaSJA4ZEoW35i9mSvE74+GDf1yTeVE+aEHA+WBpjDk/g=="],
214249
215 "@oxc-project/types": ["@oxc-project/types@0.112.0", "", {}, "sha512-m6RebKHIRsax2iCwVpYW2ErQwa4ywHJrE4sCK3/8JK8ZZAWOKXaRJFl/uP51gaVyyXlaS4+chU1nSCdzYf6QqQ=="],250 "@oxc-project/types": ["@oxc-project/types@0.112.0", "", {}, "sha512-m6RebKHIRsax2iCwVpYW2ErQwa4ywHJrE4sCK3/8JK8ZZAWOKXaRJFl/uP51gaVyyXlaS4+chU1nSCdzYf6QqQ=="],
...@@ -436,6 +471,8 @@...@@ -436,6 +471,8 @@
436471
437 "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],472 "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
438473
474 "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="],
475
439 "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],476 "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
440477
441 "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],478 "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
...@@ -464,6 +501,8 @@...@@ -464,6 +501,8 @@
464501
465 "@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="],502 "@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="],
466503
504 "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
505
467 "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],506 "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
468507
469 "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],508 "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
...@@ -518,8 +557,16 @@...@@ -518,8 +557,16 @@
518557
519 "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],558 "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
520559
560 "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
561
562 "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
563
564 "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=="],
565
521 "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],566 "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="],
522567
568 "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
569
523 "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],570 "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
524571
525 "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],572 "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
...@@ -530,6 +577,8 @@...@@ -530,6 +577,8 @@
530577
531 "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],578 "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
532579
580 "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
581
533 "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],582 "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
534583
535 "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],584 "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
...@@ -572,6 +621,8 @@...@@ -572,6 +621,8 @@
572621
573 "electronmon": ["electronmon@2.0.4", "", { "dependencies": { "chalk": "^3.0.0", "import-from": "^3.0.0", "runtime-required": "^1.1.0", "watchboy": "^0.4.3" }, "bin": { "electronmon": "bin/cli.js" } }, "sha512-u6eDrvUbqa+wsnMrhG2vHmo5neL1owLg2e5i1avGWcOb4rHsUf9lSfbs0FvfPsBNpLxxlPO98nrMhAGV+zw/fQ=="],622 "electronmon": ["electronmon@2.0.4", "", { "dependencies": { "chalk": "^3.0.0", "import-from": "^3.0.0", "runtime-required": "^1.1.0", "watchboy": "^0.4.3" }, "bin": { "electronmon": "bin/cli.js" } }, "sha512-u6eDrvUbqa+wsnMrhG2vHmo5neL1owLg2e5i1avGWcOb4rHsUf9lSfbs0FvfPsBNpLxxlPO98nrMhAGV+zw/fQ=="],
574623
624 "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
625
575 "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],626 "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
576627
577 "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],628 "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
...@@ -626,6 +677,8 @@...@@ -626,6 +677,8 @@
626677
627 "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],678 "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
628679
680 "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
681
629 "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],682 "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
630683
631 "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],684 "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
...@@ -644,6 +697,8 @@...@@ -644,6 +697,8 @@
644697
645 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],698 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
646699
700 "graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="],
701
647 "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],702 "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
648703
649 "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],704 "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
...@@ -662,6 +717,8 @@...@@ -662,6 +717,8 @@
662717
663 "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],718 "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
664719
720 "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],
721
665 "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],722 "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
666723
667 "hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="],724 "hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="],
...@@ -686,10 +743,14 @@...@@ -686,10 +743,14 @@
686743
687 "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],744 "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
688745
746 "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
747
689 "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],748 "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
690749
691 "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],750 "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
692751
752 "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="],
753
693 "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],754 "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
694755
695 "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],756 "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
...@@ -750,7 +811,7 @@...@@ -750,7 +811,7 @@
750811
751 "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],812 "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
752813
753 "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=="],814 "lucide-react": ["lucide-react@0.564.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg=="],
754815
755 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],816 "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
756817
...@@ -858,6 +919,10 @@...@@ -858,6 +919,10 @@
858919
859 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],920 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
860921
922 "msw": ["msw@2.12.10", "", { "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-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw=="],
923
924 "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
925
861 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],926 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
862927
863 "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],928 "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
...@@ -878,6 +943,8 @@...@@ -878,6 +943,8 @@
878943
879 "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],944 "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
880945
946 "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="],
947
881 "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=="],948 "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=="],
882949
883 "oxlint": ["oxlint@1.46.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.46.0", "@oxlint/binding-android-arm64": "1.46.0", "@oxlint/binding-darwin-arm64": "1.46.0", "@oxlint/binding-darwin-x64": "1.46.0", "@oxlint/binding-freebsd-x64": "1.46.0", "@oxlint/binding-linux-arm-gnueabihf": "1.46.0", "@oxlint/binding-linux-arm-musleabihf": "1.46.0", "@oxlint/binding-linux-arm64-gnu": "1.46.0", "@oxlint/binding-linux-arm64-musl": "1.46.0", "@oxlint/binding-linux-ppc64-gnu": "1.46.0", "@oxlint/binding-linux-riscv64-gnu": "1.46.0", "@oxlint/binding-linux-riscv64-musl": "1.46.0", "@oxlint/binding-linux-s390x-gnu": "1.46.0", "@oxlint/binding-linux-x64-gnu": "1.46.0", "@oxlint/binding-linux-x64-musl": "1.46.0", "@oxlint/binding-openharmony-arm64": "1.46.0", "@oxlint/binding-win32-arm64-msvc": "1.46.0", "@oxlint/binding-win32-ia32-msvc": "1.46.0", "@oxlint/binding-win32-x64-msvc": "1.46.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.11.2" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-I9h42QDtAVsRwoueJ4PL/7qN5jFzIUXvbO4Z5ddtII92ZCiD7uiS/JW2V4viBSfGLsbZkQp3YEs6Ls4I8q+8tA=="],950 "oxlint": ["oxlint@1.46.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.46.0", "@oxlint/binding-android-arm64": "1.46.0", "@oxlint/binding-darwin-arm64": "1.46.0", "@oxlint/binding-darwin-x64": "1.46.0", "@oxlint/binding-freebsd-x64": "1.46.0", "@oxlint/binding-linux-arm-gnueabihf": "1.46.0", "@oxlint/binding-linux-arm-musleabihf": "1.46.0", "@oxlint/binding-linux-arm64-gnu": "1.46.0", "@oxlint/binding-linux-arm64-musl": "1.46.0", "@oxlint/binding-linux-ppc64-gnu": "1.46.0", "@oxlint/binding-linux-riscv64-gnu": "1.46.0", "@oxlint/binding-linux-riscv64-musl": "1.46.0", "@oxlint/binding-linux-s390x-gnu": "1.46.0", "@oxlint/binding-linux-x64-gnu": "1.46.0", "@oxlint/binding-linux-x64-musl": "1.46.0", "@oxlint/binding-openharmony-arm64": "1.46.0", "@oxlint/binding-win32-arm64-msvc": "1.46.0", "@oxlint/binding-win32-ia32-msvc": "1.46.0", "@oxlint/binding-win32-x64-msvc": "1.46.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.11.2" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-I9h42QDtAVsRwoueJ4PL/7qN5jFzIUXvbO4Z5ddtII92ZCiD7uiS/JW2V4viBSfGLsbZkQp3YEs6Ls4I8q+8tA=="],
...@@ -886,6 +953,8 @@...@@ -886,6 +953,8 @@
886953
887 "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],954 "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
888955
956 "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
957
889 "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],958 "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
890959
891 "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],960 "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
...@@ -930,6 +999,10 @@...@@ -930,6 +999,10 @@
930999
931 "remove-trailing-separator": ["remove-trailing-separator@1.1.0", "", {}, "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw=="],1000 "remove-trailing-separator": ["remove-trailing-separator@1.1.0", "", {}, "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw=="],
9321001
1002 "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
1003
1004 "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
1005
933 "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],1006 "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
9341007
935 "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],1008 "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
...@@ -938,6 +1011,8 @@...@@ -938,6 +1011,8 @@
9381011
939 "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],1012 "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="],
9401013
1014 "rettime": ["rettime@0.10.1", "", {}, "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw=="],
1015
941 "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=="],1016 "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=="],
9421017
943 "rolldown": ["rolldown@1.0.0-rc.3", "", { "dependencies": { "@oxc-project/types": "=0.112.0", "@rolldown/pluginutils": "1.0.0-rc.3" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.3", "@rolldown/binding-darwin-arm64": "1.0.0-rc.3", "@rolldown/binding-darwin-x64": "1.0.0-rc.3", "@rolldown/binding-freebsd-x64": "1.0.0-rc.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.3", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.3", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.3", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.3", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.3", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.3", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.3", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.3", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.3" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Po/YZECDOqVXjIXrtC5h++a5NLvKAQNrd9ggrIG3sbDfGO5BqTUsrI6l8zdniKRp3r5Tp/2JTrXqx4GIguFCMw=="],1018 "rolldown": ["rolldown@1.0.0-rc.3", "", { "dependencies": { "@oxc-project/types": "=0.112.0", "@rolldown/pluginutils": "1.0.0-rc.3" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.3", "@rolldown/binding-darwin-arm64": "1.0.0-rc.3", "@rolldown/binding-darwin-x64": "1.0.0-rc.3", "@rolldown/binding-freebsd-x64": "1.0.0-rc.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.3", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.3", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.3", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.3", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.3", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.3", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.3", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.3", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.3" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Po/YZECDOqVXjIXrtC5h++a5NLvKAQNrd9ggrIG3sbDfGO5BqTUsrI6l8zdniKRp3r5Tp/2JTrXqx4GIguFCMw=="],
...@@ -962,6 +1037,8 @@...@@ -962,6 +1037,8 @@
9621037
963 "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],1038 "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
9641039
1040 "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
1041
965 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],1042 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
9661043
967 "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],1044 "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
...@@ -970,10 +1047,18 @@...@@ -970,10 +1047,18 @@
9701047
971 "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],1048 "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
9721049
1050 "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
1051
973 "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],1052 "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
9741053
1054 "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="],
1055
1056 "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=="],
1057
975 "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],1058 "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
9761059
1060 "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
1061
977 "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],1062 "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
9781063
979 "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],1064 "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
...@@ -984,6 +1069,12 @@...@@ -984,6 +1069,12 @@
9841069
985 "t3": ["t3@workspace:apps/server"],1070 "t3": ["t3@workspace:apps/server"],
9861071
1072 "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
1073
1074 "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
1075
1076 "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
1077
987 "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],1078 "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
9881079
989 "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],1080 "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
...@@ -998,8 +1089,14 @@...@@ -998,8 +1089,14 @@
9981089
999 "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],1090 "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
10001091
1092 "tldts": ["tldts@7.0.23", "", { "dependencies": { "tldts-core": "^7.0.23" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw=="],
1093
1094 "tldts-core": ["tldts-core@7.0.23", "", {}, "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ=="],
1095
1001 "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],1096 "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
10021097
1098 "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="],
1099
1003 "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],1100 "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
10041101
1005 "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],1102 "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
...@@ -1024,7 +1121,7 @@...@@ -1024,7 +1121,7 @@
10241121
1025 "turbo-windows-arm64": ["turbo-windows-arm64@2.8.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-WyGiOI2Zp3AhuzVagzQN+T+iq0fWx0oGxDfAWT3ZiLEd4U0cDUkwUZDKVGb3rKqPjDL6lWnuxKKu73ge5xtovQ=="],1122 "turbo-windows-arm64": ["turbo-windows-arm64@2.8.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-WyGiOI2Zp3AhuzVagzQN+T+iq0fWx0oGxDfAWT3ZiLEd4U0cDUkwUZDKVGb3rKqPjDL6lWnuxKKu73ge5xtovQ=="],
10261123
1027 "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],1124 "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="],
10281125
1029 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],1126 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
10301127
...@@ -1052,8 +1149,12 @@...@@ -1052,8 +1149,12 @@
10521149
1053 "unrun": ["unrun@0.2.27", "", { "dependencies": { "rolldown": "1.0.0-rc.3" }, "peerDependencies": { "synckit": "^0.11.11" }, "optionalPeers": ["synckit"], "bin": { "unrun": "dist/cli.mjs" } }, "sha512-Mmur1UJpIbfxasLOhPRvox/QS4xBiDii71hMP7smfRthGcwFL2OAmYRgduLANOAU4LUkvVamuP+02U+c90jlrw=="],1150 "unrun": ["unrun@0.2.27", "", { "dependencies": { "rolldown": "1.0.0-rc.3" }, "peerDependencies": { "synckit": "^0.11.11" }, "optionalPeers": ["synckit"], "bin": { "unrun": "dist/cli.mjs" } }, "sha512-Mmur1UJpIbfxasLOhPRvox/QS4xBiDii71hMP7smfRthGcwFL2OAmYRgduLANOAU4LUkvVamuP+02U+c90jlrw=="],
10541151
1152 "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="],
1153
1055 "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=="],1154 "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=="],
10561155
1156 "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=="],
1157
1057 "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],1158 "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
10581159
1059 "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],1160 "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
...@@ -1068,16 +1169,26 @@...@@ -1068,16 +1169,26 @@
10681169
1069 "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],1170 "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
10701171
1172 "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=="],
1173
1071 "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],1174 "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
10721175
1073 "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],1176 "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
10741177
1075 "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],1178 "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
10761179
1180 "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
1181
1077 "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],1182 "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
10781183
1184 "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
1185
1186 "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
1187
1079 "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],1188 "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
10801189
1190 "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
1191
1081 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],1192 "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
10821193
1083 "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],1194 "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
...@@ -1130,6 +1241,8 @@...@@ -1130,6 +1241,8 @@
11301241
1131 "@vitejs/plugin-react/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],1242 "@vitejs/plugin-react/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
11321243
1244 "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
1245
1133 "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],1246 "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
11341247
1135 "electron/@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="],1248 "electron/@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="],
...@@ -1142,6 +1255,8 @@...@@ -1142,6 +1255,8 @@
11421255
1143 "rolldown-plugin-dts/@babel/types": ["@babel/types@8.0.0-rc.1", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.1", "@babel/helper-validator-identifier": "^8.0.0-rc.1" } }, "sha512-ubmJ6TShyaD69VE9DQrlXcdkvJbmwWPB8qYj0H2kaJi29O7vJT9ajSdBd2W8CG34pwL9pYA74fi7RHC1qbLoVQ=="],1256 "rolldown-plugin-dts/@babel/types": ["@babel/types@8.0.0-rc.1", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.1", "@babel/helper-validator-identifier": "^8.0.0-rc.1" } }, "sha512-ubmJ6TShyaD69VE9DQrlXcdkvJbmwWPB8qYj0H2kaJi29O7vJT9ajSdBd2W8CG34pwL9pYA74fi7RHC1qbLoVQ=="],
11441257
1258 "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
1259
1145 "vitest/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],1260 "vitest/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
11461261
1147 "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="],1262 "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.1", "", {}, "sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw=="],
packages/contracts/src/model.test.ts+1-1
...@@ -29,7 +29,7 @@ describe("resolveModelSlug", () => {...@@ -29,7 +29,7 @@ describe("resolveModelSlug", () => {
2929
30 it("resolves only supported model options", () => {30 it("resolves only supported model options", () => {
31 for (const model of MODEL_OPTIONS) {31 for (const model of MODEL_OPTIONS) {
32 expect(resolveModelSlug(model)).toBe(model);32 expect(resolveModelSlug(model.slug)).toBe(model.slug);
33 }33 }
34 });34 });
35});35});
packages/contracts/src/model.ts+11-11
...@@ -1,20 +1,22 @@...@@ -1,20 +1,22 @@
1export const MODEL_OPTIONS = [1export const MODEL_OPTIONS = [
2 "gpt-5.3-codex",2 { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
3 "gpt-5.3-codex-spark",3 { slug: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" },
4 "gpt-5.2-codex",4 { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
5 "gpt-5.2",5 { slug: "gpt-5.2", name: "GPT-5.2" },
6] as const;6] as const;
77
8export type ModelSlug = (typeof MODEL_OPTIONS)[number]["slug"];
9
8export const DEFAULT_MODEL = "gpt-5.3-codex";10export const DEFAULT_MODEL = "gpt-5.3-codex";
911
10export const MODEL_SLUG_ALIASES: Record<string, string> = {12export const MODEL_SLUG_ALIASES: Record<string, ModelSlug> = {
11 "5.3": "gpt-5.3-codex",13 "5.3": "gpt-5.3-codex",
12 "gpt-5.3": "gpt-5.3-codex",14 "gpt-5.3": "gpt-5.3-codex",
13 "5.3-spark": "gpt-5.3-codex-spark",15 "5.3-spark": "gpt-5.3-codex-spark",
14 "gpt-5.3-spark": "gpt-5.3-codex-spark",16 "gpt-5.3-spark": "gpt-5.3-codex-spark",
15};17};
1618
17export function normalizeModelSlug(model: string | null | undefined): string | null {19export function normalizeModelSlug(model: string | null | undefined): ModelSlug | null {
18 if (typeof model !== "string") {20 if (typeof model !== "string") {
19 return null;21 return null;
20 }22 }
...@@ -24,16 +26,14 @@ export function normalizeModelSlug(model: string | null | undefined): string | n...@@ -24,16 +26,14 @@ export function normalizeModelSlug(model: string | null | undefined): string | n
24 return null;26 return null;
25 }27 }
2628
27 return MODEL_SLUG_ALIASES[trimmed] ?? trimmed;29 return MODEL_SLUG_ALIASES[trimmed] ?? (trimmed as ModelSlug);
28}30}
2931
30export function resolveModelSlug(model: string | null | undefined): string {32export function resolveModelSlug(model: string | null | undefined): ModelSlug {
31 const normalized = normalizeModelSlug(model);33 const normalized = normalizeModelSlug(model);
32 if (!normalized) {34 if (!normalized) {
33 return DEFAULT_MODEL;35 return DEFAULT_MODEL;
34 }36 }
3537
36 return MODEL_OPTIONS.includes(normalized as (typeof MODEL_OPTIONS)[number])38 return MODEL_OPTIONS.some((option) => option.slug === normalized) ? normalized : DEFAULT_MODEL;
37 ? normalized
38 : DEFAULT_MODEL;
39}39}