| author | |
| committer | |
| log | d3c93752e5c89b2a77b15630a629e70295878582 |
| tree | 8baabdb3705c77f73fd8f6477a8997d7cd96eae7 |
| parent | 4b4abcdd7d197647b8b21b18d2344e4e12ae4c34 |
- 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 flow48 files changed, 4540 insertions(+), 1202 deletions(-)
apps/server/src/wsServer.ts+24-3| ... | ... | @@ -324,11 +324,32 @@ export function createServer(options: ServerOptions) { |
| 324 | 324 | if (!params?.cwd) throw new Error("cwd is required"); |
| 325 | 325 | const editorDef = EDITORS.find((e) => e.id === params.editor); |
| 326 | 326 | if (!editorDef) throw new Error(`Unknown editor: ${params.editor}`); |
| 327 | if (!editorDef.command) { | |
| 328 | // Can't open file manager programmatically from server, just succeed silently | |
| 327 | ||
| 328 | 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 | 349 | return undefined; |
| 330 | 350 | } |
| 331 | const child = spawn(editorDef.command, [params.cwd], { | |
| 351 | ||
| 352 | const child = spawn(command, args, { | |
| 332 | 353 | detached: true, |
| 333 | 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 | 11 | "test": "vitest run --passWithNoTests" |
| 12 | 12 | }, |
| 13 | 13 | "dependencies": { |
| 14 | "@xterm/addon-fit": "^0.11.0", | |
| 15 | "@xterm/xterm": "^6.0.0", | |
| 14 | "@base-ui/react": "^1.2.0", | |
| 16 | 15 | "@t3tools/contracts": "workspace:*", |
| 17 | 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 | 20 | "highlight.js": "^11.11.1", |
| 19 | "lucide-react": "^0.563.0", | |
| 21 | "lucide-react": "^0.564.0", | |
| 20 | 22 | "react": "^19.0.0", |
| 21 | 23 | "react-dom": "^19.0.0", |
| 22 | 24 | "react-markdown": "^10.1.0", |
| 23 | 25 | "rehype-highlight": "^7.0.2", |
| 24 | 26 | "remark-gfm": "^4.0.1", |
| 27 | "tailwind-merge": "^3.4.0", | |
| 25 | 28 | "zod": "^3.24.1" |
| 26 | 29 | }, |
| 27 | 30 | "devDependencies": { |
apps/web/src/App.tsx+7-2| ... | ... | @@ -10,6 +10,7 @@ import { StoreProvider, useStore } from "./store"; |
| 10 | 10 | import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID } from "./types"; |
| 11 | 11 | import { onServerWelcome } from "./wsNativeApi"; |
| 12 | 12 | import { useNativeApi } from "./hooks/useNativeApi"; |
| 13 | import { AnchoredToastProvider, ToastProvider } from "./components/ui/toast"; | |
| 13 | 14 | |
| 14 | 15 | function EventRouter() { |
| 15 | 16 | const api = useNativeApi(); |
| ... | ... | @@ -181,7 +182,7 @@ function Layout() { |
| 181 | 182 | } |
| 182 | 183 | |
| 183 | 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 | 186 | <EventRouter /> |
| 186 | 187 | <AutoProjectBootstrap /> |
| 187 | 188 | <DesktopProjectBootstrap /> |
| ... | ... | @@ -198,7 +199,11 @@ export default function App() { |
| 198 | 199 | return ( |
| 199 | 200 | <QueryClientProvider client={queryClient}> |
| 200 | 201 | <StoreProvider> |
| 201 | <Layout /> | |
| 202 | <ToastProvider> | |
| 203 | <AnchoredToastProvider> | |
| 204 | <Layout /> | |
| 205 | </AnchoredToastProvider> | |
| 206 | </ToastProvider> | |
| 202 | 207 | </StoreProvider> |
| 203 | 208 | </QueryClientProvider> |
| 204 | 209 | ); |
apps/web/src/components/BranchToolbar.tsx+117-109| ... | ... | @@ -1,9 +1,21 @@ |
| 1 | 1 | import type { GitBranch } from "@t3tools/contracts"; |
| 2 | 2 | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; |
| 3 | import { useEffect, useRef, useState } from "react"; | |
| 3 | import { useEffect, useState } from "react"; | |
| 4 | 4 | |
| 5 | import { useStore } from "../store"; | |
| 6 | 5 | import { useNativeApi } from "../hooks/useNativeApi"; |
| 6 | import { useStore } from "../store"; | |
| 7 | import { Button } from "./ui/button"; | |
| 8 | import { | |
| 9 | Combobox, | |
| 10 | ComboboxEmpty, | |
| 11 | ComboboxInput, | |
| 12 | ComboboxItem, | |
| 13 | ComboboxList, | |
| 14 | ComboboxPopup, | |
| 15 | ComboboxTrigger, | |
| 16 | } from "./ui/combobox"; | |
| 17 | import { ChevronDownIcon, PlusIcon } from "lucide-react"; | |
| 18 | import { InputGroup, InputGroupAddon, InputGroupInput } from "./ui/input-group"; | |
| 7 | 19 | |
| 8 | 20 | interface BranchToolbarProps { |
| 9 | 21 | envMode: "local" | "worktree"; |
| ... | ... | @@ -19,7 +31,6 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B |
| 19 | 31 | const [isBranchMenuOpen, setIsBranchMenuOpen] = useState(false); |
| 20 | 32 | const [isCreatingBranch, setIsCreatingBranch] = useState(false); |
| 21 | 33 | const [newBranchName, setNewBranchName] = useState(""); |
| 22 | const branchMenuRef = useRef<HTMLDivElement>(null); | |
| 23 | 34 | |
| 24 | 35 | const activeThread = state.threads.find((thread) => thread.id === state.activeThreadId); |
| 25 | 36 | const activeProject = state.projects.find((project) => project.id === activeThread?.projectId); |
| ... | ... | @@ -37,6 +48,8 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B |
| 37 | 48 | }); |
| 38 | 49 | |
| 39 | 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 | 53 | // Default to true while loading — showing "Initialize git" during a fetch is wrong, |
| 41 | 54 | // and worktrees are inherently git repos. |
| 42 | 55 | const isRepo = branchesQuery.data?.isRepo ?? !branchesQuery.isLoading; |
| ... | ... | @@ -46,8 +59,10 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B |
| 46 | 59 | const checkoutMutation = useMutation({ |
| 47 | 60 | mutationFn: (branch: string) => api!.git.checkout({ cwd: branchCwd!, branch }), |
| 48 | 61 | onSuccess: () => queryClient.invalidateQueries({ queryKey: ["git", "branches", branchCwd] }), |
| 49 | onError: (error) => | |
| 50 | setThreadError(error instanceof Error ? error.message : "Failed to checkout branch."), | |
| 62 | onError: (error) => { | |
| 63 | setThreadError(error instanceof Error ? error.message : "Failed to checkout branch."); | |
| 64 | setIsBranchMenuOpen(true); | |
| 65 | }, | |
| 51 | 66 | }); |
| 52 | 67 | |
| 53 | 68 | const createBranchMutation = useMutation({ |
| ... | ... | @@ -88,17 +103,9 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B |
| 88 | 103 | }, [activeThreadId, activeWorktreePath, activeThreadBranch, queryBranches, dispatch]); |
| 89 | 104 | |
| 90 | 105 | useEffect(() => { |
| 91 | if (!isBranchMenuOpen) return; | |
| 92 | const handleClickOutside = (event: MouseEvent) => { | |
| 93 | if (!branchMenuRef.current) return; | |
| 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 | }; | |
| 106 | if (isBranchMenuOpen) return; | |
| 107 | setIsCreatingBranch(false); | |
| 108 | setNewBranchName(""); | |
| 102 | 109 | }, [isBranchMenuOpen]); |
| 103 | 110 | |
| 104 | 111 | // ── Helpers ─────────────────────────────────────────────────────────── |
| ... | ... | @@ -171,21 +178,19 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B |
| 171 | 178 | <div className="mx-auto flex w-full max-w-3xl items-center justify-between px-5 pb-3 pt-1"> |
| 172 | 179 | <div className="flex items-center gap-2"> |
| 173 | 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 | 182 | {activeWorktreePath ? "Worktree" : "Local"} |
| 176 | 183 | </span> |
| 177 | 184 | ) : ( |
| 178 | <button | |
| 185 | <Button | |
| 179 | 186 | type="button" |
| 180 | className={`inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-[12px] transition-colors duration-150 ${ | |
| 181 | envMode === "worktree" | |
| 182 | ? "bg-accent text-foreground/80" | |
| 183 | : "text-muted-foreground/60 hover:bg-accent/50 hover:text-muted-foreground/80" | |
| 184 | }`} | |
| 187 | variant="ghost" | |
| 188 | className="text-muted-foreground/70 hover:text-foreground/80" | |
| 189 | size="xs" | |
| 185 | 190 | onClick={() => onEnvModeChange(envMode === "local" ? "worktree" : "local")} |
| 186 | 191 | > |
| 187 | 192 | {envMode === "worktree" ? "New worktree" : "Local"} |
| 188 | </button> | |
| 193 | </Button> | |
| 189 | 194 | )} |
| 190 | 195 | </div> |
| 191 | 196 | |
| ... | ... | @@ -199,59 +204,56 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B |
| 199 | 204 | {initMutation.isPending ? "Initializing\u2026" : "Initialize git"} |
| 200 | 205 | </button> |
| 201 | 206 | ) : ( |
| 202 | <div className="relative" ref={branchMenuRef}> | |
| 203 | <button | |
| 204 | type="button" | |
| 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" | |
| 206 | onClick={() => setIsBranchMenuOpen((open) => !open)} | |
| 207 | <Combobox | |
| 208 | items={branchNames} | |
| 209 | autoHighlight | |
| 210 | onOpenChange={(open) => setIsBranchMenuOpen(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 | 217 | disabled={branchesQuery.isLoading} |
| 208 | 218 | > |
| 209 | <span className="max-w-[240px] truncate font-mono"> | |
| 219 | <span className="max-w-[240px] truncate"> | |
| 210 | 220 | {activeThread.branch |
| 211 | 221 | ? envMode === "worktree" && !activeWorktreePath |
| 212 | 222 | ? `From ${activeThread.branch}` |
| 213 | 223 | : activeThread.branch |
| 214 | 224 | : "Select branch"} |
| 215 | 225 | </span> |
| 216 | <svg | |
| 217 | width="10" | |
| 218 | height="10" | |
| 219 | viewBox="0 0 10 10" | |
| 220 | fill="none" | |
| 221 | className="opacity-40" | |
| 222 | aria-hidden="true" | |
| 223 | > | |
| 224 | <path | |
| 225 | d="M2.5 4L5 6.5L7.5 4" | |
| 226 | stroke="currentColor" | |
| 227 | strokeWidth="1.2" | |
| 228 | strokeLinecap="round" | |
| 229 | strokeLinejoin="round" | |
| 226 | <ChevronDownIcon /> | |
| 227 | </ComboboxTrigger> | |
| 228 | <ComboboxPopup align="end" side="top" className="w-64"> | |
| 229 | <div className="border-b"> | |
| 230 | <ComboboxInput | |
| 231 | className="rounded-b-none before:rounded-b-none [&_input]:font-sans" | |
| 232 | placeholder="Search branches..." | |
| 233 | showClear | |
| 234 | showTrigger={false} | |
| 235 | size="sm" | |
| 230 | 236 | /> |
| 231 | </svg> | |
| 232 | </button> | |
| 233 | {isBranchMenuOpen && ( | |
| 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"> | |
| 235 | <p className="px-2 py-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/55"> | |
| 236 | Branch | |
| 237 | </p> | |
| 238 | <div className="max-h-64 overflow-y-auto"> | |
| 239 | {branches.map((branch) => { | |
| 240 | const isSelected = branch.name === activeThread.branch; | |
| 241 | const hasSecondaryWorktree = | |
| 242 | branch.worktreePath && branch.worktreePath !== activeProject.cwd; | |
| 243 | return ( | |
| 244 | <button | |
| 245 | key={branch.name} | |
| 246 | type="button" | |
| 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 ${ | |
| 248 | isSelected | |
| 249 | ? "bg-accent text-foreground" | |
| 250 | : "text-foreground/90 hover:bg-accent/50" | |
| 251 | }`} | |
| 252 | onClick={() => selectBranch(branch)} | |
| 253 | > | |
| 254 | <span className="truncate">{branch.name}</span> | |
| 237 | </div> | |
| 238 | <ComboboxEmpty>No branches found.</ComboboxEmpty> | |
| 239 | ||
| 240 | <ComboboxList className="max-h-56"> | |
| 241 | {(branchName) => { | |
| 242 | const branch = branchByName.get(branchName); | |
| 243 | if (!branch) return null; | |
| 244 | ||
| 245 | const hasSecondaryWorktree = | |
| 246 | branch.worktreePath && branch.worktreePath !== activeProject.cwd; | |
| 247 | return ( | |
| 248 | <ComboboxItem | |
| 249 | hideIndicator | |
| 250 | key={branchName} | |
| 251 | value={branchName} | |
| 252 | className={branchName === activeThread.branch ? "bg-accent text-foreground" : undefined} | |
| 253 | onClick={() => selectBranch(branch)} | |
| 254 | > | |
| 255 | <div className="flex w-full items-center justify-between gap-2"> | |
| 256 | <span className="truncate">{branchName}</span> | |
| 255 | 257 | {(branch.current || branch.isDefault || hasSecondaryWorktree) && ( |
| 256 | 258 | <span className="shrink-0 text-[10px] text-muted-foreground/45"> |
| 257 | 259 | {branch.current |
| ... | ... | @@ -261,58 +263,64 @@ export default function BranchToolbar({ envMode, onEnvModeChange, envLocked }: B |
| 261 | 263 | : "default"} |
| 262 | 264 | </span> |
| 263 | 265 | )} |
| 264 | </button> | |
| 265 | ); | |
| 266 | })} | |
| 267 | </div> | |
| 268 | {envMode === "local" && ( | |
| 269 | <> | |
| 270 | <div className="mx-1 my-1 h-px bg-border" /> | |
| 271 | {isCreatingBranch ? ( | |
| 272 | <form | |
| 273 | className="flex items-center gap-1 px-1" | |
| 274 | onSubmit={(event) => { | |
| 275 | event.preventDefault(); | |
| 276 | createBranch(); | |
| 277 | }} | |
| 278 | > | |
| 279 | <input | |
| 280 | type="text" | |
| 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" | |
| 266 | </div> | |
| 267 | </ComboboxItem> | |
| 268 | ); | |
| 269 | }} | |
| 270 | </ComboboxList> | |
| 271 | {envMode === "local" && ( | |
| 272 | <> | |
| 273 | {isCreatingBranch ? ( | |
| 274 | <form | |
| 275 | onSubmit={(event) => { | |
| 276 | event.preventDefault(); | |
| 277 | createBranch(); | |
| 278 | }} | |
| 279 | > | |
| 280 | <InputGroup className="rounded-t-none before:rounded-t-none"> | |
| 281 | <InputGroupInput | |
| 282 | className="[&_input]:font-sans" | |
| 283 | size="sm" | |
| 282 | 284 | placeholder="branch-name" |
| 285 | type="text" | |
| 283 | 286 | value={newBranchName} |
| 284 | 287 | onChange={(event) => setNewBranchName(event.target.value)} |
| 285 | 288 | onKeyDown={(event) => { |
| 286 | 289 | if (event.key === "Escape") { |
| 290 | event.stopPropagation(); | |
| 287 | 291 | setIsCreatingBranch(false); |
| 288 | 292 | setNewBranchName(""); |
| 289 | 293 | } |
| 290 | 294 | }} |
| 291 | // biome-ignore lint/a11y/noAutofocus: branch name input should focus when shown | |
| 292 | 295 | autoFocus |
| 293 | 296 | /> |
| 294 | <button | |
| 295 | type="submit" | |
| 296 | disabled={!newBranchName.trim() || createBranchMutation.isPending} | |
| 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" | |
| 298 | > | |
| 299 | Create | |
| 300 | </button> | |
| 301 | </form> | |
| 302 | ) : ( | |
| 303 | <button | |
| 304 | type="button" | |
| 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" | |
| 306 | onClick={() => setIsCreatingBranch(true)} | |
| 307 | > | |
| 308 | + New branch | |
| 309 | </button> | |
| 310 | )} | |
| 311 | </> | |
| 312 | )} | |
| 313 | </div> | |
| 314 | )} | |
| 315 | </div> | |
| 297 | <InputGroupAddon align="inline-end"> | |
| 298 | <Button | |
| 299 | size="xs" | |
| 300 | variant="secondary" | |
| 301 | type="submit" | |
| 302 | disabled={!newBranchName.trim() || createBranchMutation.isPending} | |
| 303 | > | |
| 304 | Create | |
| 305 | </Button> | |
| 306 | </InputGroupAddon> | |
| 307 | </InputGroup> | |
| 308 | </form> | |
| 309 | ) : ( | |
| 310 | <Button | |
| 311 | size="sm" | |
| 312 | className="w-full justify-start h-9 rounded-t-none rounded-b-lg before:rounded-t-none" | |
| 313 | variant="outline" | |
| 314 | onClick={() => setIsCreatingBranch(true)} | |
| 315 | > | |
| 316 | <PlusIcon /> | |
| 317 | New branch | |
| 318 | </Button> | |
| 319 | )} | |
| 320 | </> | |
| 321 | )} | |
| 322 | </ComboboxPopup> | |
| 323 | </Combobox> | |
| 316 | 324 | )} |
| 317 | 325 | </div> |
| 318 | 326 | ); |
apps/web/src/components/ChatView.tsx+274-353| ... | ... | @@ -4,6 +4,7 @@ import { |
| 4 | 4 | PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, |
| 5 | 5 | type ProviderSendTurnAttachmentInput, |
| 6 | 6 | type ProviderApprovalDecision, |
| 7 | ModelSlug, | |
| 7 | 8 | } from "@t3tools/contracts"; |
| 8 | 9 | import { |
| 9 | 10 | type ClipboardEvent, |
| ... | ... | @@ -27,6 +28,7 @@ import { |
| 27 | 28 | DEFAULT_REASONING, |
| 28 | 29 | MODEL_OPTIONS, |
| 29 | 30 | REASONING_OPTIONS, |
| 31 | ReasoningEffort, | |
| 30 | 32 | resolveModelSlug, |
| 31 | 33 | } from "../model-logic"; |
| 32 | 34 | import { |
| ... | ... | @@ -46,22 +48,30 @@ import { isTerminalToggleShortcut } from "../terminal-shortcuts"; |
| 46 | 48 | import ChatMarkdown from "./ChatMarkdown"; |
| 47 | 49 | import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; |
| 48 | 50 | import { useNativeApi } from "../hooks/useNativeApi"; |
| 51 | import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert"; | |
| 52 | import { | |
| 53 | ChevronDownIcon, | |
| 54 | CircleAlertIcon, | |
| 55 | FolderClosedIcon, | |
| 56 | InfoIcon, | |
| 57 | LockIcon, | |
| 58 | LockOpenIcon, | |
| 59 | XIcon, | |
| 60 | } from "lucide-react"; | |
| 61 | import { Button } from "./ui/button"; | |
| 62 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; | |
| 63 | import { Separator } from "./ui/separator"; | |
| 64 | import { Group, GroupSeparator } from "./ui/group"; | |
| 65 | import { Menu, MenuItem, MenuPopup, MenuShortcut, MenuTrigger } from "./ui/menu"; | |
| 66 | import { CursorIcon, Icon } from "./Icons"; | |
| 67 | import { cn } from "~/lib/utils"; | |
| 68 | import { Badge } from "./ui/badge"; | |
| 49 | 69 | |
| 50 | 70 | function formatMessageMeta(createdAt: string, duration: string | null): string { |
| 51 | 71 | if (!duration) return formatTimestamp(createdAt); |
| 52 | 72 | return `${formatTimestamp(createdAt)} • ${duration}`; |
| 53 | 73 | } |
| 54 | 74 | |
| 55 | const FILE_MANAGER_LABEL = navigator.platform.includes("Mac") | |
| 56 | ? "Finder" | |
| 57 | : navigator.platform.includes("Win") | |
| 58 | ? "Explorer" | |
| 59 | : "Files"; | |
| 60 | ||
| 61 | function editorLabel(editor: (typeof EDITORS)[number]): string { | |
| 62 | return editor.command ? editor.label : FILE_MANAGER_LABEL; | |
| 63 | } | |
| 64 | ||
| 65 | 75 | const LAST_EDITOR_KEY = "t3code:last-editor"; |
| 66 | 76 | const MAX_VISIBLE_WORK_LOG_ENTRIES = 6; |
| 67 | 77 | const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; |
| ... | ... | @@ -119,13 +129,7 @@ export default function ChatView() { |
| 119 | 129 | const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null); |
| 120 | 130 | const [isSending, setIsSending] = useState(false); |
| 121 | 131 | const [isConnecting, setIsConnecting] = useState(false); |
| 122 | const [isModelMenuOpen, setIsModelMenuOpen] = useState(false); | |
| 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); | |
| 132 | const [selectedEffort, setSelectedEffort] = useState(DEFAULT_REASONING); | |
| 129 | 133 | const [envMode, setEnvMode] = useState<"local" | "worktree">("local"); |
| 130 | 134 | const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false); |
| 131 | 135 | const [respondingRequestIds, setRespondingRequestIds] = useState<string[]>([]); |
| ... | ... | @@ -137,8 +141,6 @@ export default function ChatView() { |
| 137 | 141 | const textareaRef = useRef<HTMLTextAreaElement>(null); |
| 138 | 142 | const composerImagesRef = useRef<ComposerImageAttachment[]>([]); |
| 139 | 143 | const dragDepthRef = useRef(0); |
| 140 | const modelMenuRef = useRef<HTMLDivElement>(null); | |
| 141 | const editorMenuRef = useRef<HTMLDivElement>(null); | |
| 142 | 144 | const terminalOpenByThreadRef = useRef<Record<string, boolean>>({}); |
| 143 | 145 | |
| 144 | 146 | const activeThread = state.threads.find((t) => t.id === state.activeThreadId); |
| ... | ... | @@ -150,7 +152,6 @@ export default function ChatView() { |
| 150 | 152 | const phase = derivePhase(activeThread?.session ?? null); |
| 151 | 153 | const isWorking = phase === "running" || isSending || isConnecting; |
| 152 | 154 | const nowIso = new Date(nowTick).toISOString(); |
| 153 | const modelOptions = MODEL_OPTIONS; | |
| 154 | 155 | const workLogEntries = useMemo( |
| 155 | 156 | () => deriveWorkLogEntries(activeThread?.events ?? [], undefined), |
| 156 | 157 | [activeThread?.events], |
| ... | ... | @@ -304,7 +305,9 @@ export default function ChatView() { |
| 304 | 305 | (terminalId: string) => { |
| 305 | 306 | if (!activeThreadId || !api) return; |
| 306 | 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 | 311 | if ("close" in api.terminal && typeof api.terminal.close === "function") { |
| 309 | 312 | void api.terminal |
| 310 | 313 | .close({ threadId: activeThreadId, terminalId }) |
| ... | ... | @@ -395,22 +398,21 @@ export default function ChatView() { |
| 395 | 398 | }; |
| 396 | 399 | }, [revokePreviewUrls]); |
| 397 | 400 | |
| 401 | /** | |
| 402 | * Close expanded image on Escape key | |
| 403 | */ | |
| 398 | 404 | useEffect(() => { |
| 399 | 405 | if (!expandedImage) { |
| 400 | 406 | return; |
| 401 | 407 | } |
| 402 | 408 | |
| 403 | 409 | const onKeyDown = (event: globalThis.KeyboardEvent) => { |
| 404 | if (event.key !== "Escape") { | |
| 405 | return; | |
| 406 | } | |
| 410 | if (event.key !== "Escape") return; | |
| 407 | 411 | setExpandedImage(null); |
| 408 | 412 | }; |
| 409 | 413 | |
| 410 | 414 | window.addEventListener("keydown", onKeyDown); |
| 411 | return () => { | |
| 412 | window.removeEventListener("keydown", onKeyDown); | |
| 413 | }; | |
| 415 | return () => window.removeEventListener("keydown", onKeyDown); | |
| 414 | 416 | }, [expandedImage]); |
| 415 | 417 | |
| 416 | 418 | const activeWorktreePath = activeThread?.worktreePath; |
| ... | ... | @@ -438,53 +440,6 @@ export default function ChatView() { |
| 438 | 440 | }; |
| 439 | 441 | }, [phase]); |
| 440 | 442 | |
| 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 | 443 | useEffect(() => { |
| 489 | 444 | if (!activeThreadId) return; |
| 490 | 445 | const previous = terminalOpenByThreadRef.current[activeThreadId] ?? false; |
| ... | ... | @@ -516,15 +471,6 @@ export default function ChatView() { |
| 516 | 471 | return () => window.removeEventListener("keydown", handler); |
| 517 | 472 | }, [activeThreadId, toggleTerminalVisibility]); |
| 518 | 473 | |
| 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 | 474 | const setThreadError = (threadId: string | null, error: string | null) => { |
| 529 | 475 | if (!threadId) return; |
| 530 | 476 | dispatch({ |
| ... | ... | @@ -579,13 +525,6 @@ export default function ChatView() { |
| 579 | 525 | }); |
| 580 | 526 | }; |
| 581 | 527 | |
| 582 | const openImageDialog = (src: string | undefined, name: string) => { | |
| 583 | if (!src) { | |
| 584 | return; | |
| 585 | } | |
| 586 | setExpandedImage({ src, name }); | |
| 587 | }; | |
| 588 | ||
| 589 | 528 | const onComposerPaste = (event: ClipboardEvent<HTMLTextAreaElement>) => { |
| 590 | 529 | const files = Array.from(event.clipboardData.files); |
| 591 | 530 | if (files.length === 0) { |
| ... | ... | @@ -782,13 +721,15 @@ export default function ChatView() { |
| 782 | 721 | setIsSending(true); |
| 783 | 722 | try { |
| 784 | 723 | const turnAttachments = await Promise.all( |
| 785 | composerImagesSnapshot.map(async (image): Promise<ProviderSendTurnAttachmentInput> => ({ | |
| 786 | type: "image", | |
| 787 | name: image.name, | |
| 788 | mimeType: image.mimeType, | |
| 789 | sizeBytes: image.sizeBytes, | |
| 790 | dataUrl: await readFileAsDataUrl(image.file), | |
| 791 | })), | |
| 724 | composerImagesSnapshot.map( | |
| 725 | async (image): Promise<ProviderSendTurnAttachmentInput> => ({ | |
| 726 | type: "image", | |
| 727 | name: image.name, | |
| 728 | mimeType: image.mimeType, | |
| 729 | sizeBytes: image.sizeBytes, | |
| 730 | dataUrl: await readFileAsDataUrl(image.file), | |
| 731 | }), | |
| 732 | ), | |
| 792 | 733 | ); |
| 793 | 734 | const shouldBootstrap = |
| 794 | 735 | previousMessages.length > 0 && |
| ... | ... | @@ -800,7 +741,7 @@ export default function ChatView() { |
| 800 | 741 | latestPromptForBootstrap, |
| 801 | 742 | PROVIDER_SEND_TURN_MAX_INPUT_CHARS, |
| 802 | 743 | ).text |
| 803 | : (trimmed || undefined); | |
| 744 | : trimmed || undefined; | |
| 804 | 745 | await api.providers.sendTurn({ |
| 805 | 746 | sessionId: sessionInfo.sessionId, |
| 806 | 747 | ...(input ? { input } : {}), |
| ... | ... | @@ -849,14 +790,13 @@ export default function ChatView() { |
| 849 | 790 | } |
| 850 | 791 | }; |
| 851 | 792 | |
| 852 | const onModelSelect = (model: string) => { | |
| 793 | const onModelSelect = (model: ModelSlug) => { | |
| 853 | 794 | if (!activeThread) return; |
| 854 | 795 | dispatch({ |
| 855 | 796 | type: "SET_THREAD_MODEL", |
| 856 | 797 | threadId: activeThread.id, |
| 857 | 798 | model: resolveModelSlug(model), |
| 858 | 799 | }); |
| 859 | setIsModelMenuOpen(false); | |
| 860 | 800 | }; |
| 861 | 801 | |
| 862 | 802 | const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => { |
| ... | ... | @@ -892,132 +832,97 @@ export default function ChatView() { |
| 892 | 832 | > |
| 893 | 833 | <div className="flex items-center gap-3"> |
| 894 | 834 | <h2 className="text-sm font-medium text-foreground">{activeThread.title}</h2> |
| 895 | {activeProject && ( | |
| 896 | <span className="rounded-full bg-accent px-2 py-0.5 text-[10px] text-muted-foreground/50"> | |
| 897 | {activeProject.name} | |
| 898 | </span> | |
| 899 | )} | |
| 835 | {activeProject && <Badge variant="outline">{activeProject.name}</Badge>} | |
| 900 | 836 | </div> |
| 901 | 837 | <div className="flex items-center gap-3"> |
| 902 | 838 | {/* Open in editor */} |
| 903 | {activeProject && ( | |
| 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 | )} | |
| 839 | {activeProject && <OpenInPicker />} | |
| 933 | 840 | {/* Git actions */} |
| 934 | 841 | {activeProject && <GitActionsControl api={api} gitCwd={gitCwd} />} |
| 935 | 842 | |
| 936 | 843 | {/* Diff toggle */} |
| 937 | <button | |
| 938 | type="button" | |
| 939 | className={`rounded-md px-2 py-1 text-[10px] transition-colors duration-150 ${ | |
| 940 | state.diffOpen | |
| 941 | ? "bg-accent text-foreground" | |
| 942 | : "text-muted-foreground/40 hover:text-muted-foreground/60" | |
| 943 | }`} | |
| 844 | <Button | |
| 845 | size="xs" | |
| 846 | variant="ghost" | |
| 847 | className={cn( | |
| 848 | "text-muted-foreground/70 hover:text-foreground/80", | |
| 849 | state.diffOpen && "bg-accent text-accent-foreground", | |
| 850 | )} | |
| 944 | 851 | onClick={() => dispatch({ type: "TOGGLE_DIFF" })} |
| 945 | 852 | > |
| 946 | 853 | Diff |
| 947 | </button> | |
| 854 | </Button> | |
| 948 | 855 | </div> |
| 949 | 856 | </header> |
| 950 | 857 | |
| 951 | 858 | {/* Error banner */} |
| 952 | 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"> | |
| 954 | {activeThread.error} | |
| 860 | <div className="pt-3 mx-auto max-w-3xl"> | |
| 861 | <Alert variant="error"> | |
| 862 | <CircleAlertIcon /> | |
| 863 | <AlertDescription className="line-clamp-3" title={activeThread.error}> | |
| 864 | {activeThread.error} | |
| 865 | </AlertDescription> | |
| 866 | </Alert> | |
| 955 | 867 | </div> |
| 956 | 868 | )} |
| 957 | 869 | |
| 958 | 870 | {pendingApprovals.length > 0 && ( |
| 959 | <div className="px-5 pt-3"> | |
| 960 | <div className="mx-auto max-w-3xl space-y-2"> | |
| 961 | {pendingApprovals.map((approval) => { | |
| 962 | const isResponding = respondingRequestIds.includes(approval.requestId); | |
| 963 | return ( | |
| 964 | <div | |
| 965 | key={approval.requestId} | |
| 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]" | |
| 871 | <div className="pt-3 mx-auto max-w-3xl space-y-2"> | |
| 872 | {pendingApprovals.map((approval) => { | |
| 873 | const isResponding = respondingRequestIds.includes(approval.requestId); | |
| 874 | ||
| 875 | return ( | |
| 876 | <Alert variant="warning" key={approval.requestId}> | |
| 877 | <InfoIcon /> | |
| 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"> | |
| 969 | {approval.requestKind === "command" | |
| 970 | ? "Command approval requested" | |
| 971 | : "File-change approval requested"} | |
| 972 | </p> | |
| 973 | {approval.detail && ( | |
| 974 | <p | |
| 975 | className="mt-1 truncate font-mono text-[11px] text-amber-900/80 dark:text-amber-100/75" | |
| 976 | title={approval.detail} | |
| 977 | > | |
| 978 | {approval.detail} | |
| 979 | </p> | |
| 980 | )} | |
| 981 | <div className="mt-2 flex flex-wrap gap-1.5"> | |
| 982 | <button | |
| 983 | type="button" | |
| 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" | |
| 985 | disabled={isResponding} | |
| 986 | onClick={() => void onRespondToApproval(approval.requestId, "accept")} | |
| 987 | > | |
| 988 | Approve once | |
| 989 | </button> | |
| 990 | <button | |
| 991 | type="button" | |
| 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" | |
| 993 | disabled={isResponding} | |
| 994 | onClick={() => | |
| 995 | void onRespondToApproval(approval.requestId, "acceptForSession") | |
| 996 | } | |
| 997 | > | |
| 998 | Always allow this session | |
| 999 | </button> | |
| 1000 | <button | |
| 1001 | type="button" | |
| 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" | |
| 1003 | disabled={isResponding} | |
| 1004 | onClick={() => void onRespondToApproval(approval.requestId, "decline")} | |
| 1005 | > | |
| 1006 | Decline | |
| 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> | |
| 887 | {approval.detail} | |
| 888 | </AlertDescription> | |
| 889 | <AlertAction className="col-start-2! -col-end-1! mt-1.5 sm:row-start-auto sm:row-end-auto"> | |
| 890 | <Button | |
| 891 | size="xs" | |
| 892 | variant="default" | |
| 893 | disabled={isResponding} | |
| 894 | onClick={() => void onRespondToApproval(approval.requestId, "accept")} | |
| 895 | > | |
| 896 | Approve once | |
| 897 | </Button> | |
| 898 | <Button | |
| 899 | size="xs" | |
| 900 | variant="outline" | |
| 901 | disabled={isResponding} | |
| 902 | onClick={() => void onRespondToApproval(approval.requestId, "acceptForSession")} | |
| 903 | > | |
| 904 | Always allow this session | |
| 905 | </Button> | |
| 906 | <Button | |
| 907 | size="xs" | |
| 908 | variant="destructive-outline" | |
| 909 | disabled={isResponding} | |
| 910 | onClick={() => void onRespondToApproval(approval.requestId, "decline")} | |
| 911 | > | |
| 912 | Decline | |
| 913 | </Button> | |
| 914 | <Button | |
| 915 | size="xs" | |
| 916 | variant="ghost" | |
| 917 | disabled={isResponding} | |
| 918 | onClick={() => void onRespondToApproval(approval.requestId, "cancel")} | |
| 919 | > | |
| 920 | Cancel turn | |
| 921 | </Button> | |
| 922 | </AlertAction> | |
| 923 | </Alert> | |
| 924 | ); | |
| 925 | })} | |
| 1021 | 926 | </div> |
| 1022 | 927 | )} |
| 1023 | 928 | |
| ... | ... | @@ -1140,7 +1045,9 @@ export default function ChatView() { |
| 1140 | 1045 | src={image.previewUrl} |
| 1141 | 1046 | alt={image.name} |
| 1142 | 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 | 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 | 1059 | </div> |
| 1153 | 1060 | )} |
| 1154 | 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 | 1063 | {timelineEntry.message.text} |
| 1157 | 1064 | </pre> |
| 1158 | 1065 | )} |
| ... | ... | @@ -1241,21 +1148,24 @@ export default function ChatView() { |
| 1241 | 1148 | src={image.previewUrl} |
| 1242 | 1149 | alt={image.name} |
| 1243 | 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 | 1156 | <div className="flex h-full w-full items-center justify-center px-1 text-center text-[10px] text-muted-foreground/70"> |
| 1248 | 1157 | {image.name} |
| 1249 | 1158 | </div> |
| 1250 | 1159 | )} |
| 1251 | <button | |
| 1252 | type="button" | |
| 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" | |
| 1160 | <Button | |
| 1161 | variant="ghost" | |
| 1162 | size="icon-xs" | |
| 1163 | className="absolute right-1 top-1 bg-background/80 hover:bg-background/90" | |
| 1254 | 1164 | onClick={() => removeComposerImage(image.id)} |
| 1255 | 1165 | aria-label={`Remove ${image.name}`} |
| 1256 | 1166 | > |
| 1257 | × | |
| 1258 | </button> | |
| 1167 | <XIcon /> | |
| 1168 | </Button> | |
| 1259 | 1169 | </div> |
| 1260 | 1170 | ))} |
| 1261 | 1171 | </div> |
| ... | ... | @@ -1281,110 +1191,23 @@ export default function ChatView() { |
| 1281 | 1191 | <div className="flex items-center justify-between px-3 pb-3"> |
| 1282 | 1192 | <div className="flex items-center gap-1"> |
| 1283 | 1193 | {/* Model picker */} |
| 1284 | <div className="relative" ref={modelMenuRef}> | |
| 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> | |
| 1194 | <ModelPicker model={selectedModel} onModelChange={onModelSelect} /> | |
| 1340 | 1195 | |
| 1341 | 1196 | {/* Divider */} |
| 1342 | <div className="mx-0.5 h-4 w-px bg-border" /> | |
| 1197 | <Separator orientation="vertical" className="mx-0.5 h-4" /> | |
| 1343 | 1198 | |
| 1344 | 1199 | {/* Reasoning effort */} |
| 1345 | <label | |
| 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> | |
| 1200 | <ReasoningEffortPicker effort={selectedEffort} onEffortChange={setSelectedEffort} /> | |
| 1380 | 1201 | |
| 1381 | 1202 | {/* Divider */} |
| 1382 | <div className="mx-0.5 h-4 w-px bg-border" /> | |
| 1203 | <Separator orientation="vertical" className="mx-0.5 h-4" /> | |
| 1383 | 1204 | |
| 1384 | 1205 | {/* Runtime mode toggle */} |
| 1385 | <button | |
| 1206 | <Button | |
| 1207 | variant="ghost" | |
| 1208 | className="text-muted-foreground/70 hover:text-foreground/80" | |
| 1209 | size="sm" | |
| 1386 | 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 | 1211 | disabled={isSwitchingRuntimeMode} |
| 1389 | 1212 | onClick={() => |
| 1390 | 1213 | void handleRuntimeModeChange( |
| ... | ... | @@ -1397,45 +1220,9 @@ export default function ChatView() { |
| 1397 | 1220 | : "Approval required — click for full access" |
| 1398 | 1221 | } |
| 1399 | 1222 | > |
| 1400 | {state.runtimeMode === "full-access" ? ( | |
| 1401 | <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"> | |
| 1402 | <rect | |
| 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> | |
| 1223 | {state.runtimeMode === "full-access" ? <LockOpenIcon /> : <LockIcon />} | |
| 1224 | {state.runtimeMode === "full-access" ? "Full access" : "Supervised"} | |
| 1225 | </Button> | |
| 1439 | 1226 | </div> |
| 1440 | 1227 | |
| 1441 | 1228 | {/* Right side: send / stop button */} |
| ... | ... | @@ -1461,7 +1248,9 @@ export default function ChatView() { |
| 1461 | 1248 | <button |
| 1462 | 1249 | type="submit" |
| 1463 | 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 | 1254 | aria-label={ |
| 1466 | 1255 | isConnecting ? "Connecting" : isSending ? "Sending" : "Send message" |
| 1467 | 1256 | } |
| ... | ... | @@ -1550,14 +1339,16 @@ export default function ChatView() { |
| 1550 | 1339 | className="relative isolate max-h-[92vh] max-w-[92vw]" |
| 1551 | 1340 | onClick={(event) => event.stopPropagation()} |
| 1552 | 1341 | > |
| 1553 | <button | |
| 1342 | <Button | |
| 1554 | 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 | 1347 | onClick={() => setExpandedImage(null)} |
| 1557 | 1348 | aria-label="Close image preview" |
| 1558 | 1349 | > |
| 1559 | <span className="pointer-events-none">×</span> | |
| 1560 | </button> | |
| 1350 | <XIcon /> | |
| 1351 | </Button> | |
| 1561 | 1352 | <img |
| 1562 | 1353 | src={expandedImage.src} |
| 1563 | 1354 | alt={expandedImage.name} |
| ... | ... | @@ -1573,3 +1364,133 @@ export default function ChatView() { |
| 1573 | 1364 | </div> |
| 1574 | 1365 | ); |
| 1575 | 1366 | } |
| 1367 | ||
| 1368 | function 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 | ||
| 1389 | function 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 | ||
| 1413 | function 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 | import { XIcon } from "lucide-react"; | |
| 1 | 2 | import { isElectron } from "../env"; |
| 2 | 3 | import { useStore } from "../store"; |
| 4 | import { Button } from "./ui/button"; | |
| 3 | 5 | |
| 4 | 6 | export default function DiffPanel() { |
| 5 | 7 | const { dispatch } = useStore(); |
| ... | ... | @@ -11,13 +13,14 @@ export default function DiffPanel() { |
| 11 | 13 | className={`flex items-center justify-between border-b border-border px-4 ${isElectron ? "drag-region h-[52px]" : "py-3"}`} |
| 12 | 14 | > |
| 13 | 15 | <h3 className="text-xs font-medium text-foreground">Uncommitted changes</h3> |
| 14 | <button | |
| 16 | <Button | |
| 15 | 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 | 20 | onClick={() => dispatch({ type: "TOGGLE_DIFF" })} |
| 18 | 21 | > |
| 19 | <span className="text-sm">&times;</span> | |
| 20 | </button> | |
| 22 | <XIcon /> | |
| 23 | </Button> | |
| 21 | 24 | </div> |
| 22 | 25 | |
| 23 | 26 | {/* Placeholder content */} |
apps/web/src/components/GitActionsControl.tsx+136-670| ... | ... | @@ -5,15 +5,13 @@ import { |
| 5 | 5 | type NativeApi, |
| 6 | 6 | } from "@t3tools/contracts"; |
| 7 | 7 | import { queryOptions, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; |
| 8 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | |
| 9 | import { | |
| 10 | CheckIcon, | |
| 11 | CloudUploadIcon, | |
| 12 | GitCommitIcon, | |
| 13 | Loader2Icon, | |
| 14 | XIcon, | |
| 15 | } from "lucide-react"; | |
| 8 | import { useCallback, useEffect, useMemo, useState } from "react"; | |
| 9 | import { ChevronDownIcon, CloudUploadIcon, GitCommitIcon } from "lucide-react"; | |
| 16 | 10 | import { GitHubIcon } from "./Icons"; |
| 11 | import { Button } from "~/components/ui/button"; | |
| 12 | import { Group, GroupSeparator } from "~/components/ui/group"; | |
| 13 | import { Menu, MenuItem, MenuPopup, MenuTrigger } from "~/components/ui/menu"; | |
| 14 | import { toastManager } from "~/components/ui/toast"; | |
| 17 | 15 | |
| 18 | 16 | interface GitActionsControlProps { |
| 19 | 17 | api: NativeApi | undefined; |
| ... | ... | @@ -27,112 +25,38 @@ interface GitActionMenuItem { |
| 27 | 25 | label: string; |
| 28 | 26 | disabled: boolean; |
| 29 | 27 | icon: GitActionIconName; |
| 30 | kind: "open_modal" | "run_action" | "open_pr"; | |
| 28 | kind: "run_action" | "open_pr"; | |
| 31 | 29 | action?: GitStackedAction; |
| 32 | 30 | } |
| 33 | 31 | |
| 34 | interface GitModalActionOption { | |
| 35 | action: GitStackedAction; | |
| 36 | label: string; | |
| 37 | disabled: boolean; | |
| 38 | icon: GitActionIconName; | |
| 39 | detail?: string; | |
| 40 | } | |
| 41 | ||
| 42 | type GitProgressStepStatus = "pending" | "running" | "completed" | "skipped" | "failed"; | |
| 43 | ||
| 44 | interface GitProgressStep { | |
| 45 | id: "generate" | "commit" | "push" | "pr"; | |
| 46 | label: string; | |
| 47 | status: GitProgressStepStatus; | |
| 48 | detail?: string; | |
| 49 | } | |
| 50 | ||
| 51 | function 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 | ||
| 65 | function 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..."; | |
| 32 | function GitActionItemIcon({ icon }: { icon: GitActionIconName }) { | |
| 33 | if (icon === "commit") return <GitCommitIcon />; | |
| 34 | if (icon === "push") return <CloudUploadIcon />; | |
| 35 | return <GitHubIcon />; | |
| 70 | 36 | } |
| 71 | 37 | |
| 72 | function gitActionModalTitle(): string { | |
| 73 | return "Commit your changes"; | |
| 74 | } | |
| 38 | function describeGitResult(result: GitRunStackedActionResult): string { | |
| 39 | const parts: string[] = []; | |
| 75 | 40 | |
| 76 | function initialGitProgressSteps( | |
| 77 | action: GitStackedAction, | |
| 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 | }); | |
| 41 | if (result.commit.status === "created") { | |
| 42 | parts.push(result.commit.subject ?? "Committed changes"); | |
| 89 | 43 | } |
| 90 | 44 | |
| 91 | steps.push({ | |
| 92 | id: "commit", | |
| 93 | label: "Commit changes", | |
| 94 | status: "pending", | |
| 95 | }); | |
| 96 | ||
| 97 | if (action !== "commit") { | |
| 98 | steps.push({ | |
| 99 | id: "push", | |
| 100 | label: "Push branch", | |
| 101 | status: "pending", | |
| 102 | }); | |
| 45 | if (result.push.status === "pushed") { | |
| 46 | parts.push( | |
| 47 | result.push.upstreamBranch | |
| 48 | ? `Pushed to ${result.push.upstreamBranch}` | |
| 49 | : "Pushed", | |
| 50 | ); | |
| 103 | 51 | } |
| 104 | 52 | |
| 105 | if (action === "commit_push_pr") { | |
| 106 | steps.push({ | |
| 107 | id: "pr", | |
| 108 | label: "Create or open PR", | |
| 109 | status: "pending", | |
| 110 | }); | |
| 53 | if (result.pr.status === "created") { | |
| 54 | parts.push(result.pr.number ? `Created PR #${result.pr.number}` : "Created PR"); | |
| 55 | } else if (result.pr.status === "opened_existing") { | |
| 56 | parts.push(result.pr.number ? `PR #${result.pr.number}` : "Opened existing PR"); | |
| 111 | 57 | } |
| 112 | 58 | |
| 113 | return steps; | |
| 114 | } | |
| 115 | ||
| 116 | function 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 | ||
| 132 | function 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"; | |
| 59 | return parts.join(" · ") || "No changes needed."; | |
| 136 | 60 | } |
| 137 | 61 | |
| 138 | 62 | function buildGitActionMenuItems( |
| ... | ... | @@ -162,7 +86,8 @@ function buildGitActionMenuItems( |
| 162 | 86 | label: "Commit", |
| 163 | 87 | disabled: !canCommit, |
| 164 | 88 | icon: "commit", |
| 165 | kind: "open_modal", | |
| 89 | kind: "run_action", | |
| 90 | action: "commit", | |
| 166 | 91 | }, |
| 167 | 92 | { |
| 168 | 93 | id: "push", |
| ... | ... | @@ -191,41 +116,6 @@ function buildGitActionMenuItems( |
| 191 | 116 | ]; |
| 192 | 117 | } |
| 193 | 118 | |
| 194 | function 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 | ||
| 229 | 119 | function getGitStatusQueryKey(gitCwd: string | null): readonly ["git", "status", string | null] { |
| 230 | 120 | return ["git", "status", gitCwd]; |
| 231 | 121 | } |
| ... | ... | @@ -245,17 +135,7 @@ function gitStatusQueryOptions(api: NativeApi | undefined, gitCwd: string | null |
| 245 | 135 | |
| 246 | 136 | export default function GitActionsControl({ api, gitCwd }: GitActionsControlProps) { |
| 247 | 137 | const queryClient = useQueryClient(); |
| 248 | const [isGitMenuOpen, setIsGitMenuOpen] = useState(false); | |
| 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); | |
| 138 | const [selectedGitActionId, setSelectedGitActionId] = useState<GitActionMenuItem["id"]>("commit"); | |
| 259 | 139 | |
| 260 | 140 | const { data: gitStatus = null, error: gitStatusError } = useQuery( |
| 261 | 141 | gitStatusQueryOptions(api, gitCwd), |
| ... | ... | @@ -268,581 +148,167 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp |
| 268 | 148 | } |
| 269 | 149 | return api.git.runStackedAction({ cwd: gitCwd, action }); |
| 270 | 150 | }, |
| 271 | onMutate: () => { | |
| 272 | setIsGitMenuOpen(false); | |
| 273 | setGitActionError(null); | |
| 274 | setGitModalError(null); | |
| 275 | }, | |
| 276 | 151 | onSuccess: async () => { |
| 277 | 152 | await queryClient.invalidateQueries({ queryKey: ["git"] }); |
| 278 | 153 | }, |
| 279 | onError: (error) => { | |
| 280 | setGitActionError(error instanceof Error ? error.message : "Git action failed."); | |
| 281 | }, | |
| 282 | 154 | }); |
| 283 | 155 | |
| 284 | const isGitActionRunning = isGitModalActionRunning || runImmediateGitActionMutation.isPending; | |
| 156 | const isGitActionRunning = runImmediateGitActionMutation.isPending; | |
| 285 | 157 | |
| 286 | 158 | const gitBaseDisabled = !api || !gitCwd || !gitStatus || isGitActionRunning; |
| 287 | 159 | const gitActionMenuItems = useMemo( |
| 288 | 160 | () => buildGitActionMenuItems(gitStatus, gitBaseDisabled), |
| 289 | 161 | [gitBaseDisabled, gitStatus], |
| 290 | 162 | ); |
| 291 | const gitModalActionOptions = useMemo( | |
| 292 | () => buildGitModalActionOptions(gitStatus, !gitStatus || isGitActionRunning), | |
| 293 | [gitStatus, isGitActionRunning], | |
| 294 | ); | |
| 295 | const selectedGitModalActionOption = useMemo( | |
| 296 | () => gitModalActionOptions.find((option) => option.action === gitModalSelectedAction) ?? null, | |
| 297 | [gitModalActionOptions, gitModalSelectedAction], | |
| 163 | const selectedGitActionItem = useMemo( | |
| 164 | () => gitActionMenuItems.find((item) => item.id === selectedGitActionId) ?? null, | |
| 165 | [gitActionMenuItems, selectedGitActionId], | |
| 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."; | |
| 321 | 167 | |
| 322 | 168 | const refreshGitStatus = useCallback(async () => { |
| 323 | 169 | if (!api || !gitCwd) return; |
| 324 | 170 | await queryClient.invalidateQueries({ queryKey: ["git"] }); |
| 325 | setGitActionError(null); | |
| 326 | 171 | }, [api, gitCwd, queryClient]); |
| 327 | 172 | |
| 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 | 173 | const runGitActionImmediately = useCallback( |
| 348 | 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 | ); |
| 353 | 205 | |
| 354 | 206 | const openExistingPr = useCallback(() => { |
| 355 | setIsGitMenuOpen(false); | |
| 356 | setGitActionError(null); | |
| 357 | 207 | if (!api) { |
| 358 | setGitActionError("Link opening is unavailable."); | |
| 208 | toastManager.add({ type: "error", title: "Link opening is unavailable." }); | |
| 359 | 209 | return; |
| 360 | 210 | } |
| 361 | 211 | |
| 362 | 212 | const prUrl = gitStatus?.openPr?.url ?? null; |
| 363 | 213 | if (!prUrl) { |
| 364 | setGitActionError("No open PR found for the current branch."); | |
| 214 | toastManager.add({ type: "error", title: "No open PR found." }); | |
| 365 | 215 | return; |
| 366 | 216 | } |
| 367 | 217 | |
| 368 | 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 | 225 | }, [api, gitStatus?.openPr?.url]); |
| 372 | 226 | |
| 373 | const openPrFromToast = useCallback(() => { | |
| 374 | if (!api) { | |
| 375 | setGitModalError("Link opening is unavailable."); | |
| 227 | const runSelectedAction = useCallback(() => { | |
| 228 | if (!selectedGitActionItem || selectedGitActionItem.disabled) return; | |
| 229 | if (selectedGitActionItem.kind === "open_pr") { | |
| 230 | openExistingPr(); | |
| 376 | 231 | return; |
| 377 | 232 | } |
| 378 | ||
| 379 | const prUrl = gitProgressToastOpenPrUrl; | |
| 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); | |
| 233 | if (selectedGitActionItem.action) { | |
| 234 | runGitActionImmediately(selectedGitActionItem.action); | |
| 518 | 235 | } |
| 519 | }, [ | |
| 520 | api, | |
| 521 | gitCwd, | |
| 522 | gitModalCommitMessage, | |
| 523 | refreshGitStatus, | |
| 524 | selectedGitModalActionOption, | |
| 525 | ]); | |
| 236 | }, [openExistingPr, runGitActionImmediately, selectedGitActionItem]); | |
| 526 | 237 | |
| 527 | 238 | useEffect(() => { |
| 528 | setGitActionError(null); | |
| 529 | setGitModalError(null); | |
| 530 | setIsGitModalOpen(false); | |
| 531 | setIsGitProgressToastVisible(false); | |
| 532 | setGitModalSelectedAction("commit"); | |
| 533 | setGitModalCommitMessage(""); | |
| 534 | setGitModalProgress([]); | |
| 535 | setGitModalResult(null); | |
| 239 | setSelectedGitActionId("commit"); | |
| 536 | 240 | }, [gitCwd]); |
| 537 | 241 | |
| 538 | 242 | useEffect(() => { |
| 539 | if (!isGitModalOpen) return; | |
| 540 | if (selectedGitModalActionOption && !selectedGitModalActionOption.disabled) return; | |
| 541 | ||
| 542 | const fallback = gitModalActionOptions.find((option) => !option.disabled); | |
| 243 | if (selectedGitActionItem) return; | |
| 244 | const fallback = gitActionMenuItems.find((item) => !item.disabled) ?? gitActionMenuItems[0]; | |
| 543 | 245 | if (fallback) { |
| 544 | setGitModalSelectedAction(fallback.action); | |
| 246 | setSelectedGitActionId(fallback.id); | |
| 545 | 247 | } |
| 546 | }, [gitModalActionOptions, isGitModalOpen, selectedGitModalActionOption]); | |
| 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]); | |
| 248 | }, [gitActionMenuItems, selectedGitActionItem]); | |
| 578 | 249 | |
| 579 | 250 | if (!gitCwd) return null; |
| 580 | 251 | |
| 581 | 252 | return ( |
| 582 | <> | |
| 583 | <div className="relative" ref={gitMenuRef}> | |
| 584 | <button | |
| 585 | type="button" | |
| 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" | |
| 587 | onClick={() => { | |
| 588 | if (!isGitMenuOpen) { | |
| 589 | void refreshGitStatus().catch(() => undefined); | |
| 590 | } | |
| 591 | setIsGitMenuOpen((v) => !v); | |
| 592 | }} | |
| 253 | <Group aria-label="Git actions"> | |
| 254 | <Button | |
| 255 | variant="ghost" | |
| 256 | size="xs" | |
| 257 | className="text-muted-foreground/70 hover:text-foreground/80" | |
| 258 | disabled={!selectedGitActionItem || selectedGitActionItem.disabled || isGitActionRunning} | |
| 259 | onClick={runSelectedAction} | |
| 260 | > | |
| 261 | {isGitActionRunning ? "Running..." : selectedGitActionItem?.label ?? "Git actions"} | |
| 262 | </Button> | |
| 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 | 271 | disabled={!gitStatus || isGitActionRunning} |
| 594 | 272 | > |
| 595 | {isGitActionRunning ? "Running..." : "Git actions"} | |
| 596 | <span aria-hidden="true">▾</span> | |
| 597 | </button> | |
| 598 | {isGitMenuOpen && ( | |
| 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"> | |
| 600 | <p className="px-3 pb-2 text-[13px] text-muted-foreground/75">Git actions</p> | |
| 601 | {gitActionMenuItems.map((item) => { | |
| 602 | return ( | |
| 603 | <button | |
| 604 | key={`${item.id}-${item.label}`} | |
| 605 | type="button" | |
| 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" | |
| 607 | disabled={item.disabled} | |
| 608 | onClick={() => { | |
| 609 | if (item.kind === "open_modal") { | |
| 610 | openGitActionModal(); | |
| 611 | return; | |
| 612 | } | |
| 613 | ||
| 614 | if (item.kind === "open_pr") { | |
| 615 | openExistingPr(); | |
| 616 | return; | |
| 617 | } | |
| 618 | ||
| 619 | if (item.action) { | |
| 620 | void runGitActionImmediately(item.action); | |
| 621 | } | |
| 622 | }} | |
| 623 | > | |
| 624 | <GitActionIcon icon={item.icon} disabled={item.disabled} /> | |
| 625 | <span>{item.label}</span> | |
| 626 | </button> | |
| 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} | |
| 273 | <ChevronDownIcon aria-hidden="true" className="size-4" /> | |
| 274 | </MenuTrigger> | |
| 275 | <MenuPopup align="end" sideOffset={4}> | |
| 276 | {gitActionMenuItems.map((item) => ( | |
| 277 | <MenuItem | |
| 278 | key={`${item.id}-${item.label}`} | |
| 279 | disabled={item.disabled} | |
| 280 | onClick={() => { | |
| 281 | setSelectedGitActionId(item.id); | |
| 282 | if (item.kind === "open_pr") { | |
| 283 | openExistingPr(); | |
| 284 | } else if (item.action) { | |
| 285 | runGitActionImmediately(item.action); | |
| 286 | } | |
| 287 | }} | |
| 288 | > | |
| 289 | <GitActionItemIcon icon={item.icon} /> | |
| 290 | {item.label} | |
| 291 | </MenuItem> | |
| 292 | ))} | |
| 293 | {gitStatus?.branch === null && ( | |
| 294 | <p className="px-2 py-1.5 text-xs text-warning"> | |
| 295 | Detached HEAD: push and PR are unavailable. | |
| 296 | </p> | |
| 297 | )} | |
| 298 | {gitStatus && | |
| 299 | gitStatus.branch !== null && | |
| 300 | !gitStatus.hasWorkingTreeChanges && | |
| 301 | gitStatus.aheadCount === 0 && | |
| 302 | gitStatus.behindCount > 0 && ( | |
| 303 | <p className="px-2 py-1.5 text-xs text-warning"> | |
| 304 | Behind upstream. Pull/rebase first. | |
| 646 | 305 | </p> |
| 647 | 306 | )} |
| 648 | </div> | |
| 649 | )} | |
| 650 | </div> | |
| 651 | ||
| 652 | {isGitModalOpen && ( | |
| 653 | <div | |
| 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 | </> | |
| 307 | {gitStatusError && ( | |
| 308 | <p className="px-2 py-1.5 text-xs text-destructive">{gitStatusError.message}</p> | |
| 309 | )} | |
| 310 | </MenuPopup> | |
| 311 | </Menu> | |
| 312 | </Group> | |
| 847 | 313 | ); |
| 848 | 314 | } |
apps/web/src/components/Icons.tsx+6| ... | ... | @@ -13,3 +13,9 @@ export const GitHubIcon: Icon = (props) => ( |
| 13 | 13 | /> |
| 14 | 14 | </svg> |
| 15 | 15 | ); |
| 16 | ||
| 17 | export 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 | ||
| 3 | import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"; | |
| 4 | ||
| 5 | import { cn } from "~/lib/utils"; | |
| 6 | ||
| 7 | const AlertDialogCreateHandle = AlertDialogPrimitive.createHandle; | |
| 8 | ||
| 9 | const AlertDialog = AlertDialogPrimitive.Root; | |
| 10 | ||
| 11 | const AlertDialogPortal = AlertDialogPrimitive.Portal; | |
| 12 | ||
| 13 | function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) { | |
| 14 | return ( | |
| 15 | <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} /> | |
| 16 | ); | |
| 17 | } | |
| 18 | ||
| 19 | function 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 | ||
| 35 | function 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 | ||
| 51 | function 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 | ||
| 82 | function 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 | ||
| 98 | function 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 | ||
| 119 | function 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 | ||
| 135 | function 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 | ||
| 148 | function AlertDialogClose(props: AlertDialogPrimitive.Close.Props) { | |
| 149 | return ( | |
| 150 | <AlertDialogPrimitive.Close data-slot="alert-dialog-close" {...props} /> | |
| 151 | ); | |
| 152 | } | |
| 153 | ||
| 154 | export { | |
| 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 @@ |
| 1 | import { cva, type VariantProps } from "class-variance-authority"; | |
| 2 | import type * as React from "react"; | |
| 3 | ||
| 4 | import { cn } from "~/lib/utils"; | |
| 5 | ||
| 6 | const alertVariants = cva( | |
| 7 | "relative grid w-full items-start gap-x-2 gap-y-0.5 rounded-xl border px-3.5 py-3 text-card-foreground text-sm has-[>svg]:has-data-[slot=alert-action]:grid-cols-[calc(var(--spacing)*4)_1fr_auto] has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-data-[slot=alert-action]:grid-cols-[1fr_auto] has-[>svg]:gap-x-2 [&>svg]:h-lh [&>svg]:w-4", | |
| 8 | { | |
| 9 | defaultVariants: { | |
| 10 | variant: "default", | |
| 11 | }, | |
| 12 | variants: { | |
| 13 | variant: { | |
| 14 | default: | |
| 15 | "bg-transparent dark:bg-input/32 [&>svg]:text-muted-foreground", | |
| 16 | error: | |
| 17 | "border-destructive/32 bg-destructive/4 [&>svg]:text-destructive", | |
| 18 | info: "border-info/32 bg-info/4 [&>svg]:text-info", | |
| 19 | success: "border-success/32 bg-success/4 [&>svg]:text-success", | |
| 20 | warning: "border-warning/32 bg-warning/4 [&>svg]:text-warning", | |
| 21 | }, | |
| 22 | }, | |
| 23 | }, | |
| 24 | ); | |
| 25 | ||
| 26 | function Alert({ | |
| 27 | className, | |
| 28 | variant, | |
| 29 | ...props | |
| 30 | }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) { | |
| 31 | return ( | |
| 32 | <div | |
| 33 | className={cn(alertVariants({ variant }), className)} | |
| 34 | data-slot="alert" | |
| 35 | role="alert" | |
| 36 | {...props} | |
| 37 | /> | |
| 38 | ); | |
| 39 | } | |
| 40 | ||
| 41 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { | |
| 42 | return ( | |
| 43 | <div | |
| 44 | className={cn("font-medium [svg~&]:col-start-2", className)} | |
| 45 | data-slot="alert-title" | |
| 46 | {...props} | |
| 47 | /> | |
| 48 | ); | |
| 49 | } | |
| 50 | ||
| 51 | function AlertDescription({ | |
| 52 | className, | |
| 53 | ...props | |
| 54 | }: React.ComponentProps<"div">) { | |
| 55 | return ( | |
| 56 | <div | |
| 57 | className={cn( | |
| 58 | "flex flex-col gap-2.5 text-muted-foreground [svg~&]:col-start-2", | |
| 59 | className, | |
| 60 | )} | |
| 61 | data-slot="alert-description" | |
| 62 | {...props} | |
| 63 | /> | |
| 64 | ); | |
| 65 | } | |
| 66 | ||
| 67 | function AlertAction({ className, ...props }: React.ComponentProps<"div">) { | |
| 68 | return ( | |
| 69 | <div | |
| 70 | className={cn( | |
| 71 | "flex gap-1 max-sm:col-start-2 max-sm:mt-2 sm:row-start-1 sm:row-end-3 sm:self-center sm:[[data-slot=alert-description]~&]:col-start-2 sm:[[data-slot=alert-title]~&]:col-start-2 sm:[svg~&]:col-start-2 sm:[svg~[data-slot=alert-description]~&]:col-start-3 sm:[svg~[data-slot=alert-title]~&]:col-start-3", | |
| 72 | className, | |
| 73 | )} | |
| 74 | data-slot="alert-action" | |
| 75 | {...props} | |
| 76 | /> | |
| 77 | ); | |
| 78 | } | |
| 79 | ||
| 80 | export { Alert, AlertTitle, AlertDescription, AlertAction }; |
apps/web/src/components/ui/autocomplete.tsx created+318| ... | ... | @@ -0,0 +1,318 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete"; | |
| 4 | import { ChevronsUpDownIcon, XIcon } from "lucide-react"; | |
| 5 | ||
| 6 | import { cn } from "~/lib/utils"; | |
| 7 | import { Input } from "~/components/ui/input"; | |
| 8 | import { ScrollArea } from "~/components/ui/scroll-area"; | |
| 9 | ||
| 10 | const Autocomplete = AutocompletePrimitive.Root; | |
| 11 | ||
| 12 | function 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 | ||
| 78 | function 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 | ||
| 124 | function 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 | ||
| 143 | function 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 | ||
| 156 | function 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 | ||
| 169 | function 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 | ||
| 185 | function 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 | ||
| 201 | function 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 | ||
| 214 | function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) { | |
| 215 | return ( | |
| 216 | <AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} /> | |
| 217 | ); | |
| 218 | } | |
| 219 | ||
| 220 | function 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 | ||
| 238 | function 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 | ||
| 256 | function 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 | ||
| 272 | function AutocompleteCollection({ | |
| 273 | ...props | |
| 274 | }: AutocompletePrimitive.Collection.Props) { | |
| 275 | return ( | |
| 276 | <AutocompletePrimitive.Collection | |
| 277 | data-slot="autocomplete-collection" | |
| 278 | {...props} | |
| 279 | /> | |
| 280 | ); | |
| 281 | } | |
| 282 | ||
| 283 | function 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 | ||
| 299 | const useAutocompleteFilter = AutocompletePrimitive.useFilter; | |
| 300 | ||
| 301 | export { | |
| 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 | ||
| 3 | import { mergeProps } from "@base-ui/react/merge-props"; | |
| 4 | import { useRender } from "@base-ui/react/use-render"; | |
| 5 | import { cva, type VariantProps } from "class-variance-authority"; | |
| 6 | ||
| 7 | import { cn } from "~/lib/utils"; | |
| 8 | ||
| 9 | const 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 | ||
| 42 | interface BadgeProps extends useRender.ComponentProps<"span"> { | |
| 43 | variant?: VariantProps<typeof badgeVariants>["variant"]; | |
| 44 | size?: VariantProps<typeof badgeVariants>["size"]; | |
| 45 | } | |
| 46 | ||
| 47 | function 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 | ||
| 60 | export { Badge, badgeVariants }; |
apps/web/src/components/ui/button.tsx created+74| ... | ... | @@ -0,0 +1,74 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { mergeProps } from "@base-ui/react/merge-props"; | |
| 4 | import { useRender } from "@base-ui/react/use-render"; | |
| 5 | import { cva, type VariantProps } from "class-variance-authority"; | |
| 6 | import type * as React from "react"; | |
| 7 | ||
| 8 | import { cn } from "~/lib/utils"; | |
| 9 | ||
| 10 | const 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 | ||
| 51 | interface ButtonProps extends useRender.ComponentProps<"button"> { | |
| 52 | variant?: VariantProps<typeof buttonVariants>["variant"]; | |
| 53 | size?: VariantProps<typeof buttonVariants>["size"]; | |
| 54 | } | |
| 55 | ||
| 56 | function 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 | ||
| 74 | export { Button, buttonVariants }; |
apps/web/src/components/ui/card.tsx created+244| ... | ... | @@ -0,0 +1,244 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { mergeProps } from "@base-ui/react/merge-props"; | |
| 4 | import { useRender } from "@base-ui/react/use-render"; | |
| 5 | ||
| 6 | import { cn } from "~/lib/utils"; | |
| 7 | ||
| 8 | function 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 | ||
| 28 | function 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 | ||
| 48 | function 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 | ||
| 65 | function 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 | ||
| 82 | function 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 | ||
| 99 | function 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 | ||
| 116 | function 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 | ||
| 136 | function 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 | ||
| 153 | function 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 | ||
| 170 | function 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 | ||
| 190 | function 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 | ||
| 210 | function 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 | ||
| 230 | export { | |
| 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 | ||
| 3 | import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"; | |
| 4 | ||
| 5 | import { cn } from "~/lib/utils"; | |
| 6 | ||
| 7 | function 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 | ||
| 60 | export { Checkbox }; |
apps/web/src/components/ui/combobox.tsx created+372| ... | ... | @@ -0,0 +1,372 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox"; | |
| 4 | import { CheckIcon, ChevronsUpDownIcon, XIcon } from "lucide-react"; | |
| 5 | import * as React from "react"; | |
| 6 | ||
| 7 | import { cn } from "~/lib/utils"; | |
| 8 | import { Input } from "~/components/ui/input"; | |
| 9 | import { ScrollArea } from "~/components/ui/scroll-area"; | |
| 10 | ||
| 11 | const ComboboxContext = React.createContext<{ | |
| 12 | chipsRef: React.RefObject<Element | null> | null; | |
| 13 | multiple: boolean; | |
| 14 | }>({ | |
| 15 | chipsRef: null, | |
| 16 | multiple: false, | |
| 17 | }); | |
| 18 | ||
| 19 | function 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 | ||
| 30 | function 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 | ||
| 55 | function 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 | ||
| 121 | function 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 | ||
| 129 | function 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 | ||
| 178 | function 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 | ||
| 203 | function 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 | ||
| 213 | function 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 | ||
| 223 | function 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 | ||
| 233 | function 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 | ||
| 246 | function ComboboxRow({ className, ...props }: ComboboxPrimitive.Row.Props) { | |
| 247 | return <ComboboxPrimitive.Row className={className} data-slot="combobox-row" {...props} />; | |
| 248 | } | |
| 249 | ||
| 250 | function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { | |
| 251 | return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />; | |
| 252 | } | |
| 253 | ||
| 254 | function 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 | ||
| 269 | function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { | |
| 270 | return <ComboboxPrimitive.Clear className={className} data-slot="combobox-clear" {...props} />; | |
| 271 | } | |
| 272 | ||
| 273 | function 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 | ||
| 286 | function ComboboxCollection(props: ComboboxPrimitive.Collection.Props) { | |
| 287 | return <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />; | |
| 288 | } | |
| 289 | ||
| 290 | function 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 | ||
| 324 | function 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 | ||
| 337 | function 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 | ||
| 350 | const useComboboxFilter = ComboboxPrimitive.useFilter; | |
| 351 | ||
| 352 | export { | |
| 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 | ||
| 3 | import { Dialog as CommandDialogPrimitive } from "@base-ui/react/dialog"; | |
| 4 | import { SearchIcon } from "lucide-react"; | |
| 5 | import type * as React from "react"; | |
| 6 | import { cn } from "~/lib/utils"; | |
| 7 | import { | |
| 8 | Autocomplete, | |
| 9 | AutocompleteCollection, | |
| 10 | AutocompleteEmpty, | |
| 11 | AutocompleteGroup, | |
| 12 | AutocompleteGroupLabel, | |
| 13 | AutocompleteInput, | |
| 14 | AutocompleteItem, | |
| 15 | AutocompleteList, | |
| 16 | AutocompleteSeparator, | |
| 17 | } from "~/components/ui/autocomplete"; | |
| 18 | ||
| 19 | const CommandDialog = CommandDialogPrimitive.Root; | |
| 20 | ||
| 21 | const CommandDialogPortal = CommandDialogPrimitive.Portal; | |
| 22 | ||
| 23 | const CommandCreateHandle = CommandDialogPrimitive.createHandle; | |
| 24 | ||
| 25 | function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) { | |
| 26 | return ( | |
| 27 | <CommandDialogPrimitive.Trigger | |
| 28 | data-slot="command-dialog-trigger" | |
| 29 | {...props} | |
| 30 | /> | |
| 31 | ); | |
| 32 | } | |
| 33 | ||
| 34 | function 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 | ||
| 50 | function 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 | ||
| 66 | function 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 | ||
| 90 | function 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 | ||
| 106 | function 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 | ||
| 128 | function 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 | ||
| 141 | function 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 | ||
| 154 | function 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 | ||
| 163 | function 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 | ||
| 176 | function 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 | ||
| 189 | function CommandCollection({ | |
| 190 | ...props | |
| 191 | }: React.ComponentProps<typeof AutocompleteCollection>) { | |
| 192 | return <AutocompleteCollection data-slot="command-collection" {...props} />; | |
| 193 | } | |
| 194 | ||
| 195 | function 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 | ||
| 208 | function 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 | ||
| 221 | function 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 | ||
| 234 | function 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 | ||
| 247 | export { | |
| 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 | ||
| 3 | import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"; | |
| 4 | import { XIcon } from "lucide-react"; | |
| 5 | import { cn } from "~/lib/utils"; | |
| 6 | import { Button } from "~/components/ui/button"; | |
| 7 | import { ScrollArea } from "~/components/ui/scroll-area"; | |
| 8 | ||
| 9 | const DialogCreateHandle = DialogPrimitive.createHandle; | |
| 10 | ||
| 11 | const Dialog = DialogPrimitive.Root; | |
| 12 | ||
| 13 | const DialogPortal = DialogPrimitive.Portal; | |
| 14 | ||
| 15 | function DialogTrigger(props: DialogPrimitive.Trigger.Props) { | |
| 16 | return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; | |
| 17 | } | |
| 18 | ||
| 19 | function DialogClose(props: DialogPrimitive.Close.Props) { | |
| 20 | return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; | |
| 21 | } | |
| 22 | ||
| 23 | function 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 | ||
| 39 | function 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 | ||
| 55 | function 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 | ||
| 100 | function 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 | ||
| 113 | function 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 | ||
| 135 | function 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 | ||
| 148 | function 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 | ||
| 161 | function 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 | ||
| 180 | export { | |
| 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 @@ |
| 1 | import { cva, type VariantProps } from "class-variance-authority"; | |
| 2 | ||
| 3 | import { cn } from "~/lib/utils"; | |
| 4 | ||
| 5 | function Empty({ className, ...props }: React.ComponentProps<"div">) { | |
| 6 | return ( | |
| 7 | <div | |
| 8 | className={cn( | |
| 9 | "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 text-balance p-6 text-center md:p-12", | |
| 10 | className, | |
| 11 | )} | |
| 12 | data-slot="empty" | |
| 13 | {...props} | |
| 14 | /> | |
| 15 | ); | |
| 16 | } | |
| 17 | ||
| 18 | function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { | |
| 19 | return ( | |
| 20 | <div | |
| 21 | className={cn( | |
| 22 | "flex max-w-sm flex-col items-center text-center", | |
| 23 | className, | |
| 24 | )} | |
| 25 | data-slot="empty-header" | |
| 26 | {...props} | |
| 27 | /> | |
| 28 | ); | |
| 29 | } | |
| 30 | ||
| 31 | const emptyMediaVariants = cva( | |
| 32 | "flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", | |
| 33 | { | |
| 34 | defaultVariants: { | |
| 35 | variant: "default", | |
| 36 | }, | |
| 37 | variants: { | |
| 38 | variant: { | |
| 39 | default: "bg-transparent", | |
| 40 | icon: "relative flex size-9 shrink-0 items-center justify-center rounded-md border bg-card not-dark:bg-clip-padding text-foreground shadow-sm/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-md)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='size-'])]:size-4.5", | |
| 41 | }, | |
| 42 | }, | |
| 43 | }, | |
| 44 | ); | |
| 45 | ||
| 46 | function EmptyMedia({ | |
| 47 | className, | |
| 48 | variant = "default", | |
| 49 | ...props | |
| 50 | }: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) { | |
| 51 | return ( | |
| 52 | <div | |
| 53 | className={cn("relative mb-6", className)} | |
| 54 | data-slot="empty-media" | |
| 55 | data-variant={variant} | |
| 56 | {...props} | |
| 57 | > | |
| 58 | {variant === "icon" && ( | |
| 59 | <> | |
| 60 | <div | |
| 61 | aria-hidden="true" | |
| 62 | className={cn( | |
| 63 | emptyMediaVariants({ className, variant }), | |
| 64 | "-translate-x-0.5 -rotate-10 pointer-events-none absolute bottom-px origin-bottom-left scale-84 shadow-none", | |
| 65 | )} | |
| 66 | /> | |
| 67 | <div | |
| 68 | aria-hidden="true" | |
| 69 | className={cn( | |
| 70 | emptyMediaVariants({ className, variant }), | |
| 71 | "pointer-events-none absolute bottom-px origin-bottom-right translate-x-0.5 rotate-10 scale-84 shadow-none", | |
| 72 | )} | |
| 73 | /> | |
| 74 | </> | |
| 75 | )} | |
| 76 | <div | |
| 77 | className={cn(emptyMediaVariants({ className, variant }))} | |
| 78 | {...props} | |
| 79 | /> | |
| 80 | </div> | |
| 81 | ); | |
| 82 | } | |
| 83 | ||
| 84 | function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { | |
| 85 | return ( | |
| 86 | <div | |
| 87 | className={cn("font-heading font-semibold text-xl", className)} | |
| 88 | data-slot="empty-title" | |
| 89 | {...props} | |
| 90 | /> | |
| 91 | ); | |
| 92 | } | |
| 93 | ||
| 94 | function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { | |
| 95 | return ( | |
| 96 | <div | |
| 97 | className={cn( | |
| 98 | "text-muted-foreground text-sm [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4 [[data-slot=empty-title]+&]:mt-1", | |
| 99 | className, | |
| 100 | )} | |
| 101 | data-slot="empty-description" | |
| 102 | {...props} | |
| 103 | /> | |
| 104 | ); | |
| 105 | } | |
| 106 | ||
| 107 | function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { | |
| 108 | return ( | |
| 109 | <div | |
| 110 | className={cn( | |
| 111 | "flex w-full min-w-0 max-w-sm flex-col items-center gap-4 text-balance text-sm", | |
| 112 | className, | |
| 113 | )} | |
| 114 | data-slot="empty-content" | |
| 115 | {...props} | |
| 116 | /> | |
| 117 | ); | |
| 118 | } | |
| 119 | ||
| 120 | export { | |
| 121 | Empty, | |
| 122 | EmptyHeader, | |
| 123 | EmptyTitle, | |
| 124 | EmptyDescription, | |
| 125 | EmptyContent, | |
| 126 | EmptyMedia, | |
| 127 | }; |
apps/web/src/components/ui/field.tsx created+74| ... | ... | @@ -0,0 +1,74 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { Field as FieldPrimitive } from "@base-ui/react/field"; | |
| 4 | ||
| 5 | import { cn } from "~/lib/utils"; | |
| 6 | ||
| 7 | function 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 | ||
| 17 | function 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 | ||
| 30 | function 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 | ||
| 40 | function 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 | ||
| 53 | function 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 | ||
| 63 | const FieldControl = FieldPrimitive.Control; | |
| 64 | const FieldValidity = FieldPrimitive.Validity; | |
| 65 | ||
| 66 | export { | |
| 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 | ||
| 3 | import { Fieldset as FieldsetPrimitive } from "@base-ui/react/fieldset"; | |
| 4 | ||
| 5 | import { cn } from "~/lib/utils"; | |
| 6 | ||
| 7 | function 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 | } | |
| 16 | function 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 | ||
| 29 | export { Fieldset, FieldsetLegend }; |
apps/web/src/components/ui/form.tsx created+17| ... | ... | @@ -0,0 +1,17 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { Form as FormPrimitive } from "@base-ui/react/form"; | |
| 4 | ||
| 5 | import { cn } from "~/lib/utils"; | |
| 6 | ||
| 7 | function 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 | ||
| 17 | export { Form }; |
apps/web/src/components/ui/group.tsx created+93| ... | ... | @@ -0,0 +1,93 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { mergeProps } from "@base-ui/react/merge-props"; | |
| 4 | import { useRender } from "@base-ui/react/use-render"; | |
| 5 | import { cva, type VariantProps } from "class-variance-authority"; | |
| 6 | import type * as React from "react"; | |
| 7 | ||
| 8 | import { cn } from "~/lib/utils"; | |
| 9 | import { Separator } from "./separator"; | |
| 10 | ||
| 11 | const 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 | ||
| 28 | function 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 | ||
| 51 | function 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 | ||
| 66 | function 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 | ||
| 85 | export { | |
| 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 | ||
| 3 | import { cva, type VariantProps } from "class-variance-authority"; | |
| 4 | import type * as React from "react"; | |
| 5 | ||
| 6 | import { cn } from "~/lib/utils"; | |
| 7 | import { Input, type InputProps } from "~/components/ui/input"; | |
| 8 | import { Textarea, type TextareaProps } from "~/components/ui/textarea"; | |
| 9 | ||
| 10 | function 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 | ||
| 24 | const 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 | ||
| 45 | function 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 | ||
| 75 | function 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 | ||
| 87 | function InputGroupInput({ className, ...props }: InputProps) { | |
| 88 | return <Input className={className} unstyled {...props} />; | |
| 89 | } | |
| 90 | ||
| 91 | function InputGroupTextarea({ className, ...props }: TextareaProps) { | |
| 92 | return <Textarea className={className} unstyled {...props} />; | |
| 93 | } | |
| 94 | ||
| 95 | export { | |
| 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 | ||
| 3 | import { Input as InputPrimitive } from "@base-ui/react/input"; | |
| 4 | import type * as React from "react"; | |
| 5 | ||
| 6 | import { cn } from "~/lib/utils"; | |
| 7 | ||
| 8 | type 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 | ||
| 17 | function 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 | ||
| 66 | export { Input, type InputProps }; |
apps/web/src/components/ui/kbd.tsx created+28| ... | ... | @@ -0,0 +1,28 @@ |
| 1 | import type * as React from "react"; | |
| 2 | ||
| 3 | import { cn } from "~/lib/utils"; | |
| 4 | ||
| 5 | function Kbd({ className, ...props }: React.ComponentProps<"kbd">) { | |
| 6 | return ( | |
| 7 | <kbd | |
| 8 | className={cn( | |
| 9 | "pointer-events-none inline-flex h-5 min-w-5 select-none items-center justify-center gap-1 rounded bg-muted px-1 font-medium font-sans text-muted-foreground text-xs [&_svg:not([class*='size-'])]:size-3", | |
| 10 | className, | |
| 11 | )} | |
| 12 | data-slot="kbd" | |
| 13 | {...props} | |
| 14 | /> | |
| 15 | ); | |
| 16 | } | |
| 17 | ||
| 18 | function KbdGroup({ className, ...props }: React.ComponentProps<"kbd">) { | |
| 19 | return ( | |
| 20 | <kbd | |
| 21 | className={cn("inline-flex items-center gap-1", className)} | |
| 22 | data-slot="kbd-group" | |
| 23 | {...props} | |
| 24 | /> | |
| 25 | ); | |
| 26 | } | |
| 27 | ||
| 28 | export { Kbd, KbdGroup }; |
apps/web/src/components/ui/label.tsx created+28| ... | ... | @@ -0,0 +1,28 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { mergeProps } from "@base-ui/react/merge-props"; | |
| 4 | import { useRender } from "@base-ui/react/use-render"; | |
| 5 | ||
| 6 | import { cn } from "~/lib/utils"; | |
| 7 | ||
| 8 | function 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 | ||
| 28 | export { Label }; |
apps/web/src/components/ui/menu.tsx created+326| ... | ... | @@ -0,0 +1,326 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { Menu as MenuPrimitive } from "@base-ui/react/menu"; | |
| 4 | import { ChevronRightIcon } from "lucide-react"; | |
| 5 | import type * as React from "react"; | |
| 6 | ||
| 7 | import { cn } from "~/lib/utils"; | |
| 8 | ||
| 9 | const MenuCreateHandle = MenuPrimitive.createHandle; | |
| 10 | ||
| 11 | const Menu = MenuPrimitive.Root; | |
| 12 | ||
| 13 | const MenuPortal = MenuPrimitive.Portal; | |
| 14 | ||
| 15 | function 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 | ||
| 31 | function 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 | ||
| 75 | function MenuGroup(props: MenuPrimitive.Group.Props) { | |
| 76 | return <MenuPrimitive.Group data-slot="menu-group" {...props} />; | |
| 77 | } | |
| 78 | ||
| 79 | function 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 | ||
| 102 | function 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 | ||
| 158 | function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) { | |
| 159 | return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />; | |
| 160 | } | |
| 161 | ||
| 162 | function 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 | ||
| 196 | function 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 | ||
| 216 | function 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 | ||
| 226 | function 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 | ||
| 239 | function MenuSub(props: MenuPrimitive.SubmenuRoot.Props) { | |
| 240 | return <MenuPrimitive.SubmenuRoot data-slot="menu-sub" {...props} />; | |
| 241 | } | |
| 242 | ||
| 243 | function 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 | ||
| 267 | function 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 | ||
| 293 | export { | |
| 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 | ||
| 3 | import { Popover as PopoverPrimitive } from "@base-ui/react/popover"; | |
| 4 | ||
| 5 | import { cn } from "~/lib/utils"; | |
| 6 | ||
| 7 | const PopoverCreateHandle = PopoverPrimitive.createHandle; | |
| 8 | ||
| 9 | const Popover = PopoverPrimitive.Root; | |
| 10 | ||
| 11 | function 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 | ||
| 27 | function 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 | ||
| 83 | function PopoverClose({ ...props }: PopoverPrimitive.Close.Props) { | |
| 84 | return <PopoverPrimitive.Close data-slot="popover-close" {...props} />; | |
| 85 | } | |
| 86 | ||
| 87 | function 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 | ||
| 97 | function 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 | ||
| 110 | export { | |
| 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 | ||
| 3 | import { Radio as RadioPrimitive } from "@base-ui/react/radio"; | |
| 4 | import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"; | |
| 5 | ||
| 6 | import { cn } from "~/lib/utils"; | |
| 7 | ||
| 8 | function 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 | ||
| 18 | function 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 | ||
| 36 | export { RadioGroup, Radio, Radio as RadioGroupItem }; |
apps/web/src/components/ui/scroll-area.tsx created+64| ... | ... | @@ -0,0 +1,64 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"; | |
| 4 | ||
| 5 | import { cn } from "~/lib/utils"; | |
| 6 | ||
| 7 | function 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 | ||
| 41 | function 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 | ||
| 64 | export { ScrollArea, ScrollBar }; |
apps/web/src/components/ui/select.tsx created+234| ... | ... | @@ -0,0 +1,234 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { mergeProps } from "@base-ui/react/merge-props"; | |
| 4 | import { Select as SelectPrimitive } from "@base-ui/react/select"; | |
| 5 | import { useRender } from "@base-ui/react/use-render"; | |
| 6 | import { cva, type VariantProps } from "class-variance-authority"; | |
| 7 | import { ChevronDownIcon, ChevronsUpDownIcon, ChevronUpIcon } from "lucide-react"; | |
| 8 | import type * as React from "react"; | |
| 9 | ||
| 10 | import { cn } from "~/lib/utils"; | |
| 11 | ||
| 12 | const Select = SelectPrimitive.Root; | |
| 13 | ||
| 14 | const 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 | ||
| 37 | const selectTriggerIconClassName = "-me-1 size-4.5 opacity-80 sm:size-4"; | |
| 38 | ||
| 39 | interface SelectButtonProps extends useRender.ComponentProps<"button"> { | |
| 40 | size?: VariantProps<typeof selectTriggerVariants>["size"]; | |
| 41 | variant?: VariantProps<typeof selectTriggerVariants>["variant"]; | |
| 42 | } | |
| 43 | ||
| 44 | function 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 | ||
| 74 | function 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 | ||
| 95 | function 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 | ||
| 105 | function 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 | ||
| 166 | function 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 | ||
| 198 | function 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 | ||
| 208 | function SelectGroup(props: SelectPrimitive.Group.Props) { | |
| 209 | return <SelectPrimitive.Group data-slot="select-group" {...props} />; | |
| 210 | } | |
| 211 | ||
| 212 | function 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 | ||
| 222 | export { | |
| 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 @@ |
| 1 | import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"; | |
| 2 | ||
| 3 | import { cn } from "~/lib/utils"; | |
| 4 | ||
| 5 | function Separator({ | |
| 6 | className, | |
| 7 | orientation = "horizontal", | |
| 8 | ...props | |
| 9 | }: SeparatorPrimitive.Props) { | |
| 10 | return ( | |
| 11 | <SeparatorPrimitive | |
| 12 | className={cn( | |
| 13 | "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px data-[orientation=vertical]:not-[[class^='h-']]:not-[[class*='_h-']]:self-stretch", | |
| 14 | className, | |
| 15 | )} | |
| 16 | data-slot="separator" | |
| 17 | orientation={orientation} | |
| 18 | {...props} | |
| 19 | /> | |
| 20 | ); | |
| 21 | } | |
| 22 | ||
| 23 | export { Separator }; |
apps/web/src/components/ui/sheet.tsx created+203| ... | ... | @@ -0,0 +1,203 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"; | |
| 4 | import { XIcon } from "lucide-react"; | |
| 5 | import { cn } from "~/lib/utils"; | |
| 6 | import { Button } from "~/components/ui/button"; | |
| 7 | import { ScrollArea } from "~/components/ui/scroll-area"; | |
| 8 | ||
| 9 | const Sheet = SheetPrimitive.Root; | |
| 10 | ||
| 11 | const SheetPortal = SheetPrimitive.Portal; | |
| 12 | ||
| 13 | function SheetTrigger(props: SheetPrimitive.Trigger.Props) { | |
| 14 | return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />; | |
| 15 | } | |
| 16 | ||
| 17 | function SheetClose(props: SheetPrimitive.Close.Props) { | |
| 18 | return <SheetPrimitive.Close data-slot="sheet-close" {...props} />; | |
| 19 | } | |
| 20 | ||
| 21 | function 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 | ||
| 34 | function 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 | ||
| 59 | function 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 | ||
| 109 | function 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 | ||
| 122 | function 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 | ||
| 144 | function 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 | ||
| 157 | function 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 | ||
| 170 | function 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 | ||
| 189 | export { | |
| 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 @@ |
| 1 | import { cn } from "~/lib/utils"; | |
| 2 | ||
| 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { | |
| 4 | return ( | |
| 5 | <div | |
| 6 | className={cn( | |
| 7 | "animate-skeleton rounded-sm [--skeleton-highlight:--alpha(var(--color-white)/64%)] [background:linear-gradient(120deg,transparent_40%,var(--skeleton-highlight),transparent_60%)_var(--color-muted)_0_0/200%_100%_fixed] dark:[--skeleton-highlight:--alpha(var(--color-white)/4%)]", | |
| 8 | className, | |
| 9 | )} | |
| 10 | data-slot="skeleton" | |
| 11 | {...props} | |
| 12 | /> | |
| 13 | ); | |
| 14 | } | |
| 15 | ||
| 16 | export { Skeleton }; |
apps/web/src/components/ui/spinner.tsx created+18| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | import { Loader2Icon } from "lucide-react"; | |
| 2 | import { cn } from "~/lib/utils"; | |
| 3 | ||
| 4 | function Spinner({ | |
| 5 | className, | |
| 6 | ...props | |
| 7 | }: React.ComponentProps<typeof Loader2Icon>) { | |
| 8 | return ( | |
| 9 | <Loader2Icon | |
| 10 | aria-label="Loading" | |
| 11 | className={cn("animate-spin", className)} | |
| 12 | role="status" | |
| 13 | {...props} | |
| 14 | /> | |
| 15 | ); | |
| 16 | } | |
| 17 | ||
| 18 | export { Spinner }; |
apps/web/src/components/ui/switch.tsx created+27| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { Switch as SwitchPrimitive } from "@base-ui/react/switch"; | |
| 4 | ||
| 5 | import { cn } from "~/lib/utils"; | |
| 6 | ||
| 7 | function 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 | ||
| 27 | export { Switch }; |
apps/web/src/components/ui/textarea.tsx created+51| ... | ... | @@ -0,0 +1,51 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { Field as FieldPrimitive } from "@base-ui/react/field"; | |
| 4 | import { mergeProps } from "@base-ui/react/merge-props"; | |
| 5 | import type * as React from "react"; | |
| 6 | ||
| 7 | import { cn } from "~/lib/utils"; | |
| 8 | ||
| 9 | type TextareaProps = React.ComponentProps<"textarea"> & { | |
| 10 | size?: "sm" | "default" | "lg" | number; | |
| 11 | unstyled?: boolean; | |
| 12 | }; | |
| 13 | ||
| 14 | function 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 | ||
| 51 | export { Textarea, type TextareaProps }; |
apps/web/src/components/ui/toast.tsx created+269| ... | ... | @@ -0,0 +1,269 @@ |
| 1 | "use client"; | |
| 2 | ||
| 3 | import { Toast } from "@base-ui/react/toast"; | |
| 4 | import { | |
| 5 | CircleAlertIcon, | |
| 6 | CircleCheckIcon, | |
| 7 | InfoIcon, | |
| 8 | LoaderCircleIcon, | |
| 9 | TriangleAlertIcon, | |
| 10 | } from "lucide-react"; | |
| 11 | ||
| 12 | import { cn } from "~/lib/utils"; | |
| 13 | import { buttonVariants } from "~/components/ui/button"; | |
| 14 | ||
| 15 | const toastManager = Toast.createToastManager(); | |
| 16 | const anchoredToastManager = Toast.createToastManager(); | |
| 17 | ||
| 18 | const TOAST_ICONS = { | |
| 19 | error: CircleAlertIcon, | |
| 20 | info: InfoIcon, | |
| 21 | loading: LoaderCircleIcon, | |
| 22 | success: CircleCheckIcon, | |
| 23 | warning: TriangleAlertIcon, | |
| 24 | } as const; | |
| 25 | ||
| 26 | type ToastPosition = | |
| 27 | | "top-left" | |
| 28 | | "top-center" | |
| 29 | | "top-right" | |
| 30 | | "bottom-left" | |
| 31 | | "bottom-center" | |
| 32 | | "bottom-right"; | |
| 33 | ||
| 34 | interface ToastProviderProps extends Toast.Provider.Props { | |
| 35 | position?: ToastPosition; | |
| 36 | } | |
| 37 | ||
| 38 | function 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 | ||
| 51 | function 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 | ||
| 169 | function AnchoredToastProvider({ children, ...props }: Toast.Provider.Props) { | |
| 170 | return ( | |
| 171 | <Toast.Provider toastManager={anchoredToastManager} {...props}> | |
| 172 | {children} | |
| 173 | <AnchoredToasts /> | |
| 174 | </Toast.Provider> | |
| 175 | ); | |
| 176 | } | |
| 177 | ||
| 178 | function 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 | ||
| 263 | export { | |
| 264 | ToastProvider, | |
| 265 | type ToastPosition, | |
| 266 | toastManager, | |
| 267 | AnchoredToastProvider, | |
| 268 | anchoredToastManager, | |
| 269 | }; |
apps/web/src/index.css+2-41| ... | ... | @@ -11,19 +11,6 @@ |
| 11 | 11 | --color-info-foreground: var(--info-foreground); |
| 12 | 12 | --color-info: var(--info); |
| 13 | 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 | 14 | --color-ring: var(--ring); |
| 28 | 15 | --color-input: var(--input); |
| 29 | 16 | --color-border: var(--border); |
| ... | ... | @@ -61,7 +48,7 @@ |
| 61 | 48 | @apply border-border outline-ring/50; |
| 62 | 49 | } |
| 63 | 50 | body { |
| 64 | @apply bg-background text-foreground; | |
| 51 | @apply bg-background text-foreground relative; | |
| 65 | 52 | } |
| 66 | 53 | } |
| 67 | 54 | |
| ... | ... | @@ -89,25 +76,12 @@ |
| 89 | 76 | --secondary-foreground: var(--color-neutral-800); |
| 90 | 77 | --muted: --alpha(var(--color-black) / 4%); |
| 91 | 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 | 80 | --accent-foreground: var(--color-neutral-800); |
| 94 | 81 | --destructive: var(--color-red-500); |
| 95 | 82 | --border: --alpha(var(--color-black) / 8%); |
| 96 | 83 | --input: --alpha(var(--color-black) / 10%); |
| 97 | 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 | 85 | --destructive-foreground: var(--color-red-700); |
| 112 | 86 | --info: var(--color-blue-500); |
| 113 | 87 | --info-foreground: var(--color-blue-700); |
| ... | ... | @@ -136,19 +110,6 @@ |
| 136 | 110 | --border: --alpha(var(--color-white) / 6%); |
| 137 | 111 | --input: --alpha(var(--color-white) / 8%); |
| 138 | 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 | 113 | --destructive-foreground: var(--color-red-400); |
| 153 | 114 | --info: var(--color-blue-500); |
| 154 | 115 | --info-foreground: var(--color-blue-400); |
apps/web/src/lib/utils.ts created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | import { type ClassValue, clsx } from "clsx"; | |
| 2 | import { twMerge } from "tailwind-merge"; | |
| 3 | ||
| 4 | export function cn(...inputs: ClassValue[]) { | |
| 5 | return twMerge(clsx(inputs)); | |
| 6 | } |
apps/web/src/model-logic.ts+2-1| ... | ... | @@ -6,4 +6,5 @@ export { |
| 6 | 6 | } from "@t3tools/contracts"; |
| 7 | 7 | |
| 8 | 8 | export const REASONING_OPTIONS = ["xhigh", "high", "medium", "low"] as const; |
| 9 | export const DEFAULT_REASONING = "high"; | |
| 9 | export type ReasoningEffort = (typeof REASONING_OPTIONS)[number]; | |
| 10 | export const DEFAULT_REASONING: ReasoningEffort = "high"; |
apps/web/tsconfig.json+4-1| ... | ... | @@ -4,7 +4,10 @@ |
| 4 | 4 | "composite": true, |
| 5 | 5 | "jsx": "react-jsx", |
| 6 | 6 | "lib": ["ES2023", "DOM", "DOM.Iterable"], |
| 7 | "types": ["vite/client"] | |
| 7 | "types": ["vite/client"], | |
| 8 | "paths": { | |
| 9 | "~/*": ["./src/*"] | |
| 10 | } | |
| 8 | 11 | }, |
| 9 | 12 | "include": ["src", "vite.config.ts"] |
| 10 | 13 | } |
apps/web/vite.config.ts+6| ... | ... | @@ -17,6 +17,12 @@ export default defineConfig({ |
| 17 | 17 | // In dev mode, tell the web app where the WebSocket server lives |
| 18 | 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 | 26 | server: { |
| 21 | 27 | port, |
| 22 | 28 | strictPort: true, |
bun.lock+118-3| ... | ... | @@ -51,17 +51,20 @@ |
| 51 | 51 | "name": "@t3tools/web", |
| 52 | 52 | "version": "0.0.0", |
| 53 | 53 | "dependencies": { |
| 54 | "@base-ui/react": "^1.2.0", | |
| 54 | 55 | "@t3tools/contracts": "workspace:*", |
| 55 | 56 | "@tanstack/react-query": "^5.90.0", |
| 56 | 57 | "@xterm/addon-fit": "^0.11.0", |
| 57 | 58 | "@xterm/xterm": "^6.0.0", |
| 59 | "class-variance-authority": "^0.7.1", | |
| 58 | 60 | "highlight.js": "^11.11.1", |
| 59 | "lucide-react": "^0.563.0", | |
| 61 | "lucide-react": "^0.564.0", | |
| 60 | 62 | "react": "^19.0.0", |
| 61 | 63 | "react-dom": "^19.0.0", |
| 62 | 64 | "react-markdown": "^10.1.0", |
| 63 | 65 | "rehype-highlight": "^7.0.2", |
| 64 | 66 | "remark-gfm": "^4.0.1", |
| 67 | "tailwind-merge": "^3.4.0", | |
| 65 | 68 | "zod": "^3.24.1", |
| 66 | 69 | }, |
| 67 | 70 | "devDependencies": { |
| ... | ... | @@ -120,12 +123,18 @@ |
| 120 | 123 | |
| 121 | 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=="], |
| 122 | 125 | |
| 126 | "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], | |
| 127 | ||
| 123 | 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=="], |
| 124 | 129 | |
| 125 | 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=="], |
| 126 | 131 | |
| 127 | 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=="], |
| 128 | 133 | |
| 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 | 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=="], |
| 130 | 139 | |
| 131 | 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 | 195 | |
| 187 | 196 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], |
| 188 | 197 | |
| 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 | 206 | "@hapi/address": ["@hapi/address@5.1.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA=="], |
| 190 | 207 | |
| 191 | 208 | "@hapi/formula": ["@hapi/formula@3.0.2", "", {}, "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw=="], |
| ... | ... | @@ -198,6 +215,16 @@ |
| 198 | 215 | |
| 199 | 216 | "@hapi/topo": ["@hapi/topo@6.0.2", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg=="], |
| 200 | 217 | |
| 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 | 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=="], |
| 202 | 229 | |
| 203 | 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 | 235 | |
| 209 | 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=="], |
| 210 | 237 | |
| 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 | 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=="], |
| 212 | 241 | |
| 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 | 248 | "@oxc-project/runtime": ["@oxc-project/runtime@0.112.0", "", {}, "sha512-4vYtWXMnXM6EaweCxbJ6bISAhkNHeN33SihvuX3wrpqaSJA4ZEoW35i9mSvE74+GDf1yTeVE+aEHA+WBpjDk/g=="], |
| 214 | 249 | |
| 215 | 250 | "@oxc-project/types": ["@oxc-project/types@0.112.0", "", {}, "sha512-m6RebKHIRsax2iCwVpYW2ErQwa4ywHJrE4sCK3/8JK8ZZAWOKXaRJFl/uP51gaVyyXlaS4+chU1nSCdzYf6QqQ=="], |
| ... | ... | @@ -436,6 +471,8 @@ |
| 436 | 471 | |
| 437 | 472 | "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], |
| 438 | 473 | |
| 474 | "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], | |
| 475 | ||
| 439 | 476 | "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], |
| 440 | 477 | |
| 441 | 478 | "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], |
| ... | ... | @@ -464,6 +501,8 @@ |
| 464 | 501 | |
| 465 | 502 | "@xterm/xterm": ["@xterm/xterm@6.0.0", "", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="], |
| 466 | 503 | |
| 504 | "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], | |
| 505 | ||
| 467 | 506 | "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], |
| 468 | 507 | |
| 469 | 508 | "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], |
| ... | ... | @@ -518,8 +557,16 @@ |
| 518 | 557 | |
| 519 | 558 | "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], |
| 520 | 559 | |
| 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 | 566 | "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], |
| 522 | 567 | |
| 568 | "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], | |
| 569 | ||
| 523 | 570 | "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], |
| 524 | 571 | |
| 525 | 572 | "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], |
| ... | ... | @@ -530,6 +577,8 @@ |
| 530 | 577 | |
| 531 | 578 | "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], |
| 532 | 579 | |
| 580 | "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], | |
| 581 | ||
| 533 | 582 | "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], |
| 534 | 583 | |
| 535 | 584 | "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], |
| ... | ... | @@ -572,6 +621,8 @@ |
| 572 | 621 | |
| 573 | 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=="], |
| 574 | 623 | |
| 624 | "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], | |
| 625 | ||
| 575 | 626 | "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], |
| 576 | 627 | |
| 577 | 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 | 677 | |
| 627 | 678 | "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], |
| 628 | 679 | |
| 680 | "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], | |
| 681 | ||
| 629 | 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=="], |
| 630 | 683 | |
| 631 | 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 | 697 | |
| 645 | 698 | "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], |
| 646 | 699 | |
| 700 | "graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="], | |
| 701 | ||
| 647 | 702 | "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], |
| 648 | 703 | |
| 649 | 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 | 717 | |
| 663 | 718 | "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], |
| 664 | 719 | |
| 720 | "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], | |
| 721 | ||
| 665 | 722 | "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], |
| 666 | 723 | |
| 667 | 724 | "hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="], |
| ... | ... | @@ -686,10 +743,14 @@ |
| 686 | 743 | |
| 687 | 744 | "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], |
| 688 | 745 | |
| 746 | "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], | |
| 747 | ||
| 689 | 748 | "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], |
| 690 | 749 | |
| 691 | 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=="], |
| 692 | 751 | |
| 752 | "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], | |
| 753 | ||
| 693 | 754 | "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], |
| 694 | 755 | |
| 695 | 756 | "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], |
| ... | ... | @@ -750,7 +811,7 @@ |
| 750 | 811 | |
| 751 | 812 | "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], |
| 752 | 813 | |
| 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=="], | |
| 754 | 815 | |
| 755 | 816 | "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], |
| 756 | 817 | |
| ... | ... | @@ -858,6 +919,10 @@ |
| 858 | 919 | |
| 859 | 920 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], |
| 860 | 921 | |
| 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 | 926 | "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], |
| 862 | 927 | |
| 863 | 928 | "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], |
| ... | ... | @@ -878,6 +943,8 @@ |
| 878 | 943 | |
| 879 | 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=="], |
| 880 | 945 | |
| 946 | "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], | |
| 947 | ||
| 881 | 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=="], |
| 882 | 949 | |
| 883 | 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 | 953 | |
| 887 | 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=="], |
| 888 | 955 | |
| 956 | "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], | |
| 957 | ||
| 889 | 958 | "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], |
| 890 | 959 | |
| 891 | 960 | "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], |
| ... | ... | @@ -930,6 +999,10 @@ |
| 930 | 999 | |
| 931 | 1000 | "remove-trailing-separator": ["remove-trailing-separator@1.1.0", "", {}, "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw=="], |
| 932 | 1001 | |
| 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 | 1006 | "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], |
| 934 | 1007 | |
| 935 | 1008 | "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], |
| ... | ... | @@ -938,6 +1011,8 @@ |
| 938 | 1011 | |
| 939 | 1012 | "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], |
| 940 | 1013 | |
| 1014 | "rettime": ["rettime@0.10.1", "", {}, "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw=="], | |
| 1015 | ||
| 941 | 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=="], |
| 942 | 1017 | |
| 943 | 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 | 1037 | |
| 963 | 1038 | "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], |
| 964 | 1039 | |
| 1040 | "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], | |
| 1041 | ||
| 965 | 1042 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], |
| 966 | 1043 | |
| 967 | 1044 | "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], |
| ... | ... | @@ -970,10 +1047,18 @@ |
| 970 | 1047 | |
| 971 | 1048 | "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], |
| 972 | 1049 | |
| 1050 | "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], | |
| 1051 | ||
| 973 | 1052 | "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], |
| 974 | 1053 | |
| 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 | 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=="], |
| 976 | 1059 | |
| 1060 | "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], | |
| 1061 | ||
| 977 | 1062 | "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], |
| 978 | 1063 | |
| 979 | 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 | 1069 | |
| 985 | 1070 | "t3": ["t3@workspace:apps/server"], |
| 986 | 1071 | |
| 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 | 1078 | "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], |
| 988 | 1079 | |
| 989 | 1080 | "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], |
| ... | ... | @@ -998,8 +1089,14 @@ |
| 998 | 1089 | |
| 999 | 1090 | "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], |
| 1000 | 1091 | |
| 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 | 1096 | "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], |
| 1002 | 1097 | |
| 1098 | "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], | |
| 1099 | ||
| 1003 | 1100 | "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], |
| 1004 | 1101 | |
| 1005 | 1102 | "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], |
| ... | ... | @@ -1024,7 +1121,7 @@ |
| 1024 | 1121 | |
| 1025 | 1122 | "turbo-windows-arm64": ["turbo-windows-arm64@2.8.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-WyGiOI2Zp3AhuzVagzQN+T+iq0fWx0oGxDfAWT3ZiLEd4U0cDUkwUZDKVGb3rKqPjDL6lWnuxKKu73ge5xtovQ=="], |
| 1026 | 1123 | |
| 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=="], | |
| 1028 | 1125 | |
| 1029 | 1126 | "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], |
| 1030 | 1127 | |
| ... | ... | @@ -1052,8 +1149,12 @@ |
| 1052 | 1149 | |
| 1053 | 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=="], |
| 1054 | 1151 | |
| 1152 | "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], | |
| 1153 | ||
| 1055 | 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=="], |
| 1056 | 1155 | |
| 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 | 1158 | "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], |
| 1058 | 1159 | |
| 1059 | 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 | 1169 | |
| 1069 | 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=="], |
| 1070 | 1171 | |
| 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 | 1174 | "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], |
| 1072 | 1175 | |
| 1073 | 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=="], |
| 1074 | 1177 | |
| 1075 | 1178 | "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], |
| 1076 | 1179 | |
| 1180 | "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], | |
| 1181 | ||
| 1077 | 1182 | "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], |
| 1078 | 1183 | |
| 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 | 1188 | "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], |
| 1080 | 1189 | |
| 1190 | "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], | |
| 1191 | ||
| 1081 | 1192 | "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], |
| 1082 | 1193 | |
| 1083 | 1194 | "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], |
| ... | ... | @@ -1130,6 +1241,8 @@ |
| 1130 | 1241 | |
| 1131 | 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=="], |
| 1132 | 1243 | |
| 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 | 1246 | "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], |
| 1134 | 1247 | |
| 1135 | 1248 | "electron/@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], |
| ... | ... | @@ -1142,6 +1255,8 @@ |
| 1142 | 1255 | |
| 1143 | 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=="], |
| 1144 | 1257 | |
| 1258 | "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], | |
| 1259 | ||
| 1145 | 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=="], |
| 1146 | 1261 | |
| 1147 | 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 | 29 | |
| 30 | 30 | it("resolves only supported model options", () => { |
| 31 | 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 | 1 | export const MODEL_OPTIONS = [ |
| 2 | "gpt-5.3-codex", | |
| 3 | "gpt-5.3-codex-spark", | |
| 4 | "gpt-5.2-codex", | |
| 5 | "gpt-5.2", | |
| 2 | { slug: "gpt-5.3-codex", name: "GPT-5.3 Codex" }, | |
| 3 | { slug: "gpt-5.3-codex-spark", name: "GPT-5.3 Codex Spark" }, | |
| 4 | { slug: "gpt-5.2-codex", name: "GPT-5.2 Codex" }, | |
| 5 | { slug: "gpt-5.2", name: "GPT-5.2" }, | |
| 6 | 6 | ] as const; |
| 7 | 7 | |
| 8 | export type ModelSlug = (typeof MODEL_OPTIONS)[number]["slug"]; | |
| 9 | ||
| 8 | 10 | export const DEFAULT_MODEL = "gpt-5.3-codex"; |
| 9 | 11 | |
| 10 | export const MODEL_SLUG_ALIASES: Record<string, string> = { | |
| 12 | export const MODEL_SLUG_ALIASES: Record<string, ModelSlug> = { | |
| 11 | 13 | "5.3": "gpt-5.3-codex", |
| 12 | 14 | "gpt-5.3": "gpt-5.3-codex", |
| 13 | 15 | "5.3-spark": "gpt-5.3-codex-spark", |
| 14 | 16 | "gpt-5.3-spark": "gpt-5.3-codex-spark", |
| 15 | 17 | }; |
| 16 | 18 | |
| 17 | export function normalizeModelSlug(model: string | null | undefined): string | null { | |
| 19 | export function normalizeModelSlug(model: string | null | undefined): ModelSlug | null { | |
| 18 | 20 | if (typeof model !== "string") { |
| 19 | 21 | return null; |
| 20 | 22 | } |
| ... | ... | @@ -24,16 +26,14 @@ export function normalizeModelSlug(model: string | null | undefined): string | n |
| 24 | 26 | return null; |
| 25 | 27 | } |
| 26 | 28 | |
| 27 | return MODEL_SLUG_ALIASES[trimmed] ?? trimmed; | |
| 29 | return MODEL_SLUG_ALIASES[trimmed] ?? (trimmed as ModelSlug); | |
| 28 | 30 | } |
| 29 | 31 | |
| 30 | export function resolveModelSlug(model: string | null | undefined): string { | |
| 32 | export function resolveModelSlug(model: string | null | undefined): ModelSlug { | |
| 31 | 33 | const normalized = normalizeModelSlug(model); |
| 32 | 34 | if (!normalized) { |
| 33 | 35 | return DEFAULT_MODEL; |
| 34 | 36 | } |
| 35 | 37 | |
| 36 | return MODEL_OPTIONS.includes(normalized as (typeof MODEL_OPTIONS)[number]) | |
| 37 | ? normalized | |
| 38 | : DEFAULT_MODEL; | |
| 38 | return MODEL_OPTIONS.some((option) => option.slug === normalized) ? normalized : DEFAULT_MODEL; | |
| 39 | 39 | } |