* init components * use them * Fix select trigger styling in dark mode and add open state highlight Remove dark mode background bleed on select triggers and add accent background when popup is open. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> ---------
Unified · Split
| @@ -13,7 +13,7 @@ · 12 unmodified lines | ||
| 13 | 13 | * the first render). For production mode (no React DevTools message), we |
| 14 | 14 | * instead check that no fatal errors appeared. |
| 15 | 15 | */ |
| 16 | −import { spawn, execSync } from "node:child_process"; | |
| 16 | +import { execSync, spawn } from "node:child_process"; | |
| 17 | 17 | import { dirname, resolve } from "node:path"; |
| 18 | 18 | import { fileURLToPath } from "node:url"; |
| 19 | 19 | |
| @@ -75,7 +75,7 @@ child.on("exit", () => { · 55 unmodified lines | ||
| 75 | 75 | for (const f of failures) { |
| 76 | 76 | console.error(` • ${f}`); |
| 77 | 77 | } |
| 78 | − console.error("\nFull output:\n" + output); | |
| 78 | + console.error(`\nFull output:\n${output}`); | |
| 79 | 79 | process.exit(1); |
| 80 | 80 | } |
| 81 | 81 | |
| @@ -0,0 +1,25 @@ | ||
| 1 | +{ | |
| 2 | + "$schema": "https://ui.shadcn.com/schema.json", | |
| 3 | + "style": "new-york", | |
| 4 | + "rsc": false, | |
| 5 | + "tsx": true, | |
| 6 | + "tailwind": { | |
| 7 | + "config": "", | |
| 8 | + "css": "src/index.css", | |
| 9 | + "baseColor": "zinc", | |
| 10 | + "cssVariables": true, | |
| 11 | + "prefix": "" | |
| 12 | + }, | |
| 13 | + "iconLibrary": "lucide", | |
| 14 | + "rtl": false, | |
| 15 | + "aliases": { | |
| 16 | + "components": "#/components", | |
| 17 | + "utils": "#/lib/utils", | |
| 18 | + "ui": "#/components/ui", | |
| 19 | + "lib": "#/lib", | |
| 20 | + "hooks": "#/hooks" | |
| 21 | + }, | |
| 22 | + "registries": { | |
| 23 | + "@coss": "https://coss.com/ui/r/{name}.json" | |
| 24 | + } | |
| 25 | +} | |
| @@ -5,7 +5,7 @@ · 4 unmodified lines | ||
| 5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| 6 | 6 | <title>Electron Todo</title> |
| 7 | 7 | </head> |
| 8 | − <body class="bg-slate-100"> | |
| 8 | + <body> | |
| 9 | 9 | <div id="root"></div> |
| 10 | 10 | <script type="module" src="/src/main.tsx"></script> |
| 11 | 11 | </body> |
| @@ -12,12 +12,17 @@ · 11 unmodified lines | ||
| 12 | 12 | }, |
| 13 | 13 | "dependencies": { |
| 14 | 14 | "@acme/contracts": "workspace:*", |
| 15 | + "@base-ui/react": "^1.1.0", | |
| 16 | + "class-variance-authority": "^0.7.1", | |
| 17 | + "clsx": "^2.1.1", | |
| 15 | 18 | "highlight.js": "^11.11.1", |
| 19 | + "lucide-react": "^0.563.0", | |
| 16 | 20 | "react": "^19.0.0", |
| 17 | 21 | "react-dom": "^19.0.0", |
| 18 | 22 | "react-markdown": "^10.1.0", |
| 19 | 23 | "rehype-highlight": "^7.0.2", |
| 20 | 24 | "remark-gfm": "^4.0.1", |
| 25 | + "tailwind-merge": "^3.4.0", | |
| 21 | 26 | "zod": "^3.24.1" |
| 22 | 27 | }, |
| 23 | 28 | "devDependencies": { |
| @@ -31,10 +31,10 @@ function Layout() { · 30 unmodified lines | ||
| 31 | 31 | |
| 32 | 32 | if (!api) { |
| 33 | 33 | return ( |
| 34 | − <div className="flex h-screen flex-col bg-[#0c0c0c] text-[#e0e0e0]"> | |
| 34 | + <div className="flex h-screen flex-col bg-background text-foreground"> | |
| 35 | 35 | <div className="drag-region h-[52px] shrink-0" /> |
| 36 | 36 | <div className="flex flex-1 items-center justify-center"> |
| 37 | − <p className="text-sm text-[#a0a0a0]/60"> | |
| 37 | + <p className="text-sm text-muted-foreground"> | |
| 38 | 38 | Native bridge unavailable. Launch through Electron. |
| 39 | 39 | </p> |
| 40 | 40 | </div> |
| @@ -43,7 +43,7 @@ function Layout() { · 2 unmodified lines | ||
| 43 | 43 | } |
| 44 | 44 | |
| 45 | 45 | return ( |
| 46 | − <div className="flex h-screen overflow-hidden bg-[#0c0c0c] text-[#e0e0e0]"> | |
| 46 | + <div className="flex h-screen overflow-hidden bg-background text-foreground"> | |
| 47 | 47 | <EventRouter /> |
| 48 | 48 | <Sidebar /> |
| 49 | 49 | <ChatView /> |
| @@ -15,7 +15,7 @@ const markdownComponents: Components = { · 14 unmodified lines | ||
| 15 | 15 | |
| 16 | 16 | export default function ChatMarkdown({ text }: ChatMarkdownProps) { |
| 17 | 17 | return ( |
| 18 | − <div className="chat-markdown text-sm leading-relaxed text-[#d0d0d0]"> | |
| 18 | + <div className="chat-markdown text-sm leading-relaxed text-foreground/80"> | |
| 19 | 19 | <ReactMarkdown |
| 20 | 20 | remarkPlugins={[remarkGfm]} |
| 21 | 21 | rehypePlugins={[[rehypeHighlight, { detect: true, ignoreMissing: true }]]} |
| @@ -1,7 +1,6 @@ | ||
| 1 | −import type { ProviderApprovalDecision, ProviderEvent } from "@acme/contracts"; | |
| 1 | +import { ArrowUpIcon, LoaderCircleIcon, SquareIcon } from "lucide-react"; | |
| 2 | 2 | import { |
| 3 | 3 | type FormEvent, |
| 4 | − Fragment, | |
| 5 | 4 | type KeyboardEvent, |
| 6 | 5 | useEffect, |
| 7 | 6 | useMemo, |
| @@ -9,7 +8,6 @@ import { · 1 unmodified line | ||
| 9 | 8 | useState, |
| 10 | 9 | } from "react"; |
| 11 | 10 | |
| 12 | −import { EDITORS, type EditorId } from "@acme/contracts"; | |
| 13 | 11 | import { |
| 14 | 12 | DEFAULT_MODEL, |
| 15 | 13 | DEFAULT_REASONING, |
| @@ -21,100 +19,44 @@ import { · 5 unmodified lines | ||
| 21 | 19 | derivePhase, |
| 22 | 20 | deriveTimelineEntries, |
| 23 | 21 | deriveWorkLogEntries, |
| 24 | − formatDuration, | |
| 25 | 22 | formatElapsed, |
| 26 | 23 | formatTimestamp, |
| 27 | 24 | readNativeApi, |
| 28 | 25 | } from "../session-logic"; |
| 29 | 26 | import { useStore } from "../store"; |
| 30 | 27 | import ChatMarkdown from "./ChatMarkdown"; |
| 28 | +import { Button } from "./ui/button"; | |
| 29 | +import { | |
| 30 | + InputGroup, | |
| 31 | + InputGroupAddon, | |
| 32 | + InputGroupText, | |
| 33 | + InputGroupTextarea, | |
| 34 | +} from "./ui/input-group"; | |
| 35 | +import { | |
| 36 | + Select, | |
| 37 | + SelectItem, | |
| 38 | + SelectPopup, | |
| 39 | + SelectTrigger, | |
| 40 | + SelectValue, | |
| 41 | +} from "./ui/select"; | |
| 31 | 42 | |
| 32 | 43 | function formatMessageMeta(createdAt: string, duration: string | null): string { |
| 33 | 44 | if (!duration) return formatTimestamp(createdAt); |
| 34 | 45 | return `${formatTimestamp(createdAt)} • ${duration}`; |
| 35 | 46 | } |
| 36 | 47 | |
| 37 | −const FILE_MANAGER_LABEL = navigator.platform.includes("Mac") | |
| 38 | − ? "Finder" | |
| 39 | − : navigator.platform.includes("Win") | |
| 40 | − ? "Explorer" | |
| 41 | − : "Files"; | |
| 42 | − | |
| 43 | −function editorLabel(editor: (typeof EDITORS)[number]): string { | |
| 44 | − return editor.command ? editor.label : FILE_MANAGER_LABEL; | |
| 48 | +function statusLabel(phase: string): string { | |
| 49 | + if (phase === "running") return "Thinking / working"; | |
| 50 | + if (phase === "connecting") return "Connecting"; | |
| 51 | + if (phase === "ready") return "Ready"; | |
| 52 | + return "Disconnected"; | |
| 45 | 53 | } |
| 46 | 54 | |
| 47 | −const LAST_EDITOR_KEY = "codething:last-editor"; | |
| 48 | − | |
| 49 | 55 | function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string { |
| 50 | − if (tone === "error") return "text-rose-300/50"; | |
| 51 | − if (tone === "tool") return "text-[#8a8a8a]"; | |
| 52 | − if (tone === "thinking") return "text-[#707070]"; | |
| 53 | − return "text-[#606060]"; | |
| 54 | −} | |
| 55 | − | |
| 56 | −interface PendingApprovalCard { | |
| 57 | − requestId: string; | |
| 58 | − requestKind: "command" | "file-change"; | |
| 59 | − createdAt: string; | |
| 60 | − detail?: string; | |
| 61 | −} | |
| 62 | − | |
| 63 | −function asRecord(value: unknown): Record<string, unknown> | undefined { | |
| 64 | − if (!value || typeof value !== "object") return undefined; | |
| 65 | − return value as Record<string, unknown>; | |
| 66 | −} | |
| 67 | − | |
| 68 | −function asString(value: unknown): string | undefined { | |
| 69 | − return typeof value === "string" ? value : undefined; | |
| 70 | −} | |
| 71 | − | |
| 72 | −function approvalDetail(event: ProviderEvent): string | undefined { | |
| 73 | − const payload = asRecord(event.payload); | |
| 74 | − const command = asString(payload?.command); | |
| 75 | − if (command) return command; | |
| 76 | − return asString(payload?.reason); | |
| 77 | −} | |
| 78 | − | |
| 79 | −function derivePendingApprovals( | |
| 80 | − events: ProviderEvent[], | |
| 81 | −): PendingApprovalCard[] { | |
| 82 | − const pending = new Map<string, PendingApprovalCard>(); | |
| 83 | − const ordered = [...events].reverse(); | |
| 84 | − | |
| 85 | − for (const event of ordered) { | |
| 86 | − if ( | |
| 87 | − event.method === "session/closed" || | |
| 88 | − event.method === "session/exited" | |
| 89 | − ) { | |
| 90 | − pending.clear(); | |
| 91 | − continue; | |
| 92 | − } | |
| 93 | − | |
| 94 | − const requestId = | |
| 95 | − event.requestId ?? asString(asRecord(event.payload)?.requestId); | |
| 96 | − if (!requestId) continue; | |
| 97 | − | |
| 98 | − if ( | |
| 99 | − event.kind === "request" && | |
| 100 | − (event.requestKind === "command" || event.requestKind === "file-change") | |
| 101 | − ) { | |
| 102 | − const detail = approvalDetail(event); | |
| 103 | − pending.set(requestId, { | |
| 104 | − requestId, | |
| 105 | − requestKind: event.requestKind, | |
| 106 | − createdAt: event.createdAt, | |
| 107 | − ...(detail ? { detail } : {}), | |
| 108 | − }); | |
| 109 | − continue; | |
| 110 | − } | |
| 111 | − | |
| 112 | − if (event.method === "item/requestApproval/decision") { | |
| 113 | − pending.delete(requestId); | |
| 114 | − } | |
| 115 | − } | |
| 116 | − | |
| 117 | − return Array.from(pending.values()); | |
| 56 | + if (tone === "error") return "text-destructive/50"; | |
| 57 | + if (tone === "tool") return "text-muted-foreground"; | |
| 58 | + if (tone === "thinking") return "text-muted-foreground/70"; | |
| 59 | + return "text-muted-foreground/60"; | |
| 118 | 60 | } |
| 119 | 61 | |
| 120 | 62 | export default function ChatView() { |
| @@ -123,46 +65,30 @@ export default function ChatView() { · 2 unmodified lines | ||
| 123 | 65 | const [prompt, setPrompt] = useState(""); |
| 124 | 66 | const [isSending, setIsSending] = useState(false); |
| 125 | 67 | const [isConnecting, setIsConnecting] = useState(false); |
| 126 | − const [isModelMenuOpen, setIsModelMenuOpen] = useState(false); | |
| 127 | − const [isEditorMenuOpen, setIsEditorMenuOpen] = useState(false); | |
| 128 | − const [lastEditor, setLastEditor] = useState<EditorId>(() => { | |
| 129 | − const stored = localStorage.getItem(LAST_EDITOR_KEY); | |
| 130 | − return EDITORS.some((e) => e.id === stored) | |
| 131 | − ? (stored as EditorId) | |
| 132 | − : EDITORS[0].id; | |
| 133 | − }); | |
| 134 | 68 | const [selectedEffort, setSelectedEffort] = |
| 135 | 69 | useState<string>(DEFAULT_REASONING); |
| 136 | − const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false); | |
| 137 | − const [respondingRequestIds, setRespondingRequestIds] = useState<string[]>( | |
| 138 | − [], | |
| 139 | − ); | |
| 140 | 70 | const [nowTick, setNowTick] = useState(() => Date.now()); |
| 141 | 71 | const messagesEndRef = useRef<HTMLDivElement>(null); |
| 142 | − const textareaRef = useRef<HTMLTextAreaElement>(null); | |
| 143 | − const modelMenuRef = useRef<HTMLDivElement>(null); | |
| 144 | − const editorMenuRef = useRef<HTMLDivElement>(null); | |
| 145 | 72 | |
| 146 | 73 | const activeThread = state.threads.find((t) => t.id === state.activeThreadId); |
| 147 | − const activeProject = state.projects.find((p) => p.id === activeThread?.projectId); | |
| 74 | + const activeProject = state.projects.find( | |
| 75 | + (p) => p.id === activeThread?.projectId, | |
| 76 | + ); | |
| 148 | 77 | const selectedModel = resolveModelSlug( |
| 149 | 78 | activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL, |
| 150 | 79 | ); |
| 151 | 80 | const phase = derivePhase(activeThread?.session ?? null); |
| 152 | 81 | const isWorking = phase === "running" || isSending || isConnecting; |
| 82 | + const activeTurnId = activeThread?.session?.activeTurnId; | |
| 153 | 83 | const nowIso = new Date(nowTick).toISOString(); |
| 154 | 84 | const modelOptions = MODEL_OPTIONS; |
| 155 | 85 | const workLogEntries = useMemo( |
| 156 | − () => deriveWorkLogEntries(activeThread?.events ?? [], undefined), | |
| 157 | − [activeThread?.events], | |
| 158 | − ); | |
| 159 | − const pendingApprovals = useMemo( | |
| 160 | − () => derivePendingApprovals(activeThread?.events ?? []), | |
| 161 | − [activeThread?.events], | |
| 86 | + () => deriveWorkLogEntries(activeThread?.events ?? [], activeTurnId), | |
| 87 | + [activeThread?.events, activeTurnId], | |
| 162 | 88 | ); |
| 163 | 89 | const assistantCompletionByItemId = useMemo(() => { |
| 164 | 90 | const map = new Map<string, string>(); |
| 165 | − const ordered = [...(activeThread?.events ?? [])].toReversed(); | |
| 91 | + const ordered = [...(activeThread?.events ?? [])].reverse(); | |
| 166 | 92 | for (const event of ordered) { |
| 167 | 93 | if (event.method !== "item/completed") continue; |
| 168 | 94 | if (!event.itemId) continue; |
| @@ -171,113 +97,27 @@ export default function ChatView() { · 2 unmodified lines | ||
| 171 | 97 | return map; |
| 172 | 98 | }, [activeThread?.events]); |
| 173 | 99 | const timelineEntries = useMemo( |
| 174 | − () => deriveTimelineEntries(activeThread?.messages ?? [], workLogEntries), | |
| 175 | − [activeThread?.messages, workLogEntries], | |
| 100 | + () => | |
| 101 | + deriveTimelineEntries( | |
| 102 | + activeThread?.messages ?? [], | |
| 103 | + isWorking ? workLogEntries : [], | |
| 104 | + ), | |
| 105 | + [activeThread?.messages, isWorking, workLogEntries], | |
| 176 | 106 | ); |
| 177 | − const completionSummary = useMemo(() => { | |
| 178 | − if (!activeThread?.latestTurnStartedAt) return null; | |
| 179 | − if (!activeThread.latestTurnCompletedAt) return null; | |
| 180 | − if (workLogEntries.length === 0) return null; | |
| 181 | − | |
| 182 | − if ( | |
| 183 | − typeof activeThread.latestTurnDurationMs === "number" && | |
| 184 | − Number.isFinite(activeThread.latestTurnDurationMs) && | |
| 185 | − activeThread.latestTurnDurationMs >= 0 | |
| 186 | − ) { | |
| 187 | − return `Worked for ${formatDuration(activeThread.latestTurnDurationMs)}`; | |
| 188 | − } | |
| 189 | − | |
| 190 | − const elapsed = formatElapsed( | |
| 191 | − activeThread.latestTurnStartedAt, | |
| 192 | − activeThread.latestTurnCompletedAt, | |
| 193 | − ); | |
| 194 | − return elapsed ? `Worked for ${elapsed}` : null; | |
| 195 | − }, [ | |
| 196 | − activeThread?.latestTurnStartedAt, | |
| 197 | − activeThread?.latestTurnCompletedAt, | |
| 198 | − activeThread?.latestTurnDurationMs, | |
| 199 | − workLogEntries.length, | |
| 200 | − ]); | |
| 201 | − const completionDividerBeforeEntryId = useMemo(() => { | |
| 202 | − if (!activeThread?.latestTurnStartedAt) return null; | |
| 203 | − if (!activeThread.latestTurnCompletedAt) return null; | |
| 204 | − if (workLogEntries.length === 0) return null; | |
| 205 | − | |
| 206 | − const turnStartedAt = Date.parse(activeThread.latestTurnStartedAt); | |
| 207 | − if (Number.isNaN(turnStartedAt)) return null; | |
| 208 | − | |
| 209 | − const entry = timelineEntries.find((timelineEntry) => { | |
| 210 | − if (timelineEntry.kind !== "message") return false; | |
| 211 | − if (timelineEntry.message.role !== "assistant") return false; | |
| 212 | − const messageAt = Date.parse(timelineEntry.message.createdAt); | |
| 213 | − return !Number.isNaN(messageAt) && messageAt >= turnStartedAt; | |
| 214 | − }); | |
| 215 | − return entry?.id ?? null; | |
| 216 | − }, [ | |
| 217 | − activeThread?.latestTurnStartedAt, | |
| 218 | − activeThread?.latestTurnCompletedAt, | |
| 219 | − timelineEntries, | |
| 220 | − workLogEntries.length, | |
| 221 | − ]); | |
| 222 | − const runtimeSessionConfig = | |
| 223 | − state.runtimeMode === "full-access" | |
| 224 | − ? ({ | |
| 225 | − approvalPolicy: "never", | |
| 226 | − sandboxMode: "danger-full-access", | |
| 227 | − } as const) | |
| 228 | − : ({ | |
| 229 | − approvalPolicy: "on-request", | |
| 230 | − sandboxMode: "workspace-write", | |
| 231 | − } as const); | |
| 232 | − | |
| 233 | − const handleRuntimeModeChange = async ( | |
| 234 | − mode: "approval-required" | "full-access", | |
| 235 | − ) => { | |
| 236 | − if (mode === state.runtimeMode) return; | |
| 237 | − dispatch({ type: "SET_RUNTIME_MODE", mode }); | |
| 238 | − if (!api) return; | |
| 239 | − | |
| 240 | − const sessionIds = state.threads | |
| 241 | − .map((t) => t.session) | |
| 242 | − .filter( | |
| 243 | − (s): s is NonNullable<typeof s> => | |
| 244 | − s !== null && s.status !== "closed", | |
| 245 | − ) | |
| 246 | − .map((s) => s.sessionId); | |
| 247 | − | |
| 248 | − if (sessionIds.length === 0) return; | |
| 249 | − | |
| 250 | − setIsSwitchingRuntimeMode(true); | |
| 251 | − try { | |
| 252 | − await Promise.all( | |
| 253 | − sessionIds.map((id) => | |
| 254 | − api.providers.stopSession({ sessionId: id }).catch(() => undefined), | |
| 255 | − ), | |
| 256 | − ); | |
| 257 | − } finally { | |
| 258 | − setIsSwitchingRuntimeMode(false); | |
| 259 | − } | |
| 260 | − }; | |
| 261 | 107 | |
| 262 | 108 | // Auto-scroll on new messages |
| 263 | 109 | const messageCount = activeThread?.messages.length ?? 0; |
| 264 | 110 | const workLogCount = workLogEntries.length; |
| 111 | + // biome-ignore lint/correctness/useExhaustiveDependencies: trigger on message count change | |
| 265 | 112 | useEffect(() => { |
| 266 | 113 | messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); |
| 267 | 114 | }, [messageCount]); |
| 115 | + // biome-ignore lint/correctness/useExhaustiveDependencies: auto-scroll while active work-log events stream in | |
| 268 | 116 | useEffect(() => { |
| 269 | 117 | if (phase !== "running") return; |
| 270 | 118 | messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); |
| 271 | 119 | }, [phase, workLogCount]); |
| 272 | 120 | |
| 273 | − // Auto-resize textarea | |
| 274 | − useEffect(() => { | |
| 275 | − const ta = textareaRef.current; | |
| 276 | − if (!ta) return; | |
| 277 | − ta.style.height = "auto"; | |
| 278 | − ta.style.height = `${Math.min(ta.scrollHeight, 200)}px`; | |
| 279 | − }, [prompt]); | |
| 280 | − | |
| 281 | 121 | useEffect(() => { |
| 282 | 122 | if (phase !== "running") return; |
| 283 | 123 | const timer = window.setInterval(() => { |
| @@ -288,63 +128,6 @@ export default function ChatView() { · 4 unmodified lines | ||
| 288 | 128 | }; |
| 289 | 129 | }, [phase]); |
| 290 | 130 | |
| 291 | − useEffect(() => { | |
| 292 | − if (!isModelMenuOpen) return; | |
| 293 | − | |
| 294 | − const handleClickOutside = (event: MouseEvent) => { | |
| 295 | − if (!modelMenuRef.current) return; | |
| 296 | − if (event.target instanceof Node && !modelMenuRef.current.contains(event.target)) { | |
| 297 | − setIsModelMenuOpen(false); | |
| 298 | − } | |
| 299 | − }; | |
| 300 | − | |
| 301 | − window.addEventListener("mousedown", handleClickOutside); | |
| 302 | − return () => { | |
| 303 | − window.removeEventListener("mousedown", handleClickOutside); | |
| 304 | − }; | |
| 305 | − }, [isModelMenuOpen]); | |
| 306 | − | |
| 307 | − useEffect(() => { | |
| 308 | − if (!isEditorMenuOpen) return; | |
| 309 | − | |
| 310 | − const handleClickOutside = (event: MouseEvent) => { | |
| 311 | − if (!editorMenuRef.current) return; | |
| 312 | − if ( | |
| 313 | − event.target instanceof Node && | |
| 314 | − !editorMenuRef.current.contains(event.target) | |
| 315 | − ) { | |
| 316 | − setIsEditorMenuOpen(false); | |
| 317 | − } | |
| 318 | − }; | |
| 319 | − | |
| 320 | − window.addEventListener("mousedown", handleClickOutside); | |
| 321 | − return () => { | |
| 322 | − window.removeEventListener("mousedown", handleClickOutside); | |
| 323 | − }; | |
| 324 | − }, [isEditorMenuOpen]); | |
| 325 | − | |
| 326 | − // Cmd+O / Ctrl+O to open in last-used editor | |
| 327 | − useEffect(() => { | |
| 328 | − const handler = (e: globalThis.KeyboardEvent) => { | |
| 329 | − if (e.key === "o" && (e.metaKey || e.ctrlKey) && !e.shiftKey) { | |
| 330 | − if (api && activeProject) { | |
| 331 | − e.preventDefault(); | |
| 332 | − void api.shell.openInEditor(activeProject.cwd, lastEditor); | |
| 333 | − } | |
| 334 | − } | |
| 335 | − }; | |
| 336 | − window.addEventListener("keydown", handler); | |
| 337 | − return () => window.removeEventListener("keydown", handler); | |
| 338 | − }, [api, activeProject, lastEditor]); | |
| 339 | − | |
| 340 | − const openInEditor = (editorId: EditorId) => { | |
| 341 | − if (!api || !activeProject) return; | |
| 342 | − void api.shell.openInEditor(activeProject.cwd, editorId); | |
| 343 | − setLastEditor(editorId); | |
| 344 | − localStorage.setItem(LAST_EDITOR_KEY, editorId); | |
| 345 | − setIsEditorMenuOpen(false); | |
| 346 | − }; | |
| 347 | − | |
| 348 | 131 | const ensureSession = async (): Promise<string | null> => { |
| 349 | 132 | if (!api || !activeThread || !activeProject) return null; |
| 350 | 133 | if (activeThread.session && activeThread.session.status !== "closed") { |
| @@ -357,8 +140,6 @@ export default function ChatView() { · 6 unmodified lines | ||
| 357 | 140 | provider: "codex", |
| 358 | 141 | cwd: activeProject.cwd || undefined, |
| 359 | 142 | model: selectedModel || undefined, |
| 360 | − approvalPolicy: runtimeSessionConfig.approvalPolicy, | |
| 361 | − sandboxMode: runtimeSessionConfig.sandboxMode, | |
| 362 | 143 | }); |
| 363 | 144 | dispatch({ |
| 364 | 145 | type: "UPDATE_SESSION", |
| @@ -386,7 +167,8 @@ export default function ChatView() { · 21 unmodified lines | ||
| 386 | 167 | |
| 387 | 168 | // Auto-title from first message |
| 388 | 169 | if (activeThread.messages.length === 0) { |
| 389 | − const title = trimmed.length > 50 ? `${trimmed.slice(0, 50)}...` : trimmed; | |
| 170 | + const title = | |
| 171 | + trimmed.length > 50 ? `${trimmed.slice(0, 50)}...` : trimmed; | |
| 390 | 172 | dispatch({ |
| 391 | 173 | type: "SET_THREAD_TITLE", |
| 392 | 174 | threadId: activeThread.id, |
| @@ -437,37 +219,6 @@ export default function ChatView() { · 44 unmodified lines | ||
| 437 | 219 | }); |
| 438 | 220 | }; |
| 439 | 221 | |
| 440 | − const onRespondToApproval = async ( | |
| 441 | − requestId: string, | |
| 442 | − decision: ProviderApprovalDecision, | |
| 443 | − ) => { | |
| 444 | − if (!api || !activeThread?.session) return; | |
| 445 | − | |
| 446 | − setRespondingRequestIds((existing) => | |
| 447 | − existing.includes(requestId) ? existing : [...existing, requestId], | |
| 448 | − ); | |
| 449 | − try { | |
| 450 | − await api.providers.respondToRequest({ | |
| 451 | − sessionId: activeThread.session.sessionId, | |
| 452 | − requestId, | |
| 453 | − decision, | |
| 454 | − }); | |
| 455 | − } catch (err) { | |
| 456 | − dispatch({ | |
| 457 | − type: "SET_ERROR", | |
| 458 | − threadId: activeThread.id, | |
| 459 | − error: | |
| 460 | − err instanceof Error | |
| 461 | − ? err.message | |
| 462 | − : "Failed to submit approval decision.", | |
| 463 | − }); | |
| 464 | − } finally { | |
| 465 | − setRespondingRequestIds((existing) => | |
| 466 | − existing.filter((id) => id !== requestId), | |
| 467 | − ); | |
| 468 | − } | |
| 469 | − }; | |
| 470 | − | |
| 471 | 222 | const onModelSelect = (model: string) => { |
| 472 | 223 | if (!activeThread) return; |
| 473 | 224 | dispatch({ |
| @@ -475,7 +226,6 @@ export default function ChatView() { · 1 unmodified line | ||
| 475 | 226 | threadId: activeThread.id, |
| 476 | 227 | model: resolveModelSlug(model), |
| 477 | 228 | }); |
| 478 | − setIsModelMenuOpen(false); | |
| 479 | 229 | }; |
| 480 | 230 | |
| 481 | 231 | const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => { |
| @@ -488,11 +238,13 @@ export default function ChatView() { · 6 unmodified lines | ||
| 488 | 238 | // Empty state: no active thread |
| 489 | 239 | if (!activeThread) { |
| 490 | 240 | return ( |
| 491 | − <div className="flex flex-1 flex-col bg-[#0c0c0c] text-[#a0a0a0]/40"> | |
| 241 | + <div className="flex flex-1 flex-col bg-background text-muted-foreground/60"> | |
| 492 | 242 | <div className="drag-region h-[52px] shrink-0" /> |
| 493 | 243 | <div className="flex flex-1 items-center justify-center"> |
| 494 | 244 | <div className="text-center"> |
| 495 | − <p className="text-sm">Select a thread or create a new one to get started.</p> | |
| 245 | + <p className="text-sm"> | |
| 246 | + Select a thread or create a new one to get started. | |
| 247 | + </p> | |
| 496 | 248 | </div> |
| 497 | 249 | </div> |
| 498 | 250 | </div> |
| @@ -500,55 +252,61 @@ export default function ChatView() { · 1 unmodified line | ||
| 500 | 252 | } |
| 501 | 253 | |
| 502 | 254 | return ( |
| 503 | − <div className="flex flex-1 flex-col bg-[#0c0c0c]"> | |
| 255 | + <div className="flex flex-1 flex-col bg-background"> | |
| 504 | 256 | {/* Top bar */} |
| 505 | − <header className="drag-region flex items-center justify-between border-b border-white/8 px-5 pt-[28px] pb-3"> | |
| 257 | + <header className="drag-region flex items-center justify-between border-b px-5 pt-[28px] pb-3"> | |
| 506 | 258 | <div className="flex items-center gap-3"> |
| 507 | − <h2 className="text-sm font-medium text-[#e0e0e0]"> | |
| 259 | + <h2 className="text-sm font-medium text-foreground"> | |
| 508 | 260 | {activeThread.title} |
| 509 | 261 | </h2> |
| 262 | + {activeProject && ( | |
| 263 | + <span className="rounded-full bg-accent px-2 py-0.5 text-[10px] text-muted-foreground/70"> | |
| 264 | + {activeProject.name} | |
| 265 | + </span> | |
| 266 | + )} | |
| 510 | 267 | </div> |
| 511 | 268 | <div className="flex items-center gap-3"> |
| 512 | − {/* Open in editor */} | |
| 513 | − {activeProject && ( | |
| 514 | − <div className="relative" ref={editorMenuRef}> | |
| 515 | − <button | |
| 516 | − type="button" | |
| 517 | − className="rounded-md px-2 py-1 text-[10px] text-[#a0a0a0]/40 transition-colors duration-150 hover:text-[#a0a0a0]/60" | |
| 518 | − onClick={() => setIsEditorMenuOpen((v) => !v)} | |
| 519 | − > | |
| 520 | − Open in… | |
| 521 | − </button> | |
| 522 | − {isEditorMenuOpen && ( | |
| 523 | − <div className="absolute right-0 top-full z-50 mt-1 min-w-[120px] rounded-md border border-white/[0.08] bg-[#1b1b1d] py-1 shadow-xl"> | |
| 524 | − {EDITORS.map((editor) => ( | |
| 525 | − <button | |
| 526 | − key={editor.id} | |
| 527 | − type="button" | |
| 528 | − className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-[11px] text-[#e0e0e0] hover:bg-white/[0.06]" | |
| 529 | − onClick={() => openInEditor(editor.id)} | |
| 530 | − > | |
| 531 | − {editorLabel(editor)} | |
| 532 | − {editor.id === lastEditor && ( | |
| 533 | − <kbd className="ml-auto text-[9px] text-[#a0a0a0]/40"> | |
| 534 | − {navigator.platform.includes("Mac") | |
| 535 | − ? "\u2318O" | |
| 536 | − : "Ctrl+O"} | |
| 537 | − </kbd> | |
| 538 | − )} | |
| 539 | − </button> | |
| 540 | − ))} | |
| 541 | − </div> | |
| 269 | + {/* Status indicator */} | |
| 270 | + <div | |
| 271 | + className={`inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-[10px] ${ | |
| 272 | + phase === "running" | |
| 273 | + ? "border-sky-400/35 bg-sky-500/8 text-sky-700 dark:text-sky-100" | |
| 274 | + : phase === "connecting" | |
| 275 | + ? "border-amber-400/35 bg-amber-500/8 text-amber-700 dark:text-amber-100" | |
| 276 | + : phase === "ready" | |
| 277 | + ? "border-emerald-400/35 bg-emerald-500/8 text-emerald-700 dark:text-emerald-100" | |
| 278 | + : "border-border bg-secondary text-muted-foreground" | |
| 279 | + }`} | |
| 280 | + > | |
| 281 | + <span className="relative inline-flex h-2.5 w-2.5"> | |
| 282 | + {(phase === "running" || phase === "connecting") && ( | |
| 283 | + <span | |
| 284 | + className={`absolute inline-flex h-full w-full animate-ping rounded-full ${ | |
| 285 | + phase === "running" ? "bg-sky-300/70" : "bg-amber-300/70" | |
| 286 | + }`} | |
| 287 | + /> | |
| 542 | 288 | )} |
| 543 | − </div> | |
| 544 | − )} | |
| 289 | + <span | |
| 290 | + className={`relative inline-flex h-2.5 w-2.5 rounded-full ${ | |
| 291 | + phase === "running" | |
| 292 | + ? "bg-sky-200" | |
| 293 | + : phase === "connecting" | |
| 294 | + ? "bg-amber-200" | |
| 295 | + : phase === "ready" | |
| 296 | + ? "bg-emerald-200" | |
| 297 | + : "bg-muted-foreground/40" | |
| 298 | + }`} | |
| 299 | + /> | |
| 300 | + </span> | |
| 301 | + <span>{statusLabel(phase)}</span> | |
| 302 | + </div> | |
| 545 | 303 | {/* Diff toggle */} |
| 546 | 304 | <button |
| 547 | 305 | type="button" |
| 548 | 306 | className={`rounded-md px-2 py-1 text-[10px] transition-colors duration-150 ${ |
| 549 | 307 | state.diffOpen |
| 550 | − ? "bg-white/10 text-white" | |
| 551 | − : "text-[#a0a0a0]/40 hover:text-[#a0a0a0]/60" | |
| 308 | + ? "bg-accent text-accent-foreground" | |
| 309 | + : "text-muted-foreground/60 hover:text-muted-foreground" | |
| 552 | 310 | }`} |
| 553 | 311 | onClick={() => dispatch({ type: "TOGGLE_DIFF" })} |
| 554 | 312 | > |
| @@ -559,192 +317,105 @@ export default function ChatView() { · 4 unmodified lines | ||
| 559 | 317 | |
| 560 | 318 | {/* Error banner */} |
| 561 | 319 | {activeThread.error && ( |
| 562 | − <div className="mx-4 mt-3 rounded-lg border border-rose-400/20 bg-rose-900/20 px-3 py-2 text-xs text-rose-200"> | |
| 320 | + <div className="mx-4 mt-3 rounded-lg border border-destructive/20 bg-destructive/10 px-3 py-2 text-xs text-destructive-foreground"> | |
| 563 | 321 | {activeThread.error} |
| 564 | 322 | </div> |
| 565 | 323 | )} |
| 566 | 324 | |
| 567 | − {pendingApprovals.length > 0 && ( | |
| 568 | − <div className="mx-4 mt-3 space-y-2"> | |
| 569 | − {pendingApprovals.map((approval) => { | |
| 570 | − const isResponding = respondingRequestIds.includes( | |
| 571 | − approval.requestId, | |
| 572 | − ); | |
| 573 | − return ( | |
| 574 | − <div | |
| 575 | − key={approval.requestId} | |
| 576 | − className="rounded-lg border border-amber-300/20 bg-amber-500/[0.07] px-3 py-2" | |
| 577 | − > | |
| 578 | − <p className="text-xs font-medium text-amber-100"> | |
| 579 | − {approval.requestKind === "command" | |
| 580 | − ? "Command approval requested" | |
| 581 | − : "File-change approval requested"} | |
| 582 | − </p> | |
| 583 | − {approval.detail && ( | |
| 584 | − <p | |
| 585 | − className="mt-1 truncate font-mono text-[11px] text-amber-100/75" | |
| 586 | − title={approval.detail} | |
| 587 | − > | |
| 588 | − {approval.detail} | |
| 589 | − </p> | |
| 590 | − )} | |
| 591 | − <div className="mt-2 flex flex-wrap gap-1.5"> | |
| 592 | − <button | |
| 593 | − type="button" | |
| 594 | − className="rounded-md border border-white/[0.15] bg-white/[0.08] px-2 py-1 text-[11px] text-[#e8e8e8] transition-colors duration-150 hover:bg-white/[0.13] disabled:cursor-not-allowed disabled:opacity-50" | |
| 595 | − disabled={isResponding} | |
| 596 | − onClick={() => | |
| 597 | − void onRespondToApproval(approval.requestId, "accept") | |
| 598 | − } | |
| 599 | − > | |
| 600 | − Approve once | |
| 601 | − </button> | |
| 602 | − <button | |
| 603 | − type="button" | |
| 604 | − className="rounded-md border border-sky-300/30 bg-sky-500/[0.15] px-2 py-1 text-[11px] text-sky-100 transition-colors duration-150 hover:bg-sky-500/[0.22] disabled:cursor-not-allowed disabled:opacity-50" | |
| 605 | − disabled={isResponding} | |
| 606 | − onClick={() => | |
| 607 | − void onRespondToApproval( | |
| 608 | − approval.requestId, | |
| 609 | − "acceptForSession", | |
| 610 | − ) | |
| 611 | − } | |
| 612 | − > | |
| 613 | − Always allow this session | |
| 614 | − </button> | |
| 615 | − <button | |
| 616 | − type="button" | |
| 617 | − className="rounded-md border border-white/[0.15] px-2 py-1 text-[11px] text-[#d8d8d8] transition-colors duration-150 hover:bg-white/[0.06] disabled:cursor-not-allowed disabled:opacity-50" | |
| 618 | − disabled={isResponding} | |
| 619 | − onClick={() => | |
| 620 | − void onRespondToApproval(approval.requestId, "decline") | |
| 621 | − } | |
| 622 | − > | |
| 623 | − Decline | |
| 624 | − </button> | |
| 625 | − <button | |
| 626 | − type="button" | |
| 627 | − className="rounded-md border border-rose-300/30 bg-rose-500/[0.12] px-2 py-1 text-[11px] text-rose-100 transition-colors duration-150 hover:bg-rose-500/[0.2] disabled:cursor-not-allowed disabled:opacity-50" | |
| 628 | − disabled={isResponding} | |
| 629 | − onClick={() => | |
| 630 | − void onRespondToApproval(approval.requestId, "cancel") | |
| 631 | − } | |
| 632 | − > | |
| 633 | − Cancel turn | |
| 634 | − </button> | |
| 635 | − </div> | |
| 636 | − </div> | |
| 637 | − ); | |
| 638 | − })} | |
| 639 | − </div> | |
| 640 | − )} | |
| 641 | − | |
| 642 | 325 | {/* Messages */} |
| 643 | 326 | <div className="flex-1 overflow-y-auto px-5 py-4"> |
| 644 | 327 | {activeThread.messages.length === 0 && !isWorking ? ( |
| 645 | 328 | <div className="flex h-full items-center justify-center"> |
| 646 | − <p className="text-sm text-[#a0a0a0]/30">Send a message to start the conversation.</p> | |
| 329 | + <p className="text-sm text-muted-foreground/40"> | |
| 330 | + Send a message to start the conversation. | |
| 331 | + </p> | |
| 647 | 332 | </div> |
| 648 | 333 | ) : ( |
| 649 | 334 | <div className="mx-auto max-w-3xl space-y-4"> |
| 650 | − {timelineEntries.map((timelineEntry, index) => ( | |
| 651 | − <Fragment key={timelineEntry.id}> | |
| 652 | − {timelineEntry.kind === "message" && | |
| 653 | − timelineEntry.message.role === "assistant" && | |
| 654 | − (completionDividerBeforeEntryId === timelineEntry.id || | |
| 655 | − timelineEntries[index - 1]?.kind === "work") && ( | |
| 656 | − <div className="my-3 flex items-center gap-3"> | |
| 657 | − <span className="h-px flex-1 bg-white/[0.1]" /> | |
| 658 | − <span className="rounded-full border border-white/[0.12] bg-[#121212] px-2.5 py-1 text-[10px] uppercase tracking-[0.14em] text-[#9a9a9a]/80"> | |
| 659 | − {completionSummary | |
| 660 | − ? `Response • ${completionSummary}` | |
| 661 | − : "Response"} | |
| 662 | − </span> | |
| 663 | − <span className="h-px flex-1 bg-white/[0.1]" /> | |
| 335 | + {timelineEntries.map((timelineEntry) => | |
| 336 | + timelineEntry.kind === "work" ? ( | |
| 337 | + <div | |
| 338 | + key={timelineEntry.id} | |
| 339 | + className="border-l-2 border-border/60 pl-4 py-1" | |
| 340 | + > | |
| 341 | + <p | |
| 342 | + className={`py-[2px] text-[12px] leading-relaxed ${workToneClass(timelineEntry.entry.tone)}`} | |
| 343 | + > | |
| 344 | + {timelineEntry.entry.detail ? ( | |
| 345 | + <> | |
| 346 | + {timelineEntry.entry.label} | |
| 347 | + <span className="ml-1.5 font-mono text-[11px] opacity-60"> | |
| 348 | + {timelineEntry.entry.detail} | |
| 349 | + </span> | |
| 350 | + </> | |
| 351 | + ) : ( | |
| 352 | + timelineEntry.entry.label | |
| 353 | + )} | |
| 354 | + </p> | |
| 355 | + </div> | |
| 356 | + ) : ( | |
| 357 | + <div key={timelineEntry.id}> | |
| 358 | + {timelineEntry.message.role === "user" ? ( | |
| 359 | + <div className="flex justify-end"> | |
| 360 | + <div className="max-w-[80%] rounded-2xl rounded-br-sm border border-border bg-accent px-4 py-3"> | |
| 361 | + <pre className="whitespace-pre-wrap wrap-break-word font-mono text-sm leading-relaxed text-foreground"> | |
| 362 | + {timelineEntry.message.text} | |
| 363 | + </pre> | |
| 364 | + <p className="mt-1.5 text-right text-[10px] text-muted-foreground/40"> | |
| 365 | + {formatTimestamp(timelineEntry.message.createdAt)} | |
| 366 | + </p> | |
| 367 | + </div> | |
| 664 | 368 | </div> |
| 665 | − )} | |
| 666 | − {timelineEntry.kind === "work" ? ( | |
| 667 | − <div className="flex items-start gap-2 py-0.5 pl-1.5"> | |
| 668 | − <span className="mt-[7px] h-1.5 w-1.5 shrink-0 rounded-full bg-white/[0.18]" /> | |
| 669 | − <p | |
| 670 | − className={`py-[2px] text-[11px] leading-relaxed ${workToneClass(timelineEntry.entry.tone)}`} | |
| 671 | − > | |
| 672 | − {timelineEntry.entry.detail ? ( | |
| 673 | − <> | |
| 674 | − {timelineEntry.entry.label} | |
| 675 | − <span | |
| 676 | − className="ml-1.5 inline-block max-w-[70ch] truncate align-bottom font-mono text-[11px] opacity-60" | |
| 677 | − title={timelineEntry.entry.detail} | |
| 678 | − > | |
| 679 | − {timelineEntry.entry.detail} | |
| 369 | + ) : ( | |
| 370 | + <div className="border-l-2 border-border pl-4"> | |
| 371 | + <ChatMarkdown | |
| 372 | + text={ | |
| 373 | + timelineEntry.message.text || | |
| 374 | + (timelineEntry.message.streaming | |
| 375 | + ? "" | |
| 376 | + : "(empty response)") | |
| 377 | + } | |
| 378 | + /> | |
| 379 | + {timelineEntry.message.streaming && ( | |
| 380 | + <div className="pt-1.5"> | |
| 381 | + <span className="inline-flex items-center gap-2 rounded-full border border-sky-400/25 bg-sky-500/8 px-2 py-0.5 text-[10px] text-sky-700/90 dark:text-sky-100/90"> | |
| 382 | + <span className="inline-flex gap-1"> | |
| 383 | + <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-500/80 dark:bg-sky-100/80" /> | |
| 384 | + <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-500/80 dark:bg-sky-100/80 [animation-delay:150ms]" /> | |
| 385 | + <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-500/80 dark:bg-sky-100/80 [animation-delay:300ms]" /> | |
| 386 | + </span> | |
| 387 | + <span>Thinking</span> | |
| 680 | 388 | </span> |
| 681 | − </> | |
| 682 | − ) : ( | |
| 683 | − timelineEntry.entry.label | |
| 389 | + </div> | |
| 684 | 390 | )} |
| 685 | − </p> | |
| 686 | − </div> | |
| 687 | − ) : timelineEntry.message.role === "user" ? ( | |
| 688 | − <div className="flex justify-end"> | |
| 689 | − <div className="max-w-[80%] rounded-2xl rounded-br-sm border border-white/[0.08] bg-white/[0.05] px-4 py-3"> | |
| 690 | − <pre className="whitespace-pre-wrap break-words font-mono text-sm leading-relaxed text-[#e0e0e0]"> | |
| 691 | − {timelineEntry.message.text} | |
| 692 | − </pre> | |
| 693 | − <p className="mt-1.5 text-right text-[10px] text-[#a0a0a0]/30"> | |
| 694 | − {formatTimestamp(timelineEntry.message.createdAt)} | |
| 391 | + <p className="mt-1.5 text-[10px] text-muted-foreground/40"> | |
| 392 | + {formatMessageMeta( | |
| 393 | + timelineEntry.message.createdAt, | |
| 394 | + timelineEntry.message.streaming | |
| 395 | + ? formatElapsed( | |
| 396 | + timelineEntry.message.createdAt, | |
| 397 | + nowIso, | |
| 398 | + ) | |
| 399 | + : formatElapsed( | |
| 400 | + timelineEntry.message.createdAt, | |
| 401 | + assistantCompletionByItemId.get( | |
| 402 | + timelineEntry.message.id, | |
| 403 | + ), | |
| 404 | + ), | |
| 405 | + )} | |
| 695 | 406 | </p> |
| 696 | 407 | </div> |
| 697 | − </div> | |
| 698 | − ) : ( | |
| 699 | − <div className="px-1 py-0.5"> | |
| 700 | − <ChatMarkdown | |
| 701 | − text={ | |
| 702 | − timelineEntry.message.text || | |
| 703 | − (timelineEntry.message.streaming | |
| 704 | − ? "" | |
| 705 | − : "(empty response)") | |
| 706 | − } | |
| 707 | − /> | |
| 708 | − {timelineEntry.message.streaming && ( | |
| 709 | − <div className="pt-1.5"> | |
| 710 | − <span className="inline-flex items-center gap-2 rounded-full border border-sky-400/25 bg-sky-500/[0.08] px-2 py-0.5 text-[10px] text-sky-100/90"> | |
| 711 | − <span className="inline-flex gap-1"> | |
| 712 | − <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-100/80" /> | |
| 713 | − <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-100/80 [animation-delay:150ms]" /> | |
| 714 | − <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-100/80 [animation-delay:300ms]" /> | |
| 715 | − </span> | |
| 716 | − <span>Thinking</span> | |
| 717 | − </span> | |
| 718 | − </div> | |
| 719 | − )} | |
| 720 | − <p className="mt-1.5 text-[10px] text-[#a0a0a0]/30"> | |
| 721 | − {formatMessageMeta( | |
| 722 | − timelineEntry.message.createdAt, | |
| 723 | − timelineEntry.message.streaming | |
| 724 | − ? formatElapsed( | |
| 725 | − timelineEntry.message.createdAt, | |
| 726 | − nowIso, | |
| 727 | − ) | |
| 728 | − : formatElapsed( | |
| 729 | − timelineEntry.message.createdAt, | |
| 730 | − assistantCompletionByItemId.get( | |
| 731 | − timelineEntry.message.id, | |
| 732 | − ), | |
| 733 | − ), | |
| 734 | − )} | |
| 735 | − </p> | |
| 736 | − </div> | |
| 737 | − )} | |
| 738 | − </Fragment> | |
| 739 | − ))} | |
| 408 | + )} | |
| 409 | + </div> | |
| 410 | + ), | |
| 411 | + )} | |
| 740 | 412 | {isWorking && ( |
| 741 | − <div className="flex items-center gap-2 py-0.5 pl-1.5"> | |
| 742 | − <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-white/[0.18]" /> | |
| 413 | + <div className="border-l-2 border-border/60 pl-4 py-1"> | |
| 743 | 414 | <div className="flex items-center pt-1"> |
| 744 | 415 | <span className="inline-flex items-center gap-[3px]"> |
| 745 | − <span className="h-1 w-1 rounded-full bg-white/20 animate-pulse" /> | |
| 746 | − <span className="h-1 w-1 rounded-full bg-white/20 animate-pulse [animation-delay:200ms]" /> | |
| 747 | − <span className="h-1 w-1 rounded-full bg-white/20 animate-pulse [animation-delay:400ms]" /> | |
| 416 | + <span className="h-1 w-1 rounded-full bg-foreground/20 animate-pulse" /> | |
| 417 | + <span className="h-1 w-1 rounded-full bg-foreground/20 animate-pulse [animation-delay:200ms]" /> | |
| 418 | + <span className="h-1 w-1 rounded-full bg-foreground/20 animate-pulse [animation-delay:400ms]" /> | |
| 748 | 419 | </span> |
| 749 | 420 | </div> |
| 750 | 421 | </div> |
| @@ -757,271 +428,101 @@ export default function ChatView() { · 6 unmodified lines | ||
| 757 | 428 | {/* Input bar */} |
| 758 | 429 | <div className="px-5 pb-4 pt-2"> |
| 759 | 430 | <form onSubmit={onSend} className="mx-auto max-w-3xl"> |
| 760 | − <div className="group rounded-[20px] border border-white/8 bg-[#141416] transition-colors duration-200 focus-within:border-white/16"> | |
| 761 | − {/* Textarea area */} | |
| 762 | − <div className="px-4 pt-4 pb-2"> | |
| 763 | − <textarea | |
| 764 | − ref={textareaRef} | |
| 765 | − className="w-full resize-none bg-transparent text-[14px] leading-relaxed text-[#e0e0e0] placeholder:text-[#a0a0a0]/35 focus:outline-none" | |
| 766 | − rows={2} | |
| 767 | − value={prompt} | |
| 768 | − onChange={(e) => setPrompt(e.target.value)} | |
| 769 | − onKeyDown={onKeyDown} | |
| 770 | − placeholder={ | |
| 771 | − phase === "disconnected" ? "Ask for follow-up changes" : "Ask anything..." | |
| 772 | − } | |
| 773 | − disabled={isSending || isConnecting} | |
| 774 | − /> | |
| 775 | − </div> | |
| 776 | − | |
| 777 | − {/* Bottom toolbar */} | |
| 778 | − <div className="flex items-center justify-between px-3 pb-3"> | |
| 779 | − <div className="flex items-center gap-1"> | |
| 780 | − {/* Model picker */} | |
| 781 | − <div className="relative" ref={modelMenuRef}> | |
| 782 | − <button | |
| 783 | − type="button" | |
| 784 | − className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/6 hover:text-[#d0d0d0]" | |
| 785 | − onClick={() => setIsModelMenuOpen((open) => !open)} | |
| 786 | − > | |
| 787 | − <span className="max-w-[180px] truncate">{selectedModel}</span> | |
| 788 | − <svg | |
| 789 | − width="10" | |
| 790 | − height="10" | |
| 791 | − viewBox="0 0 10 10" | |
| 792 | − fill="none" | |
| 793 | − className="opacity-50" | |
| 794 | − aria-hidden="true" | |
| 795 | − > | |
| 796 | − <path | |
| 797 | − d="M2.5 4L5 6.5L7.5 4" | |
| 798 | − stroke="currentColor" | |
| 799 | − strokeWidth="1.2" | |
| 800 | − strokeLinecap="round" | |
| 801 | − strokeLinejoin="round" | |
| 802 | − /> | |
| 803 | − </svg> | |
| 804 | − </button> | |
| 805 | − {isModelMenuOpen && ( | |
| 806 | − <div className="absolute bottom-full left-0 z-20 mb-2 w-[320px] rounded-2xl border border-white/10 bg-[#1b1b1d]/95 p-2 shadow-[0_16px_40px_rgba(0,0,0,0.55)] backdrop-blur"> | |
| 807 | − <p className="px-2 py-1 text-[11px] text-[#a0a0a0]/70">Select model</p> | |
| 808 | − <div className="max-h-72 overflow-y-auto"> | |
| 809 | − {modelOptions.map((model) => { | |
| 810 | − const isSelected = model === selectedModel; | |
| 811 | − return ( | |
| 812 | − <button | |
| 813 | − key={model} | |
| 814 | − type="button" | |
| 815 | − className={`mb-0.5 flex w-full items-center justify-between gap-2 rounded-xl px-2 py-2 text-left font-mono text-sm transition-colors duration-150 ${ | |
| 816 | − isSelected | |
| 817 | − ? "bg-white/8 text-white" | |
| 818 | − : "text-[#d4d4d4] hover:bg-white/5" | |
| 819 | − }`} | |
| 820 | − onClick={() => onModelSelect(model)} | |
| 821 | − > | |
| 822 | − <span className="truncate">{model}</span> | |
| 823 | − <span | |
| 824 | − className={`pt-0.5 text-sm ${ | |
| 825 | − isSelected ? "text-white" : "text-transparent" | |
| 826 | − }`} | |
| 827 | − > | |
| 828 | − ✓ | |
| 829 | − </span> | |
| 830 | − </button> | |
| 831 | − ); | |
| 832 | − })} | |
| 833 | − </div> | |
| 834 | − </div> | |
| 835 | − )} | |
| 836 | − </div> | |
| 837 | − | |
| 838 | − {/* Divider */} | |
| 839 | − <div className="mx-0.5 h-4 w-px bg-white/8" /> | |
| 431 | + <InputGroup className="rounded-[20px] before:rounded-[19px] **:[textarea]:max-h-[200px]"> | |
| 432 | + <InputGroupTextarea | |
| 433 | + value={prompt} | |
| 434 | + onChange={(e) => setPrompt(e.target.value)} | |
| 435 | + onKeyDown={onKeyDown} | |
| 436 | + placeholder={ | |
| 437 | + phase === "disconnected" | |
| 438 | + ? "Ask for follow-up changes" | |
| 439 | + : "Ask anything..." | |
| 440 | + } | |
| 441 | + disabled={isSending || isConnecting} | |
| 442 | + rows={2} | |
| 443 | + className="font-sans!" | |
| 444 | + /> | |
| 445 | + <InputGroupAddon align="block-end"> | |
| 446 | + {/* Model picker */} | |
| 447 | + <Select | |
| 448 | + value={selectedModel} | |
| 449 | + onValueChange={(val) => val && onModelSelect(val)} | |
| 450 | + > | |
| 451 | + <SelectTrigger | |
| 452 | + size="sm" | |
| 453 | + className="w-auto min-w-0 border-0 shadow-none bg-transparent dark:bg-transparent before:hidden data-[popup-open]:bg-accent dark:data-[popup-open]:bg-accent" | |
| 454 | + > | |
| 455 | + <SelectValue /> | |
| 456 | + </SelectTrigger> | |
| 457 | + <SelectPopup side="top" align="start"> | |
| 458 | + {modelOptions.map((model) => ( | |
| 459 | + <SelectItem key={model} value={model}> | |
| 460 | + {model} | |
| 461 | + </SelectItem> | |
| 462 | + ))} | |
| 463 | + </SelectPopup> | |
| 464 | + </Select> | |
| 840 | 465 | |
| 841 | − {/* Reasoning effort */} | |
| 842 | − <label | |
| 843 | − className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/6 hover:text-[#d0d0d0]" | |
| 844 | − htmlFor="reasoning-effort" | |
| 466 | + {/* Reasoning effort */} | |
| 467 | + <Select | |
| 468 | + value={selectedEffort} | |
| 469 | + onValueChange={(val) => val && setSelectedEffort(val)} | |
| 470 | + > | |
| 471 | + <SelectTrigger | |
| 472 | + size="sm" | |
| 473 | + className="w-auto min-w-0 border-0 shadow-none bg-transparent dark:bg-transparent before:hidden data-[popup-open]:bg-accent dark:data-[popup-open]:bg-accent capitalize" | |
| 845 | 474 | > |
| 846 | − <span>{selectedEffort.charAt(0).toUpperCase() + selectedEffort.slice(1)}</span> | |
| 847 | − <select | |
| 848 | − id="reasoning-effort" | |
| 849 | − className="absolute opacity-0 w-0 h-0" | |
| 850 | − value={selectedEffort} | |
| 851 | − onChange={(event) => setSelectedEffort(event.target.value)} | |
| 852 | − > | |
| 853 | − {REASONING_OPTIONS.map((effort) => ( | |
| 854 | − <option key={effort} value={effort} className="bg-[#1b1b1d]"> | |
| 855 | − {effort} | |
| 856 | − {effort === DEFAULT_REASONING ? " (default)" : ""} | |
| 857 | − </option> | |
| 858 | − ))} | |
| 859 | − </select> | |
| 860 | − <svg | |
| 861 | − width="10" | |
| 862 | − height="10" | |
| 863 | − viewBox="0 0 10 10" | |
| 864 | − fill="none" | |
| 865 | − className="opacity-50" | |
| 866 | − aria-hidden="true" | |
| 867 | − > | |
| 868 | − <path | |
| 869 | − d="M2.5 4L5 6.5L7.5 4" | |
| 870 | − stroke="currentColor" | |
| 871 | − strokeWidth="1.2" | |
| 872 | − strokeLinecap="round" | |
| 873 | − strokeLinejoin="round" | |
| 874 | − /> | |
| 875 | − </svg> | |
| 876 | − </label> | |
| 475 | + <SelectValue /> | |
| 476 | + </SelectTrigger> | |
| 477 | + <SelectPopup side="top" align="start"> | |
| 478 | + {REASONING_OPTIONS.map((effort) => ( | |
| 479 | + <SelectItem key={effort} value={effort}> | |
| 480 | + {effort.charAt(0).toUpperCase() + effort.slice(1)} | |
| 481 | + {effort === DEFAULT_REASONING ? " (default)" : ""} | |
| 482 | + </SelectItem> | |
| 483 | + ))} | |
| 484 | + </SelectPopup> | |
| 485 | + </Select> | |
| 877 | 486 | |
| 878 | − {/* Divider */} | |
| 879 | − <div className="mx-0.5 h-4 w-px bg-white/[0.08]" /> | |
| 487 | + {activeProject && ( | |
| 488 | + <InputGroupText className="ml-auto text-muted-foreground/40"> | |
| 489 | + {activeProject.name} | |
| 490 | + </InputGroupText> | |
| 491 | + )} | |
| 880 | 492 | |
| 881 | − {/* Runtime mode toggle */} | |
| 882 | − <button | |
| 883 | − type="button" | |
| 884 | − className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-[13px] text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/[0.06] hover:text-[#d0d0d0]" | |
| 885 | − disabled={isSwitchingRuntimeMode} | |
| 886 | − onClick={() => | |
| 887 | − void handleRuntimeModeChange( | |
| 888 | − state.runtimeMode === "full-access" | |
| 889 | − ? "approval-required" | |
| 890 | − : "full-access", | |
| 891 | − ) | |
| 892 | − } | |
| 893 | − title={ | |
| 894 | − state.runtimeMode === "full-access" | |
| 895 | − ? "Full access — click to require approvals" | |
| 896 | − : "Approval required — click for full access" | |
| 493 | + {phase === "running" ? ( | |
| 494 | + <Button | |
| 495 | + variant="destructive" | |
| 496 | + size="icon-sm" | |
| 497 | + className="rounded-full" | |
| 498 | + onClick={() => void onInterrupt()} | |
| 499 | + aria-label="Stop generation" | |
| 500 | + > | |
| 501 | + <SquareIcon className="size-3" /> | |
| 502 | + </Button> | |
| 503 | + ) : ( | |
| 504 | + <Button | |
| 505 | + type="submit" | |
| 506 | + size="icon-sm" | |
| 507 | + className="rounded-full" | |
| 508 | + disabled={isSending || isConnecting || !prompt.trim()} | |
| 509 | + aria-label={ | |
| 510 | + isConnecting | |
| 511 | + ? "Connecting" | |
| 512 | + : isSending | |
| 513 | + ? "Sending" | |
| 514 | + : "Send message" | |
| 897 | 515 | } |
| 898 | 516 | > |
| 899 | − {state.runtimeMode === "full-access" ? ( | |
| 900 | − <svg | |
| 901 | − width="14" | |
| 902 | − height="14" | |
| 903 | − viewBox="0 0 14 14" | |
| 904 | − fill="none" | |
| 905 | − aria-hidden="true" | |
| 906 | − > | |
| 907 | − <rect | |
| 908 | − x="2" | |
| 909 | − y="5.5" | |
| 910 | − width="10" | |
| 911 | − height="7" | |
| 912 | − rx="1.5" | |
| 913 | − stroke="currentColor" | |
| 914 | − strokeWidth="1.3" | |
| 915 | − /> | |
| 916 | − <path | |
| 917 | − d="M9.5 5.5V4a2.5 2.5 0 0 0-5 0" | |
| 918 | − stroke="currentColor" | |
| 919 | − strokeWidth="1.3" | |
| 920 | − strokeLinecap="round" | |
| 921 | − /> | |
| 922 | − </svg> | |
| 517 | + {isConnecting || isSending ? ( | |
| 518 | + <LoaderCircleIcon className="animate-spin" /> | |
| 923 | 519 | ) : ( |
| 924 | − <svg | |
| 925 | − width="14" | |
| 926 | − height="14" | |
| 927 | − viewBox="0 0 14 14" | |
| 928 | − fill="none" | |
| 929 | − aria-hidden="true" | |
| 930 | − > | |
| 931 | − <rect | |
| 932 | − x="2" | |
| 933 | − y="5.5" | |
| 934 | − width="10" | |
| 935 | − height="7" | |
| 936 | − rx="1.5" | |
| 937 | − stroke="currentColor" | |
| 938 | − strokeWidth="1.3" | |
| 939 | − /> | |
| 940 | − <path | |
| 941 | − d="M4.5 5.5V4a2.5 2.5 0 0 1 5 0v1.5" | |
| 942 | − stroke="currentColor" | |
| 943 | − strokeWidth="1.3" | |
| 944 | − strokeLinecap="round" | |
| 945 | − /> | |
| 946 | − </svg> | |
| 520 | + <ArrowUpIcon /> | |
| 947 | 521 | )} |
| 948 | − <span> | |
| 949 | − {state.runtimeMode === "full-access" | |
| 950 | − ? "Full access" | |
| 951 | − : "Supervised"} | |
| 952 | − </span> | |
| 953 | − </button> | |
| 954 | − </div> | |
| 955 | − | |
| 956 | − {/* Right side: send / stop button */} | |
| 957 | − <div className="flex items-center gap-2"> | |
| 958 | − {phase === "running" ? ( | |
| 959 | − <button | |
| 960 | − type="button" | |
| 961 | − className="flex h-8 w-8 items-center justify-center rounded-full bg-rose-500/90 text-white transition-all duration-150 hover:bg-rose-500 hover:scale-105" | |
| 962 | − onClick={() => void onInterrupt()} | |
| 963 | − aria-label="Stop generation" | |
| 964 | − > | |
| 965 | − <svg | |
| 966 | − width="12" | |
| 967 | − height="12" | |
| 968 | − viewBox="0 0 12 12" | |
| 969 | − fill="currentColor" | |
| 970 | − aria-hidden="true" | |
| 971 | − > | |
| 972 | − <rect x="2" y="2" width="8" height="8" rx="1.5" /> | |
| 973 | − </svg> | |
| 974 | − </button> | |
| 975 | − ) : ( | |
| 976 | − <button | |
| 977 | − type="submit" | |
| 978 | − className="flex h-8 w-8 items-center justify-center rounded-full bg-white/90 text-[#0c0c0c] transition-all duration-150 hover:bg-white hover:scale-105 disabled:opacity-30 disabled:hover:scale-100" | |
| 979 | − disabled={isSending || isConnecting || !prompt.trim()} | |
| 980 | − aria-label={ | |
| 981 | − isConnecting ? "Connecting" : isSending ? "Sending" : "Send message" | |
| 982 | − } | |
| 983 | − > | |
| 984 | − {isConnecting || isSending ? ( | |
| 985 | − <svg | |
| 986 | − width="14" | |
| 987 | − height="14" | |
| 988 | − viewBox="0 0 14 14" | |
| 989 | − fill="none" | |
| 990 | − className="animate-spin" | |
| 991 | − aria-hidden="true" | |
| 992 | − > | |
| 993 | − <circle | |
| 994 | − cx="7" | |
| 995 | − cy="7" | |
| 996 | − r="5.5" | |
| 997 | − stroke="currentColor" | |
| 998 | − strokeWidth="1.5" | |
| 999 | − strokeLinecap="round" | |
| 1000 | − strokeDasharray="20 12" | |
| 1001 | − /> | |
| 1002 | − </svg> | |
| 1003 | − ) : ( | |
| 1004 | − <svg | |
| 1005 | − width="14" | |
| 1006 | − height="14" | |
| 1007 | − viewBox="0 0 14 14" | |
| 1008 | − fill="none" | |
| 1009 | − aria-hidden="true" | |
| 1010 | − > | |
| 1011 | − <path | |
| 1012 | − d="M7 11.5V2.5M7 2.5L3 6.5M7 2.5L11 6.5" | |
| 1013 | − stroke="currentColor" | |
| 1014 | − strokeWidth="1.8" | |
| 1015 | − strokeLinecap="round" | |
| 1016 | − strokeLinejoin="round" | |
| 1017 | − /> | |
| 1018 | − </svg> | |
| 1019 | − )} | |
| 1020 | − </button> | |
| 1021 | − )} | |
| 1022 | − </div> | |
| 1023 | − </div> | |
| 1024 | − </div> | |
| 522 | + </Button> | |
| 523 | + )} | |
| 524 | + </InputGroupAddon> | |
| 525 | + </InputGroup> | |
| 1025 | 526 | </form> |
| 1026 | 527 | </div> |
| 1027 | 528 | </div> |
| @@ -4,13 +4,15 @@ export default function DiffPanel() { · 3 unmodified lines | ||
| 4 | 4 | const { dispatch } = useStore(); |
| 5 | 5 | |
| 6 | 6 | return ( |
| 7 | − <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-white/[0.08] bg-[#141414]"> | |
| 7 | + <aside className="flex h-full w-[400px] shrink-0 flex-col border-l border-border bg-card"> | |
| 8 | 8 | {/* Header */} |
| 9 | − <div className="drag-region flex items-center justify-between border-b border-white/[0.08] px-4 pt-[28px] pb-3"> | |
| 10 | − <h3 className="text-xs font-medium text-[#e0e0e0]">Uncommitted changes</h3> | |
| 9 | + <div className="drag-region flex items-center justify-between border-b border-border px-4 pt-[28px] pb-3"> | |
| 10 | + <h3 className="text-xs font-medium text-foreground"> | |
| 11 | + Uncommitted changes | |
| 12 | + </h3> | |
| 11 | 13 | <button |
| 12 | 14 | type="button" |
| 13 | − className="text-[#a0a0a0]/40 transition-colors duration-150 hover:text-[#a0a0a0]/70" | |
| 15 | + className="text-muted-foreground/60 transition-colors duration-150 hover:text-muted-foreground" | |
| 14 | 16 | onClick={() => dispatch({ type: "TOGGLE_DIFF" })} |
| 15 | 17 | > |
| 16 | 18 | <span className="text-sm">×</span> |
| @@ -20,30 +22,36 @@ export default function DiffPanel() { · 3 unmodified lines | ||
| 20 | 22 | {/* Placeholder content */} |
| 21 | 23 | <div className="flex-1 overflow-y-auto p-4"> |
| 22 | 24 | <div className="space-y-3"> |
| 23 | − <div className="rounded-lg border border-white/[0.06] bg-white/[0.02] p-3"> | |
| 25 | + <div className="rounded-lg border border-border/80 bg-muted p-3"> | |
| 24 | 26 | <div className="mb-2 flex items-center gap-2"> |
| 25 | 27 | <span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[9px] font-medium text-emerald-400"> |
| 26 | 28 | M |
| 27 | 29 | </span> |
| 28 | − <span className="font-mono text-xs text-[#a0a0a0]/60">src/App.tsx</span> | |
| 30 | + <span className="font-mono text-xs text-muted-foreground/80"> | |
| 31 | + src/App.tsx | |
| 32 | + </span> | |
| 29 | 33 | </div> |
| 30 | 34 | <div className="space-y-0.5 font-mono text-[11px]"> |
| 31 | 35 | <div className="text-red-400/60">- const oldValue = "hello";</div> |
| 32 | − <div className="text-emerald-400/60">+ const newValue = "world";</div> | |
| 36 | + <div className="text-emerald-400/60"> | |
| 37 | + + const newValue = "world"; | |
| 38 | + </div> | |
| 33 | 39 | </div> |
| 34 | 40 | </div> |
| 35 | 41 | |
| 36 | − <div className="rounded-lg border border-white/[0.06] bg-white/[0.02] p-3"> | |
| 42 | + <div className="rounded-lg border border-border/80 bg-muted p-3"> | |
| 37 | 43 | <div className="flex items-center gap-2"> |
| 38 | 44 | <span className="rounded bg-blue-500/20 px-1.5 py-0.5 text-[9px] font-medium text-blue-400"> |
| 39 | 45 | A |
| 40 | 46 | </span> |
| 41 | − <span className="font-mono text-xs text-[#a0a0a0]/60">src/utils.ts</span> | |
| 47 | + <span className="font-mono text-xs text-muted-foreground/80"> | |
| 48 | + src/utils.ts | |
| 49 | + </span> | |
| 42 | 50 | </div> |
| 43 | 51 | </div> |
| 44 | 52 | </div> |
| 45 | 53 | |
| 46 | − <p className="mt-6 text-center text-[10px] text-[#a0a0a0]/25"> | |
| 54 | + <p className="mt-6 text-center text-[10px] text-muted-foreground/30"> | |
| 47 | 55 | Diff integration coming soon |
| 48 | 56 | </p> |
| 49 | 57 | </div> |
| @@ -1,10 +1,11 @@ | ||
| 1 | −import { useCallback, useEffect, useMemo, useState } from "react"; | |
| 2 | −import { DEFAULT_MODEL } from "../model-logic"; | |
| 1 | +import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react"; | |
| 2 | +import { useMemo, useState } from "react"; | |
| 3 | +import { useTheme } from "../hooks/useTheme"; | |
| 4 | +import { DEFAULT_MODEL, MODEL_OPTIONS, resolveModelSlug } from "../model-logic"; | |
| 3 | 5 | import { readNativeApi } from "../session-logic"; |
| 4 | 6 | import { useStore } from "../store"; |
| 5 | 7 | import type { Project } from "../types"; |
| 6 | 8 | |
| 7 | − | |
| 8 | 9 | function formatRelativeTime(iso: string): string { |
| 9 | 10 | const diff = Date.now() - new Date(iso).getTime(); |
| 10 | 11 | const minutes = Math.floor(diff / 60_000); |
| @@ -15,58 +16,53 @@ function formatRelativeTime(iso: string): string { · 4 unmodified lines | ||
| 15 | 16 | return `${Math.floor(hours / 24)}d ago`; |
| 16 | 17 | } |
| 17 | 18 | |
| 18 | −function threadStatusLabel( | |
| 19 | − status: "connecting" | "ready" | "running" | "error" | "closed" | undefined, | |
| 20 | −): "Working" | "Connecting" | null { | |
| 21 | − if (status === "running") return "Working"; | |
| 22 | − if (status === "connecting") return "Connecting"; | |
| 23 | − return null; | |
| 24 | −} | |
| 19 | +const THEME_CYCLE = { system: "light", light: "dark", dark: "system" } as const; | |
| 25 | 20 | |
| 26 | 21 | export default function Sidebar() { |
| 27 | 22 | const { state, dispatch } = useStore(); |
| 28 | 23 | const api = useMemo(() => readNativeApi(), []); |
| 24 | + const { theme, setTheme } = useTheme(); | |
| 29 | 25 | const [addingProject, setAddingProject] = useState(false); |
| 30 | 26 | const [newCwd, setNewCwd] = useState(""); |
| 27 | + const [newModel, setNewModel] = useState(DEFAULT_MODEL); | |
| 31 | 28 | const [isPickingFolder, setIsPickingFolder] = useState(false); |
| 32 | 29 | |
| 33 | 30 | const handleAddProject = () => { |
| 34 | 31 | const cwd = newCwd.trim(); |
| 35 | 32 | if (!cwd) return; |
| 36 | 33 | const name = cwd.split("/").filter(Boolean).pop() ?? "project"; |
| 34 | + const normalizedModel = resolveModelSlug(newModel); | |
| 37 | 35 | const project: Project = { |
| 38 | 36 | id: crypto.randomUUID(), |
| 39 | 37 | name, |
| 40 | 38 | cwd, |
| 41 | − model: DEFAULT_MODEL, | |
| 39 | + model: normalizedModel, | |
| 42 | 40 | expanded: true, |
| 43 | 41 | }; |
| 44 | 42 | dispatch({ type: "ADD_PROJECT", project }); |
| 45 | 43 | setNewCwd(""); |
| 44 | + setNewModel(DEFAULT_MODEL); | |
| 46 | 45 | setAddingProject(false); |
| 47 | 46 | }; |
| 48 | 47 | |
| 49 | − const handleNewThread = useCallback( | |
| 50 | − (projectId: string) => { | |
| 51 | − dispatch({ | |
| 52 | − type: "ADD_THREAD", | |
| 53 | − thread: { | |
| 54 | − id: crypto.randomUUID(), | |
| 55 | − projectId, | |
| 56 | − title: "New thread", | |
| 57 | − model: | |
| 58 | − state.projects.find((p) => p.id === projectId)?.model ?? | |
| 59 | − DEFAULT_MODEL, | |
| 60 | − session: null, | |
| 61 | − messages: [], | |
| 62 | − events: [], | |
| 63 | − error: null, | |
| 64 | − createdAt: new Date().toISOString(), | |
| 65 | − }, | |
| 66 | − }); | |
| 67 | − }, | |
| 68 | − [dispatch, state.projects], | |
| 69 | − ); | |
| 48 | + const handleNewThread = (projectId: string) => { | |
| 49 | + dispatch({ | |
| 50 | + type: "ADD_THREAD", | |
| 51 | + thread: { | |
| 52 | + id: crypto.randomUUID(), | |
| 53 | + projectId, | |
| 54 | + title: "New thread", | |
| 55 | + model: | |
| 56 | + state.projects.find((p) => p.id === projectId)?.model ?? | |
| 57 | + DEFAULT_MODEL, | |
| 58 | + session: null, | |
| 59 | + messages: [], | |
| 60 | + events: [], | |
| 61 | + error: null, | |
| 62 | + createdAt: new Date().toISOString(), | |
| 63 | + }, | |
| 64 | + }); | |
| 65 | + }; | |
| 70 | 66 | |
| 71 | 67 | const handlePickFolder = async () => { |
| 72 | 68 | if (!api || isPickingFolder) return; |
| @@ -80,62 +76,40 @@ export default function Sidebar() { · 7 unmodified lines | ||
| 80 | 76 | } |
| 81 | 77 | }; |
| 82 | 78 | |
| 83 | − useEffect(() => { | |
| 84 | − const onWindowKeyDown = (event: KeyboardEvent) => { | |
| 85 | − const isNewThreadShortcut = | |
| 86 | − event.metaKey && | |
| 87 | − event.shiftKey && | |
| 88 | − !event.ctrlKey && | |
| 89 | − !event.altKey && | |
| 90 | − event.key.toLowerCase() === "o"; | |
| 91 | − if (!isNewThreadShortcut) return; | |
| 92 | − | |
| 93 | − const target = event.target; | |
| 94 | − if (target instanceof HTMLElement) { | |
| 95 | − const tagName = target.tagName; | |
| 96 | − if ( | |
| 97 | − target.isContentEditable || | |
| 98 | − tagName === "INPUT" || | |
| 99 | − tagName === "TEXTAREA" || | |
| 100 | − tagName === "SELECT" | |
| 101 | − ) { | |
| 102 | − return; | |
| 103 | − } | |
| 104 | − } | |
| 105 | − | |
| 106 | − const activeThread = state.threads.find( | |
| 107 | − (t) => t.id === state.activeThreadId, | |
| 108 | − ); | |
| 109 | − const projectId = activeThread?.projectId ?? state.projects[0]?.id; | |
| 110 | − if (!projectId) return; | |
| 111 | − | |
| 112 | − event.preventDefault(); | |
| 113 | − handleNewThread(projectId); | |
| 114 | − }; | |
| 115 | − | |
| 116 | − window.addEventListener("keydown", onWindowKeyDown); | |
| 117 | − return () => { | |
| 118 | − window.removeEventListener("keydown", onWindowKeyDown); | |
| 119 | − }; | |
| 120 | − }, [handleNewThread, state.activeThreadId, state.projects, state.threads]); | |
| 121 | − | |
| 122 | 79 | return ( |
| 123 | − <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-white/8 bg-[#141414]"> | |
| 80 | + <aside className="sidebar flex h-full w-[260px] shrink-0 flex-col border-r border-border bg-card"> | |
| 124 | 81 | {/* Drag region / traffic light space */} |
| 125 | 82 | <div className="drag-region h-[52px] shrink-0" /> |
| 126 | 83 | {/* Branding */} |
| 127 | 84 | <div className="flex items-center gap-2 px-4 pb-4"> |
| 128 | − <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-white text-xs font-bold text-[#0c0c0c]"> | |
| 85 | + <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-primary text-xs font-bold text-primary-foreground"> | |
| 129 | 86 | CT |
| 130 | 87 | </div> |
| 131 | − <span className="text-sm font-semibold tracking-tight text-[#e0e0e0]">CodeThing</span> | |
| 88 | + <span className="flex-1 text-sm font-semibold tracking-tight text-foreground"> | |
| 89 | + CodeThing | |
| 90 | + </span> | |
| 91 | + <button | |
| 92 | + type="button" | |
| 93 | + className="rounded-md p-1.5 text-muted-foreground/80 transition-colors duration-150 hover:bg-accent hover:text-muted-foreground" | |
| 94 | + onClick={() => setTheme(THEME_CYCLE[theme])} | |
| 95 | + aria-label={`Theme: ${theme}`} | |
| 96 | + title={`Theme: ${theme}`} | |
| 97 | + > | |
| 98 | + {theme === "system" ? ( | |
| 99 | + <MonitorIcon className="size-3.5" /> | |
| 100 | + ) : theme === "light" ? ( | |
| 101 | + <SunIcon className="size-3.5" /> | |
| 102 | + ) : ( | |
| 103 | + <MoonIcon className="size-3.5" /> | |
| 104 | + )} | |
| 105 | + </button> | |
| 132 | 106 | </div> |
| 133 | 107 | |
| 134 | 108 | {/* New thread (global) */} |
| 135 | 109 | <div className="px-3 pb-3"> |
| 136 | 110 | <button |
| 137 | 111 | type="button" |
| 138 | − className="flex w-full items-center gap-2 rounded-lg border border-white/8 bg-white/4 px-3 py-2 text-xs text-[#a0a0a0]/80 transition-colors duration-150 hover:bg-white/[0.07]" | |
| 112 | + className="flex w-full items-center gap-2 rounded-lg border border-border bg-secondary px-3 py-2 text-xs text-muted-foreground transition-colors duration-150 hover:bg-accent" | |
| 139 | 113 | onClick={() => { |
| 140 | 114 | if (state.projects.length === 0) { |
| 141 | 115 | setAddingProject(true); |
| @@ -145,7 +119,7 @@ export default function Sidebar() { · 3 unmodified lines | ||
| 145 | 119 | if (firstProject) handleNewThread(firstProject.id); |
| 146 | 120 | }} |
| 147 | 121 | > |
| 148 | − <span className="text-white">+</span> | |
| 122 | + <span className="text-foreground">+</span> | |
| 149 | 123 | New thread |
| 150 | 124 | </button> |
| 151 | 125 | </div> |
| @@ -153,46 +127,43 @@ export default function Sidebar() { · 1 unmodified line | ||
| 153 | 127 | {/* Project list */} |
| 154 | 128 | <nav className="flex-1 overflow-y-auto px-2"> |
| 155 | 129 | {state.projects.map((project) => { |
| 156 | − const threads = state.threads | |
| 157 | − .filter((t) => t.projectId === project.id) | |
| 158 | − .sort((a, b) => { | |
| 159 | − const byDate = | |
| 160 | − new Date(b.createdAt).getTime() - | |
| 161 | − new Date(a.createdAt).getTime(); | |
| 162 | − if (byDate !== 0) return byDate; | |
| 163 | − return b.id.localeCompare(a.id); | |
| 164 | − }); | |
| 130 | + const threads = state.threads.filter( | |
| 131 | + (t) => t.projectId === project.id, | |
| 132 | + ); | |
| 165 | 133 | return ( |
| 166 | 134 | <div key={project.id} className="mb-1"> |
| 167 | 135 | {/* Project header */} |
| 168 | 136 | <button |
| 169 | 137 | type="button" |
| 170 | − className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 hover:bg-white/5" | |
| 171 | − onClick={() => dispatch({ type: "TOGGLE_PROJECT", projectId: project.id })} | |
| 138 | + className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 hover:bg-accent" | |
| 139 | + onClick={() => | |
| 140 | + dispatch({ type: "TOGGLE_PROJECT", projectId: project.id }) | |
| 141 | + } | |
| 172 | 142 | > |
| 173 | − <span className="text-[10px] text-[#a0a0a0]/50"> | |
| 143 | + <span className="text-[10px] text-muted-foreground/70"> | |
| 174 | 144 | {project.expanded ? "▼" : "▶"} |
| 175 | 145 | </span> |
| 176 | − <span className="flex-1 truncate text-xs font-medium text-[#e0e0e0]/90"> | |
| 146 | + <span className="flex-1 truncate text-xs font-medium text-foreground/90"> | |
| 177 | 147 | {project.name} |
| 178 | 148 | </span> |
| 179 | − <span className="text-[10px] text-[#a0a0a0]/40">{threads.length}</span> | |
| 149 | + <span className="text-[10px] text-muted-foreground/60"> | |
| 150 | + {threads.length} | |
| 151 | + </span> | |
| 180 | 152 | </button> |
| 181 | 153 | |
| 182 | 154 | {/* Threads */} |
| 183 | 155 | {project.expanded && ( |
| 184 | − <div className="ml-2 border-l border-white/6 pl-2"> | |
| 156 | + <div className="ml-2 border-l border-border/80 pl-2"> | |
| 185 | 157 | {threads.map((thread) => { |
| 186 | 158 | const isActive = state.activeThreadId === thread.id; |
| 187 | − const threadStatus = threadStatusLabel( | |
| 188 | − thread.session?.status, | |
| 189 | − ); | |
| 190 | 159 | return ( |
| 191 | 160 | <button |
| 192 | 161 | key={thread.id} |
| 193 | 162 | type="button" |
| 194 | 163 | className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-150 ${ |
| 195 | − isActive ? "bg-white/10 text-white" : "text-[#a0a0a0]/70 hover:bg-white/4" | |
| 164 | + isActive | |
| 165 | + ? "bg-accent text-foreground" | |
| 166 | + : "text-muted-foreground hover:bg-secondary" | |
| 196 | 167 | }`} |
| 197 | 168 | onClick={() => |
| 198 | 169 | dispatch({ |
| @@ -201,20 +172,10 @@ export default function Sidebar() { · 2 unmodified lines | ||
| 201 | 172 | }) |
| 202 | 173 | } |
| 203 | 174 | > |
| 204 | − <div className="flex min-w-0 flex-1 items-center gap-1.5"> | |
| 205 | − {threadStatus && ( | |
| 206 | − <span className="inline-flex items-center gap-1 text-[10px] text-[#7ed8ff]/80"> | |
| 207 | − <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-sky-300/80" /> | |
| 208 | − <span className="hidden xl:inline"> | |
| 209 | − {threadStatus} | |
| 210 | − </span> | |
| 211 | − </span> | |
| 212 | − )} | |
| 213 | − <span className="min-w-0 flex-1 truncate text-xs"> | |
| 214 | − {thread.title} | |
| 215 | − </span> | |
| 216 | − </div> | |
| 217 | − <span className="shrink-0 text-[10px] text-[#a0a0a0]/30"> | |
| 175 | + <span className="flex-1 truncate text-xs"> | |
| 176 | + {thread.title} | |
| 177 | + </span> | |
| 178 | + <span className="shrink-0 text-[10px] text-muted-foreground/40"> | |
| 218 | 179 | {formatRelativeTime(thread.createdAt)} |
| 219 | 180 | </span> |
| 220 | 181 | </button> |
| @@ -224,7 +185,7 @@ export default function Sidebar() { · 3 unmodified lines | ||
| 224 | 185 | {/* New thread within project */} |
| 225 | 186 | <button |
| 226 | 187 | type="button" |
| 227 | − className="flex w-full items-center gap-1 px-2 py-1 text-[10px] text-[#a0a0a0]/40 transition-colors duration-150 hover:text-[#a0a0a0]/60" | |
| 188 | + className="flex w-full items-center gap-1 px-2 py-1 text-[10px] text-muted-foreground/60 transition-colors duration-150 hover:text-muted-foreground/80" | |
| 228 | 189 | onClick={() => handleNewThread(project.id)} |
| 229 | 190 | > |
| 230 | 191 | <span>+</span> New thread |
| @@ -236,7 +197,7 @@ export default function Sidebar() { · 5 unmodified lines | ||
| 236 | 197 | })} |
| 237 | 198 | |
| 238 | 199 | {state.projects.length === 0 && !addingProject && ( |
| 239 | − <div className="px-2 pt-4 text-center text-xs text-[#a0a0a0]/40"> | |
| 200 | + <div className="px-2 pt-4 text-center text-xs text-muted-foreground/60"> | |
| 240 | 201 | No projects yet. |
| 241 | 202 | <br /> |
| 242 | 203 | Add one to get started. |
| @@ -246,12 +207,12 @@ export default function Sidebar() { · 3 unmodified lines | ||
| 246 | 207 | |
| 247 | 208 | {/* Add project form */} |
| 248 | 209 | {addingProject ? ( |
| 249 | − <div className="border-t border-white/8 p-3"> | |
| 250 | − <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-[#a0a0a0]/50"> | |
| 210 | + <div className="border-t border-border p-3"> | |
| 211 | + <p className="mb-2 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/70"> | |
| 251 | 212 | Add project |
| 252 | 213 | </p> |
| 253 | 214 | <input |
| 254 | − className="mb-2 w-full rounded-md border border-white/10 bg-white/4 px-2 py-1.5 font-mono text-xs text-[#e0e0e0] placeholder:text-[#a0a0a0]/30 focus:border-white/30 focus:outline-none" | |
| 215 | + className="mb-2 w-full rounded-md border border-border bg-secondary px-2 py-1.5 font-mono text-xs text-foreground placeholder:text-muted-foreground/40 focus:border-ring focus:outline-none" | |
| 255 | 216 | placeholder="/path/to/project" |
| 256 | 217 | value={newCwd} |
| 257 | 218 | onChange={(e) => setNewCwd(e.target.value)} |
| @@ -263,24 +224,39 @@ export default function Sidebar() { · 5 unmodified lines | ||
| 263 | 224 | {api && ( |
| 264 | 225 | <button |
| 265 | 226 | type="button" |
| 266 | − className="mb-2 flex w-full items-center justify-center rounded-md border border-white/10 px-2 py-1.5 text-xs text-[#a0a0a0]/70 transition-colors duration-150 hover:bg-white/4 disabled:cursor-not-allowed disabled:opacity-60" | |
| 227 | + className="mb-2 flex w-full items-center justify-center rounded-md border border-border px-2 py-1.5 text-xs text-muted-foreground transition-colors duration-150 hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60" | |
| 267 | 228 | onClick={() => void handlePickFolder()} |
| 268 | 229 | disabled={isPickingFolder} |
| 269 | 230 | > |
| 270 | 231 | {isPickingFolder ? "Picking folder..." : "Browse for folder"} |
| 271 | 232 | </button> |
| 272 | 233 | )} |
| 234 | + <select | |
| 235 | + className="mb-2 w-full rounded-md border border-border bg-secondary px-2 py-1.5 font-mono text-xs text-foreground placeholder:text-muted-foreground/40 focus:border-ring focus:outline-none" | |
| 236 | + value={newModel} | |
| 237 | + onChange={(e) => setNewModel(e.target.value)} | |
| 238 | + onKeyDown={(e) => { | |
| 239 | + if (e.key === "Enter") handleAddProject(); | |
| 240 | + if (e.key === "Escape") setAddingProject(false); | |
| 241 | + }} | |
| 242 | + > | |
| 243 | + {MODEL_OPTIONS.map((model) => ( | |
| 244 | + <option key={model} value={model} className="bg-card"> | |
| 245 | + {model} | |
| 246 | + </option> | |
| 247 | + ))} | |
| 248 | + </select> | |
| 273 | 249 | <div className="flex gap-2"> |
| 274 | 250 | <button |
| 275 | 251 | type="button" |
| 276 | − className="flex-1 rounded-md bg-white px-2 py-1 text-xs font-medium text-[#0c0c0c] transition-colors duration-150 hover:bg-white/90" | |
| 252 | + className="flex-1 rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors duration-150 hover:bg-primary/90" | |
| 277 | 253 | onClick={handleAddProject} |
| 278 | 254 | > |
| 279 | 255 | Add |
| 280 | 256 | </button> |
| 281 | 257 | <button |
| 282 | 258 | type="button" |
| 283 | − className="flex-1 rounded-md border border-white/10 px-2 py-1 text-xs text-[#a0a0a0]/60 transition-colors duration-150 hover:bg-white/4" | |
| 259 | + className="flex-1 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground/80 transition-colors duration-150 hover:bg-secondary" | |
| 284 | 260 | onClick={() => setAddingProject(false)} |
| 285 | 261 | > |
| 286 | 262 | Cancel |
| @@ -288,10 +264,10 @@ export default function Sidebar() { · 1 unmodified line | ||
| 288 | 264 | </div> |
| 289 | 265 | </div> |
| 290 | 266 | ) : ( |
| 291 | − <div className="border-t border-white/8 p-3"> | |
| 267 | + <div className="border-t border-border p-3"> | |
| 292 | 268 | <button |
| 293 | 269 | type="button" |
| 294 | − className="flex w-full items-center justify-center gap-1 rounded-md border border-dashed border-white/12 py-2 text-xs text-[#a0a0a0]/50 transition-colors duration-150 hover:border-white/20 hover:text-[#a0a0a0]/70" | |
| 270 | + className="flex w-full items-center justify-center gap-1 rounded-md border border-dashed border-border py-2 text-xs text-muted-foreground/70 transition-colors duration-150 hover:border-ring hover:text-muted-foreground" | |
| 295 | 271 | onClick={() => setAddingProject(true)} |
| 296 | 272 | > |
| 297 | 273 | + Add project |
| @@ -0,0 +1,67 @@ | ||
| 1 | +import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion"; | |
| 2 | +import { ChevronDownIcon } from "lucide-react"; | |
| 3 | + | |
| 4 | +import { cn } from "#/lib/utils"; | |
| 5 | + | |
| 6 | +function Accordion(props: AccordionPrimitive.Root.Props) { | |
| 7 | + return <AccordionPrimitive.Root data-slot="accordion" {...props} />; | |
| 8 | +} | |
| 9 | + | |
| 10 | +function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) { | |
| 11 | + return ( | |
| 12 | + <AccordionPrimitive.Item | |
| 13 | + className={cn("border-b last:border-b-0", className)} | |
| 14 | + data-slot="accordion-item" | |
| 15 | + {...props} | |
| 16 | + /> | |
| 17 | + ); | |
| 18 | +} | |
| 19 | + | |
| 20 | +function AccordionTrigger({ | |
| 21 | + className, | |
| 22 | + children, | |
| 23 | + ...props | |
| 24 | +}: AccordionPrimitive.Trigger.Props) { | |
| 25 | + return ( | |
| 26 | + <AccordionPrimitive.Header className="flex"> | |
| 27 | + <AccordionPrimitive.Trigger | |
| 28 | + className={cn( | |
| 29 | + "flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left font-medium text-sm outline-none transition-all focus-visible:ring-[3px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-64 data-panel-open:*:data-[slot=accordion-indicator]:rotate-180", | |
| 30 | + className, | |
| 31 | + )} | |
| 32 | + data-slot="accordion-trigger" | |
| 33 | + {...props} | |
| 34 | + > | |
| 35 | + {children} | |
| 36 | + <ChevronDownIcon | |
| 37 | + className="pointer-events-none size-4 shrink-0 translate-y-0.5 opacity-80 transition-transform duration-200 ease-in-out" | |
| 38 | + data-slot="accordion-indicator" | |
| 39 | + /> | |
| 40 | + </AccordionPrimitive.Trigger> | |
| 41 | + </AccordionPrimitive.Header> | |
| 42 | + ); | |
| 43 | +} | |
| 44 | + | |
| 45 | +function AccordionPanel({ | |
| 46 | + className, | |
| 47 | + children, | |
| 48 | + ...props | |
| 49 | +}: AccordionPrimitive.Panel.Props) { | |
| 50 | + return ( | |
| 51 | + <AccordionPrimitive.Panel | |
| 52 | + className="h-(--accordion-panel-height) overflow-hidden text-muted-foreground text-sm transition-[height] duration-200 ease-in-out data-ending-style:h-0 data-starting-style:h-0" | |
| 53 | + data-slot="accordion-panel" | |
| 54 | + {...props} | |
| 55 | + > | |
| 56 | + <div className={cn("pt-0 pb-4", className)}>{children}</div> | |
| 57 | + </AccordionPrimitive.Panel> | |
| 58 | + ); | |
| 59 | +} | |
| 60 | + | |
| 61 | +export { | |
| 62 | + Accordion, | |
| 63 | + AccordionItem, | |
| 64 | + AccordionTrigger, | |
| 65 | + AccordionPanel, | |
| 66 | + AccordionPanel as AccordionContent, | |
| 67 | +}; | |
| @@ -0,0 +1,167 @@ | ||
| 1 | +import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +const AlertDialogCreateHandle = AlertDialogPrimitive.createHandle; | |
| 6 | + | |
| 7 | +const AlertDialog = AlertDialogPrimitive.Root; | |
| 8 | + | |
| 9 | +const AlertDialogPortal = AlertDialogPrimitive.Portal; | |
| 10 | + | |
| 11 | +function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) { | |
| 12 | + return ( | |
| 13 | + <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} /> | |
| 14 | + ); | |
| 15 | +} | |
| 16 | + | |
| 17 | +function AlertDialogBackdrop({ | |
| 18 | + className, | |
| 19 | + ...props | |
| 20 | +}: AlertDialogPrimitive.Backdrop.Props) { | |
| 21 | + return ( | |
| 22 | + <AlertDialogPrimitive.Backdrop | |
| 23 | + className={cn( | |
| 24 | + "fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 ease-out data-ending-style:opacity-0 data-starting-style:opacity-0", | |
| 25 | + className, | |
| 26 | + )} | |
| 27 | + data-slot="alert-dialog-backdrop" | |
| 28 | + {...props} | |
| 29 | + /> | |
| 30 | + ); | |
| 31 | +} | |
| 32 | + | |
| 33 | +function AlertDialogViewport({ | |
| 34 | + className, | |
| 35 | + ...props | |
| 36 | +}: AlertDialogPrimitive.Viewport.Props) { | |
| 37 | + return ( | |
| 38 | + <AlertDialogPrimitive.Viewport | |
| 39 | + className={cn( | |
| 40 | + "fixed inset-0 z-50 grid grid-rows-[1fr_auto_3fr] justify-items-center p-4", | |
| 41 | + className, | |
| 42 | + )} | |
| 43 | + data-slot="alert-dialog-viewport" | |
| 44 | + {...props} | |
| 45 | + /> | |
| 46 | + ); | |
| 47 | +} | |
| 48 | + | |
| 49 | +function AlertDialogPopup({ | |
| 50 | + className, | |
| 51 | + bottomStickOnMobile = true, | |
| 52 | + ...props | |
| 53 | +}: AlertDialogPrimitive.Popup.Props & { | |
| 54 | + bottomStickOnMobile?: boolean; | |
| 55 | +}) { | |
| 56 | + return ( | |
| 57 | + <AlertDialogPortal> | |
| 58 | + <AlertDialogBackdrop /> | |
| 59 | + <AlertDialogViewport | |
| 60 | + className={cn( | |
| 61 | + bottomStickOnMobile && | |
| 62 | + "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12", | |
| 63 | + )} | |
| 64 | + > | |
| 65 | + <AlertDialogPrimitive.Popup | |
| 66 | + className={cn( | |
| 67 | + "-translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 max-w-lg scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border bg-popover not-dark:bg-clip-padding text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 68 | + bottomStickOnMobile && | |
| 69 | + "max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4 max-sm:before:hidden max-sm:before:rounded-none", | |
| 70 | + className, | |
| 71 | + )} | |
| 72 | + data-slot="alert-dialog-popup" | |
| 73 | + {...props} | |
| 74 | + /> | |
| 75 | + </AlertDialogViewport> | |
| 76 | + </AlertDialogPortal> | |
| 77 | + ); | |
| 78 | +} | |
| 79 | + | |
| 80 | +function AlertDialogHeader({ | |
| 81 | + className, | |
| 82 | + ...props | |
| 83 | +}: React.ComponentProps<"div">) { | |
| 84 | + return ( | |
| 85 | + <div | |
| 86 | + className={cn( | |
| 87 | + "flex flex-col gap-2 p-6 text-center max-sm:pb-4 sm:text-left", | |
| 88 | + className, | |
| 89 | + )} | |
| 90 | + data-slot="alert-dialog-header" | |
| 91 | + {...props} | |
| 92 | + /> | |
| 93 | + ); | |
| 94 | +} | |
| 95 | + | |
| 96 | +function AlertDialogFooter({ | |
| 97 | + className, | |
| 98 | + variant = "default", | |
| 99 | + ...props | |
| 100 | +}: React.ComponentProps<"div"> & { | |
| 101 | + variant?: "default" | "bare"; | |
| 102 | +}) { | |
| 103 | + return ( | |
| 104 | + <div | |
| 105 | + className={cn( | |
| 106 | + "flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end sm:rounded-b-[calc(var(--radius-2xl)-1px)]", | |
| 107 | + variant === "default" && "border-t bg-muted/72 py-4", | |
| 108 | + variant === "bare" && "pb-6", | |
| 109 | + className, | |
| 110 | + )} | |
| 111 | + data-slot="alert-dialog-footer" | |
| 112 | + {...props} | |
| 113 | + /> | |
| 114 | + ); | |
| 115 | +} | |
| 116 | + | |
| 117 | +function AlertDialogTitle({ | |
| 118 | + className, | |
| 119 | + ...props | |
| 120 | +}: AlertDialogPrimitive.Title.Props) { | |
| 121 | + return ( | |
| 122 | + <AlertDialogPrimitive.Title | |
| 123 | + className={cn( | |
| 124 | + "font-heading font-semibold text-xl leading-none", | |
| 125 | + className, | |
| 126 | + )} | |
| 127 | + data-slot="alert-dialog-title" | |
| 128 | + {...props} | |
| 129 | + /> | |
| 130 | + ); | |
| 131 | +} | |
| 132 | + | |
| 133 | +function AlertDialogDescription({ | |
| 134 | + className, | |
| 135 | + ...props | |
| 136 | +}: AlertDialogPrimitive.Description.Props) { | |
| 137 | + return ( | |
| 138 | + <AlertDialogPrimitive.Description | |
| 139 | + className={cn("text-muted-foreground text-sm", className)} | |
| 140 | + data-slot="alert-dialog-description" | |
| 141 | + {...props} | |
| 142 | + /> | |
| 143 | + ); | |
| 144 | +} | |
| 145 | + | |
| 146 | +function AlertDialogClose(props: AlertDialogPrimitive.Close.Props) { | |
| 147 | + return ( | |
| 148 | + <AlertDialogPrimitive.Close data-slot="alert-dialog-close" {...props} /> | |
| 149 | + ); | |
| 150 | +} | |
| 151 | + | |
| 152 | +export { | |
| 153 | + AlertDialogCreateHandle, | |
| 154 | + AlertDialog, | |
| 155 | + AlertDialogPortal, | |
| 156 | + AlertDialogBackdrop, | |
| 157 | + AlertDialogBackdrop as AlertDialogOverlay, | |
| 158 | + AlertDialogTrigger, | |
| 159 | + AlertDialogPopup, | |
| 160 | + AlertDialogPopup as AlertDialogContent, | |
| 161 | + AlertDialogHeader, | |
| 162 | + AlertDialogFooter, | |
| 163 | + AlertDialogTitle, | |
| 164 | + AlertDialogDescription, | |
| 165 | + AlertDialogClose, | |
| 166 | + AlertDialogViewport, | |
| 167 | +}; | |
| @@ -0,0 +1,80 @@ | ||
| 1 | +import { type VariantProps, cva } from "class-variance-authority"; | |
| 2 | +import type * as React from "react"; | |
| 3 | + | |
| 4 | +import { cn } from "#/lib/utils"; | |
| 5 | + | |
| 6 | +const alertVariants = cva( | |
| 7 | + "relative grid w-full items-start gap-x-2 gap-y-0.5 rounded-xl border px-3.5 py-3 text-card-foreground text-sm has-[>svg]:has-data-[slot=alert-action]:grid-cols-[calc(var(--spacing)*4)_1fr_auto] has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-data-[slot=alert-action]:grid-cols-[1fr_auto] has-[>svg]:gap-x-2 [&>svg]:h-lh [&>svg]:w-4", | |
| 8 | + { | |
| 9 | + defaultVariants: { | |
| 10 | + variant: "default", | |
| 11 | + }, | |
| 12 | + variants: { | |
| 13 | + variant: { | |
| 14 | + default: | |
| 15 | + "bg-transparent dark:bg-input/32 [&>svg]:text-muted-foreground", | |
| 16 | + error: | |
| 17 | + "border-destructive/32 bg-destructive/4 [&>svg]:text-destructive", | |
| 18 | + info: "border-info/32 bg-info/4 [&>svg]:text-info", | |
| 19 | + success: "border-success/32 bg-success/4 [&>svg]:text-success", | |
| 20 | + warning: "border-warning/32 bg-warning/4 [&>svg]:text-warning", | |
| 21 | + }, | |
| 22 | + }, | |
| 23 | + }, | |
| 24 | +); | |
| 25 | + | |
| 26 | +function Alert({ | |
| 27 | + className, | |
| 28 | + variant, | |
| 29 | + ...props | |
| 30 | +}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) { | |
| 31 | + return ( | |
| 32 | + <div | |
| 33 | + className={cn(alertVariants({ variant }), className)} | |
| 34 | + data-slot="alert" | |
| 35 | + role="alert" | |
| 36 | + {...props} | |
| 37 | + /> | |
| 38 | + ); | |
| 39 | +} | |
| 40 | + | |
| 41 | +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { | |
| 42 | + return ( | |
| 43 | + <div | |
| 44 | + className={cn("font-medium [svg~&]:col-start-2", className)} | |
| 45 | + data-slot="alert-title" | |
| 46 | + {...props} | |
| 47 | + /> | |
| 48 | + ); | |
| 49 | +} | |
| 50 | + | |
| 51 | +function AlertDescription({ | |
| 52 | + className, | |
| 53 | + ...props | |
| 54 | +}: React.ComponentProps<"div">) { | |
| 55 | + return ( | |
| 56 | + <div | |
| 57 | + className={cn( | |
| 58 | + "flex flex-col gap-2.5 text-muted-foreground [svg~&]:col-start-2", | |
| 59 | + className, | |
| 60 | + )} | |
| 61 | + data-slot="alert-description" | |
| 62 | + {...props} | |
| 63 | + /> | |
| 64 | + ); | |
| 65 | +} | |
| 66 | + | |
| 67 | +function AlertAction({ className, ...props }: React.ComponentProps<"div">) { | |
| 68 | + return ( | |
| 69 | + <div | |
| 70 | + className={cn( | |
| 71 | + "flex gap-1 max-sm:col-start-2 max-sm:mt-2 sm:row-start-1 sm:row-end-3 sm:self-center sm:[[data-slot=alert-description]~&]:col-start-2 sm:[[data-slot=alert-title]~&]:col-start-2 sm:[svg~&]:col-start-2 sm:[svg~[data-slot=alert-description]~&]:col-start-3 sm:[svg~[data-slot=alert-title]~&]:col-start-3", | |
| 72 | + className, | |
| 73 | + )} | |
| 74 | + data-slot="alert-action" | |
| 75 | + {...props} | |
| 76 | + /> | |
| 77 | + ); | |
| 78 | +} | |
| 79 | + | |
| 80 | +export { Alert, AlertTitle, AlertDescription, AlertAction }; | |
| @@ -0,0 +1,308 @@ | ||
| 1 | +import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete"; | |
| 2 | +import { ChevronsUpDownIcon, XIcon } from "lucide-react"; | |
| 3 | + | |
| 4 | +import { Input } from "#/components/ui/input"; | |
| 5 | +import { ScrollArea } from "#/components/ui/scroll-area"; | |
| 6 | +import { cn } from "#/lib/utils"; | |
| 7 | + | |
| 8 | +const Autocomplete = AutocompletePrimitive.Root; | |
| 9 | + | |
| 10 | +function AutocompleteInput({ | |
| 11 | + className, | |
| 12 | + showTrigger = false, | |
| 13 | + showClear = false, | |
| 14 | + startAddon, | |
| 15 | + size, | |
| 16 | + ...props | |
| 17 | +}: Omit<AutocompletePrimitive.Input.Props, "size"> & { | |
| 18 | + showTrigger?: boolean; | |
| 19 | + showClear?: boolean; | |
| 20 | + startAddon?: React.ReactNode; | |
| 21 | + size?: "sm" | "default" | "lg" | number; | |
| 22 | + ref?: React.Ref<HTMLInputElement>; | |
| 23 | +}) { | |
| 24 | + const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number; | |
| 25 | + | |
| 26 | + return ( | |
| 27 | + <div className="relative not-has-[>*.w-full]:w-fit w-full text-foreground has-disabled:opacity-64"> | |
| 28 | + {startAddon && ( | |
| 29 | + <div | |
| 30 | + aria-hidden="true" | |
| 31 | + className="[&_svg]:-mx-0.5 pointer-events-none absolute inset-y-0 start-px z-10 flex items-center ps-[calc(--spacing(3)-1px)] opacity-80 has-[+[data-size=sm]]:ps-[calc(--spacing(2.5)-1px)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4" | |
| 32 | + data-slot="autocomplete-start-addon" | |
| 33 | + > | |
| 34 | + {startAddon} | |
| 35 | + </div> | |
| 36 | + )} | |
| 37 | + <AutocompletePrimitive.Input | |
| 38 | + className={cn( | |
| 39 | + startAddon && | |
| 40 | + "data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7.5)-1px)] *:data-[slot=autocomplete-input]:ps-[calc(--spacing(8.5)-1px)] sm:data-[size=sm]:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(7)-1px)] sm:*:data-[slot=autocomplete-input]:ps-[calc(--spacing(8)-1px)]", | |
| 41 | + sizeValue === "sm" | |
| 42 | + ? "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-6.5" | |
| 43 | + : "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-7", | |
| 44 | + className, | |
| 45 | + )} | |
| 46 | + data-slot="autocomplete-input" | |
| 47 | + render={<Input nativeInput size={sizeValue} />} | |
| 48 | + {...props} | |
| 49 | + /> | |
| 50 | + {showTrigger && ( | |
| 51 | + <AutocompleteTrigger | |
| 52 | + className={cn( | |
| 53 | + "-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", | |
| 54 | + sizeValue === "sm" ? "end-0" : "end-0.5", | |
| 55 | + )} | |
| 56 | + > | |
| 57 | + <ChevronsUpDownIcon /> | |
| 58 | + </AutocompleteTrigger> | |
| 59 | + )} | |
| 60 | + {showClear && ( | |
| 61 | + <AutocompleteClear | |
| 62 | + className={cn( | |
| 63 | + "-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", | |
| 64 | + sizeValue === "sm" ? "end-0" : "end-0.5", | |
| 65 | + )} | |
| 66 | + > | |
| 67 | + <XIcon /> | |
| 68 | + </AutocompleteClear> | |
| 69 | + )} | |
| 70 | + </div> | |
| 71 | + ); | |
| 72 | +} | |
| 73 | + | |
| 74 | +function AutocompletePopup({ | |
| 75 | + className, | |
| 76 | + children, | |
| 77 | + side = "bottom", | |
| 78 | + sideOffset = 4, | |
| 79 | + alignOffset, | |
| 80 | + align = "start", | |
| 81 | + ...props | |
| 82 | +}: AutocompletePrimitive.Popup.Props & { | |
| 83 | + align?: AutocompletePrimitive.Positioner.Props["align"]; | |
| 84 | + sideOffset?: AutocompletePrimitive.Positioner.Props["sideOffset"]; | |
| 85 | + alignOffset?: AutocompletePrimitive.Positioner.Props["alignOffset"]; | |
| 86 | + side?: AutocompletePrimitive.Positioner.Props["side"]; | |
| 87 | +}) { | |
| 88 | + return ( | |
| 89 | + <AutocompletePrimitive.Portal> | |
| 90 | + <AutocompletePrimitive.Positioner | |
| 91 | + align={align} | |
| 92 | + alignOffset={alignOffset} | |
| 93 | + className="z-50 select-none" | |
| 94 | + data-slot="autocomplete-positioner" | |
| 95 | + side={side} | |
| 96 | + sideOffset={sideOffset} | |
| 97 | + > | |
| 98 | + <span | |
| 99 | + className={cn( | |
| 100 | + "relative flex max-h-full min-w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 101 | + className, | |
| 102 | + )} | |
| 103 | + > | |
| 104 | + <AutocompletePrimitive.Popup | |
| 105 | + className="flex max-h-[min(var(--available-height),23rem)] flex-1 flex-col text-foreground" | |
| 106 | + data-slot="autocomplete-popup" | |
| 107 | + {...props} | |
| 108 | + > | |
| 109 | + {children} | |
| 110 | + </AutocompletePrimitive.Popup> | |
| 111 | + </span> | |
| 112 | + </AutocompletePrimitive.Positioner> | |
| 113 | + </AutocompletePrimitive.Portal> | |
| 114 | + ); | |
| 115 | +} | |
| 116 | + | |
| 117 | +function AutocompleteItem({ | |
| 118 | + className, | |
| 119 | + children, | |
| 120 | + ...props | |
| 121 | +}: AutocompletePrimitive.Item.Props) { | |
| 122 | + return ( | |
| 123 | + <AutocompletePrimitive.Item | |
| 124 | + className={cn( | |
| 125 | + "flex min-h-8 cursor-default select-none items-center rounded-sm px-2 py-1 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm", | |
| 126 | + className, | |
| 127 | + )} | |
| 128 | + data-slot="autocomplete-item" | |
| 129 | + {...props} | |
| 130 | + > | |
| 131 | + {children} | |
| 132 | + </AutocompletePrimitive.Item> | |
| 133 | + ); | |
| 134 | +} | |
| 135 | + | |
| 136 | +function AutocompleteSeparator({ | |
| 137 | + className, | |
| 138 | + ...props | |
| 139 | +}: AutocompletePrimitive.Separator.Props) { | |
| 140 | + return ( | |
| 141 | + <AutocompletePrimitive.Separator | |
| 142 | + className={cn("mx-2 my-1 h-px bg-border last:hidden", className)} | |
| 143 | + data-slot="autocomplete-separator" | |
| 144 | + {...props} | |
| 145 | + /> | |
| 146 | + ); | |
| 147 | +} | |
| 148 | + | |
| 149 | +function AutocompleteGroup({ | |
| 150 | + className, | |
| 151 | + ...props | |
| 152 | +}: AutocompletePrimitive.Group.Props) { | |
| 153 | + return ( | |
| 154 | + <AutocompletePrimitive.Group | |
| 155 | + className={cn("[[role=group]+&]:mt-1.5", className)} | |
| 156 | + data-slot="autocomplete-group" | |
| 157 | + {...props} | |
| 158 | + /> | |
| 159 | + ); | |
| 160 | +} | |
| 161 | + | |
| 162 | +function AutocompleteGroupLabel({ | |
| 163 | + className, | |
| 164 | + ...props | |
| 165 | +}: AutocompletePrimitive.GroupLabel.Props) { | |
| 166 | + return ( | |
| 167 | + <AutocompletePrimitive.GroupLabel | |
| 168 | + className={cn( | |
| 169 | + "px-2 py-1.5 font-medium text-muted-foreground text-xs", | |
| 170 | + className, | |
| 171 | + )} | |
| 172 | + data-slot="autocomplete-group-label" | |
| 173 | + {...props} | |
| 174 | + /> | |
| 175 | + ); | |
| 176 | +} | |
| 177 | + | |
| 178 | +function AutocompleteEmpty({ | |
| 179 | + className, | |
| 180 | + ...props | |
| 181 | +}: AutocompletePrimitive.Empty.Props) { | |
| 182 | + return ( | |
| 183 | + <AutocompletePrimitive.Empty | |
| 184 | + className={cn( | |
| 185 | + "not-empty:p-2 text-center text-base text-muted-foreground sm:text-sm", | |
| 186 | + className, | |
| 187 | + )} | |
| 188 | + data-slot="autocomplete-empty" | |
| 189 | + {...props} | |
| 190 | + /> | |
| 191 | + ); | |
| 192 | +} | |
| 193 | + | |
| 194 | +function AutocompleteRow({ | |
| 195 | + className, | |
| 196 | + ...props | |
| 197 | +}: AutocompletePrimitive.Row.Props) { | |
| 198 | + return ( | |
| 199 | + <AutocompletePrimitive.Row | |
| 200 | + className={className} | |
| 201 | + data-slot="autocomplete-row" | |
| 202 | + {...props} | |
| 203 | + /> | |
| 204 | + ); | |
| 205 | +} | |
| 206 | + | |
| 207 | +function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) { | |
| 208 | + return ( | |
| 209 | + <AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} /> | |
| 210 | + ); | |
| 211 | +} | |
| 212 | + | |
| 213 | +function AutocompleteList({ | |
| 214 | + className, | |
| 215 | + ...props | |
| 216 | +}: AutocompletePrimitive.List.Props) { | |
| 217 | + return ( | |
| 218 | + <ScrollArea scrollbarGutter scrollFade> | |
| 219 | + <AutocompletePrimitive.List | |
| 220 | + className={cn( | |
| 221 | + "not-empty:scroll-py-1 not-empty:p-1 in-data-has-overflow-y:pe-3", | |
| 222 | + className, | |
| 223 | + )} | |
| 224 | + data-slot="autocomplete-list" | |
| 225 | + {...props} | |
| 226 | + /> | |
| 227 | + </ScrollArea> | |
| 228 | + ); | |
| 229 | +} | |
| 230 | + | |
| 231 | +function AutocompleteClear({ | |
| 232 | + className, | |
| 233 | + ...props | |
| 234 | +}: AutocompletePrimitive.Clear.Props) { | |
| 235 | + return ( | |
| 236 | + <AutocompletePrimitive.Clear | |
| 237 | + className={cn( | |
| 238 | + "-translate-y-1/2 absolute end-0.5 top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-[color,background-color,box-shadow,opacity] pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", | |
| 239 | + className, | |
| 240 | + )} | |
| 241 | + data-slot="autocomplete-clear" | |
| 242 | + {...props} | |
| 243 | + > | |
| 244 | + <XIcon /> | |
| 245 | + </AutocompletePrimitive.Clear> | |
| 246 | + ); | |
| 247 | +} | |
| 248 | + | |
| 249 | +function AutocompleteStatus({ | |
| 250 | + className, | |
| 251 | + ...props | |
| 252 | +}: AutocompletePrimitive.Status.Props) { | |
| 253 | + return ( | |
| 254 | + <AutocompletePrimitive.Status | |
| 255 | + className={cn( | |
| 256 | + "px-3 py-2 font-medium text-muted-foreground text-xs empty:m-0 empty:p-0", | |
| 257 | + className, | |
| 258 | + )} | |
| 259 | + data-slot="autocomplete-status" | |
| 260 | + {...props} | |
| 261 | + /> | |
| 262 | + ); | |
| 263 | +} | |
| 264 | + | |
| 265 | +function AutocompleteCollection({ | |
| 266 | + ...props | |
| 267 | +}: AutocompletePrimitive.Collection.Props) { | |
| 268 | + return ( | |
| 269 | + <AutocompletePrimitive.Collection | |
| 270 | + data-slot="autocomplete-collection" | |
| 271 | + {...props} | |
| 272 | + /> | |
| 273 | + ); | |
| 274 | +} | |
| 275 | + | |
| 276 | +function AutocompleteTrigger({ | |
| 277 | + className, | |
| 278 | + ...props | |
| 279 | +}: AutocompletePrimitive.Trigger.Props) { | |
| 280 | + return ( | |
| 281 | + <AutocompletePrimitive.Trigger | |
| 282 | + className={className} | |
| 283 | + data-slot="autocomplete-trigger" | |
| 284 | + {...props} | |
| 285 | + /> | |
| 286 | + ); | |
| 287 | +} | |
| 288 | + | |
| 289 | +const useAutocompleteFilter = AutocompletePrimitive.useFilter; | |
| 290 | + | |
| 291 | +export { | |
| 292 | + Autocomplete, | |
| 293 | + AutocompleteInput, | |
| 294 | + AutocompleteTrigger, | |
| 295 | + AutocompletePopup, | |
| 296 | + AutocompleteItem, | |
| 297 | + AutocompleteSeparator, | |
| 298 | + AutocompleteGroup, | |
| 299 | + AutocompleteGroupLabel, | |
| 300 | + AutocompleteEmpty, | |
| 301 | + AutocompleteValue, | |
| 302 | + AutocompleteList, | |
| 303 | + AutocompleteClear, | |
| 304 | + AutocompleteStatus, | |
| 305 | + AutocompleteRow, | |
| 306 | + AutocompleteCollection, | |
| 307 | + useAutocompleteFilter, | |
| 308 | +}; | |
| @@ -0,0 +1,44 @@ | ||
| 1 | +import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +function Avatar({ className, ...props }: AvatarPrimitive.Root.Props) { | |
| 6 | + return ( | |
| 7 | + <AvatarPrimitive.Root | |
| 8 | + className={cn( | |
| 9 | + "inline-flex size-8 shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-background align-middle font-medium text-xs", | |
| 10 | + className, | |
| 11 | + )} | |
| 12 | + data-slot="avatar" | |
| 13 | + {...props} | |
| 14 | + /> | |
| 15 | + ); | |
| 16 | +} | |
| 17 | + | |
| 18 | +function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) { | |
| 19 | + return ( | |
| 20 | + <AvatarPrimitive.Image | |
| 21 | + className={cn("size-full object-cover", className)} | |
| 22 | + data-slot="avatar-image" | |
| 23 | + {...props} | |
| 24 | + /> | |
| 25 | + ); | |
| 26 | +} | |
| 27 | + | |
| 28 | +function AvatarFallback({ | |
| 29 | + className, | |
| 30 | + ...props | |
| 31 | +}: AvatarPrimitive.Fallback.Props) { | |
| 32 | + return ( | |
| 33 | + <AvatarPrimitive.Fallback | |
| 34 | + className={cn( | |
| 35 | + "flex size-full items-center justify-center rounded-full bg-muted", | |
| 36 | + className, | |
| 37 | + )} | |
| 38 | + data-slot="avatar-fallback" | |
| 39 | + {...props} | |
| 40 | + /> | |
| 41 | + ); | |
| 42 | +} | |
| 43 | + | |
| 44 | +export { Avatar, AvatarImage, AvatarFallback }; | |
| @@ -0,0 +1,58 @@ | ||
| 1 | +import { mergeProps } from "@base-ui/react/merge-props"; | |
| 2 | +import { useRender } from "@base-ui/react/use-render"; | |
| 3 | +import { type VariantProps, cva } from "class-variance-authority"; | |
| 4 | + | |
| 5 | +import { cn } from "#/lib/utils"; | |
| 6 | + | |
| 7 | +const badgeVariants = cva( | |
| 8 | + "relative inline-flex shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-sm border border-transparent font-medium outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-3.5 sm:[&_svg:not([class*='size-'])]:size-3 [&_svg]:pointer-events-none [&_svg]:shrink-0 [button,a&]:cursor-pointer [button,a&]:pointer-coarse:after:absolute [button,a&]:pointer-coarse:after:size-full [button,a&]:pointer-coarse:after:min-h-11 [button,a&]:pointer-coarse:after:min-w-11", | |
| 9 | + { | |
| 10 | + defaultVariants: { | |
| 11 | + size: "default", | |
| 12 | + variant: "default", | |
| 13 | + }, | |
| 14 | + variants: { | |
| 15 | + size: { | |
| 16 | + default: | |
| 17 | + "h-5.5 min-w-5.5 px-[calc(--spacing(1)-1px)] text-sm sm:h-4.5 sm:min-w-4.5 sm:text-xs", | |
| 18 | + lg: "h-6.5 min-w-6.5 px-[calc(--spacing(1.5)-1px)] text-base sm:h-5.5 sm:min-w-5.5 sm:text-sm", | |
| 19 | + sm: "h-5 min-w-5 rounded-[calc(var(--radius-sm)-2px)] px-[calc(--spacing(1)-1px)] text-xs sm:h-4 sm:min-w-4 sm:text-[.625rem]", | |
| 20 | + }, | |
| 21 | + variant: { | |
| 22 | + default: | |
| 23 | + "bg-primary text-primary-foreground [button,a&]:hover:bg-primary/90", | |
| 24 | + destructive: | |
| 25 | + "bg-destructive text-white [button,a&]:hover:bg-destructive/90", | |
| 26 | + error: | |
| 27 | + "bg-destructive/8 text-destructive-foreground dark:bg-destructive/16", | |
| 28 | + info: "bg-info/8 text-info-foreground dark:bg-info/16", | |
| 29 | + outline: | |
| 30 | + "border-input bg-background text-foreground dark:bg-input/32 [button,a&]:hover:bg-accent/50 dark:[button,a&]:hover:bg-input/48", | |
| 31 | + secondary: | |
| 32 | + "bg-secondary text-secondary-foreground [button,a&]:hover:bg-secondary/90", | |
| 33 | + success: "bg-success/8 text-success-foreground dark:bg-success/16", | |
| 34 | + warning: "bg-warning/8 text-warning-foreground dark:bg-warning/16", | |
| 35 | + }, | |
| 36 | + }, | |
| 37 | + }, | |
| 38 | +); | |
| 39 | + | |
| 40 | +interface BadgeProps extends useRender.ComponentProps<"span"> { | |
| 41 | + variant?: VariantProps<typeof badgeVariants>["variant"]; | |
| 42 | + size?: VariantProps<typeof badgeVariants>["size"]; | |
| 43 | +} | |
| 44 | + | |
| 45 | +function Badge({ className, variant, size, render, ...props }: BadgeProps) { | |
| 46 | + const defaultProps = { | |
| 47 | + className: cn(badgeVariants({ className, size, variant })), | |
| 48 | + "data-slot": "badge", | |
| 49 | + }; | |
| 50 | + | |
| 51 | + return useRender({ | |
| 52 | + defaultTagName: "span", | |
| 53 | + props: mergeProps<"span">(defaultProps, props), | |
| 54 | + render, | |
| 55 | + }); | |
| 56 | +} | |
| 57 | + | |
| 58 | +export { Badge, badgeVariants }; | |
| @@ -0,0 +1,71 @@ | ||
| 1 | +import { mergeProps } from "@base-ui/react/merge-props"; | |
| 2 | +import { useRender } from "@base-ui/react/use-render"; | |
| 3 | +import { type VariantProps, cva } from "class-variance-authority"; | |
| 4 | +import type * as React from "react"; | |
| 5 | + | |
| 6 | +import { cn } from "#/lib/utils"; | |
| 7 | + | |
| 8 | +const buttonVariants = cva( | |
| 9 | + "[&_svg]:-mx-0.5 relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-lg border font-medium text-base outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-64 sm:text-sm [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", | |
| 10 | + { | |
| 11 | + defaultVariants: { | |
| 12 | + size: "default", | |
| 13 | + variant: "default", | |
| 14 | + }, | |
| 15 | + variants: { | |
| 16 | + size: { | |
| 17 | + default: "h-9 px-[calc(--spacing(3)-1px)] sm:h-8", | |
| 18 | + icon: "size-9 sm:size-8", | |
| 19 | + "icon-lg": "size-10 sm:size-9", | |
| 20 | + "icon-sm": "size-8 sm:size-7", | |
| 21 | + "icon-xl": | |
| 22 | + "size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5", | |
| 23 | + "icon-xs": | |
| 24 | + "size-7 rounded-md before:rounded-[calc(var(--radius-md)-1px)] sm:size-6 not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-4 sm:not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3.5", | |
| 25 | + lg: "h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9", | |
| 26 | + sm: "h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:h-7", | |
| 27 | + xl: "h-11 px-[calc(--spacing(4)-1px)] text-lg sm:h-10 sm:text-base [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5", | |
| 28 | + xs: "h-7 gap-1 rounded-md px-[calc(--spacing(2)-1px)] text-sm before:rounded-[calc(var(--radius-md)-1px)] sm:h-6 sm:text-xs [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5", | |
| 29 | + }, | |
| 30 | + variant: { | |
| 31 | + default: | |
| 32 | + "not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-primary bg-primary text-primary-foreground shadow-primary/24 shadow-xs [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-primary/90", | |
| 33 | + destructive: | |
| 34 | + "not-disabled:inset-shadow-[0_1px_--theme(--color-white/16%)] border-destructive bg-destructive text-white shadow-destructive/24 shadow-xs [:active,[data-pressed]]:inset-shadow-[0_1px_--theme(--color-black/8%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-destructive/90", | |
| 35 | + "destructive-outline": | |
| 36 | + "border-input bg-popover not-dark:bg-clip-padding text-destructive-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:border-destructive/32 [:hover,[data-pressed]]:bg-destructive/4", | |
| 37 | + ghost: | |
| 38 | + "border-transparent text-foreground data-pressed:bg-accent [:hover,[data-pressed]]:bg-accent", | |
| 39 | + link: "border-transparent underline-offset-4 [:hover,[data-pressed]]:underline", | |
| 40 | + outline: | |
| 41 | + "border-input bg-popover not-dark:bg-clip-padding text-foreground shadow-xs/5 not-disabled:not-active:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] dark:bg-input/32 dark:not-disabled:before:shadow-[0_-1px_--theme(--color-white/2%)] dark:not-disabled:not-active:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [:disabled,:active,[data-pressed]]:shadow-none [:hover,[data-pressed]]:bg-accent/50 dark:[:hover,[data-pressed]]:bg-input/64", | |
| 42 | + secondary: | |
| 43 | + "border-transparent bg-secondary text-secondary-foreground [:active,[data-pressed]]:bg-secondary/80 [:hover,[data-pressed]]:bg-secondary/90", | |
| 44 | + }, | |
| 45 | + }, | |
| 46 | + }, | |
| 47 | +); | |
| 48 | + | |
| 49 | +interface ButtonProps extends useRender.ComponentProps<"button"> { | |
| 50 | + variant?: VariantProps<typeof buttonVariants>["variant"]; | |
| 51 | + size?: VariantProps<typeof buttonVariants>["size"]; | |
| 52 | +} | |
| 53 | + | |
| 54 | +function Button({ className, variant, size, render, ...props }: ButtonProps) { | |
| 55 | + const typeValue: React.ButtonHTMLAttributes<HTMLButtonElement>["type"] = | |
| 56 | + render ? undefined : "button"; | |
| 57 | + | |
| 58 | + const defaultProps = { | |
| 59 | + className: cn(buttonVariants({ className, size, variant })), | |
| 60 | + "data-slot": "button", | |
| 61 | + type: typeValue, | |
| 62 | + }; | |
| 63 | + | |
| 64 | + return useRender({ | |
| 65 | + defaultTagName: "button", | |
| 66 | + props: mergeProps<"button">(defaultProps, props), | |
| 67 | + render, | |
| 68 | + }); | |
| 69 | +} | |
| 70 | + | |
| 71 | +export { Button, buttonVariants }; | |
| @@ -0,0 +1,242 @@ | ||
| 1 | +import { mergeProps } from "@base-ui/react/merge-props"; | |
| 2 | +import { useRender } from "@base-ui/react/use-render"; | |
| 3 | + | |
| 4 | +import { cn } from "#/lib/utils"; | |
| 5 | + | |
| 6 | +function Card({ | |
| 7 | + className, | |
| 8 | + render, | |
| 9 | + ...props | |
| 10 | +}: useRender.ComponentProps<"div">) { | |
| 11 | + const defaultProps = { | |
| 12 | + className: cn( | |
| 13 | + "relative flex flex-col rounded-2xl border bg-card not-dark:bg-clip-padding text-card-foreground shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 14 | + className, | |
| 15 | + ), | |
| 16 | + "data-slot": "card", | |
| 17 | + }; | |
| 18 | + | |
| 19 | + return useRender({ | |
| 20 | + defaultTagName: "div", | |
| 21 | + props: mergeProps<"div">(defaultProps, props), | |
| 22 | + render, | |
| 23 | + }); | |
| 24 | +} | |
| 25 | + | |
| 26 | +function CardFrame({ | |
| 27 | + className, | |
| 28 | + render, | |
| 29 | + ...props | |
| 30 | +}: useRender.ComponentProps<"div">) { | |
| 31 | + const defaultProps = { | |
| 32 | + className: cn( | |
| 33 | + "flex flex-col relative rounded-2xl border bg-card before:bg-muted/72 not-dark:bg-clip-padding text-card-foreground shadow-xs/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)] *:data-[slot=card]:-m-px *:not-last:data-[slot=card]:rounded-b-xl *:not-last:data-[slot=card]:before:rounded-b-[calc(var(--radius-xl)-1px)] *:not-first:data-[slot=card]:rounded-t-xl *:not-first:data-[slot=card]:before:rounded-t-[calc(var(--radius-xl)-1px)] *:data-[slot=card]:[clip-path:inset(-1rem_1px)] *:data-[slot=card]:first:[clip-path:inset(1px_1px_-1rem_1px_round_calc(var(--radius-2xl)-1px))] *:data-[slot=card]:last:[clip-path:inset(-1rem_1px_1px_1px_round_calc(var(--radius-2xl)-1px))] *:data-[slot=card]:shadow-none *:data-[slot=card]:before:hidden *:data-[slot=card]:bg-clip-padding", | |
| 34 | + className, | |
| 35 | + ), | |
| 36 | + "data-slot": "card-frame", | |
| 37 | + }; | |
| 38 | + | |
| 39 | + return useRender({ | |
| 40 | + defaultTagName: "div", | |
| 41 | + props: mergeProps<"div">(defaultProps, props), | |
| 42 | + render, | |
| 43 | + }); | |
| 44 | +} | |
| 45 | + | |
| 46 | +function CardFrameHeader({ | |
| 47 | + className, | |
| 48 | + render, | |
| 49 | + ...props | |
| 50 | +}: useRender.ComponentProps<"div">) { | |
| 51 | + const defaultProps = { | |
| 52 | + className: cn("relative flex flex-col px-6 py-4", className), | |
| 53 | + "data-slot": "card-frame-header", | |
| 54 | + }; | |
| 55 | + | |
| 56 | + return useRender({ | |
| 57 | + defaultTagName: "div", | |
| 58 | + props: mergeProps<"div">(defaultProps, props), | |
| 59 | + render, | |
| 60 | + }); | |
| 61 | +} | |
| 62 | + | |
| 63 | +function CardFrameTitle({ | |
| 64 | + className, | |
| 65 | + render, | |
| 66 | + ...props | |
| 67 | +}: useRender.ComponentProps<"div">) { | |
| 68 | + const defaultProps = { | |
| 69 | + className: cn("font-semibold text-sm", className), | |
| 70 | + "data-slot": "card-frame-title", | |
| 71 | + }; | |
| 72 | + | |
| 73 | + return useRender({ | |
| 74 | + defaultTagName: "div", | |
| 75 | + props: mergeProps<"div">(defaultProps, props), | |
| 76 | + render, | |
| 77 | + }); | |
| 78 | +} | |
| 79 | + | |
| 80 | +function CardFrameDescription({ | |
| 81 | + className, | |
| 82 | + render, | |
| 83 | + ...props | |
| 84 | +}: useRender.ComponentProps<"div">) { | |
| 85 | + const defaultProps = { | |
| 86 | + className: cn("text-muted-foreground text-sm", className), | |
| 87 | + "data-slot": "card-frame-description", | |
| 88 | + }; | |
| 89 | + | |
| 90 | + return useRender({ | |
| 91 | + defaultTagName: "div", | |
| 92 | + props: mergeProps<"div">(defaultProps, props), | |
| 93 | + render, | |
| 94 | + }); | |
| 95 | +} | |
| 96 | + | |
| 97 | +function CardFrameFooter({ | |
| 98 | + className, | |
| 99 | + render, | |
| 100 | + ...props | |
| 101 | +}: useRender.ComponentProps<"div">) { | |
| 102 | + const defaultProps = { | |
| 103 | + className: cn("px-6 py-4", className), | |
| 104 | + "data-slot": "card-frame-footer", | |
| 105 | + }; | |
| 106 | + | |
| 107 | + return useRender({ | |
| 108 | + defaultTagName: "div", | |
| 109 | + props: mergeProps<"div">(defaultProps, props), | |
| 110 | + render, | |
| 111 | + }); | |
| 112 | +} | |
| 113 | + | |
| 114 | +function CardHeader({ | |
| 115 | + className, | |
| 116 | + render, | |
| 117 | + ...props | |
| 118 | +}: useRender.ComponentProps<"div">) { | |
| 119 | + const defaultProps = { | |
| 120 | + className: cn( | |
| 121 | + "grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pb-4 has-data-[slot=card-action]:grid-cols-[1fr_auto]", | |
| 122 | + className, | |
| 123 | + ), | |
| 124 | + "data-slot": "card-header", | |
| 125 | + }; | |
| 126 | + | |
| 127 | + return useRender({ | |
| 128 | + defaultTagName: "div", | |
| 129 | + props: mergeProps<"div">(defaultProps, props), | |
| 130 | + render, | |
| 131 | + }); | |
| 132 | +} | |
| 133 | + | |
| 134 | +function CardTitle({ | |
| 135 | + className, | |
| 136 | + render, | |
| 137 | + ...props | |
| 138 | +}: useRender.ComponentProps<"div">) { | |
| 139 | + const defaultProps = { | |
| 140 | + className: cn("font-semibold text-lg leading-none", className), | |
| 141 | + "data-slot": "card-title", | |
| 142 | + }; | |
| 143 | + | |
| 144 | + return useRender({ | |
| 145 | + defaultTagName: "div", | |
| 146 | + props: mergeProps<"div">(defaultProps, props), | |
| 147 | + render, | |
| 148 | + }); | |
| 149 | +} | |
| 150 | + | |
| 151 | +function CardDescription({ | |
| 152 | + className, | |
| 153 | + render, | |
| 154 | + ...props | |
| 155 | +}: useRender.ComponentProps<"div">) { | |
| 156 | + const defaultProps = { | |
| 157 | + className: cn("text-muted-foreground text-sm", className), | |
| 158 | + "data-slot": "card-description", | |
| 159 | + }; | |
| 160 | + | |
| 161 | + return useRender({ | |
| 162 | + defaultTagName: "div", | |
| 163 | + props: mergeProps<"div">(defaultProps, props), | |
| 164 | + render, | |
| 165 | + }); | |
| 166 | +} | |
| 167 | + | |
| 168 | +function CardAction({ | |
| 169 | + className, | |
| 170 | + render, | |
| 171 | + ...props | |
| 172 | +}: useRender.ComponentProps<"div">) { | |
| 173 | + const defaultProps = { | |
| 174 | + className: cn( | |
| 175 | + "col-start-2 row-span-2 row-start-1 self-start justify-self-end inline-flex", | |
| 176 | + className, | |
| 177 | + ), | |
| 178 | + "data-slot": "card-action", | |
| 179 | + }; | |
| 180 | + | |
| 181 | + return useRender({ | |
| 182 | + defaultTagName: "div", | |
| 183 | + props: mergeProps<"div">(defaultProps, props), | |
| 184 | + render, | |
| 185 | + }); | |
| 186 | +} | |
| 187 | + | |
| 188 | +function CardPanel({ | |
| 189 | + className, | |
| 190 | + render, | |
| 191 | + ...props | |
| 192 | +}: useRender.ComponentProps<"div">) { | |
| 193 | + const defaultProps = { | |
| 194 | + className: cn( | |
| 195 | + "flex-1 p-6 in-[[data-slot=card]:has(>[data-slot=card-header]:not(.border-b))]:pt-0 in-[[data-slot=card]:has(>[data-slot=card-footer]:not(.border-t))]:pb-0", | |
| 196 | + className, | |
| 197 | + ), | |
| 198 | + "data-slot": "card-panel", | |
| 199 | + }; | |
| 200 | + | |
| 201 | + return useRender({ | |
| 202 | + defaultTagName: "div", | |
| 203 | + props: mergeProps<"div">(defaultProps, props), | |
| 204 | + render, | |
| 205 | + }); | |
| 206 | +} | |
| 207 | + | |
| 208 | +function CardFooter({ | |
| 209 | + className, | |
| 210 | + render, | |
| 211 | + ...props | |
| 212 | +}: useRender.ComponentProps<"div">) { | |
| 213 | + const defaultProps = { | |
| 214 | + className: cn( | |
| 215 | + "flex items-center p-6 in-[[data-slot=card]:has(>[data-slot=card-panel])]:pt-4", | |
| 216 | + className, | |
| 217 | + ), | |
| 218 | + "data-slot": "card-footer", | |
| 219 | + }; | |
| 220 | + | |
| 221 | + return useRender({ | |
| 222 | + defaultTagName: "div", | |
| 223 | + props: mergeProps<"div">(defaultProps, props), | |
| 224 | + render, | |
| 225 | + }); | |
| 226 | +} | |
| 227 | + | |
| 228 | +export { | |
| 229 | + Card, | |
| 230 | + CardFrame, | |
| 231 | + CardFrameHeader, | |
| 232 | + CardFrameTitle, | |
| 233 | + CardFrameDescription, | |
| 234 | + CardFrameFooter, | |
| 235 | + CardAction, | |
| 236 | + CardDescription, | |
| 237 | + CardFooter, | |
| 238 | + CardHeader, | |
| 239 | + CardPanel, | |
| 240 | + CardPanel as CardContent, | |
| 241 | + CardTitle, | |
| 242 | +}; | |
| @@ -0,0 +1,14 @@ | ||
| 1 | +import { CheckboxGroup as CheckboxGroupPrimitive } from "@base-ui/react/checkbox-group"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +function CheckboxGroup({ className, ...props }: CheckboxGroupPrimitive.Props) { | |
| 6 | + return ( | |
| 7 | + <CheckboxGroupPrimitive | |
| 8 | + className={cn("flex flex-col items-start gap-3", className)} | |
| 9 | + {...props} | |
| 10 | + /> | |
| 11 | + ); | |
| 12 | +} | |
| 13 | + | |
| 14 | +export { CheckboxGroup }; | |
| @@ -0,0 +1,58 @@ | ||
| 1 | +import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { | |
| 6 | + return ( | |
| 7 | + <CheckboxPrimitive.Root | |
| 8 | + className={cn( | |
| 9 | + "relative inline-flex size-4.5 shrink-0 items-center justify-center rounded-[4px] border border-input bg-background not-dark:bg-clip-padding shadow-xs/5 outline-none ring-ring transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[3px] not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-background aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/48 data-disabled:opacity-64 sm:size-4 dark:not-data-checked:bg-input/32 dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)] [[data-disabled],[data-checked],[aria-invalid]]:shadow-none", | |
| 10 | + className, | |
| 11 | + )} | |
| 12 | + data-slot="checkbox" | |
| 13 | + {...props} | |
| 14 | + > | |
| 15 | + <CheckboxPrimitive.Indicator | |
| 16 | + className="-inset-px absolute flex items-center justify-center rounded-[4px] text-primary-foreground data-unchecked:hidden data-checked:bg-primary data-indeterminate:text-foreground" | |
| 17 | + data-slot="checkbox-indicator" | |
| 18 | + render={(props, state) => ( | |
| 19 | + <span {...props}> | |
| 20 | + {state.indeterminate ? ( | |
| 21 | + <svg | |
| 22 | + className="size-3.5 sm:size-3" | |
| 23 | + fill="none" | |
| 24 | + height="24" | |
| 25 | + stroke="currentColor" | |
| 26 | + strokeLinecap="round" | |
| 27 | + strokeLinejoin="round" | |
| 28 | + strokeWidth="3" | |
| 29 | + viewBox="0 0 24 24" | |
| 30 | + width="24" | |
| 31 | + xmlns="http://www.w3.org/2000/svg" | |
| 32 | + > | |
| 33 | + <path d="M5.252 12h13.496" /> | |
| 34 | + </svg> | |
| 35 | + ) : ( | |
| 36 | + <svg | |
| 37 | + className="size-3.5 sm:size-3" | |
| 38 | + fill="none" | |
| 39 | + height="24" | |
| 40 | + stroke="currentColor" | |
| 41 | + strokeLinecap="round" | |
| 42 | + strokeLinejoin="round" | |
| 43 | + strokeWidth="3" | |
| 44 | + viewBox="0 0 24 24" | |
| 45 | + width="24" | |
| 46 | + xmlns="http://www.w3.org/2000/svg" | |
| 47 | + > | |
| 48 | + <path d="M5.252 12.7 10.2 18.63 18.748 5.37" /> | |
| 49 | + </svg> | |
| 50 | + )} | |
| 51 | + </span> | |
| 52 | + )} | |
| 53 | + /> | |
| 54 | + </CheckboxPrimitive.Root> | |
| 55 | + ); | |
| 56 | +} | |
| 57 | + | |
| 58 | +export { Checkbox }; | |
| @@ -0,0 +1,43 @@ | ||
| 1 | +import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) { | |
| 6 | + return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />; | |
| 7 | +} | |
| 8 | + | |
| 9 | +function CollapsibleTrigger({ | |
| 10 | + className, | |
| 11 | + ...props | |
| 12 | +}: CollapsiblePrimitive.Trigger.Props) { | |
| 13 | + return ( | |
| 14 | + <CollapsiblePrimitive.Trigger | |
| 15 | + className={cn("cursor-pointer", className)} | |
| 16 | + data-slot="collapsible-trigger" | |
| 17 | + {...props} | |
| 18 | + /> | |
| 19 | + ); | |
| 20 | +} | |
| 21 | + | |
| 22 | +function CollapsiblePanel({ | |
| 23 | + className, | |
| 24 | + ...props | |
| 25 | +}: CollapsiblePrimitive.Panel.Props) { | |
| 26 | + return ( | |
| 27 | + <CollapsiblePrimitive.Panel | |
| 28 | + className={cn( | |
| 29 | + "h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-200 data-ending-style:h-0 data-starting-style:h-0", | |
| 30 | + className, | |
| 31 | + )} | |
| 32 | + data-slot="collapsible-panel" | |
| 33 | + {...props} | |
| 34 | + /> | |
| 35 | + ); | |
| 36 | +} | |
| 37 | + | |
| 38 | +export { | |
| 39 | + Collapsible, | |
| 40 | + CollapsibleTrigger, | |
| 41 | + CollapsiblePanel, | |
| 42 | + CollapsiblePanel as CollapsibleContent, | |
| 43 | +}; | |
| @@ -0,0 +1,422 @@ | ||
| 1 | +import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox"; | |
| 2 | +import { ChevronsUpDownIcon, XIcon } from "lucide-react"; | |
| 3 | +import * as React from "react"; | |
| 4 | + | |
| 5 | +import { Input } from "#/components/ui/input"; | |
| 6 | +import { ScrollArea } from "#/components/ui/scroll-area"; | |
| 7 | +import { cn } from "#/lib/utils"; | |
| 8 | + | |
| 9 | +const ComboboxContext = React.createContext<{ | |
| 10 | + chipsRef: React.RefObject<Element | null> | null; | |
| 11 | + multiple: boolean; | |
| 12 | +}>({ | |
| 13 | + chipsRef: null, | |
| 14 | + multiple: false, | |
| 15 | +}); | |
| 16 | + | |
| 17 | +function Combobox<Value, Multiple extends boolean | undefined = false>( | |
| 18 | + props: ComboboxPrimitive.Root.Props<Value, Multiple>, | |
| 19 | +): React.JSX.Element { | |
| 20 | + const chipsRef = React.useRef<Element | null>(null); | |
| 21 | + return ( | |
| 22 | + <ComboboxContext.Provider value={{ chipsRef, multiple: !!props.multiple }}> | |
| 23 | + <ComboboxPrimitive.Root {...props} /> | |
| 24 | + </ComboboxContext.Provider> | |
| 25 | + ); | |
| 26 | +} | |
| 27 | + | |
| 28 | +function ComboboxChipsInput({ | |
| 29 | + className, | |
| 30 | + size, | |
| 31 | + ...props | |
| 32 | +}: Omit<ComboboxPrimitive.Input.Props, "size"> & { | |
| 33 | + size?: "sm" | "default" | "lg" | number; | |
| 34 | + ref?: React.Ref<HTMLInputElement>; | |
| 35 | +}) { | |
| 36 | + const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number; | |
| 37 | + | |
| 38 | + return ( | |
| 39 | + <ComboboxPrimitive.Input | |
| 40 | + className={cn( | |
| 41 | + "min-w-12 flex-1 text-base outline-none sm:text-sm [[data-slot=combobox-chip]+&]:ps-0.5", | |
| 42 | + sizeValue === "sm" ? "ps-1.5" : "ps-2", | |
| 43 | + className, | |
| 44 | + )} | |
| 45 | + data-size={typeof sizeValue === "string" ? sizeValue : undefined} | |
| 46 | + data-slot="combobox-chips-input" | |
| 47 | + size={typeof sizeValue === "number" ? sizeValue : undefined} | |
| 48 | + {...props} | |
| 49 | + /> | |
| 50 | + ); | |
| 51 | +} | |
| 52 | + | |
| 53 | +function ComboboxInput({ | |
| 54 | + className, | |
| 55 | + showTrigger = true, | |
| 56 | + showClear = false, | |
| 57 | + startAddon, | |
| 58 | + size, | |
| 59 | + ...props | |
| 60 | +}: Omit<ComboboxPrimitive.Input.Props, "size"> & { | |
| 61 | + showTrigger?: boolean; | |
| 62 | + showClear?: boolean; | |
| 63 | + startAddon?: React.ReactNode; | |
| 64 | + size?: "sm" | "default" | "lg" | number; | |
| 65 | + ref?: React.Ref<HTMLInputElement>; | |
| 66 | +}) { | |
| 67 | + const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number; | |
| 68 | + | |
| 69 | + return ( | |
| 70 | + <div className="relative not-has-[>*.w-full]:w-fit w-full text-foreground has-disabled:opacity-64"> | |
| 71 | + {startAddon && ( | |
| 72 | + <div | |
| 73 | + aria-hidden="true" | |
| 74 | + className="[&_svg]:-mx-0.5 pointer-events-none absolute inset-y-0 start-px z-10 flex items-center ps-[calc(--spacing(3)-1px)] opacity-80 has-[+[data-size=sm]]:ps-[calc(--spacing(2.5)-1px)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4" | |
| 75 | + data-slot="combobox-start-addon" | |
| 76 | + > | |
| 77 | + {startAddon} | |
| 78 | + </div> | |
| 79 | + )} | |
| 80 | + <ComboboxPrimitive.Input | |
| 81 | + className={cn( | |
| 82 | + startAddon && | |
| 83 | + "data-[size=sm]:*:data-[slot=combobox-input]:ps-[calc(--spacing(7.5)-1px)] *:data-[slot=combobox-input]:ps-[calc(--spacing(8.5)-1px)] sm:data-[size=sm]:*:data-[slot=combobox-input]:ps-[calc(--spacing(7)-1px)] sm:*:data-[slot=combobox-input]:ps-[calc(--spacing(8)-1px)]", | |
| 84 | + sizeValue === "sm" | |
| 85 | + ? "has-[+[data-slot=combobox-trigger],+[data-slot=combobox-clear]]:*:data-[slot=combobox-input]:pe-6.5" | |
| 86 | + : "has-[+[data-slot=combobox-trigger],+[data-slot=combobox-clear]]:*:data-[slot=combobox-input]:pe-7", | |
| 87 | + className, | |
| 88 | + )} | |
| 89 | + data-slot="combobox-input" | |
| 90 | + render={ | |
| 91 | + <Input | |
| 92 | + className="has-disabled:opacity-100" | |
| 93 | + nativeInput | |
| 94 | + size={sizeValue} | |
| 95 | + /> | |
| 96 | + } | |
| 97 | + {...props} | |
| 98 | + /> | |
| 99 | + {showTrigger && ( | |
| 100 | + <ComboboxTrigger | |
| 101 | + className={cn( | |
| 102 | + "-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-opacity pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=combobox-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", | |
| 103 | + sizeValue === "sm" ? "end-0" : "end-0.5", | |
| 104 | + )} | |
| 105 | + > | |
| 106 | + <ChevronsUpDownIcon /> | |
| 107 | + </ComboboxTrigger> | |
| 108 | + )} | |
| 109 | + {showClear && ( | |
| 110 | + <ComboboxClear | |
| 111 | + className={cn( | |
| 112 | + "-translate-y-1/2 absolute top-1/2 inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-80 outline-none transition-opacity pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:opacity-100 has-[+[data-slot=combobox-clear]]:hidden sm:size-7 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", | |
| 113 | + sizeValue === "sm" ? "end-0" : "end-0.5", | |
| 114 | + )} | |
| 115 | + > | |
| 116 | + <XIcon /> | |
| 117 | + </ComboboxClear> | |
| 118 | + )} | |
| 119 | + </div> | |
| 120 | + ); | |
| 121 | +} | |
| 122 | + | |
| 123 | +function ComboboxTrigger({ | |
| 124 | + className, | |
| 125 | + ...props | |
| 126 | +}: ComboboxPrimitive.Trigger.Props) { | |
| 127 | + return ( | |
| 128 | + <ComboboxPrimitive.Trigger | |
| 129 | + className={className} | |
| 130 | + data-slot="combobox-trigger" | |
| 131 | + {...props} | |
| 132 | + /> | |
| 133 | + ); | |
| 134 | +} | |
| 135 | + | |
| 136 | +function ComboboxPopup({ | |
| 137 | + className, | |
| 138 | + children, | |
| 139 | + side = "bottom", | |
| 140 | + sideOffset = 4, | |
| 141 | + alignOffset, | |
| 142 | + align = "start", | |
| 143 | + ...props | |
| 144 | +}: ComboboxPrimitive.Popup.Props & { | |
| 145 | + align?: ComboboxPrimitive.Positioner.Props["align"]; | |
| 146 | + sideOffset?: ComboboxPrimitive.Positioner.Props["sideOffset"]; | |
| 147 | + alignOffset?: ComboboxPrimitive.Positioner.Props["alignOffset"]; | |
| 148 | + side?: ComboboxPrimitive.Positioner.Props["side"]; | |
| 149 | +}) { | |
| 150 | + const { chipsRef } = React.useContext(ComboboxContext); | |
| 151 | + | |
| 152 | + return ( | |
| 153 | + <ComboboxPrimitive.Portal> | |
| 154 | + <ComboboxPrimitive.Positioner | |
| 155 | + align={align} | |
| 156 | + alignOffset={alignOffset} | |
| 157 | + anchor={chipsRef} | |
| 158 | + className="z-50 select-none" | |
| 159 | + data-slot="combobox-positioner" | |
| 160 | + side={side} | |
| 161 | + sideOffset={sideOffset} | |
| 162 | + > | |
| 163 | + <span | |
| 164 | + className={cn( | |
| 165 | + "relative flex max-h-full min-w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 166 | + className, | |
| 167 | + )} | |
| 168 | + > | |
| 169 | + <ComboboxPrimitive.Popup | |
| 170 | + className="flex max-h-[min(var(--available-height),23rem)] flex-1 flex-col text-foreground" | |
| 171 | + data-slot="combobox-popup" | |
| 172 | + {...props} | |
| 173 | + > | |
| 174 | + {children} | |
| 175 | + </ComboboxPrimitive.Popup> | |
| 176 | + </span> | |
| 177 | + </ComboboxPrimitive.Positioner> | |
| 178 | + </ComboboxPrimitive.Portal> | |
| 179 | + ); | |
| 180 | +} | |
| 181 | + | |
| 182 | +function ComboboxItem({ | |
| 183 | + className, | |
| 184 | + children, | |
| 185 | + ...props | |
| 186 | +}: ComboboxPrimitive.Item.Props) { | |
| 187 | + return ( | |
| 188 | + <ComboboxPrimitive.Item | |
| 189 | + className={cn( | |
| 190 | + "grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", | |
| 191 | + className, | |
| 192 | + )} | |
| 193 | + data-slot="combobox-item" | |
| 194 | + {...props} | |
| 195 | + > | |
| 196 | + <ComboboxPrimitive.ItemIndicator className="col-start-1"> | |
| 197 | + <svg | |
| 198 | + fill="none" | |
| 199 | + height="24" | |
| 200 | + stroke="currentColor" | |
| 201 | + strokeLinecap="round" | |
| 202 | + strokeLinejoin="round" | |
| 203 | + strokeWidth="2" | |
| 204 | + viewBox="0 0 24 24" | |
| 205 | + width="24" | |
| 206 | + xmlns="http://www.w3.org/1500/svg" | |
| 207 | + > | |
| 208 | + <path d="M5.252 12.7 10.2 18.63 18.748 5.37" /> | |
| 209 | + </svg> | |
| 210 | + </ComboboxPrimitive.ItemIndicator> | |
| 211 | + <div className="col-start-2">{children}</div> | |
| 212 | + </ComboboxPrimitive.Item> | |
| 213 | + ); | |
| 214 | +} | |
| 215 | + | |
| 216 | +function ComboboxSeparator({ | |
| 217 | + className, | |
| 218 | + ...props | |
| 219 | +}: ComboboxPrimitive.Separator.Props) { | |
| 220 | + return ( | |
| 221 | + <ComboboxPrimitive.Separator | |
| 222 | + className={cn("mx-2 my-1 h-px bg-border last:hidden", className)} | |
| 223 | + data-slot="combobox-separator" | |
| 224 | + {...props} | |
| 225 | + /> | |
| 226 | + ); | |
| 227 | +} | |
| 228 | + | |
| 229 | +function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) { | |
| 230 | + return ( | |
| 231 | + <ComboboxPrimitive.Group | |
| 232 | + className={cn("[[role=group]+&]:mt-1.5", className)} | |
| 233 | + data-slot="combobox-group" | |
| 234 | + {...props} | |
| 235 | + /> | |
| 236 | + ); | |
| 237 | +} | |
| 238 | + | |
| 239 | +function ComboboxGroupLabel({ | |
| 240 | + className, | |
| 241 | + ...props | |
| 242 | +}: ComboboxPrimitive.GroupLabel.Props) { | |
| 243 | + return ( | |
| 244 | + <ComboboxPrimitive.GroupLabel | |
| 245 | + className={cn( | |
| 246 | + "px-2 py-1.5 font-medium text-muted-foreground text-xs", | |
| 247 | + className, | |
| 248 | + )} | |
| 249 | + data-slot="combobox-group-label" | |
| 250 | + {...props} | |
| 251 | + /> | |
| 252 | + ); | |
| 253 | +} | |
| 254 | + | |
| 255 | +function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) { | |
| 256 | + return ( | |
| 257 | + <ComboboxPrimitive.Empty | |
| 258 | + className={cn( | |
| 259 | + "not-empty:p-2 text-center text-base text-muted-foreground sm:text-sm", | |
| 260 | + className, | |
| 261 | + )} | |
| 262 | + data-slot="combobox-empty" | |
| 263 | + {...props} | |
| 264 | + /> | |
| 265 | + ); | |
| 266 | +} | |
| 267 | + | |
| 268 | +function ComboboxRow({ className, ...props }: ComboboxPrimitive.Row.Props) { | |
| 269 | + return ( | |
| 270 | + <ComboboxPrimitive.Row | |
| 271 | + className={className} | |
| 272 | + data-slot="combobox-row" | |
| 273 | + {...props} | |
| 274 | + /> | |
| 275 | + ); | |
| 276 | +} | |
| 277 | + | |
| 278 | +function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) { | |
| 279 | + return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />; | |
| 280 | +} | |
| 281 | + | |
| 282 | +function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) { | |
| 283 | + return ( | |
| 284 | + <ScrollArea scrollbarGutter scrollFade> | |
| 285 | + <ComboboxPrimitive.List | |
| 286 | + className={cn( | |
| 287 | + "not-empty:scroll-py-1 not-empty:px-1 not-empty:py-1 in-data-has-overflow-y:pe-3", | |
| 288 | + className, | |
| 289 | + )} | |
| 290 | + data-slot="combobox-list" | |
| 291 | + {...props} | |
| 292 | + /> | |
| 293 | + </ScrollArea> | |
| 294 | + ); | |
| 295 | +} | |
| 296 | + | |
| 297 | +function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) { | |
| 298 | + return ( | |
| 299 | + <ComboboxPrimitive.Clear | |
| 300 | + className={className} | |
| 301 | + data-slot="combobox-clear" | |
| 302 | + {...props} | |
| 303 | + /> | |
| 304 | + ); | |
| 305 | +} | |
| 306 | + | |
| 307 | +function ComboboxStatus({ | |
| 308 | + className, | |
| 309 | + ...props | |
| 310 | +}: ComboboxPrimitive.Status.Props) { | |
| 311 | + return ( | |
| 312 | + <ComboboxPrimitive.Status | |
| 313 | + className={cn( | |
| 314 | + "px-3 py-2 font-medium text-muted-foreground text-xs empty:m-0 empty:p-0", | |
| 315 | + className, | |
| 316 | + )} | |
| 317 | + data-slot="combobox-status" | |
| 318 | + {...props} | |
| 319 | + /> | |
| 320 | + ); | |
| 321 | +} | |
| 322 | + | |
| 323 | +function ComboboxCollection(props: ComboboxPrimitive.Collection.Props) { | |
| 324 | + return ( | |
| 325 | + <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} /> | |
| 326 | + ); | |
| 327 | +} | |
| 328 | + | |
| 329 | +function ComboboxChips({ | |
| 330 | + className, | |
| 331 | + children, | |
| 332 | + startAddon, | |
| 333 | + ...props | |
| 334 | +}: ComboboxPrimitive.Chips.Props & { | |
| 335 | + startAddon?: React.ReactNode; | |
| 336 | +}) { | |
| 337 | + const { chipsRef } = React.useContext(ComboboxContext); | |
| 338 | + | |
| 339 | + return ( | |
| 340 | + <ComboboxPrimitive.Chips | |
| 341 | + className={cn( | |
| 342 | + "relative inline-flex min-h-9 w-full flex-wrap gap-1 rounded-lg border border-input bg-background not-dark:bg-clip-padding p-[calc(--spacing(1)-1px)] text-base shadow-xs/5 outline-none ring-ring/24 transition-shadow *:min-h-7 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] focus-within:border-ring focus-within:ring-[3px] has-disabled:pointer-events-none has-data-[size=lg]:min-h-10 has-data-[size=sm]:min-h-8 has-aria-invalid:border-destructive/36 has-autofill:bg-foreground/4 has-disabled:opacity-64 has-[:disabled,:focus-within,[aria-invalid]]:shadow-none focus-within:has-aria-invalid:border-destructive/64 focus-within:has-aria-invalid:ring-destructive/16 has-data-[size=lg]:*:min-h-8 has-data-[size=sm]:*:min-h-6 sm:min-h-8 sm:text-sm sm:has-data-[size=lg]:min-h-9 sm:has-data-[size=sm]:min-h-7 sm:*:min-h-6 sm:has-data-[size=lg]:*:min-h-7 sm:has-data-[size=sm]:*:min-h-5 dark:not-has-disabled:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 343 | + className, | |
| 344 | + )} | |
| 345 | + data-slot="combobox-chips" | |
| 346 | + onMouseDown={(e) => { | |
| 347 | + const target = e.target as HTMLElement; | |
| 348 | + const isChip = target.closest('[data-slot="combobox-chip"]'); | |
| 349 | + if (isChip || !chipsRef?.current) return; | |
| 350 | + e.preventDefault(); | |
| 351 | + const input: HTMLInputElement | null = | |
| 352 | + chipsRef.current.querySelector("input"); | |
| 353 | + if (input && !chipsRef.current.querySelector("input:focus")) { | |
| 354 | + input.focus(); | |
| 355 | + } | |
| 356 | + }} | |
| 357 | + ref={chipsRef as React.Ref<HTMLDivElement> | null} | |
| 358 | + {...props} | |
| 359 | + > | |
| 360 | + {startAddon && ( | |
| 361 | + <div | |
| 362 | + aria-hidden="true" | |
| 363 | + className="[&_svg]:-ms-0.5 [&_svg]:-me-1.5 flex shrink-0 items-center ps-2 opacity-80 has-[~[data-size=sm]]:has-[+[data-slot=combobox-chip]]:pe-1.5 has-[~[data-size=sm]]:ps-1.5 has-[+[data-slot=combobox-chip]]:pe-2 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none" | |
| 364 | + data-slot="combobox-start-addon" | |
| 365 | + > | |
| 366 | + {startAddon} | |
| 367 | + </div> | |
| 368 | + )} | |
| 369 | + {children} | |
| 370 | + </ComboboxPrimitive.Chips> | |
| 371 | + ); | |
| 372 | +} | |
| 373 | + | |
| 374 | +function ComboboxChip({ children, ...props }: ComboboxPrimitive.Chip.Props) { | |
| 375 | + return ( | |
| 376 | + <ComboboxPrimitive.Chip | |
| 377 | + className="flex items-center rounded-[calc(var(--radius-md)-1px)] bg-accent ps-2 font-medium text-accent-foreground text-sm outline-none sm:text-xs/(--text-xs--line-height) [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5" | |
| 378 | + data-slot="combobox-chip" | |
| 379 | + {...props} | |
| 380 | + > | |
| 381 | + {children} | |
| 382 | + <ComboboxChipRemove /> | |
| 383 | + </ComboboxPrimitive.Chip> | |
| 384 | + ); | |
| 385 | +} | |
| 386 | + | |
| 387 | +function ComboboxChipRemove(props: ComboboxPrimitive.ChipRemove.Props) { | |
| 388 | + return ( | |
| 389 | + <ComboboxPrimitive.ChipRemove | |
| 390 | + aria-label="Remove" | |
| 391 | + className="h-full shrink-0 cursor-pointer px-1.5 opacity-80 hover:opacity-100 [&_svg:not([class*='size-'])]:size-4 sm:[&_svg:not([class*='size-'])]:size-3.5" | |
| 392 | + data-slot="combobox-chip-remove" | |
| 393 | + {...props} | |
| 394 | + > | |
| 395 | + <XIcon /> | |
| 396 | + </ComboboxPrimitive.ChipRemove> | |
| 397 | + ); | |
| 398 | +} | |
| 399 | + | |
| 400 | +const useComboboxFilter = ComboboxPrimitive.useFilter; | |
| 401 | + | |
| 402 | +export { | |
| 403 | + Combobox, | |
| 404 | + ComboboxChipsInput, | |
| 405 | + ComboboxInput, | |
| 406 | + ComboboxTrigger, | |
| 407 | + ComboboxPopup, | |
| 408 | + ComboboxItem, | |
| 409 | + ComboboxSeparator, | |
| 410 | + ComboboxGroup, | |
| 411 | + ComboboxGroupLabel, | |
| 412 | + ComboboxEmpty, | |
| 413 | + ComboboxValue, | |
| 414 | + ComboboxList, | |
| 415 | + ComboboxClear, | |
| 416 | + ComboboxStatus, | |
| 417 | + ComboboxRow, | |
| 418 | + ComboboxCollection, | |
| 419 | + ComboboxChips, | |
| 420 | + ComboboxChip, | |
| 421 | + useComboboxFilter, | |
| 422 | +}; | |
| @@ -0,0 +1,262 @@ | ||
| 1 | +import { Dialog as CommandDialogPrimitive } from "@base-ui/react/dialog"; | |
| 2 | +import { SearchIcon } from "lucide-react"; | |
| 3 | +import type * as React from "react"; | |
| 4 | +import { | |
| 5 | + Autocomplete, | |
| 6 | + AutocompleteCollection, | |
| 7 | + AutocompleteEmpty, | |
| 8 | + AutocompleteGroup, | |
| 9 | + AutocompleteGroupLabel, | |
| 10 | + AutocompleteInput, | |
| 11 | + AutocompleteItem, | |
| 12 | + AutocompleteList, | |
| 13 | + AutocompleteSeparator, | |
| 14 | +} from "#/components/ui/autocomplete"; | |
| 15 | +import { cn } from "#/lib/utils"; | |
| 16 | + | |
| 17 | +const CommandDialog = CommandDialogPrimitive.Root; | |
| 18 | + | |
| 19 | +const CommandDialogPortal = CommandDialogPrimitive.Portal; | |
| 20 | + | |
| 21 | +const CommandCreateHandle = CommandDialogPrimitive.createHandle; | |
| 22 | + | |
| 23 | +function CommandDialogTrigger(props: CommandDialogPrimitive.Trigger.Props) { | |
| 24 | + return ( | |
| 25 | + <CommandDialogPrimitive.Trigger | |
| 26 | + data-slot="command-dialog-trigger" | |
| 27 | + {...props} | |
| 28 | + /> | |
| 29 | + ); | |
| 30 | +} | |
| 31 | + | |
| 32 | +function CommandDialogBackdrop({ | |
| 33 | + className, | |
| 34 | + ...props | |
| 35 | +}: CommandDialogPrimitive.Backdrop.Props) { | |
| 36 | + return ( | |
| 37 | + <CommandDialogPrimitive.Backdrop | |
| 38 | + className={cn( | |
| 39 | + "fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0", | |
| 40 | + className, | |
| 41 | + )} | |
| 42 | + data-slot="command-dialog-backdrop" | |
| 43 | + {...props} | |
| 44 | + /> | |
| 45 | + ); | |
| 46 | +} | |
| 47 | + | |
| 48 | +function CommandDialogViewport({ | |
| 49 | + className, | |
| 50 | + ...props | |
| 51 | +}: CommandDialogPrimitive.Viewport.Props) { | |
| 52 | + return ( | |
| 53 | + <CommandDialogPrimitive.Viewport | |
| 54 | + className={cn( | |
| 55 | + "fixed inset-0 z-50 flex flex-col items-center px-4 py-[max(--spacing(4),4vh)] sm:py-[10vh]", | |
| 56 | + className, | |
| 57 | + )} | |
| 58 | + data-slot="command-dialog-viewport" | |
| 59 | + {...props} | |
| 60 | + /> | |
| 61 | + ); | |
| 62 | +} | |
| 63 | + | |
| 64 | +function CommandDialogPopup({ | |
| 65 | + className, | |
| 66 | + children, | |
| 67 | + ...props | |
| 68 | +}: CommandDialogPrimitive.Popup.Props) { | |
| 69 | + return ( | |
| 70 | + <CommandDialogPortal> | |
| 71 | + <CommandDialogBackdrop /> | |
| 72 | + <CommandDialogViewport> | |
| 73 | + <CommandDialogPrimitive.Popup | |
| 74 | + className={cn( | |
| 75 | + "-translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-105 min-h-0 w-full min-w-0 max-w-xl scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border bg-popover not-dark:bg-clip-padding text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 outline-none transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:bg-muted/72 before:shadow-[0_1px_--theme(--color-black/4%)] data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 **:data-[slot=scroll-area-viewport]:data-has-overflow-y:pe-1 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 76 | + className, | |
| 77 | + )} | |
| 78 | + data-slot="command-dialog-popup" | |
| 79 | + {...props} | |
| 80 | + > | |
| 81 | + {children} | |
| 82 | + </CommandDialogPrimitive.Popup> | |
| 83 | + </CommandDialogViewport> | |
| 84 | + </CommandDialogPortal> | |
| 85 | + ); | |
| 86 | +} | |
| 87 | + | |
| 88 | +function Command({ | |
| 89 | + autoHighlight = "always", | |
| 90 | + keepHighlight = true, | |
| 91 | + ...props | |
| 92 | +}: React.ComponentProps<typeof Autocomplete>) { | |
| 93 | + return ( | |
| 94 | + <Autocomplete | |
| 95 | + autoHighlight={autoHighlight} | |
| 96 | + inline | |
| 97 | + keepHighlight={keepHighlight} | |
| 98 | + open | |
| 99 | + {...props} | |
| 100 | + /> | |
| 101 | + ); | |
| 102 | +} | |
| 103 | + | |
| 104 | +function CommandInput({ | |
| 105 | + className, | |
| 106 | + placeholder = undefined, | |
| 107 | + ...props | |
| 108 | +}: React.ComponentProps<typeof AutocompleteInput>) { | |
| 109 | + return ( | |
| 110 | + <div className="px-2.5 py-1.5"> | |
| 111 | + <AutocompleteInput | |
| 112 | + autoFocus | |
| 113 | + className={cn( | |
| 114 | + "border-transparent! bg-transparent! shadow-none before:hidden has-focus-visible:ring-0", | |
| 115 | + className, | |
| 116 | + )} | |
| 117 | + placeholder={placeholder} | |
| 118 | + size="lg" | |
| 119 | + startAddon={<SearchIcon />} | |
| 120 | + {...props} | |
| 121 | + /> | |
| 122 | + </div> | |
| 123 | + ); | |
| 124 | +} | |
| 125 | + | |
| 126 | +function CommandList({ | |
| 127 | + className, | |
| 128 | + ...props | |
| 129 | +}: React.ComponentProps<typeof AutocompleteList>) { | |
| 130 | + return ( | |
| 131 | + <AutocompleteList | |
| 132 | + className={cn("not-empty:scroll-py-2 not-empty:p-2", className)} | |
| 133 | + data-slot="command-list" | |
| 134 | + {...props} | |
| 135 | + /> | |
| 136 | + ); | |
| 137 | +} | |
| 138 | + | |
| 139 | +function CommandEmpty({ | |
| 140 | + className, | |
| 141 | + ...props | |
| 142 | +}: React.ComponentProps<typeof AutocompleteEmpty>) { | |
| 143 | + return ( | |
| 144 | + <AutocompleteEmpty | |
| 145 | + className={cn("not-empty:py-6", className)} | |
| 146 | + data-slot="command-empty" | |
| 147 | + {...props} | |
| 148 | + /> | |
| 149 | + ); | |
| 150 | +} | |
| 151 | + | |
| 152 | +function CommandPanel({ className, ...props }: React.ComponentProps<"div">) { | |
| 153 | + return ( | |
| 154 | + <div | |
| 155 | + className="-mx-px not-has-[+[data-slot=command-footer]]:-mb-px relative min-h-0 rounded-t-xl not-has-[+[data-slot=command-footer]]:rounded-b-2xl border border-b-0 bg-popover bg-clip-padding shadow-xs/5 [clip-path:inset(0_1px)] not-has-[+[data-slot=command-footer]]:[clip-path:inset(0_1px_1px_1px_round_0_0_calc(var(--radius-2xl)-1px)_calc(var(--radius-2xl)-1px))] before:pointer-events-none before:absolute before:inset-0 before:rounded-t-[calc(var(--radius-xl)-1px)] **:data-[slot=scroll-area-scrollbar]:mt-2" | |
| 156 | + {...props} | |
| 157 | + /> | |
| 158 | + ); | |
| 159 | +} | |
| 160 | + | |
| 161 | +function CommandGroup({ | |
| 162 | + className, | |
| 163 | + ...props | |
| 164 | +}: React.ComponentProps<typeof AutocompleteGroup>) { | |
| 165 | + return ( | |
| 166 | + <AutocompleteGroup | |
| 167 | + className={className} | |
| 168 | + data-slot="command-group" | |
| 169 | + {...props} | |
| 170 | + /> | |
| 171 | + ); | |
| 172 | +} | |
| 173 | + | |
| 174 | +function CommandGroupLabel({ | |
| 175 | + className, | |
| 176 | + ...props | |
| 177 | +}: React.ComponentProps<typeof AutocompleteGroupLabel>) { | |
| 178 | + return ( | |
| 179 | + <AutocompleteGroupLabel | |
| 180 | + className={className} | |
| 181 | + data-slot="command-group-label" | |
| 182 | + {...props} | |
| 183 | + /> | |
| 184 | + ); | |
| 185 | +} | |
| 186 | + | |
| 187 | +function CommandCollection({ | |
| 188 | + ...props | |
| 189 | +}: React.ComponentProps<typeof AutocompleteCollection>) { | |
| 190 | + return <AutocompleteCollection data-slot="command-collection" {...props} />; | |
| 191 | +} | |
| 192 | + | |
| 193 | +function CommandItem({ | |
| 194 | + className, | |
| 195 | + ...props | |
| 196 | +}: React.ComponentProps<typeof AutocompleteItem>) { | |
| 197 | + return ( | |
| 198 | + <AutocompleteItem | |
| 199 | + className={cn("py-1.5", className)} | |
| 200 | + data-slot="command-item" | |
| 201 | + {...props} | |
| 202 | + /> | |
| 203 | + ); | |
| 204 | +} | |
| 205 | + | |
| 206 | +function CommandSeparator({ | |
| 207 | + className, | |
| 208 | + ...props | |
| 209 | +}: React.ComponentProps<typeof AutocompleteSeparator>) { | |
| 210 | + return ( | |
| 211 | + <AutocompleteSeparator | |
| 212 | + className={cn("my-2", className)} | |
| 213 | + data-slot="command-separator" | |
| 214 | + {...props} | |
| 215 | + /> | |
| 216 | + ); | |
| 217 | +} | |
| 218 | + | |
| 219 | +function CommandShortcut({ className, ...props }: React.ComponentProps<"kbd">) { | |
| 220 | + return ( | |
| 221 | + <kbd | |
| 222 | + className={cn( | |
| 223 | + "ms-auto font-medium font-sans text-muted-foreground/72 text-xs tracking-widest", | |
| 224 | + className, | |
| 225 | + )} | |
| 226 | + data-slot="command-shortcut" | |
| 227 | + {...props} | |
| 228 | + /> | |
| 229 | + ); | |
| 230 | +} | |
| 231 | + | |
| 232 | +function CommandFooter({ className, ...props }: React.ComponentProps<"div">) { | |
| 233 | + return ( | |
| 234 | + <div | |
| 235 | + className={cn( | |
| 236 | + "flex items-center justify-between gap-2 rounded-b-[calc(var(--radius-2xl)-1px)] border-t px-5 py-3 text-muted-foreground text-xs", | |
| 237 | + className, | |
| 238 | + )} | |
| 239 | + data-slot="command-footer" | |
| 240 | + {...props} | |
| 241 | + /> | |
| 242 | + ); | |
| 243 | +} | |
| 244 | + | |
| 245 | +export { | |
| 246 | + CommandCreateHandle, | |
| 247 | + Command, | |
| 248 | + CommandCollection, | |
| 249 | + CommandDialog, | |
| 250 | + CommandDialogPopup, | |
| 251 | + CommandDialogTrigger, | |
| 252 | + CommandEmpty, | |
| 253 | + CommandFooter, | |
| 254 | + CommandGroup, | |
| 255 | + CommandGroupLabel, | |
| 256 | + CommandInput, | |
| 257 | + CommandItem, | |
| 258 | + CommandList, | |
| 259 | + CommandPanel, | |
| 260 | + CommandSeparator, | |
| 261 | + CommandShortcut, | |
| 262 | +}; | |
| @@ -0,0 +1,194 @@ | ||
| 1 | +import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"; | |
| 2 | +import { XIcon } from "lucide-react"; | |
| 3 | +import { Button } from "#/components/ui/button"; | |
| 4 | +import { ScrollArea } from "#/components/ui/scroll-area"; | |
| 5 | +import { cn } from "#/lib/utils"; | |
| 6 | + | |
| 7 | +const DialogCreateHandle = DialogPrimitive.createHandle; | |
| 8 | + | |
| 9 | +const Dialog = DialogPrimitive.Root; | |
| 10 | + | |
| 11 | +const DialogPortal = DialogPrimitive.Portal; | |
| 12 | + | |
| 13 | +function DialogTrigger(props: DialogPrimitive.Trigger.Props) { | |
| 14 | + return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; | |
| 15 | +} | |
| 16 | + | |
| 17 | +function DialogClose(props: DialogPrimitive.Close.Props) { | |
| 18 | + return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; | |
| 19 | +} | |
| 20 | + | |
| 21 | +function DialogBackdrop({ | |
| 22 | + className, | |
| 23 | + ...props | |
| 24 | +}: DialogPrimitive.Backdrop.Props) { | |
| 25 | + return ( | |
| 26 | + <DialogPrimitive.Backdrop | |
| 27 | + className={cn( | |
| 28 | + "fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0", | |
| 29 | + className, | |
| 30 | + )} | |
| 31 | + data-slot="dialog-backdrop" | |
| 32 | + {...props} | |
| 33 | + /> | |
| 34 | + ); | |
| 35 | +} | |
| 36 | + | |
| 37 | +function DialogViewport({ | |
| 38 | + className, | |
| 39 | + ...props | |
| 40 | +}: DialogPrimitive.Viewport.Props) { | |
| 41 | + return ( | |
| 42 | + <DialogPrimitive.Viewport | |
| 43 | + className={cn( | |
| 44 | + "fixed inset-0 z-50 grid grid-rows-[1fr_auto_3fr] justify-items-center p-4", | |
| 45 | + className, | |
| 46 | + )} | |
| 47 | + data-slot="dialog-viewport" | |
| 48 | + {...props} | |
| 49 | + /> | |
| 50 | + ); | |
| 51 | +} | |
| 52 | + | |
| 53 | +function DialogPopup({ | |
| 54 | + className, | |
| 55 | + children, | |
| 56 | + showCloseButton = true, | |
| 57 | + bottomStickOnMobile = true, | |
| 58 | + ...props | |
| 59 | +}: DialogPrimitive.Popup.Props & { | |
| 60 | + showCloseButton?: boolean; | |
| 61 | + bottomStickOnMobile?: boolean; | |
| 62 | +}) { | |
| 63 | + return ( | |
| 64 | + <DialogPortal> | |
| 65 | + <DialogBackdrop /> | |
| 66 | + <DialogViewport | |
| 67 | + className={cn( | |
| 68 | + bottomStickOnMobile && | |
| 69 | + "max-sm:grid-rows-[1fr_auto] max-sm:p-0 max-sm:pt-12", | |
| 70 | + )} | |
| 71 | + > | |
| 72 | + <DialogPrimitive.Popup | |
| 73 | + className={cn( | |
| 74 | + "-translate-y-[calc(1.25rem*var(--nested-dialogs))] relative row-start-2 flex max-h-full min-h-0 w-full min-w-0 max-w-lg scale-[calc(1-0.1*var(--nested-dialogs))] flex-col rounded-2xl border bg-popover not-dark:bg-clip-padding text-popover-foreground opacity-[calc(1-0.1*var(--nested-dialogs))] shadow-lg/5 transition-[scale,opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-2xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-nested:data-ending-style:translate-y-8 data-nested:data-starting-style:translate-y-8 data-nested-dialog-open:origin-top data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 75 | + bottomStickOnMobile && | |
| 76 | + "max-sm:max-w-none max-sm:rounded-none max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:opacity-[calc(1-min(var(--nested-dialogs),1))] max-sm:data-ending-style:translate-y-4 max-sm:data-starting-style:translate-y-4 max-sm:before:hidden max-sm:before:rounded-none", | |
| 77 | + className, | |
| 78 | + )} | |
| 79 | + data-slot="dialog-popup" | |
| 80 | + {...props} | |
| 81 | + > | |
| 82 | + {children} | |
| 83 | + {showCloseButton && ( | |
| 84 | + <DialogPrimitive.Close | |
| 85 | + aria-label="Close" | |
| 86 | + className="absolute end-2 top-2" | |
| 87 | + render={<Button size="icon" variant="ghost" />} | |
| 88 | + > | |
| 89 | + <XIcon /> | |
| 90 | + </DialogPrimitive.Close> | |
| 91 | + )} | |
| 92 | + </DialogPrimitive.Popup> | |
| 93 | + </DialogViewport> | |
| 94 | + </DialogPortal> | |
| 95 | + ); | |
| 96 | +} | |
| 97 | + | |
| 98 | +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { | |
| 99 | + return ( | |
| 100 | + <div | |
| 101 | + className={cn( | |
| 102 | + "flex flex-col gap-2 p-6 in-[[data-slot=dialog-popup]:has([data-slot=dialog-panel])]:pb-3 max-sm:pb-4", | |
| 103 | + className, | |
| 104 | + )} | |
| 105 | + data-slot="dialog-header" | |
| 106 | + {...props} | |
| 107 | + /> | |
| 108 | + ); | |
| 109 | +} | |
| 110 | + | |
| 111 | +function DialogFooter({ | |
| 112 | + className, | |
| 113 | + variant = "default", | |
| 114 | + ...props | |
| 115 | +}: React.ComponentProps<"div"> & { | |
| 116 | + variant?: "default" | "bare"; | |
| 117 | +}) { | |
| 118 | + return ( | |
| 119 | + <div | |
| 120 | + className={cn( | |
| 121 | + "flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end sm:rounded-b-[calc(var(--radius-2xl)-1px)]", | |
| 122 | + variant === "default" && "border-t bg-muted/72 py-4", | |
| 123 | + variant === "bare" && | |
| 124 | + "in-[[data-slot=dialog-popup]:has([data-slot=dialog-panel])]:pt-3 pt-4 pb-6", | |
| 125 | + className, | |
| 126 | + )} | |
| 127 | + data-slot="dialog-footer" | |
| 128 | + {...props} | |
| 129 | + /> | |
| 130 | + ); | |
| 131 | +} | |
| 132 | + | |
| 133 | +function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { | |
| 134 | + return ( | |
| 135 | + <DialogPrimitive.Title | |
| 136 | + className={cn( | |
| 137 | + "font-heading font-semibold text-xl leading-none", | |
| 138 | + className, | |
| 139 | + )} | |
| 140 | + data-slot="dialog-title" | |
| 141 | + {...props} | |
| 142 | + /> | |
| 143 | + ); | |
| 144 | +} | |
| 145 | + | |
| 146 | +function DialogDescription({ | |
| 147 | + className, | |
| 148 | + ...props | |
| 149 | +}: DialogPrimitive.Description.Props) { | |
| 150 | + return ( | |
| 151 | + <DialogPrimitive.Description | |
| 152 | + className={cn("text-muted-foreground text-sm", className)} | |
| 153 | + data-slot="dialog-description" | |
| 154 | + {...props} | |
| 155 | + /> | |
| 156 | + ); | |
| 157 | +} | |
| 158 | + | |
| 159 | +function DialogPanel({ | |
| 160 | + className, | |
| 161 | + scrollFade = true, | |
| 162 | + ...props | |
| 163 | +}: React.ComponentProps<"div"> & { scrollFade?: boolean }) { | |
| 164 | + return ( | |
| 165 | + <ScrollArea scrollFade={scrollFade}> | |
| 166 | + <div | |
| 167 | + className={cn( | |
| 168 | + "p-6 in-[[data-slot=dialog-popup]:has([data-slot=dialog-header])]:pt-1 in-[[data-slot=dialog-popup]:has([data-slot=dialog-footer]:not(.border-t))]:pb-1", | |
| 169 | + className, | |
| 170 | + )} | |
| 171 | + data-slot="dialog-panel" | |
| 172 | + {...props} | |
| 173 | + /> | |
| 174 | + </ScrollArea> | |
| 175 | + ); | |
| 176 | +} | |
| 177 | + | |
| 178 | +export { | |
| 179 | + DialogCreateHandle, | |
| 180 | + Dialog, | |
| 181 | + DialogTrigger, | |
| 182 | + DialogPortal, | |
| 183 | + DialogClose, | |
| 184 | + DialogBackdrop, | |
| 185 | + DialogBackdrop as DialogOverlay, | |
| 186 | + DialogPopup, | |
| 187 | + DialogPopup as DialogContent, | |
| 188 | + DialogHeader, | |
| 189 | + DialogFooter, | |
| 190 | + DialogTitle, | |
| 191 | + DialogDescription, | |
| 192 | + DialogPanel, | |
| 193 | + DialogViewport, | |
| 194 | +}; | |
| @@ -0,0 +1,127 @@ | ||
| 1 | +import { type VariantProps, cva } from "class-variance-authority"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +function Empty({ className, ...props }: React.ComponentProps<"div">) { | |
| 6 | + return ( | |
| 7 | + <div | |
| 8 | + className={cn( | |
| 9 | + "flex min-w-0 flex-1 flex-col items-center justify-center gap-6 text-balance p-6 text-center md:p-12", | |
| 10 | + className, | |
| 11 | + )} | |
| 12 | + data-slot="empty" | |
| 13 | + {...props} | |
| 14 | + /> | |
| 15 | + ); | |
| 16 | +} | |
| 17 | + | |
| 18 | +function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { | |
| 19 | + return ( | |
| 20 | + <div | |
| 21 | + className={cn( | |
| 22 | + "flex max-w-sm flex-col items-center text-center", | |
| 23 | + className, | |
| 24 | + )} | |
| 25 | + data-slot="empty-header" | |
| 26 | + {...props} | |
| 27 | + /> | |
| 28 | + ); | |
| 29 | +} | |
| 30 | + | |
| 31 | +const emptyMediaVariants = cva( | |
| 32 | + "flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", | |
| 33 | + { | |
| 34 | + defaultVariants: { | |
| 35 | + variant: "default", | |
| 36 | + }, | |
| 37 | + variants: { | |
| 38 | + variant: { | |
| 39 | + default: "bg-transparent", | |
| 40 | + icon: "relative flex size-9 shrink-0 items-center justify-center rounded-md border bg-card not-dark:bg-clip-padding text-foreground shadow-sm/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-md)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='size-'])]:size-4.5", | |
| 41 | + }, | |
| 42 | + }, | |
| 43 | + }, | |
| 44 | +); | |
| 45 | + | |
| 46 | +function EmptyMedia({ | |
| 47 | + className, | |
| 48 | + variant = "default", | |
| 49 | + ...props | |
| 50 | +}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) { | |
| 51 | + return ( | |
| 52 | + <div | |
| 53 | + className={cn("relative mb-6", className)} | |
| 54 | + data-slot="empty-media" | |
| 55 | + data-variant={variant} | |
| 56 | + {...props} | |
| 57 | + > | |
| 58 | + {variant === "icon" && ( | |
| 59 | + <> | |
| 60 | + <div | |
| 61 | + aria-hidden="true" | |
| 62 | + className={cn( | |
| 63 | + emptyMediaVariants({ className, variant }), | |
| 64 | + "-translate-x-0.5 -rotate-10 pointer-events-none absolute bottom-px origin-bottom-left scale-84 shadow-none", | |
| 65 | + )} | |
| 66 | + /> | |
| 67 | + <div | |
| 68 | + aria-hidden="true" | |
| 69 | + className={cn( | |
| 70 | + emptyMediaVariants({ className, variant }), | |
| 71 | + "pointer-events-none absolute bottom-px origin-bottom-right translate-x-0.5 rotate-10 scale-84 shadow-none", | |
| 72 | + )} | |
| 73 | + /> | |
| 74 | + </> | |
| 75 | + )} | |
| 76 | + <div | |
| 77 | + className={cn(emptyMediaVariants({ className, variant }))} | |
| 78 | + {...props} | |
| 79 | + /> | |
| 80 | + </div> | |
| 81 | + ); | |
| 82 | +} | |
| 83 | + | |
| 84 | +function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { | |
| 85 | + return ( | |
| 86 | + <div | |
| 87 | + className={cn("font-heading font-semibold text-xl", className)} | |
| 88 | + data-slot="empty-title" | |
| 89 | + {...props} | |
| 90 | + /> | |
| 91 | + ); | |
| 92 | +} | |
| 93 | + | |
| 94 | +function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) { | |
| 95 | + return ( | |
| 96 | + <div | |
| 97 | + className={cn( | |
| 98 | + "text-muted-foreground text-sm [&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4 [[data-slot=empty-title]+&]:mt-1", | |
| 99 | + className, | |
| 100 | + )} | |
| 101 | + data-slot="empty-description" | |
| 102 | + {...props} | |
| 103 | + /> | |
| 104 | + ); | |
| 105 | +} | |
| 106 | + | |
| 107 | +function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { | |
| 108 | + return ( | |
| 109 | + <div | |
| 110 | + className={cn( | |
| 111 | + "flex w-full min-w-0 max-w-sm flex-col items-center gap-4 text-balance text-sm", | |
| 112 | + className, | |
| 113 | + )} | |
| 114 | + data-slot="empty-content" | |
| 115 | + {...props} | |
| 116 | + /> | |
| 117 | + ); | |
| 118 | +} | |
| 119 | + | |
| 120 | +export { | |
| 121 | + Empty, | |
| 122 | + EmptyHeader, | |
| 123 | + EmptyTitle, | |
| 124 | + EmptyDescription, | |
| 125 | + EmptyContent, | |
| 126 | + EmptyMedia, | |
| 127 | +}; | |
| @@ -0,0 +1,72 @@ | ||
| 1 | +import { Field as FieldPrimitive } from "@base-ui/react/field"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +function Field({ className, ...props }: FieldPrimitive.Root.Props) { | |
| 6 | + return ( | |
| 7 | + <FieldPrimitive.Root | |
| 8 | + className={cn("flex flex-col items-start gap-2", className)} | |
| 9 | + data-slot="field" | |
| 10 | + {...props} | |
| 11 | + /> | |
| 12 | + ); | |
| 13 | +} | |
| 14 | + | |
| 15 | +function FieldLabel({ className, ...props }: FieldPrimitive.Label.Props) { | |
| 16 | + return ( | |
| 17 | + <FieldPrimitive.Label | |
| 18 | + className={cn( | |
| 19 | + "inline-flex items-center gap-2 font-medium text-base/4.5 text-foreground sm:text-sm/4", | |
| 20 | + className, | |
| 21 | + )} | |
| 22 | + data-slot="field-label" | |
| 23 | + {...props} | |
| 24 | + /> | |
| 25 | + ); | |
| 26 | +} | |
| 27 | + | |
| 28 | +function FieldItem({ className, ...props }: FieldPrimitive.Item.Props) { | |
| 29 | + return ( | |
| 30 | + <FieldPrimitive.Item | |
| 31 | + className={cn("flex", className)} | |
| 32 | + data-slot="field-item" | |
| 33 | + {...props} | |
| 34 | + /> | |
| 35 | + ); | |
| 36 | +} | |
| 37 | + | |
| 38 | +function FieldDescription({ | |
| 39 | + className, | |
| 40 | + ...props | |
| 41 | +}: FieldPrimitive.Description.Props) { | |
| 42 | + return ( | |
| 43 | + <FieldPrimitive.Description | |
| 44 | + className={cn("text-muted-foreground text-xs", className)} | |
| 45 | + data-slot="field-description" | |
| 46 | + {...props} | |
| 47 | + /> | |
| 48 | + ); | |
| 49 | +} | |
| 50 | + | |
| 51 | +function FieldError({ className, ...props }: FieldPrimitive.Error.Props) { | |
| 52 | + return ( | |
| 53 | + <FieldPrimitive.Error | |
| 54 | + className={cn("text-destructive-foreground text-xs", className)} | |
| 55 | + data-slot="field-error" | |
| 56 | + {...props} | |
| 57 | + /> | |
| 58 | + ); | |
| 59 | +} | |
| 60 | + | |
| 61 | +const FieldControl = FieldPrimitive.Control; | |
| 62 | +const FieldValidity = FieldPrimitive.Validity; | |
| 63 | + | |
| 64 | +export { | |
| 65 | + Field, | |
| 66 | + FieldLabel, | |
| 67 | + FieldDescription, | |
| 68 | + FieldError, | |
| 69 | + FieldControl, | |
| 70 | + FieldItem, | |
| 71 | + FieldValidity, | |
| 72 | +}; | |
| @@ -0,0 +1,27 @@ | ||
| 1 | +import { Fieldset as FieldsetPrimitive } from "@base-ui/react/fieldset"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +function Fieldset({ className, ...props }: FieldsetPrimitive.Root.Props) { | |
| 6 | + return ( | |
| 7 | + <FieldsetPrimitive.Root | |
| 8 | + className={cn("flex w-full max-w-64 flex-col gap-6", className)} | |
| 9 | + data-slot="fieldset" | |
| 10 | + {...props} | |
| 11 | + /> | |
| 12 | + ); | |
| 13 | +} | |
| 14 | +function FieldsetLegend({ | |
| 15 | + className, | |
| 16 | + ...props | |
| 17 | +}: FieldsetPrimitive.Legend.Props) { | |
| 18 | + return ( | |
| 19 | + <FieldsetPrimitive.Legend | |
| 20 | + className={cn("font-semibold text-foreground", className)} | |
| 21 | + data-slot="fieldset-legend" | |
| 22 | + {...props} | |
| 23 | + /> | |
| 24 | + ); | |
| 25 | +} | |
| 26 | + | |
| 27 | +export { Fieldset, FieldsetLegend }; | |
| @@ -0,0 +1,15 @@ | ||
| 1 | +import { Form as FormPrimitive } from "@base-ui/react/form"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +function Form({ className, ...props }: FormPrimitive.Props) { | |
| 6 | + return ( | |
| 7 | + <FormPrimitive | |
| 8 | + className={cn("flex w-full flex-col gap-4", className)} | |
| 9 | + data-slot="form" | |
| 10 | + {...props} | |
| 11 | + /> | |
| 12 | + ); | |
| 13 | +} | |
| 14 | + | |
| 15 | +export { Form }; | |
| @@ -0,0 +1,99 @@ | ||
| 1 | +import { type VariantProps, cva } from "class-variance-authority"; | |
| 2 | +import type * as React from "react"; | |
| 3 | + | |
| 4 | +import { Input, type InputProps } from "#/components/ui/input"; | |
| 5 | +import { Textarea, type TextareaProps } from "#/components/ui/textarea"; | |
| 6 | +import { cn } from "#/lib/utils"; | |
| 7 | + | |
| 8 | +function InputGroup({ className, ...props }: React.ComponentProps<"div">) { | |
| 9 | + return ( | |
| 10 | + <div | |
| 11 | + className={cn( | |
| 12 | + "relative inline-flex w-full min-w-0 items-center rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_1px_--theme(--color-black/4%)] has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/64 has-[input:focus-visible,textarea:focus-visible]:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/16 has-[textarea]:h-auto has-data-[align=block-end]:h-auto has-data-[align=block-start]:h-auto has-data-[align=block-end]:flex-col has-data-[align=block-start]:flex-col has-[input:focus-visible,textarea:focus-visible]:border-ring has-[input[aria-invalid],textarea[aria-invalid]]:border-destructive/36 has-autofill:bg-foreground/4 has-[input:disabled,textarea:disabled]:opacity-64 has-[input:disabled,textarea:disabled,input:focus-visible,textarea:focus-visible,input[aria-invalid],textarea[aria-invalid]]:shadow-none has-[input:focus-visible,textarea:focus-visible]:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-[input[aria-invalid],textarea[aria-invalid]]:ring-destructive/24 dark:not-has-[input:disabled,textarea:disabled]:not-has-[input:focus-visible,textarea:focus-visible]:not-has-[input[aria-invalid],textarea[aria-invalid]]:before:shadow-[0_-1px_--theme(--color-white/6%)] has-data-[align=inline-start]:**:[[data-size=sm]_input]:ps-1.5 has-data-[align=inline-end]:**:[[data-size=sm]_input]:pe-1.5 *:[[data-slot=input-control],[data-slot=textarea-control]]:contents *:[[data-slot=input-control],[data-slot=textarea-control]]:before:hidden has-[[data-align=block-start],[data-align=block-end]]:**:[input]:h-auto has-data-[align=inline-start]:**:[input]:ps-2 has-data-[align=inline-end]:**:[input]:pe-2 has-data-[align=block-end]:**:[input]:pt-1.5 has-data-[align=block-start]:**:[input]:pb-1.5 **:[textarea]:min-h-20.5 **:[textarea]:resize-none **:[textarea]:py-[calc(--spacing(3)-1px)] **:[textarea]:max-sm:min-h-23.5 **:[textarea_button]:rounded-[calc(var(--radius-md)-1px)]", | |
| 13 | + className, | |
| 14 | + )} | |
| 15 | + data-slot="input-group" | |
| 16 | + role="group" | |
| 17 | + {...props} | |
| 18 | + /> | |
| 19 | + ); | |
| 20 | +} | |
| 21 | + | |
| 22 | +const inputGroupAddonVariants = cva( | |
| 23 | + "[&_svg]:-mx-0.5 flex h-auto cursor-text select-none items-center justify-center gap-2 leading-none [&>kbd]:rounded-[calc(var(--radius)-5px)] in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 not-has-[button]:**:[svg:not([class*='opacity-'])]:opacity-80", | |
| 24 | + { | |
| 25 | + defaultVariants: { | |
| 26 | + align: "inline-start", | |
| 27 | + }, | |
| 28 | + variants: { | |
| 29 | + align: { | |
| 30 | + "block-end": | |
| 31 | + "order-last w-full justify-start px-[calc(--spacing(3)-1px)] pb-[calc(--spacing(3)-1px)] [.border-t]:pt-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]", | |
| 32 | + "block-start": | |
| 33 | + "order-first w-full justify-start px-[calc(--spacing(3)-1px)] pt-[calc(--spacing(3)-1px)] [.border-b]:pb-[calc(--spacing(3)-1px)] [[data-size=sm]+&]:px-[calc(--spacing(2.5)-1px)]", | |
| 34 | + "inline-end": | |
| 35 | + "has-[>:last-child[data-slot=badge]]:-me-1.5 has-[>button]:-me-2 order-last pe-[calc(--spacing(3)-1px)] has-[>kbd:last-child]:me-[-0.35rem] [[data-size=sm]+&]:pe-[calc(--spacing(2.5)-1px)]", | |
| 36 | + "inline-start": | |
| 37 | + "has-[>:last-child[data-slot=badge]]:-ms-1.5 has-[>button]:-ms-2 order-first ps-[calc(--spacing(3)-1px)] has-[>kbd:last-child]:ms-[-0.35rem] [[data-size=sm]+&]:ps-[calc(--spacing(2.5)-1px)]", | |
| 38 | + }, | |
| 39 | + }, | |
| 40 | + }, | |
| 41 | +); | |
| 42 | + | |
| 43 | +function InputGroupAddon({ | |
| 44 | + className, | |
| 45 | + align = "inline-start", | |
| 46 | + ...props | |
| 47 | +}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) { | |
| 48 | + return ( | |
| 49 | + <div | |
| 50 | + className={cn(inputGroupAddonVariants({ align }), className)} | |
| 51 | + data-align={align} | |
| 52 | + data-slot="input-group-addon" | |
| 53 | + onMouseDown={(e) => { | |
| 54 | + const target = e.target as HTMLElement; | |
| 55 | + const isInteractive = target.closest( | |
| 56 | + "button, a, input, select, textarea, [role='button'], [role='combobox'], [role='listbox'], [data-slot='select-trigger']", | |
| 57 | + ); | |
| 58 | + if (isInteractive) return; | |
| 59 | + e.preventDefault(); | |
| 60 | + const parent = e.currentTarget.parentElement; | |
| 61 | + const input = parent?.querySelector< | |
| 62 | + HTMLInputElement | HTMLTextAreaElement | |
| 63 | + >("input, textarea"); | |
| 64 | + if (input && !parent?.querySelector("input:focus, textarea:focus")) { | |
| 65 | + input.focus(); | |
| 66 | + } | |
| 67 | + }} | |
| 68 | + {...props} | |
| 69 | + /> | |
| 70 | + ); | |
| 71 | +} | |
| 72 | + | |
| 73 | +function InputGroupText({ className, ...props }: React.ComponentProps<"span">) { | |
| 74 | + return ( | |
| 75 | + <span | |
| 76 | + className={cn( | |
| 77 | + "[&_svg]:-mx-0.5 line-clamp-1 flex items-center gap-2 text-muted-foreground leading-none in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4.5 sm:in-[[data-slot=input-group]:has([data-slot=input-control],[data-slot=textarea-control])]:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none", | |
| 78 | + className, | |
| 79 | + )} | |
| 80 | + {...props} | |
| 81 | + /> | |
| 82 | + ); | |
| 83 | +} | |
| 84 | + | |
| 85 | +function InputGroupInput({ className, ...props }: InputProps) { | |
| 86 | + return <Input className={className} unstyled {...props} />; | |
| 87 | +} | |
| 88 | + | |
| 89 | +function InputGroupTextarea({ className, ...props }: TextareaProps) { | |
| 90 | + return <Textarea className={className} unstyled {...props} />; | |
| 91 | +} | |
| 92 | + | |
| 93 | +export { | |
| 94 | + InputGroup, | |
| 95 | + InputGroupAddon, | |
| 96 | + InputGroupText, | |
| 97 | + InputGroupInput, | |
| 98 | + InputGroupTextarea, | |
| 99 | +}; | |
| @@ -0,0 +1,64 @@ | ||
| 1 | +import { Input as InputPrimitive } from "@base-ui/react/input"; | |
| 2 | +import type * as React from "react"; | |
| 3 | + | |
| 4 | +import { cn } from "#/lib/utils"; | |
| 5 | + | |
| 6 | +type InputProps = Omit< | |
| 7 | + InputPrimitive.Props & React.RefAttributes<HTMLInputElement>, | |
| 8 | + "size" | |
| 9 | +> & { | |
| 10 | + size?: "sm" | "default" | "lg" | number; | |
| 11 | + unstyled?: boolean; | |
| 12 | + nativeInput?: boolean; | |
| 13 | +}; | |
| 14 | + | |
| 15 | +function Input({ | |
| 16 | + className, | |
| 17 | + size = "default", | |
| 18 | + unstyled = false, | |
| 19 | + nativeInput = false, | |
| 20 | + ...props | |
| 21 | +}: InputProps) { | |
| 22 | + const inputClassName = cn( | |
| 23 | + "h-8.5 w-full min-w-0 rounded-[inherit] px-[calc(--spacing(3)-1px)] leading-8.5 outline-none placeholder:text-muted-foreground/72 sm:h-7.5 sm:leading-7.5 [transition:background-color_5000000s_ease-in-out_0s]", | |
| 24 | + size === "sm" && | |
| 25 | + "h-7.5 px-[calc(--spacing(2.5)-1px)] leading-7.5 sm:h-6.5 sm:leading-6.5", | |
| 26 | + size === "lg" && "h-9.5 leading-9.5 sm:h-8.5 sm:leading-8.5", | |
| 27 | + props.type === "search" && | |
| 28 | + "[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none", | |
| 29 | + props.type === "file" && | |
| 30 | + "text-muted-foreground file:me-3 file:bg-transparent file:font-medium file:text-foreground file:text-sm", | |
| 31 | + ); | |
| 32 | + | |
| 33 | + return ( | |
| 34 | + <span | |
| 35 | + className={ | |
| 36 | + cn( | |
| 37 | + !unstyled && | |
| 38 | + "relative inline-flex w-full rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-autofill:bg-foreground/4 has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:not-has-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 39 | + className, | |
| 40 | + ) || undefined | |
| 41 | + } | |
| 42 | + data-size={size} | |
| 43 | + data-slot="input-control" | |
| 44 | + > | |
| 45 | + {nativeInput ? ( | |
| 46 | + <input | |
| 47 | + className={inputClassName} | |
| 48 | + data-slot="input" | |
| 49 | + size={typeof size === "number" ? size : undefined} | |
| 50 | + {...props} | |
| 51 | + /> | |
| 52 | + ) : ( | |
| 53 | + <InputPrimitive | |
| 54 | + className={inputClassName} | |
| 55 | + data-slot="input" | |
| 56 | + size={typeof size === "number" ? size : undefined} | |
| 57 | + {...props} | |
| 58 | + /> | |
| 59 | + )} | |
| 60 | + </span> | |
| 61 | + ); | |
| 62 | +} | |
| 63 | + | |
| 64 | +export { Input, type InputProps }; | |
| @@ -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 }; | |
| @@ -0,0 +1,26 @@ | ||
| 1 | +import { mergeProps } from "@base-ui/react/merge-props"; | |
| 2 | +import { useRender } from "@base-ui/react/use-render"; | |
| 3 | + | |
| 4 | +import { cn } from "#/lib/utils"; | |
| 5 | + | |
| 6 | +function Label({ | |
| 7 | + className, | |
| 8 | + render, | |
| 9 | + ...props | |
| 10 | +}: useRender.ComponentProps<"label">) { | |
| 11 | + const defaultProps = { | |
| 12 | + className: cn( | |
| 13 | + "inline-flex items-center gap-2 text-base/4.5 sm:text-sm/4 font-medium text-foreground", | |
| 14 | + className, | |
| 15 | + ), | |
| 16 | + "data-slot": "label", | |
| 17 | + }; | |
| 18 | + | |
| 19 | + return useRender({ | |
| 20 | + defaultTagName: "label", | |
| 21 | + props: mergeProps<"label">(defaultProps, props), | |
| 22 | + render, | |
| 23 | + }); | |
| 24 | +} | |
| 25 | + | |
| 26 | +export { Label }; | |
| @@ -0,0 +1,308 @@ | ||
| 1 | +import { Menu as MenuPrimitive } from "@base-ui/react/menu"; | |
| 2 | +import { ChevronRightIcon } from "lucide-react"; | |
| 3 | +import type * as React from "react"; | |
| 4 | +import { cn } from "#/lib/utils"; | |
| 5 | + | |
| 6 | +const MenuCreateHandle = MenuPrimitive.createHandle; | |
| 7 | + | |
| 8 | +const Menu = MenuPrimitive.Root; | |
| 9 | + | |
| 10 | +const MenuPortal = MenuPrimitive.Portal; | |
| 11 | + | |
| 12 | +function MenuTrigger(props: MenuPrimitive.Trigger.Props) { | |
| 13 | + return <MenuPrimitive.Trigger data-slot="menu-trigger" {...props} />; | |
| 14 | +} | |
| 15 | + | |
| 16 | +function MenuPopup({ | |
| 17 | + children, | |
| 18 | + className, | |
| 19 | + sideOffset = 4, | |
| 20 | + align = "center", | |
| 21 | + alignOffset, | |
| 22 | + side = "bottom", | |
| 23 | + ...props | |
| 24 | +}: MenuPrimitive.Popup.Props & { | |
| 25 | + align?: MenuPrimitive.Positioner.Props["align"]; | |
| 26 | + sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"]; | |
| 27 | + alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"]; | |
| 28 | + side?: MenuPrimitive.Positioner.Props["side"]; | |
| 29 | +}) { | |
| 30 | + return ( | |
| 31 | + <MenuPrimitive.Portal> | |
| 32 | + <MenuPrimitive.Positioner | |
| 33 | + align={align} | |
| 34 | + alignOffset={alignOffset} | |
| 35 | + className="z-50" | |
| 36 | + data-slot="menu-positioner" | |
| 37 | + side={side} | |
| 38 | + sideOffset={sideOffset} | |
| 39 | + > | |
| 40 | + <MenuPrimitive.Popup | |
| 41 | + className={cn( | |
| 42 | + "relative flex not-[class*='w-']:min-w-32 origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 outline-none before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] focus:outline-none dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 43 | + className, | |
| 44 | + )} | |
| 45 | + data-slot="menu-popup" | |
| 46 | + {...props} | |
| 47 | + > | |
| 48 | + <div className="max-h-(--available-height) w-full overflow-y-auto p-1"> | |
| 49 | + {children} | |
| 50 | + </div> | |
| 51 | + </MenuPrimitive.Popup> | |
| 52 | + </MenuPrimitive.Positioner> | |
| 53 | + </MenuPrimitive.Portal> | |
| 54 | + ); | |
| 55 | +} | |
| 56 | + | |
| 57 | +function MenuGroup(props: MenuPrimitive.Group.Props) { | |
| 58 | + return <MenuPrimitive.Group data-slot="menu-group" {...props} />; | |
| 59 | +} | |
| 60 | + | |
| 61 | +function MenuItem({ | |
| 62 | + className, | |
| 63 | + inset, | |
| 64 | + variant = "default", | |
| 65 | + ...props | |
| 66 | +}: MenuPrimitive.Item.Props & { | |
| 67 | + inset?: boolean; | |
| 68 | + variant?: "default" | "destructive"; | |
| 69 | +}) { | |
| 70 | + return ( | |
| 71 | + <MenuPrimitive.Item | |
| 72 | + className={cn( | |
| 73 | + "[&>svg]:-mx-0.5 flex min-h-8 cursor-default select-none items-center gap-2 rounded-sm px-2 py-1 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-inset:ps-8 data-[variant=destructive]:text-destructive-foreground data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&>svg:not([class*='opacity-'])]:opacity-80 [&>svg:not([class*='size-'])]:size-4.5 sm:[&>svg:not([class*='size-'])]:size-4 [&>svg]:pointer-events-none [&>svg]:shrink-0", | |
| 74 | + className, | |
| 75 | + )} | |
| 76 | + data-inset={inset} | |
| 77 | + data-slot="menu-item" | |
| 78 | + data-variant={variant} | |
| 79 | + {...props} | |
| 80 | + /> | |
| 81 | + ); | |
| 82 | +} | |
| 83 | + | |
| 84 | +function MenuCheckboxItem({ | |
| 85 | + className, | |
| 86 | + children, | |
| 87 | + checked, | |
| 88 | + variant = "default", | |
| 89 | + ...props | |
| 90 | +}: MenuPrimitive.CheckboxItem.Props & { | |
| 91 | + variant?: "default" | "switch"; | |
| 92 | +}) { | |
| 93 | + return ( | |
| 94 | + <MenuPrimitive.CheckboxItem | |
| 95 | + checked={checked} | |
| 96 | + className={cn( | |
| 97 | + "grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default items-center gap-2 rounded-sm py-1 ps-2 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", | |
| 98 | + variant === "switch" | |
| 99 | + ? "grid-cols-[1fr_auto] gap-4 pe-1.5" | |
| 100 | + : "grid-cols-[1rem_1fr] pe-4", | |
| 101 | + className, | |
| 102 | + )} | |
| 103 | + data-slot="menu-checkbox-item" | |
| 104 | + {...props} | |
| 105 | + > | |
| 106 | + {variant === "switch" ? ( | |
| 107 | + <> | |
| 108 | + <span className="col-start-1">{children}</span> | |
| 109 | + <MenuPrimitive.CheckboxItemIndicator | |
| 110 | + className="inset-shadow-[0_1px_--theme(--color-black/4%)] inline-flex h-[calc(var(--thumb-size)+2px)] w-[calc(var(--thumb-size)*2-2px)] shrink-0 items-center rounded-full p-px outline-none transition-[background-color,box-shadow] duration-200 [--thumb-size:--spacing(4)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-checked:bg-primary data-unchecked:bg-input data-disabled:opacity-64 sm:[--thumb-size:--spacing(3)]" | |
| 111 | + keepMounted | |
| 112 | + > | |
| 113 | + <span className="pointer-events-none block aspect-square h-full in-[[data-slot=menu-checkbox-item][data-checked]]:origin-[var(--thumb-size)_50%] origin-left in-[[data-slot=menu-checkbox-item][data-checked]]:translate-x-[calc(var(--thumb-size)-4px)] in-[[data-slot=menu-checkbox-item]:active]:not-data-disabled:scale-x-110 in-[[data-slot=menu-checkbox-item]:active]:rounded-[var(--thumb-size)/calc(var(--thumb-size)*1.10)] rounded-(--thumb-size) bg-background shadow-sm/5 will-change-transform [transition:translate_.15s,border-radius_.15s,scale_.1s_.1s,transform-origin_.15s]" /> | |
| 114 | + </MenuPrimitive.CheckboxItemIndicator> | |
| 115 | + </> | |
| 116 | + ) : ( | |
| 117 | + <> | |
| 118 | + <MenuPrimitive.CheckboxItemIndicator className="col-start-1"> | |
| 119 | + <svg | |
| 120 | + fill="none" | |
| 121 | + height="24" | |
| 122 | + stroke="currentColor" | |
| 123 | + strokeLinecap="round" | |
| 124 | + strokeLinejoin="round" | |
| 125 | + strokeWidth="2" | |
| 126 | + viewBox="0 0 24 24" | |
| 127 | + width="24" | |
| 128 | + xmlns="http://www.w3.org/2000/svg" | |
| 129 | + > | |
| 130 | + <path d="M5.252 12.7 10.2 18.63 18.748 5.37" /> | |
| 131 | + </svg> | |
| 132 | + </MenuPrimitive.CheckboxItemIndicator> | |
| 133 | + <span className="col-start-2">{children}</span> | |
| 134 | + </> | |
| 135 | + )} | |
| 136 | + </MenuPrimitive.CheckboxItem> | |
| 137 | + ); | |
| 138 | +} | |
| 139 | + | |
| 140 | +function MenuRadioGroup(props: MenuPrimitive.RadioGroup.Props) { | |
| 141 | + return <MenuPrimitive.RadioGroup data-slot="menu-radio-group" {...props} />; | |
| 142 | +} | |
| 143 | + | |
| 144 | +function MenuRadioItem({ | |
| 145 | + className, | |
| 146 | + children, | |
| 147 | + ...props | |
| 148 | +}: MenuPrimitive.RadioItem.Props) { | |
| 149 | + return ( | |
| 150 | + <MenuPrimitive.RadioItem | |
| 151 | + className={cn( | |
| 152 | + "grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", | |
| 153 | + className, | |
| 154 | + )} | |
| 155 | + data-slot="menu-radio-item" | |
| 156 | + {...props} | |
| 157 | + > | |
| 158 | + <MenuPrimitive.RadioItemIndicator className="col-start-1"> | |
| 159 | + <svg | |
| 160 | + fill="none" | |
| 161 | + height="24" | |
| 162 | + stroke="currentColor" | |
| 163 | + strokeLinecap="round" | |
| 164 | + strokeLinejoin="round" | |
| 165 | + strokeWidth="2" | |
| 166 | + viewBox="0 0 24 24" | |
| 167 | + width="24" | |
| 168 | + xmlns="http://www.w3.org/2000/svg" | |
| 169 | + > | |
| 170 | + <path d="M5.252 12.7 10.2 18.63 18.748 5.37" /> | |
| 171 | + </svg> | |
| 172 | + </MenuPrimitive.RadioItemIndicator> | |
| 173 | + <span className="col-start-2">{children}</span> | |
| 174 | + </MenuPrimitive.RadioItem> | |
| 175 | + ); | |
| 176 | +} | |
| 177 | + | |
| 178 | +function MenuGroupLabel({ | |
| 179 | + className, | |
| 180 | + inset, | |
| 181 | + ...props | |
| 182 | +}: MenuPrimitive.GroupLabel.Props & { | |
| 183 | + inset?: boolean; | |
| 184 | +}) { | |
| 185 | + return ( | |
| 186 | + <MenuPrimitive.GroupLabel | |
| 187 | + className={cn( | |
| 188 | + "px-2 py-1.5 font-medium text-muted-foreground text-xs data-inset:ps-9 sm:data-inset:ps-8", | |
| 189 | + className, | |
| 190 | + )} | |
| 191 | + data-inset={inset} | |
| 192 | + data-slot="menu-label" | |
| 193 | + {...props} | |
| 194 | + /> | |
| 195 | + ); | |
| 196 | +} | |
| 197 | + | |
| 198 | +function MenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) { | |
| 199 | + return ( | |
| 200 | + <MenuPrimitive.Separator | |
| 201 | + className={cn("mx-2 my-1 h-px bg-border", className)} | |
| 202 | + data-slot="menu-separator" | |
| 203 | + {...props} | |
| 204 | + /> | |
| 205 | + ); | |
| 206 | +} | |
| 207 | + | |
| 208 | +function MenuShortcut({ className, ...props }: React.ComponentProps<"kbd">) { | |
| 209 | + return ( | |
| 210 | + <kbd | |
| 211 | + className={cn( | |
| 212 | + "ms-auto font-medium font-sans text-muted-foreground/72 text-xs tracking-widest", | |
| 213 | + className, | |
| 214 | + )} | |
| 215 | + data-slot="menu-shortcut" | |
| 216 | + {...props} | |
| 217 | + /> | |
| 218 | + ); | |
| 219 | +} | |
| 220 | + | |
| 221 | +function MenuSub(props: MenuPrimitive.SubmenuRoot.Props) { | |
| 222 | + return <MenuPrimitive.SubmenuRoot data-slot="menu-sub" {...props} />; | |
| 223 | +} | |
| 224 | + | |
| 225 | +function MenuSubTrigger({ | |
| 226 | + className, | |
| 227 | + inset, | |
| 228 | + children, | |
| 229 | + ...props | |
| 230 | +}: MenuPrimitive.SubmenuTrigger.Props & { | |
| 231 | + inset?: boolean; | |
| 232 | +}) { | |
| 233 | + return ( | |
| 234 | + <MenuPrimitive.SubmenuTrigger | |
| 235 | + className={cn( | |
| 236 | + "flex min-h-8 items-center gap-2 rounded-sm px-2 py-1 text-base text-foreground outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-popup-open:bg-accent data-inset:ps-8 data-highlighted:text-accent-foreground data-popup-open:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none", | |
| 237 | + className, | |
| 238 | + )} | |
| 239 | + data-inset={inset} | |
| 240 | + data-slot="menu-sub-trigger" | |
| 241 | + {...props} | |
| 242 | + > | |
| 243 | + {children} | |
| 244 | + <ChevronRightIcon className="-me-0.5 ms-auto opacity-80" /> | |
| 245 | + </MenuPrimitive.SubmenuTrigger> | |
| 246 | + ); | |
| 247 | +} | |
| 248 | + | |
| 249 | +function MenuSubPopup({ | |
| 250 | + className, | |
| 251 | + sideOffset = 0, | |
| 252 | + alignOffset, | |
| 253 | + align = "start", | |
| 254 | + ...props | |
| 255 | +}: MenuPrimitive.Popup.Props & { | |
| 256 | + align?: MenuPrimitive.Positioner.Props["align"]; | |
| 257 | + sideOffset?: MenuPrimitive.Positioner.Props["sideOffset"]; | |
| 258 | + alignOffset?: MenuPrimitive.Positioner.Props["alignOffset"]; | |
| 259 | +}) { | |
| 260 | + const defaultAlignOffset = align !== "center" ? -5 : undefined; | |
| 261 | + | |
| 262 | + return ( | |
| 263 | + <MenuPopup | |
| 264 | + align={align} | |
| 265 | + alignOffset={alignOffset ?? defaultAlignOffset} | |
| 266 | + className={className} | |
| 267 | + data-slot="menu-sub-content" | |
| 268 | + side="inline-end" | |
| 269 | + sideOffset={sideOffset} | |
| 270 | + {...props} | |
| 271 | + /> | |
| 272 | + ); | |
| 273 | +} | |
| 274 | + | |
| 275 | +export { | |
| 276 | + MenuCreateHandle, | |
| 277 | + MenuCreateHandle as DropdownMenuCreateHandle, | |
| 278 | + Menu, | |
| 279 | + Menu as DropdownMenu, | |
| 280 | + MenuPortal, | |
| 281 | + MenuPortal as DropdownMenuPortal, | |
| 282 | + MenuTrigger, | |
| 283 | + MenuTrigger as DropdownMenuTrigger, | |
| 284 | + MenuPopup, | |
| 285 | + MenuPopup as DropdownMenuContent, | |
| 286 | + MenuGroup, | |
| 287 | + MenuGroup as DropdownMenuGroup, | |
| 288 | + MenuItem, | |
| 289 | + MenuItem as DropdownMenuItem, | |
| 290 | + MenuCheckboxItem, | |
| 291 | + MenuCheckboxItem as DropdownMenuCheckboxItem, | |
| 292 | + MenuRadioGroup, | |
| 293 | + MenuRadioGroup as DropdownMenuRadioGroup, | |
| 294 | + MenuRadioItem, | |
| 295 | + MenuRadioItem as DropdownMenuRadioItem, | |
| 296 | + MenuGroupLabel, | |
| 297 | + MenuGroupLabel as DropdownMenuLabel, | |
| 298 | + MenuSeparator, | |
| 299 | + MenuSeparator as DropdownMenuSeparator, | |
| 300 | + MenuShortcut, | |
| 301 | + MenuShortcut as DropdownMenuShortcut, | |
| 302 | + MenuSub, | |
| 303 | + MenuSub as DropdownMenuSub, | |
| 304 | + MenuSubTrigger, | |
| 305 | + MenuSubTrigger as DropdownMenuSubTrigger, | |
| 306 | + MenuSubPopup, | |
| 307 | + MenuSubPopup as DropdownMenuSubContent, | |
| 308 | +}; | |
| @@ -0,0 +1,102 @@ | ||
| 1 | +import { Popover as PopoverPrimitive } from "@base-ui/react/popover"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +const PopoverCreateHandle = PopoverPrimitive.createHandle; | |
| 6 | + | |
| 7 | +const Popover = PopoverPrimitive.Root; | |
| 8 | + | |
| 9 | +function PopoverTrigger(props: PopoverPrimitive.Trigger.Props) { | |
| 10 | + return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />; | |
| 11 | +} | |
| 12 | + | |
| 13 | +function PopoverPopup({ | |
| 14 | + children, | |
| 15 | + className, | |
| 16 | + side = "bottom", | |
| 17 | + align = "center", | |
| 18 | + sideOffset = 4, | |
| 19 | + alignOffset = 0, | |
| 20 | + tooltipStyle = false, | |
| 21 | + ...props | |
| 22 | +}: PopoverPrimitive.Popup.Props & { | |
| 23 | + side?: PopoverPrimitive.Positioner.Props["side"]; | |
| 24 | + align?: PopoverPrimitive.Positioner.Props["align"]; | |
| 25 | + sideOffset?: PopoverPrimitive.Positioner.Props["sideOffset"]; | |
| 26 | + alignOffset?: PopoverPrimitive.Positioner.Props["alignOffset"]; | |
| 27 | + tooltipStyle?: boolean; | |
| 28 | +}) { | |
| 29 | + return ( | |
| 30 | + <PopoverPrimitive.Portal> | |
| 31 | + <PopoverPrimitive.Positioner | |
| 32 | + align={align} | |
| 33 | + alignOffset={alignOffset} | |
| 34 | + className="z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom,transform] data-instant:transition-none" | |
| 35 | + data-slot="popover-positioner" | |
| 36 | + side={side} | |
| 37 | + sideOffset={sideOffset} | |
| 38 | + > | |
| 39 | + <PopoverPrimitive.Popup | |
| 40 | + className={cn( | |
| 41 | + "relative flex h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-lg border bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 transition-[width,height,scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] data-starting-style:scale-98 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 42 | + tooltipStyle && | |
| 43 | + "w-fit text-balance rounded-md text-xs shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]", | |
| 44 | + className, | |
| 45 | + )} | |
| 46 | + data-slot="popover-popup" | |
| 47 | + {...props} | |
| 48 | + > | |
| 49 | + <PopoverPrimitive.Viewport | |
| 50 | + className={cn( | |
| 51 | + "relative size-full max-h-(--available-height) overflow-clip px-(--viewport-inline-padding) py-4 outline-none [--viewport-inline-padding:--spacing(4)] data-instant:transition-none **:data-current:data-ending-style:opacity-0 **:data-current:data-starting-style:opacity-0 **:data-previous:data-ending-style:opacity-0 **:data-previous:data-starting-style:opacity-0 **:data-current:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-previous:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding)-2px)] **:data-current:opacity-100 **:data-previous:opacity-100 **:data-current:transition-opacity **:data-previous:transition-opacity", | |
| 52 | + tooltipStyle | |
| 53 | + ? "py-1 [--viewport-inline-padding:--spacing(2)]" | |
| 54 | + : "not-data-transitioning:overflow-y-auto", | |
| 55 | + )} | |
| 56 | + data-slot="popover-viewport" | |
| 57 | + > | |
| 58 | + {children} | |
| 59 | + </PopoverPrimitive.Viewport> | |
| 60 | + </PopoverPrimitive.Popup> | |
| 61 | + </PopoverPrimitive.Positioner> | |
| 62 | + </PopoverPrimitive.Portal> | |
| 63 | + ); | |
| 64 | +} | |
| 65 | + | |
| 66 | +function PopoverClose({ ...props }: PopoverPrimitive.Close.Props) { | |
| 67 | + return <PopoverPrimitive.Close data-slot="popover-close" {...props} />; | |
| 68 | +} | |
| 69 | + | |
| 70 | +function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) { | |
| 71 | + return ( | |
| 72 | + <PopoverPrimitive.Title | |
| 73 | + className={cn("font-semibold text-lg leading-none", className)} | |
| 74 | + data-slot="popover-title" | |
| 75 | + {...props} | |
| 76 | + /> | |
| 77 | + ); | |
| 78 | +} | |
| 79 | + | |
| 80 | +function PopoverDescription({ | |
| 81 | + className, | |
| 82 | + ...props | |
| 83 | +}: PopoverPrimitive.Description.Props) { | |
| 84 | + return ( | |
| 85 | + <PopoverPrimitive.Description | |
| 86 | + className={cn("text-muted-foreground text-sm", className)} | |
| 87 | + data-slot="popover-description" | |
| 88 | + {...props} | |
| 89 | + /> | |
| 90 | + ); | |
| 91 | +} | |
| 92 | + | |
| 93 | +export { | |
| 94 | + PopoverCreateHandle, | |
| 95 | + Popover, | |
| 96 | + PopoverTrigger, | |
| 97 | + PopoverPopup, | |
| 98 | + PopoverPopup as PopoverContent, | |
| 99 | + PopoverTitle, | |
| 100 | + PopoverDescription, | |
| 101 | + PopoverClose, | |
| 102 | +}; | |
| @@ -0,0 +1,79 @@ | ||
| 1 | +import { Progress as ProgressPrimitive } from "@base-ui/react/progress"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +function Progress({ | |
| 6 | + className, | |
| 7 | + children, | |
| 8 | + ...props | |
| 9 | +}: ProgressPrimitive.Root.Props) { | |
| 10 | + return ( | |
| 11 | + <ProgressPrimitive.Root | |
| 12 | + className={cn("flex w-full flex-col gap-2", className)} | |
| 13 | + data-slot="progress" | |
| 14 | + {...props} | |
| 15 | + > | |
| 16 | + {children ? ( | |
| 17 | + children | |
| 18 | + ) : ( | |
| 19 | + <ProgressTrack> | |
| 20 | + <ProgressIndicator /> | |
| 21 | + </ProgressTrack> | |
| 22 | + )} | |
| 23 | + </ProgressPrimitive.Root> | |
| 24 | + ); | |
| 25 | +} | |
| 26 | + | |
| 27 | +function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) { | |
| 28 | + return ( | |
| 29 | + <ProgressPrimitive.Label | |
| 30 | + className={cn("font-medium text-sm", className)} | |
| 31 | + data-slot="progress-label" | |
| 32 | + {...props} | |
| 33 | + /> | |
| 34 | + ); | |
| 35 | +} | |
| 36 | + | |
| 37 | +function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) { | |
| 38 | + return ( | |
| 39 | + <ProgressPrimitive.Track | |
| 40 | + className={cn( | |
| 41 | + "block h-1.5 w-full overflow-hidden rounded-full bg-input", | |
| 42 | + className, | |
| 43 | + )} | |
| 44 | + data-slot="progress-track" | |
| 45 | + {...props} | |
| 46 | + /> | |
| 47 | + ); | |
| 48 | +} | |
| 49 | + | |
| 50 | +function ProgressIndicator({ | |
| 51 | + className, | |
| 52 | + ...props | |
| 53 | +}: ProgressPrimitive.Indicator.Props) { | |
| 54 | + return ( | |
| 55 | + <ProgressPrimitive.Indicator | |
| 56 | + className={cn("bg-primary transition-all duration-500", className)} | |
| 57 | + data-slot="progress-indicator" | |
| 58 | + {...props} | |
| 59 | + /> | |
| 60 | + ); | |
| 61 | +} | |
| 62 | + | |
| 63 | +function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) { | |
| 64 | + return ( | |
| 65 | + <ProgressPrimitive.Value | |
| 66 | + className={cn("text-sm tabular-nums", className)} | |
| 67 | + data-slot="progress-value" | |
| 68 | + {...props} | |
| 69 | + /> | |
| 70 | + ); | |
| 71 | +} | |
| 72 | + | |
| 73 | +export { | |
| 74 | + Progress, | |
| 75 | + ProgressLabel, | |
| 76 | + ProgressTrack, | |
| 77 | + ProgressIndicator, | |
| 78 | + ProgressValue, | |
| 79 | +}; | |
| @@ -0,0 +1,34 @@ | ||
| 1 | +import { Radio as RadioPrimitive } from "@base-ui/react/radio"; | |
| 2 | +import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"; | |
| 3 | + | |
| 4 | +import { cn } from "#/lib/utils"; | |
| 5 | + | |
| 6 | +function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) { | |
| 7 | + return ( | |
| 8 | + <RadioGroupPrimitive | |
| 9 | + className={cn("flex flex-col gap-3", className)} | |
| 10 | + data-slot="radio-group" | |
| 11 | + {...props} | |
| 12 | + /> | |
| 13 | + ); | |
| 14 | +} | |
| 15 | + | |
| 16 | +function Radio({ className, ...props }: RadioPrimitive.Root.Props) { | |
| 17 | + return ( | |
| 18 | + <RadioPrimitive.Root | |
| 19 | + className={cn( | |
| 20 | + "relative inline-flex size-4.5 shrink-0 items-center justify-center rounded-full border border-input bg-background not-dark:bg-clip-padding shadow-xs/5 outline-none transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-full not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/48 data-disabled:opacity-64 sm:size-4 dark:not-data-checked:bg-input/32 dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-data-checked:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)] [[data-disabled],[data-checked],[aria-invalid]]:shadow-none", | |
| 21 | + className, | |
| 22 | + )} | |
| 23 | + data-slot="radio" | |
| 24 | + {...props} | |
| 25 | + > | |
| 26 | + <RadioPrimitive.Indicator | |
| 27 | + className="-inset-px absolute flex size-4.5 items-center justify-center rounded-full before:size-2 before:rounded-full before:bg-primary-foreground data-unchecked:hidden data-checked:bg-primary sm:size-4 sm:before:size-1.5" | |
| 28 | + data-slot="radio-indicator" | |
| 29 | + /> | |
| 30 | + </RadioPrimitive.Root> | |
| 31 | + ); | |
| 32 | +} | |
| 33 | + | |
| 34 | +export { RadioGroup, Radio, Radio as RadioGroupItem }; | |
| @@ -0,0 +1,62 @@ | ||
| 1 | +import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +function ScrollArea({ | |
| 6 | + className, | |
| 7 | + children, | |
| 8 | + scrollFade = false, | |
| 9 | + scrollbarGutter = false, | |
| 10 | + ...props | |
| 11 | +}: ScrollAreaPrimitive.Root.Props & { | |
| 12 | + scrollFade?: boolean; | |
| 13 | + scrollbarGutter?: boolean; | |
| 14 | +}) { | |
| 15 | + return ( | |
| 16 | + <ScrollAreaPrimitive.Root | |
| 17 | + className={cn("size-full min-h-0", className)} | |
| 18 | + {...props} | |
| 19 | + > | |
| 20 | + <ScrollAreaPrimitive.Viewport | |
| 21 | + className={cn( | |
| 22 | + "h-full overscroll-contain rounded-[inherit] outline-none transition-shadows focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-has-overflow-x:overscroll-x-contain", | |
| 23 | + scrollFade && | |
| 24 | + "mask-t-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-start)))] mask-b-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-y-end)))] mask-l-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-start)))] mask-r-from-[calc(100%-min(var(--fade-size),var(--scroll-area-overflow-x-end)))] [--fade-size:1.5rem]", | |
| 25 | + scrollbarGutter && | |
| 26 | + "data-has-overflow-y:pe-2.5 data-has-overflow-x:pb-2.5", | |
| 27 | + )} | |
| 28 | + data-slot="scroll-area-viewport" | |
| 29 | + > | |
| 30 | + {children} | |
| 31 | + </ScrollAreaPrimitive.Viewport> | |
| 32 | + <ScrollBar orientation="vertical" /> | |
| 33 | + <ScrollBar orientation="horizontal" /> | |
| 34 | + <ScrollAreaPrimitive.Corner data-slot="scroll-area-corner" /> | |
| 35 | + </ScrollAreaPrimitive.Root> | |
| 36 | + ); | |
| 37 | +} | |
| 38 | + | |
| 39 | +function ScrollBar({ | |
| 40 | + className, | |
| 41 | + orientation = "vertical", | |
| 42 | + ...props | |
| 43 | +}: ScrollAreaPrimitive.Scrollbar.Props) { | |
| 44 | + return ( | |
| 45 | + <ScrollAreaPrimitive.Scrollbar | |
| 46 | + className={cn( | |
| 47 | + "m-1 flex opacity-0 transition-opacity delay-300 data-[orientation=horizontal]:h-1.5 data-[orientation=vertical]:w-1.5 data-[orientation=horizontal]:flex-col data-hovering:opacity-100 data-scrolling:opacity-100 data-hovering:delay-0 data-scrolling:delay-0 data-hovering:duration-100 data-scrolling:duration-100", | |
| 48 | + className, | |
| 49 | + )} | |
| 50 | + data-slot="scroll-area-scrollbar" | |
| 51 | + orientation={orientation} | |
| 52 | + {...props} | |
| 53 | + > | |
| 54 | + <ScrollAreaPrimitive.Thumb | |
| 55 | + className="relative flex-1 rounded-full bg-foreground/20" | |
| 56 | + data-slot="scroll-area-thumb" | |
| 57 | + /> | |
| 58 | + </ScrollAreaPrimitive.Scrollbar> | |
| 59 | + ); | |
| 60 | +} | |
| 61 | + | |
| 62 | +export { ScrollArea, ScrollBar }; | |
| @@ -0,0 +1,187 @@ | ||
| 1 | +import { Select as SelectPrimitive } from "@base-ui/react/select"; | |
| 2 | +import { | |
| 3 | + ChevronDownIcon, | |
| 4 | + ChevronUpIcon, | |
| 5 | + ChevronsUpDownIcon, | |
| 6 | +} from "lucide-react"; | |
| 7 | + | |
| 8 | +import { cn } from "#/lib/utils"; | |
| 9 | + | |
| 10 | +const Select = SelectPrimitive.Root; | |
| 11 | + | |
| 12 | +function SelectTrigger({ | |
| 13 | + className, | |
| 14 | + size = "default", | |
| 15 | + children, | |
| 16 | + ...props | |
| 17 | +}: SelectPrimitive.Trigger.Props & { | |
| 18 | + size?: "sm" | "default" | "lg"; | |
| 19 | +}) { | |
| 20 | + return ( | |
| 21 | + <SelectPrimitive.Trigger | |
| 22 | + className={cn( | |
| 23 | + "relative inline-flex min-h-9 w-full min-w-36 select-none items-center justify-center gap-2 rounded-lg border border-input bg-background not-dark:bg-clip-padding px-[calc(--spacing(3)-1px)] text-left text-base text-foreground shadow-xs/5 outline-none ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_1px_--theme(--color-black/4%)] pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 focus-visible:border-ring focus-visible:ring-[3px] aria-invalid:border-destructive/36 focus-visible:aria-invalid:border-destructive/64 focus-visible:aria-invalid:ring-destructive/16 data-disabled:pointer-events-none data-disabled:opacity-64 sm:min-h-8 sm:text-sm dark:bg-input/32 dark:aria-invalid:ring-destructive/24 dark:not-data-disabled:not-focus-visible:not-aria-invalid:not-data-pressed:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='opacity-'])]:opacity-80 [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 [[data-disabled],:focus-visible,[aria-invalid],[data-pressed]]:shadow-none", | |
| 24 | + size === "sm" && | |
| 25 | + "min-h-8 gap-1.5 px-[calc(--spacing(2.5)-1px)] sm:min-h-7", | |
| 26 | + size === "lg" && "min-h-10 sm:min-h-9", | |
| 27 | + className, | |
| 28 | + )} | |
| 29 | + data-slot="select-trigger" | |
| 30 | + {...props} | |
| 31 | + > | |
| 32 | + {children} | |
| 33 | + <SelectPrimitive.Icon data-slot="select-icon"> | |
| 34 | + <ChevronsUpDownIcon className="-me-1 size-4.5 opacity-80 sm:size-4" /> | |
| 35 | + </SelectPrimitive.Icon> | |
| 36 | + </SelectPrimitive.Trigger> | |
| 37 | + ); | |
| 38 | +} | |
| 39 | + | |
| 40 | +function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { | |
| 41 | + return ( | |
| 42 | + <SelectPrimitive.Value | |
| 43 | + className={cn( | |
| 44 | + "flex-1 truncate data-placeholder:text-muted-foreground", | |
| 45 | + className, | |
| 46 | + )} | |
| 47 | + data-slot="select-value" | |
| 48 | + {...props} | |
| 49 | + /> | |
| 50 | + ); | |
| 51 | +} | |
| 52 | + | |
| 53 | +function SelectPopup({ | |
| 54 | + className, | |
| 55 | + children, | |
| 56 | + side = "bottom", | |
| 57 | + sideOffset = 4, | |
| 58 | + align = "start", | |
| 59 | + alignOffset = 0, | |
| 60 | + alignItemWithTrigger = true, | |
| 61 | + ...props | |
| 62 | +}: SelectPrimitive.Popup.Props & { | |
| 63 | + side?: SelectPrimitive.Positioner.Props["side"]; | |
| 64 | + sideOffset?: SelectPrimitive.Positioner.Props["sideOffset"]; | |
| 65 | + align?: SelectPrimitive.Positioner.Props["align"]; | |
| 66 | + alignOffset?: SelectPrimitive.Positioner.Props["alignOffset"]; | |
| 67 | + alignItemWithTrigger?: SelectPrimitive.Positioner.Props["alignItemWithTrigger"]; | |
| 68 | +}) { | |
| 69 | + return ( | |
| 70 | + <SelectPrimitive.Portal> | |
| 71 | + <SelectPrimitive.Positioner | |
| 72 | + align={align} | |
| 73 | + alignItemWithTrigger={alignItemWithTrigger} | |
| 74 | + alignOffset={alignOffset} | |
| 75 | + className="z-50 select-none" | |
| 76 | + data-slot="select-positioner" | |
| 77 | + side={side} | |
| 78 | + sideOffset={sideOffset} | |
| 79 | + > | |
| 80 | + <SelectPrimitive.Popup | |
| 81 | + className="origin-(--transform-origin) text-foreground" | |
| 82 | + data-slot="select-popup" | |
| 83 | + {...props} | |
| 84 | + > | |
| 85 | + <SelectPrimitive.ScrollUpArrow | |
| 86 | + className="top-0 z-50 flex h-6 w-full cursor-default items-center justify-center before:pointer-events-none before:absolute before:inset-x-px before:top-px before:h-[200%] before:rounded-t-[calc(var(--radius-lg)-1px)] before:bg-linear-to-b before:from-50% before:from-popover" | |
| 87 | + data-slot="select-scroll-up-arrow" | |
| 88 | + > | |
| 89 | + <ChevronUpIcon className="relative size-4.5 sm:size-4" /> | |
| 90 | + </SelectPrimitive.ScrollUpArrow> | |
| 91 | + <div className="relative h-full min-w-(--anchor-width) rounded-lg border bg-popover not-dark:bg-clip-padding shadow-lg/5 before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]"> | |
| 92 | + <SelectPrimitive.List | |
| 93 | + className={cn( | |
| 94 | + "max-h-(--available-height) overflow-y-auto p-1", | |
| 95 | + className, | |
| 96 | + )} | |
| 97 | + data-slot="select-list" | |
| 98 | + > | |
| 99 | + {children} | |
| 100 | + </SelectPrimitive.List> | |
| 101 | + </div> | |
| 102 | + <SelectPrimitive.ScrollDownArrow | |
| 103 | + className="bottom-0 z-50 flex h-6 w-full cursor-default items-center justify-center before:pointer-events-none before:absolute before:inset-x-px before:bottom-px before:h-[200%] before:rounded-b-[calc(var(--radius-lg)-1px)] before:bg-linear-to-t before:from-50% before:from-popover" | |
| 104 | + data-slot="select-scroll-down-arrow" | |
| 105 | + > | |
| 106 | + <ChevronDownIcon className="relative size-4.5 sm:size-4" /> | |
| 107 | + </SelectPrimitive.ScrollDownArrow> | |
| 108 | + </SelectPrimitive.Popup> | |
| 109 | + </SelectPrimitive.Positioner> | |
| 110 | + </SelectPrimitive.Portal> | |
| 111 | + ); | |
| 112 | +} | |
| 113 | + | |
| 114 | +function SelectItem({ | |
| 115 | + className, | |
| 116 | + children, | |
| 117 | + ...props | |
| 118 | +}: SelectPrimitive.Item.Props) { | |
| 119 | + return ( | |
| 120 | + <SelectPrimitive.Item | |
| 121 | + className={cn( | |
| 122 | + "grid min-h-8 in-data-[side=none]:min-w-[calc(var(--anchor-width)+1.25rem)] cursor-default grid-cols-[1rem_1fr] items-center gap-2 rounded-sm py-1 ps-2 pe-4 text-base outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:opacity-64 sm:min-h-7 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", | |
| 123 | + className, | |
| 124 | + )} | |
| 125 | + data-slot="select-item" | |
| 126 | + {...props} | |
| 127 | + > | |
| 128 | + <SelectPrimitive.ItemIndicator className="col-start-1"> | |
| 129 | + <svg | |
| 130 | + fill="none" | |
| 131 | + height="24" | |
| 132 | + stroke="currentColor" | |
| 133 | + strokeLinecap="round" | |
| 134 | + strokeLinejoin="round" | |
| 135 | + strokeWidth="2" | |
| 136 | + viewBox="0 0 24 24" | |
| 137 | + width="24" | |
| 138 | + xmlns="http://www.w3.org/1500/svg" | |
| 139 | + > | |
| 140 | + <path d="M5.252 12.7 10.2 18.63 18.748 5.37" /> | |
| 141 | + </svg> | |
| 142 | + </SelectPrimitive.ItemIndicator> | |
| 143 | + <SelectPrimitive.ItemText className="col-start-2 min-w-0"> | |
| 144 | + {children} | |
| 145 | + </SelectPrimitive.ItemText> | |
| 146 | + </SelectPrimitive.Item> | |
| 147 | + ); | |
| 148 | +} | |
| 149 | + | |
| 150 | +function SelectSeparator({ | |
| 151 | + className, | |
| 152 | + ...props | |
| 153 | +}: SelectPrimitive.Separator.Props) { | |
| 154 | + return ( | |
| 155 | + <SelectPrimitive.Separator | |
| 156 | + className={cn("mx-2 my-1 h-px bg-border", className)} | |
| 157 | + data-slot="select-separator" | |
| 158 | + {...props} | |
| 159 | + /> | |
| 160 | + ); | |
| 161 | +} | |
| 162 | + | |
| 163 | +function SelectGroup(props: SelectPrimitive.Group.Props) { | |
| 164 | + return <SelectPrimitive.Group data-slot="select-group" {...props} />; | |
| 165 | +} | |
| 166 | + | |
| 167 | +function SelectGroupLabel(props: SelectPrimitive.GroupLabel.Props) { | |
| 168 | + return ( | |
| 169 | + <SelectPrimitive.GroupLabel | |
| 170 | + className="px-2 py-1.5 font-medium text-muted-foreground text-xs" | |
| 171 | + data-slot="select-group-label" | |
| 172 | + {...props} | |
| 173 | + /> | |
| 174 | + ); | |
| 175 | +} | |
| 176 | + | |
| 177 | +export { | |
| 178 | + Select, | |
| 179 | + SelectTrigger, | |
| 180 | + SelectValue, | |
| 181 | + SelectPopup, | |
| 182 | + SelectPopup as SelectContent, | |
| 183 | + SelectItem, | |
| 184 | + SelectSeparator, | |
| 185 | + SelectGroup, | |
| 186 | + SelectGroupLabel, | |
| 187 | +}; | |
| @@ -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 }; | |
| @@ -0,0 +1,201 @@ | ||
| 1 | +import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"; | |
| 2 | +import { XIcon } from "lucide-react"; | |
| 3 | +import { Button } from "#/components/ui/button"; | |
| 4 | +import { ScrollArea } from "#/components/ui/scroll-area"; | |
| 5 | +import { cn } from "#/lib/utils"; | |
| 6 | + | |
| 7 | +const Sheet = SheetPrimitive.Root; | |
| 8 | + | |
| 9 | +const SheetPortal = SheetPrimitive.Portal; | |
| 10 | + | |
| 11 | +function SheetTrigger(props: SheetPrimitive.Trigger.Props) { | |
| 12 | + return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />; | |
| 13 | +} | |
| 14 | + | |
| 15 | +function SheetClose(props: SheetPrimitive.Close.Props) { | |
| 16 | + return <SheetPrimitive.Close data-slot="sheet-close" {...props} />; | |
| 17 | +} | |
| 18 | + | |
| 19 | +function SheetBackdrop({ className, ...props }: SheetPrimitive.Backdrop.Props) { | |
| 20 | + return ( | |
| 21 | + <SheetPrimitive.Backdrop | |
| 22 | + className={cn( | |
| 23 | + "fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0", | |
| 24 | + className, | |
| 25 | + )} | |
| 26 | + data-slot="sheet-backdrop" | |
| 27 | + {...props} | |
| 28 | + /> | |
| 29 | + ); | |
| 30 | +} | |
| 31 | + | |
| 32 | +function SheetViewport({ | |
| 33 | + className, | |
| 34 | + side, | |
| 35 | + variant = "default", | |
| 36 | + ...props | |
| 37 | +}: SheetPrimitive.Viewport.Props & { | |
| 38 | + side?: "right" | "left" | "top" | "bottom"; | |
| 39 | + variant?: "default" | "inset"; | |
| 40 | +}) { | |
| 41 | + return ( | |
| 42 | + <SheetPrimitive.Viewport | |
| 43 | + className={cn( | |
| 44 | + "fixed inset-0 z-50 grid", | |
| 45 | + side === "bottom" && "grid grid-rows-[1fr_auto] pt-12", | |
| 46 | + side === "top" && "grid grid-rows-[auto_1fr] pb-12", | |
| 47 | + side === "left" && "flex justify-start", | |
| 48 | + side === "right" && "flex justify-end", | |
| 49 | + variant === "inset" && "sm:p-4", | |
| 50 | + )} | |
| 51 | + data-slot="sheet-viewport" | |
| 52 | + {...props} | |
| 53 | + /> | |
| 54 | + ); | |
| 55 | +} | |
| 56 | + | |
| 57 | +function SheetPopup({ | |
| 58 | + className, | |
| 59 | + children, | |
| 60 | + showCloseButton = true, | |
| 61 | + side = "right", | |
| 62 | + variant = "default", | |
| 63 | + ...props | |
| 64 | +}: SheetPrimitive.Popup.Props & { | |
| 65 | + showCloseButton?: boolean; | |
| 66 | + side?: "right" | "left" | "top" | "bottom"; | |
| 67 | + variant?: "default" | "inset"; | |
| 68 | +}) { | |
| 69 | + return ( | |
| 70 | + <SheetPortal> | |
| 71 | + <SheetBackdrop /> | |
| 72 | + <SheetViewport side={side} variant={variant}> | |
| 73 | + <SheetPrimitive.Popup | |
| 74 | + className={cn( | |
| 75 | + "relative flex max-h-full min-h-0 w-full min-w-0 flex-col bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 transition-[opacity,translate] duration-200 ease-in-out will-change-transform before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:opacity-0 data-starting-style:opacity-0 max-sm:before:hidden dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 76 | + side === "bottom" && | |
| 77 | + "row-start-2 border-t data-ending-style:translate-y-8 data-starting-style:translate-y-8", | |
| 78 | + side === "top" && | |
| 79 | + "data-ending-style:-translate-y-8 data-starting-style:-translate-y-8 border-b", | |
| 80 | + side === "left" && | |
| 81 | + "data-ending-style:-translate-x-8 data-starting-style:-translate-x-8 w-[calc(100%-(--spacing(12)))] max-w-md border-e", | |
| 82 | + side === "right" && | |
| 83 | + "col-start-2 w-[calc(100%-(--spacing(12)))] max-w-md border-s data-ending-style:translate-x-8 data-starting-style:translate-x-8", | |
| 84 | + variant === "inset" && | |
| 85 | + "before:hidden sm:rounded-2xl sm:border sm:before:rounded-[calc(var(--radius-2xl)-1px)] sm:**:data-[slot=sheet-footer]:rounded-b-[calc(var(--radius-2xl)-1px)]", | |
| 86 | + className, | |
| 87 | + )} | |
| 88 | + data-slot="sheet-popup" | |
| 89 | + {...props} | |
| 90 | + > | |
| 91 | + {children} | |
| 92 | + {showCloseButton && ( | |
| 93 | + <SheetPrimitive.Close | |
| 94 | + aria-label="Close" | |
| 95 | + className="absolute end-2 top-2" | |
| 96 | + render={<Button size="icon" variant="ghost" />} | |
| 97 | + > | |
| 98 | + <XIcon /> | |
| 99 | + </SheetPrimitive.Close> | |
| 100 | + )} | |
| 101 | + </SheetPrimitive.Popup> | |
| 102 | + </SheetViewport> | |
| 103 | + </SheetPortal> | |
| 104 | + ); | |
| 105 | +} | |
| 106 | + | |
| 107 | +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { | |
| 108 | + return ( | |
| 109 | + <div | |
| 110 | + className={cn( | |
| 111 | + "flex flex-col gap-2 p-6 in-[[data-slot=sheet-popup]:has([data-slot=sheet-panel])]:pb-3 max-sm:pb-4", | |
| 112 | + className, | |
| 113 | + )} | |
| 114 | + data-slot="sheet-header" | |
| 115 | + {...props} | |
| 116 | + /> | |
| 117 | + ); | |
| 118 | +} | |
| 119 | + | |
| 120 | +function SheetFooter({ | |
| 121 | + className, | |
| 122 | + variant = "default", | |
| 123 | + ...props | |
| 124 | +}: React.ComponentProps<"div"> & { | |
| 125 | + variant?: "default" | "bare"; | |
| 126 | +}) { | |
| 127 | + return ( | |
| 128 | + <div | |
| 129 | + className={cn( | |
| 130 | + "flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end", | |
| 131 | + variant === "default" && "border-t bg-muted/72 py-4", | |
| 132 | + variant === "bare" && | |
| 133 | + "in-[[data-slot=sheet-popup]:has([data-slot=sheet-panel])]:pt-3 pt-4 pb-6", | |
| 134 | + className, | |
| 135 | + )} | |
| 136 | + data-slot="sheet-footer" | |
| 137 | + {...props} | |
| 138 | + /> | |
| 139 | + ); | |
| 140 | +} | |
| 141 | + | |
| 142 | +function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) { | |
| 143 | + return ( | |
| 144 | + <SheetPrimitive.Title | |
| 145 | + className={cn( | |
| 146 | + "font-heading font-semibold text-xl leading-none", | |
| 147 | + className, | |
| 148 | + )} | |
| 149 | + data-slot="sheet-title" | |
| 150 | + {...props} | |
| 151 | + /> | |
| 152 | + ); | |
| 153 | +} | |
| 154 | + | |
| 155 | +function SheetDescription({ | |
| 156 | + className, | |
| 157 | + ...props | |
| 158 | +}: SheetPrimitive.Description.Props) { | |
| 159 | + return ( | |
| 160 | + <SheetPrimitive.Description | |
| 161 | + className={cn("text-muted-foreground text-sm", className)} | |
| 162 | + data-slot="sheet-description" | |
| 163 | + {...props} | |
| 164 | + /> | |
| 165 | + ); | |
| 166 | +} | |
| 167 | + | |
| 168 | +function SheetPanel({ | |
| 169 | + className, | |
| 170 | + scrollFade = true, | |
| 171 | + ...props | |
| 172 | +}: React.ComponentProps<"div"> & { scrollFade?: boolean }) { | |
| 173 | + return ( | |
| 174 | + <ScrollArea scrollFade={scrollFade}> | |
| 175 | + <div | |
| 176 | + className={cn( | |
| 177 | + "p-6 in-[[data-slot=sheet-popup]:has([data-slot=sheet-header])]:pt-1 in-[[data-slot=sheet-popup]:has([data-slot=sheet-footer]:not(.border-t))]:pb-1", | |
| 178 | + className, | |
| 179 | + )} | |
| 180 | + data-slot="sheet-panel" | |
| 181 | + {...props} | |
| 182 | + /> | |
| 183 | + </ScrollArea> | |
| 184 | + ); | |
| 185 | +} | |
| 186 | + | |
| 187 | +export { | |
| 188 | + Sheet, | |
| 189 | + SheetTrigger, | |
| 190 | + SheetPortal, | |
| 191 | + SheetClose, | |
| 192 | + SheetBackdrop, | |
| 193 | + SheetBackdrop as SheetOverlay, | |
| 194 | + SheetPopup, | |
| 195 | + SheetPopup as SheetContent, | |
| 196 | + SheetHeader, | |
| 197 | + SheetFooter, | |
| 198 | + SheetTitle, | |
| 199 | + SheetDescription, | |
| 200 | + SheetPanel, | |
| 201 | +}; | |
| @@ -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 }; | |
| @@ -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 }; | |
| @@ -0,0 +1,25 @@ | ||
| 1 | +import { Switch as SwitchPrimitive } from "@base-ui/react/switch"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +function Switch({ className, ...props }: SwitchPrimitive.Root.Props) { | |
| 6 | + return ( | |
| 7 | + <SwitchPrimitive.Root | |
| 8 | + className={cn( | |
| 9 | + "inline-flex h-[calc(var(--thumb-size)+2px)] w-[calc(var(--thumb-size)*2-2px)] shrink-0 items-center rounded-full p-px outline-none transition-[background-color,box-shadow] duration-200 [--thumb-size:--spacing(5)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-checked:bg-primary data-unchecked:bg-input data-disabled:opacity-64 sm:[--thumb-size:--spacing(4)]", | |
| 10 | + className, | |
| 11 | + )} | |
| 12 | + data-slot="switch" | |
| 13 | + {...props} | |
| 14 | + > | |
| 15 | + <SwitchPrimitive.Thumb | |
| 16 | + className={cn( | |
| 17 | + "pointer-events-none block aspect-square h-full origin-left in-[[role=switch]:active,[data-slot=label]:active]:not-data-disabled:scale-x-110 in-[[role=switch]:active,[data-slot=label]:active]:rounded-[var(--thumb-size)/calc(var(--thumb-size)*1.1)] rounded-(--thumb-size) bg-background shadow-sm/5 will-change-transform [transition:translate_.15s,border-radius_.15s,scale_.1s_.1s,transform-origin_.15s] data-checked:origin-[var(--thumb-size)_50%] data-checked:translate-x-[calc(var(--thumb-size)-4px)]", | |
| 18 | + )} | |
| 19 | + data-slot="switch-thumb" | |
| 20 | + /> | |
| 21 | + </SwitchPrimitive.Root> | |
| 22 | + ); | |
| 23 | +} | |
| 24 | + | |
| 25 | +export { Switch }; | |
| @@ -0,0 +1,126 @@ | ||
| 1 | +import type * as React from "react"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +function Table({ className, ...props }: React.ComponentProps<"table">) { | |
| 6 | + return ( | |
| 7 | + <div | |
| 8 | + className="relative w-full overflow-x-auto" | |
| 9 | + data-slot="table-container" | |
| 10 | + > | |
| 11 | + <table | |
| 12 | + className={cn( | |
| 13 | + "w-full caption-bottom in-data-[slot=frame]:border-separate in-data-[slot=frame]:border-spacing-0 text-sm", | |
| 14 | + className, | |
| 15 | + )} | |
| 16 | + data-slot="table" | |
| 17 | + {...props} | |
| 18 | + /> | |
| 19 | + </div> | |
| 20 | + ); | |
| 21 | +} | |
| 22 | + | |
| 23 | +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { | |
| 24 | + return ( | |
| 25 | + <thead | |
| 26 | + className={cn( | |
| 27 | + "[&_tr]:border-b in-data-[slot=frame]:**:[th]:h-9 in-data-[slot=frame]:*:[tr]:border-none in-data-[slot=frame]:*:[tr]:hover:bg-transparent", | |
| 28 | + className, | |
| 29 | + )} | |
| 30 | + data-slot="table-header" | |
| 31 | + {...props} | |
| 32 | + /> | |
| 33 | + ); | |
| 34 | +} | |
| 35 | + | |
| 36 | +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { | |
| 37 | + return ( | |
| 38 | + <tbody | |
| 39 | + className={cn( | |
| 40 | + "relative in-data-[slot=frame]:rounded-xl in-data-[slot=frame]:shadow-xs/5 before:pointer-events-none before:absolute before:inset-px not-in-data-[slot=frame]:before:hidden before:rounded-[calc(var(--radius-xl)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/8%)] [&_tr:last-child]:border-0 in-data-[slot=frame]:*:[tr]:border-0 in-data-[slot=frame]:*:[tr]:*:[td]:border-b in-data-[slot=frame]:*:[tr]:*:[td]:bg-background in-data-[slot=frame]:*:[tr]:*:[td]:bg-clip-padding in-data-[slot=frame]:*:[tr]:first:*:[td]:first:rounded-ss-xl in-data-[slot=frame]:*:[tr]:*:[td]:first:border-s in-data-[slot=frame]:*:[tr]:first:*:[td]:border-t in-data-[slot=frame]:*:[tr]:last:*:[td]:last:rounded-ee-xl in-data-[slot=frame]:*:[tr]:*:[td]:last:border-e in-data-[slot=frame]:*:[tr]:first:*:[td]:last:rounded-se-xl in-data-[slot=frame]:*:[tr]:last:*:[td]:first:rounded-es-xl in-data-[slot=frame]:*:[tr]:hover:*:[td]:bg-transparent in-data-[slot=frame]:*:[tr]:data-[state=selected]:*:[td]:bg-muted/72", | |
| 41 | + className, | |
| 42 | + )} | |
| 43 | + data-slot="table-body" | |
| 44 | + {...props} | |
| 45 | + /> | |
| 46 | + ); | |
| 47 | +} | |
| 48 | + | |
| 49 | +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { | |
| 50 | + return ( | |
| 51 | + <tfoot | |
| 52 | + className={cn( | |
| 53 | + "border-t in-data-[slot=frame]:border-none bg-muted/72 in-data-[slot=frame]:bg-transparent font-medium [&>tr]:last:border-b-0 in-data-[slot=frame]:*:[tr]:hover:bg-transparent", | |
| 54 | + className, | |
| 55 | + )} | |
| 56 | + data-slot="table-footer" | |
| 57 | + {...props} | |
| 58 | + /> | |
| 59 | + ); | |
| 60 | +} | |
| 61 | + | |
| 62 | +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { | |
| 63 | + return ( | |
| 64 | + <tr | |
| 65 | + className={cn( | |
| 66 | + "border-b transition-colors hover:bg-muted/72 in-data-[slot=frame]:hover:bg-transparent data-[state=selected]:bg-muted/72 in-data-[slot=frame]:data-[state=selected]:bg-transparent", | |
| 67 | + className, | |
| 68 | + )} | |
| 69 | + data-slot="table-row" | |
| 70 | + {...props} | |
| 71 | + /> | |
| 72 | + ); | |
| 73 | +} | |
| 74 | + | |
| 75 | +function TableHead({ className, ...props }: React.ComponentProps<"th">) { | |
| 76 | + return ( | |
| 77 | + <th | |
| 78 | + className={cn( | |
| 79 | + "h-10 whitespace-nowrap px-2.5 text-left align-middle font-medium text-muted-foreground leading-none has-[[role=checkbox]]:w-px has-[[role=checkbox]]:pe-0", | |
| 80 | + className, | |
| 81 | + )} | |
| 82 | + data-slot="table-head" | |
| 83 | + {...props} | |
| 84 | + /> | |
| 85 | + ); | |
| 86 | +} | |
| 87 | + | |
| 88 | +function TableCell({ className, ...props }: React.ComponentProps<"td">) { | |
| 89 | + return ( | |
| 90 | + <td | |
| 91 | + className={cn( | |
| 92 | + "whitespace-nowrap p-2.5 align-middle leading-none in-data-[slot=frame]:first:p-[calc(--spacing(2.5)-1px)] in-data-[slot=frame]:last:p-[calc(--spacing(2.5)-1px)] has-[[role=checkbox]]:pe-0", | |
| 93 | + className, | |
| 94 | + )} | |
| 95 | + data-slot="table-cell" | |
| 96 | + {...props} | |
| 97 | + /> | |
| 98 | + ); | |
| 99 | +} | |
| 100 | + | |
| 101 | +function TableCaption({ | |
| 102 | + className, | |
| 103 | + ...props | |
| 104 | +}: React.ComponentProps<"caption">) { | |
| 105 | + return ( | |
| 106 | + <caption | |
| 107 | + className={cn( | |
| 108 | + "in-data-[slot=frame]:my-4 mt-4 text-muted-foreground text-sm", | |
| 109 | + className, | |
| 110 | + )} | |
| 111 | + data-slot="table-caption" | |
| 112 | + {...props} | |
| 113 | + /> | |
| 114 | + ); | |
| 115 | +} | |
| 116 | + | |
| 117 | +export { | |
| 118 | + Table, | |
| 119 | + TableHeader, | |
| 120 | + TableBody, | |
| 121 | + TableFooter, | |
| 122 | + TableHead, | |
| 123 | + TableRow, | |
| 124 | + TableCell, | |
| 125 | + TableCaption, | |
| 126 | +}; | |
| @@ -0,0 +1,85 @@ | ||
| 1 | +import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"; | |
| 2 | + | |
| 3 | +import { cn } from "#/lib/utils"; | |
| 4 | + | |
| 5 | +type TabsVariant = "default" | "underline"; | |
| 6 | + | |
| 7 | +function Tabs({ className, ...props }: TabsPrimitive.Root.Props) { | |
| 8 | + return ( | |
| 9 | + <TabsPrimitive.Root | |
| 10 | + className={cn( | |
| 11 | + "flex flex-col gap-2 data-[orientation=vertical]:flex-row", | |
| 12 | + className, | |
| 13 | + )} | |
| 14 | + data-slot="tabs" | |
| 15 | + {...props} | |
| 16 | + /> | |
| 17 | + ); | |
| 18 | +} | |
| 19 | + | |
| 20 | +function TabsList({ | |
| 21 | + variant = "default", | |
| 22 | + className, | |
| 23 | + children, | |
| 24 | + ...props | |
| 25 | +}: TabsPrimitive.List.Props & { | |
| 26 | + variant?: TabsVariant; | |
| 27 | +}) { | |
| 28 | + return ( | |
| 29 | + <TabsPrimitive.List | |
| 30 | + className={cn( | |
| 31 | + "relative z-0 flex w-fit items-center justify-center gap-x-0.5 text-muted-foreground", | |
| 32 | + "data-[orientation=vertical]:flex-col", | |
| 33 | + variant === "default" | |
| 34 | + ? "rounded-lg bg-muted p-0.5 text-muted-foreground/72" | |
| 35 | + : "data-[orientation=vertical]:px-1 data-[orientation=horizontal]:py-1 *:data-[slot=tabs-tab]:hover:bg-accent", | |
| 36 | + className, | |
| 37 | + )} | |
| 38 | + data-slot="tabs-list" | |
| 39 | + {...props} | |
| 40 | + > | |
| 41 | + {children} | |
| 42 | + <TabsPrimitive.Indicator | |
| 43 | + className={cn( | |
| 44 | + "-translate-y-(--active-tab-bottom) absolute bottom-0 left-0 h-(--active-tab-height) w-(--active-tab-width) translate-x-(--active-tab-left) transition-[width,translate] duration-200 ease-in-out", | |
| 45 | + variant === "underline" | |
| 46 | + ? "data-[orientation=vertical]:-translate-x-px z-10 bg-primary data-[orientation=horizontal]:h-0.5 data-[orientation=vertical]:w-0.5 data-[orientation=horizontal]:translate-y-px" | |
| 47 | + : "-z-1 rounded-md bg-background shadow-sm/5 dark:bg-input", | |
| 48 | + )} | |
| 49 | + data-slot="tab-indicator" | |
| 50 | + /> | |
| 51 | + </TabsPrimitive.List> | |
| 52 | + ); | |
| 53 | +} | |
| 54 | + | |
| 55 | +function TabsTab({ className, ...props }: TabsPrimitive.Tab.Props) { | |
| 56 | + return ( | |
| 57 | + <TabsPrimitive.Tab | |
| 58 | + className={cn( | |
| 59 | + "[&_svg]:-mx-0.5 relative flex h-9 shrink-0 grow cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-[calc(--spacing(2.5)-1px)] font-medium text-base outline-none transition-[color,background-color,box-shadow] hover:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring data-disabled:pointer-events-none data-[orientation=vertical]:w-full data-[orientation=vertical]:justify-start data-active:text-foreground data-disabled:opacity-64 sm:h-8 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", | |
| 60 | + className, | |
| 61 | + )} | |
| 62 | + data-slot="tabs-tab" | |
| 63 | + {...props} | |
| 64 | + /> | |
| 65 | + ); | |
| 66 | +} | |
| 67 | + | |
| 68 | +function TabsPanel({ className, ...props }: TabsPrimitive.Panel.Props) { | |
| 69 | + return ( | |
| 70 | + <TabsPrimitive.Panel | |
| 71 | + className={cn("flex-1 outline-none", className)} | |
| 72 | + data-slot="tabs-content" | |
| 73 | + {...props} | |
| 74 | + /> | |
| 75 | + ); | |
| 76 | +} | |
| 77 | + | |
| 78 | +export { | |
| 79 | + Tabs, | |
| 80 | + TabsList, | |
| 81 | + TabsTab, | |
| 82 | + TabsTab as TabsTrigger, | |
| 83 | + TabsPanel, | |
| 84 | + TabsPanel as TabsContent, | |
| 85 | +}; | |
| @@ -0,0 +1,49 @@ | ||
| 1 | +import { Field as FieldPrimitive } from "@base-ui/react/field"; | |
| 2 | +import { mergeProps } from "@base-ui/react/merge-props"; | |
| 3 | +import type * as React from "react"; | |
| 4 | + | |
| 5 | +import { cn } from "#/lib/utils"; | |
| 6 | + | |
| 7 | +type TextareaProps = React.ComponentProps<"textarea"> & { | |
| 8 | + size?: "sm" | "default" | "lg" | number; | |
| 9 | + unstyled?: boolean; | |
| 10 | +}; | |
| 11 | + | |
| 12 | +function Textarea({ | |
| 13 | + className, | |
| 14 | + size = "default", | |
| 15 | + unstyled = false, | |
| 16 | + ...props | |
| 17 | +}: TextareaProps) { | |
| 18 | + return ( | |
| 19 | + <span | |
| 20 | + className={ | |
| 21 | + cn( | |
| 22 | + !unstyled && | |
| 23 | + "relative inline-flex w-full rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] sm:text-sm dark:bg-input/32 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 24 | + className, | |
| 25 | + ) || undefined | |
| 26 | + } | |
| 27 | + data-size={size} | |
| 28 | + data-slot="textarea-control" | |
| 29 | + > | |
| 30 | + <FieldPrimitive.Control | |
| 31 | + render={(defaultProps) => ( | |
| 32 | + <textarea | |
| 33 | + className={cn( | |
| 34 | + "field-sizing-content min-h-17.5 w-full rounded-[inherit] px-[calc(--spacing(3)-1px)] py-[calc(--spacing(1.5)-1px)] outline-none max-sm:min-h-20.5", | |
| 35 | + size === "sm" && | |
| 36 | + "min-h-16.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)] max-sm:min-h-19.5", | |
| 37 | + size === "lg" && | |
| 38 | + "min-h-18.5 py-[calc(--spacing(2)-1px)] max-sm:min-h-21.5", | |
| 39 | + )} | |
| 40 | + data-slot="textarea" | |
| 41 | + {...mergeProps(defaultProps, props)} | |
| 42 | + /> | |
| 43 | + )} | |
| 44 | + /> | |
| 45 | + </span> | |
| 46 | + ); | |
| 47 | +} | |
| 48 | + | |
| 49 | +export { Textarea, type TextareaProps }; | |
| @@ -0,0 +1,267 @@ | ||
| 1 | +import { Toast } from "@base-ui/react/toast"; | |
| 2 | +import { | |
| 3 | + CircleAlertIcon, | |
| 4 | + CircleCheckIcon, | |
| 5 | + InfoIcon, | |
| 6 | + LoaderCircleIcon, | |
| 7 | + TriangleAlertIcon, | |
| 8 | +} from "lucide-react"; | |
| 9 | + | |
| 10 | +import { buttonVariants } from "#/components/ui/button"; | |
| 11 | +import { cn } from "#/lib/utils"; | |
| 12 | + | |
| 13 | +const toastManager = Toast.createToastManager(); | |
| 14 | +const anchoredToastManager = Toast.createToastManager(); | |
| 15 | + | |
| 16 | +const TOAST_ICONS = { | |
| 17 | + error: CircleAlertIcon, | |
| 18 | + info: InfoIcon, | |
| 19 | + loading: LoaderCircleIcon, | |
| 20 | + success: CircleCheckIcon, | |
| 21 | + warning: TriangleAlertIcon, | |
| 22 | +} as const; | |
| 23 | + | |
| 24 | +type ToastPosition = | |
| 25 | + | "top-left" | |
| 26 | + | "top-center" | |
| 27 | + | "top-right" | |
| 28 | + | "bottom-left" | |
| 29 | + | "bottom-center" | |
| 30 | + | "bottom-right"; | |
| 31 | + | |
| 32 | +interface ToastProviderProps extends Toast.Provider.Props { | |
| 33 | + position?: ToastPosition; | |
| 34 | +} | |
| 35 | + | |
| 36 | +function ToastProvider({ | |
| 37 | + children, | |
| 38 | + position = "bottom-right", | |
| 39 | + ...props | |
| 40 | +}: ToastProviderProps) { | |
| 41 | + return ( | |
| 42 | + <Toast.Provider toastManager={toastManager} {...props}> | |
| 43 | + {children} | |
| 44 | + <Toasts position={position} /> | |
| 45 | + </Toast.Provider> | |
| 46 | + ); | |
| 47 | +} | |
| 48 | + | |
| 49 | +function Toasts({ position = "bottom-right" }: { position: ToastPosition }) { | |
| 50 | + const { toasts } = Toast.useToastManager(); | |
| 51 | + const isTop = position.startsWith("top"); | |
| 52 | + | |
| 53 | + return ( | |
| 54 | + <Toast.Portal data-slot="toast-portal"> | |
| 55 | + <Toast.Viewport | |
| 56 | + className={cn( | |
| 57 | + "fixed z-50 mx-auto flex w-[calc(100%-var(--toast-inset)*2)] max-w-90 [--toast-inset:--spacing(4)] sm:[--toast-inset:--spacing(8)]", | |
| 58 | + // Vertical positioning | |
| 59 | + "data-[position*=top]:top-(--toast-inset)", | |
| 60 | + "data-[position*=bottom]:bottom-(--toast-inset)", | |
| 61 | + // Horizontal positioning | |
| 62 | + "data-[position*=left]:left-(--toast-inset)", | |
| 63 | + "data-[position*=right]:right-(--toast-inset)", | |
| 64 | + "data-[position*=center]:-translate-x-1/2 data-[position*=center]:left-1/2", | |
| 65 | + )} | |
| 66 | + data-position={position} | |
| 67 | + data-slot="toast-viewport" | |
| 68 | + > | |
| 69 | + {toasts.map((toast) => { | |
| 70 | + const Icon = toast.type | |
| 71 | + ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] | |
| 72 | + : null; | |
| 73 | + | |
| 74 | + return ( | |
| 75 | + <Toast.Root | |
| 76 | + className={cn( | |
| 77 | + "absolute z-[calc(9999-var(--toast-index))] h-(--toast-calc-height) w-full select-none rounded-lg border bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 [transition:transform_.5s_cubic-bezier(.22,1,.36,1),opacity_.5s,height_.15s] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/4%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 78 | + // Base positioning using data-position | |
| 79 | + "data-[position*=right]:right-0 data-[position*=right]:left-auto", | |
| 80 | + "data-[position*=left]:right-auto data-[position*=left]:left-0", | |
| 81 | + "data-[position*=center]:right-0 data-[position*=center]:left-0", | |
| 82 | + "data-[position*=top]:top-0 data-[position*=top]:bottom-auto data-[position*=top]:origin-top", | |
| 83 | + "data-[position*=bottom]:top-auto data-[position*=bottom]:bottom-0 data-[position*=bottom]:origin-bottom", | |
| 84 | + // Gap fill for hover | |
| 85 | + "after:absolute after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full", | |
| 86 | + "data-[position*=top]:after:top-full", | |
| 87 | + "data-[position*=bottom]:after:bottom-full", | |
| 88 | + // Define some variables | |
| 89 | + "[--toast-calc-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:--spacing(3)] [--toast-peek:--spacing(3)] [--toast-scale:calc(max(0,1-(var(--toast-index)*.1)))] [--toast-shrink:calc(1-var(--toast-scale))]", | |
| 90 | + // Define offset-y variable | |
| 91 | + "data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+var(--toast-index)*var(--toast-gap)+var(--toast-swipe-movement-y))]", | |
| 92 | + "data-[position*=bottom]:[--toast-calc-offset-y:calc(var(--toast-offset-y)*-1+var(--toast-index)*var(--toast-gap)*-1+var(--toast-swipe-movement-y))]", | |
| 93 | + // Default state transform | |
| 94 | + "data-[position*=top]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]", | |
| 95 | + "data-[position*=bottom]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]", | |
| 96 | + // Limited state | |
| 97 | + "data-limited:opacity-0", | |
| 98 | + // Expanded state | |
| 99 | + "data-expanded:h-(--toast-height)", | |
| 100 | + "data-position:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-calc-offset-y))]", | |
| 101 | + // Starting and ending animations | |
| 102 | + "data-[position*=top]:data-starting-style:transform-[translateY(calc(-100%-var(--toast-inset)))]", | |
| 103 | + "data-[position*=bottom]:data-starting-style:transform-[translateY(calc(100%+var(--toast-inset)))]", | |
| 104 | + "data-ending-style:opacity-0", | |
| 105 | + // Ending animations (direction-aware) | |
| 106 | + "data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateY(calc(100%+var(--toast-inset)))]", | |
| 107 | + "data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]", | |
| 108 | + "data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]", | |
| 109 | + "data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]", | |
| 110 | + "data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]", | |
| 111 | + // Ending animations (expanded) | |
| 112 | + "data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]", | |
| 113 | + "data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]", | |
| 114 | + "data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]", | |
| 115 | + "data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]", | |
| 116 | + )} | |
| 117 | + data-position={position} | |
| 118 | + key={toast.id} | |
| 119 | + swipeDirection={ | |
| 120 | + position.includes("center") | |
| 121 | + ? [isTop ? "up" : "down"] | |
| 122 | + : position.includes("left") | |
| 123 | + ? ["left", isTop ? "up" : "down"] | |
| 124 | + : ["right", isTop ? "up" : "down"] | |
| 125 | + } | |
| 126 | + toast={toast} | |
| 127 | + > | |
| 128 | + <Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm transition-opacity duration-250 data-behind:not-data-expanded:pointer-events-none data-behind:opacity-0 data-expanded:opacity-100"> | |
| 129 | + <div className="flex gap-2"> | |
| 130 | + {Icon && ( | |
| 131 | + <div | |
| 132 | + className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0" | |
| 133 | + data-slot="toast-icon" | |
| 134 | + > | |
| 135 | + <Icon className="in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80" /> | |
| 136 | + </div> | |
| 137 | + )} | |
| 138 | + | |
| 139 | + <div className="flex flex-col gap-0.5"> | |
| 140 | + <Toast.Title | |
| 141 | + className="font-medium" | |
| 142 | + data-slot="toast-title" | |
| 143 | + /> | |
| 144 | + <Toast.Description | |
| 145 | + className="text-muted-foreground" | |
| 146 | + data-slot="toast-description" | |
| 147 | + /> | |
| 148 | + </div> | |
| 149 | + </div> | |
| 150 | + {toast.actionProps && ( | |
| 151 | + <Toast.Action | |
| 152 | + className={buttonVariants({ size: "xs" })} | |
| 153 | + data-slot="toast-action" | |
| 154 | + > | |
| 155 | + {toast.actionProps.children} | |
| 156 | + </Toast.Action> | |
| 157 | + )} | |
| 158 | + </Toast.Content> | |
| 159 | + </Toast.Root> | |
| 160 | + ); | |
| 161 | + })} | |
| 162 | + </Toast.Viewport> | |
| 163 | + </Toast.Portal> | |
| 164 | + ); | |
| 165 | +} | |
| 166 | + | |
| 167 | +function AnchoredToastProvider({ children, ...props }: Toast.Provider.Props) { | |
| 168 | + return ( | |
| 169 | + <Toast.Provider toastManager={anchoredToastManager} {...props}> | |
| 170 | + {children} | |
| 171 | + <AnchoredToasts /> | |
| 172 | + </Toast.Provider> | |
| 173 | + ); | |
| 174 | +} | |
| 175 | + | |
| 176 | +function AnchoredToasts() { | |
| 177 | + const { toasts } = Toast.useToastManager(); | |
| 178 | + | |
| 179 | + return ( | |
| 180 | + <Toast.Portal data-slot="toast-portal-anchored"> | |
| 181 | + <Toast.Viewport | |
| 182 | + className="outline-none" | |
| 183 | + data-slot="toast-viewport-anchored" | |
| 184 | + > | |
| 185 | + {toasts.map((toast) => { | |
| 186 | + const Icon = toast.type | |
| 187 | + ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS] | |
| 188 | + : null; | |
| 189 | + const tooltipStyle = | |
| 190 | + (toast.data as { tooltipStyle?: boolean })?.tooltipStyle ?? false; | |
| 191 | + const positionerProps = toast.positionerProps; | |
| 192 | + | |
| 193 | + if (!positionerProps?.anchor) { | |
| 194 | + return null; | |
| 195 | + } | |
| 196 | + | |
| 197 | + return ( | |
| 198 | + <Toast.Positioner | |
| 199 | + className="z-50 max-w-[min(--spacing(64),var(--available-width))]" | |
| 200 | + data-slot="toast-positioner" | |
| 201 | + key={toast.id} | |
| 202 | + sideOffset={positionerProps.sideOffset ?? 4} | |
| 203 | + toast={toast} | |
| 204 | + > | |
| 205 | + <Toast.Root | |
| 206 | + className={cn( | |
| 207 | + "relative text-balance border bg-popover not-dark:bg-clip-padding text-popover-foreground text-xs transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/4%)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]", | |
| 208 | + tooltipStyle | |
| 209 | + ? "rounded-md shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]" | |
| 210 | + : "rounded-lg shadow-lg/5 before:rounded-[calc(var(--radius-lg)-1px)]", | |
| 211 | + )} | |
| 212 | + data-slot="toast-popup" | |
| 213 | + toast={toast} | |
| 214 | + > | |
| 215 | + {tooltipStyle ? ( | |
| 216 | + <Toast.Content className="pointer-events-auto px-2 py-1"> | |
| 217 | + <Toast.Title data-slot="toast-title" /> | |
| 218 | + </Toast.Content> | |
| 219 | + ) : ( | |
| 220 | + <Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm"> | |
| 221 | + <div className="flex gap-2"> | |
| 222 | + {Icon && ( | |
| 223 | + <div | |
| 224 | + className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0" | |
| 225 | + data-slot="toast-icon" | |
| 226 | + > | |
| 227 | + <Icon className="in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80" /> | |
| 228 | + </div> | |
| 229 | + )} | |
| 230 | + | |
| 231 | + <div className="flex flex-col gap-0.5"> | |
| 232 | + <Toast.Title | |
| 233 | + className="font-medium" | |
| 234 | + data-slot="toast-title" | |
| 235 | + /> | |
| 236 | + <Toast.Description | |
| 237 | + className="text-muted-foreground" | |
| 238 | + data-slot="toast-description" | |
| 239 | + /> | |
| 240 | + </div> | |
| 241 | + </div> | |
| 242 | + {toast.actionProps && ( | |
| 243 | + <Toast.Action | |
| 244 | + className={buttonVariants({ size: "xs" })} | |
| 245 | + data-slot="toast-action" | |
| 246 | + > | |
| 247 | + {toast.actionProps.children} | |
| 248 | + </Toast.Action> | |
| 249 | + )} | |
| 250 | + </Toast.Content> | |
| 251 | + )} | |
| 252 | + </Toast.Root> | |
| 253 | + </Toast.Positioner> | |
| 254 | + ); | |
| 255 | + })} | |
| 256 | + </Toast.Viewport> | |
| 257 | + </Toast.Portal> | |
| 258 | + ); | |
| 259 | +} | |
| 260 | + | |
| 261 | +export { | |
| 262 | + ToastProvider, | |
| 263 | + type ToastPosition, | |
| 264 | + toastManager, | |
| 265 | + AnchoredToastProvider, | |
| 266 | + anchoredToastManager, | |
| 267 | +}; | |
| @@ -0,0 +1,90 @@ | ||
| 1 | +import { useCallback, useEffect, useSyncExternalStore } from "react"; | |
| 2 | + | |
| 3 | +type Theme = "light" | "dark" | "system"; | |
| 4 | + | |
| 5 | +const STORAGE_KEY = "codething:theme"; | |
| 6 | +const MEDIA_QUERY = "(prefers-color-scheme: dark)"; | |
| 7 | + | |
| 8 | +let listeners: Array<() => void> = []; | |
| 9 | +function emitChange() { | |
| 10 | + for (const listener of listeners) listener(); | |
| 11 | +} | |
| 12 | + | |
| 13 | +function getSystemDark(): boolean { | |
| 14 | + return window.matchMedia(MEDIA_QUERY).matches; | |
| 15 | +} | |
| 16 | + | |
| 17 | +function getStored(): Theme { | |
| 18 | + const raw = localStorage.getItem(STORAGE_KEY); | |
| 19 | + if (raw === "light" || raw === "dark" || raw === "system") return raw; | |
| 20 | + return "system"; | |
| 21 | +} | |
| 22 | + | |
| 23 | +function applyTheme(theme: Theme, suppressTransitions = false) { | |
| 24 | + if (suppressTransitions) { | |
| 25 | + document.documentElement.classList.add("no-transitions"); | |
| 26 | + } | |
| 27 | + const isDark = theme === "dark" || (theme === "system" && getSystemDark()); | |
| 28 | + document.documentElement.classList.toggle("dark", isDark); | |
| 29 | + if (suppressTransitions) { | |
| 30 | + // Force a reflow so the no-transitions class takes effect before removal | |
| 31 | + document.documentElement.offsetHeight; | |
| 32 | + requestAnimationFrame(() => { | |
| 33 | + document.documentElement.classList.remove("no-transitions"); | |
| 34 | + }); | |
| 35 | + } | |
| 36 | +} | |
| 37 | + | |
| 38 | +// Apply immediately on module load to prevent flash | |
| 39 | +applyTheme(getStored()); | |
| 40 | + | |
| 41 | +function getSnapshot(): Theme { | |
| 42 | + return getStored(); | |
| 43 | +} | |
| 44 | + | |
| 45 | +function subscribe(listener: () => void): () => void { | |
| 46 | + listeners.push(listener); | |
| 47 | + | |
| 48 | + // Listen for system preference changes | |
| 49 | + const mq = window.matchMedia(MEDIA_QUERY); | |
| 50 | + const handleChange = () => { | |
| 51 | + if (getStored() === "system") applyTheme("system", true); | |
| 52 | + emitChange(); | |
| 53 | + }; | |
| 54 | + mq.addEventListener("change", handleChange); | |
| 55 | + | |
| 56 | + // Listen for storage changes from other tabs | |
| 57 | + const handleStorage = (e: StorageEvent) => { | |
| 58 | + if (e.key === STORAGE_KEY) { | |
| 59 | + applyTheme(getStored(), true); | |
| 60 | + emitChange(); | |
| 61 | + } | |
| 62 | + }; | |
| 63 | + window.addEventListener("storage", handleStorage); | |
| 64 | + | |
| 65 | + return () => { | |
| 66 | + listeners = listeners.filter((l) => l !== listener); | |
| 67 | + mq.removeEventListener("change", handleChange); | |
| 68 | + window.removeEventListener("storage", handleStorage); | |
| 69 | + }; | |
| 70 | +} | |
| 71 | + | |
| 72 | +export function useTheme() { | |
| 73 | + const theme = useSyncExternalStore(subscribe, getSnapshot); | |
| 74 | + | |
| 75 | + const resolvedTheme: "light" | "dark" = | |
| 76 | + theme === "system" ? (getSystemDark() ? "dark" : "light") : theme; | |
| 77 | + | |
| 78 | + const setTheme = useCallback((next: Theme) => { | |
| 79 | + localStorage.setItem(STORAGE_KEY, next); | |
| 80 | + applyTheme(next, true); | |
| 81 | + emitChange(); | |
| 82 | + }, []); | |
| 83 | + | |
| 84 | + // Keep DOM in sync on mount/change | |
| 85 | + useEffect(() => { | |
| 86 | + applyTheme(theme); | |
| 87 | + }, [theme]); | |
| 88 | + | |
| 89 | + return { theme, setTheme, resolvedTheme } as const; | |
| 90 | +} | |
| @@ -1,7 +1,178 @@ | ||
| 1 | 1 | @import "tailwindcss"; |
| 2 | 2 | |
| 3 | +@custom-variant dark (&:is(.dark, .dark *)); | |
| 4 | + | |
| 5 | +@theme inline { | |
| 6 | + --animate-skeleton: skeleton 2s -1s infinite linear; | |
| 7 | + --color-warning-foreground: var(--warning-foreground); | |
| 8 | + --color-warning: var(--warning); | |
| 9 | + --color-success-foreground: var(--success-foreground); | |
| 10 | + --color-success: var(--success); | |
| 11 | + --color-info-foreground: var(--info-foreground); | |
| 12 | + --color-info: var(--info); | |
| 13 | + --color-destructive-foreground: var(--destructive-foreground); | |
| 14 | + --color-sidebar-ring: var(--sidebar-ring); | |
| 15 | + --color-sidebar-border: var(--sidebar-border); | |
| 16 | + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); | |
| 17 | + --color-sidebar-accent: var(--sidebar-accent); | |
| 18 | + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); | |
| 19 | + --color-sidebar-primary: var(--sidebar-primary); | |
| 20 | + --color-sidebar-foreground: var(--sidebar-foreground); | |
| 21 | + --color-sidebar: var(--sidebar); | |
| 22 | + --color-chart-5: var(--chart-5); | |
| 23 | + --color-chart-4: var(--chart-4); | |
| 24 | + --color-chart-3: var(--chart-3); | |
| 25 | + --color-chart-2: var(--chart-2); | |
| 26 | + --color-chart-1: var(--chart-1); | |
| 27 | + --color-ring: var(--ring); | |
| 28 | + --color-input: var(--input); | |
| 29 | + --color-border: var(--border); | |
| 30 | + --color-destructive: var(--destructive); | |
| 31 | + --color-accent-foreground: var(--accent-foreground); | |
| 32 | + --color-accent: var(--accent); | |
| 33 | + --color-muted-foreground: var(--muted-foreground); | |
| 34 | + --color-muted: var(--muted); | |
| 35 | + --color-secondary-foreground: var(--secondary-foreground); | |
| 36 | + --color-secondary: var(--secondary); | |
| 37 | + --color-primary-foreground: var(--primary-foreground); | |
| 38 | + --color-primary: var(--primary); | |
| 39 | + --color-popover-foreground: var(--popover-foreground); | |
| 40 | + --color-popover: var(--popover); | |
| 41 | + --color-card-foreground: var(--card-foreground); | |
| 42 | + --color-card: var(--card); | |
| 43 | + --color-foreground: var(--foreground); | |
| 44 | + --color-background: var(--background); | |
| 45 | + --radius-sm: calc(var(--radius) - 4px); | |
| 46 | + --radius-md: calc(var(--radius) - 2px); | |
| 47 | + --radius-lg: var(--radius); | |
| 48 | + --radius-xl: calc(var(--radius) + 4px); | |
| 49 | + --radius-2xl: calc(var(--radius) + 8px); | |
| 50 | + --radius-3xl: calc(var(--radius) + 12px); | |
| 51 | + --radius-4xl: calc(var(--radius) + 16px); | |
| 52 | + @keyframes skeleton { | |
| 53 | + to { | |
| 54 | + background-position: -200% 0; | |
| 55 | + } | |
| 56 | + } | |
| 57 | +} | |
| 58 | + | |
| 59 | +@layer base { | |
| 60 | + * { | |
| 61 | + @apply border-border outline-ring/50; | |
| 62 | + } | |
| 63 | + body { | |
| 64 | + @apply bg-background text-foreground; | |
| 65 | + } | |
| 66 | +} | |
| 67 | + | |
| 68 | +/* Suppress all transitions during theme changes */ | |
| 69 | +.no-transitions, | |
| 70 | +.no-transitions *, | |
| 71 | +.no-transitions *::before, | |
| 72 | +.no-transitions *::after { | |
| 73 | + transition-duration: 0s !important; | |
| 74 | + animation-duration: 0s !important; | |
| 75 | +} | |
| 76 | + | |
| 3 | 77 | :root { |
| 4 | − color-scheme: dark; | |
| 78 | + color-scheme: light; | |
| 79 | + --radius: 0.625rem; | |
| 80 | + --background: var(--color-white); | |
| 81 | + --foreground: var(--color-neutral-800); | |
| 82 | + --card: var(--color-white); | |
| 83 | + --card-foreground: var(--color-neutral-800); | |
| 84 | + --popover: var(--color-white); | |
| 85 | + --popover-foreground: var(--color-neutral-800); | |
| 86 | + --primary: var(--color-neutral-800); | |
| 87 | + --primary-foreground: var(--color-neutral-50); | |
| 88 | + --secondary: --alpha(var(--color-black) / 4%); | |
| 89 | + --secondary-foreground: var(--color-neutral-800); | |
| 90 | + --muted: --alpha(var(--color-black) / 4%); | |
| 91 | + --muted-foreground: color-mix( | |
| 92 | + in srgb, | |
| 93 | + var(--color-neutral-500) 90%, | |
| 94 | + var(--color-black) | |
| 95 | + ); | |
| 96 | + --accent: --alpha(var(--color-black) / 4%); | |
| 97 | + --accent-foreground: var(--color-neutral-800); | |
| 98 | + --destructive: var(--color-red-500); | |
| 99 | + --border: --alpha(var(--color-black) / 8%); | |
| 100 | + --input: --alpha(var(--color-black) / 10%); | |
| 101 | + --ring: var(--color-neutral-400); | |
| 102 | + --chart-1: oklch(0.646 0.222 41.116); | |
| 103 | + --chart-2: oklch(0.6 0.118 184.704); | |
| 104 | + --chart-3: oklch(0.398 0.07 227.392); | |
| 105 | + --chart-4: oklch(0.828 0.189 84.429); | |
| 106 | + --chart-5: oklch(0.769 0.188 70.08); | |
| 107 | + --sidebar: oklch(0.985 0 0); | |
| 108 | + --sidebar-foreground: oklch(0.141 0.005 285.823); | |
| 109 | + --sidebar-primary: oklch(0.21 0.006 285.885); | |
| 110 | + --sidebar-primary-foreground: oklch(0.985 0 0); | |
| 111 | + --sidebar-accent: oklch(0.967 0.001 286.375); | |
| 112 | + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); | |
| 113 | + --sidebar-border: oklch(0.92 0.004 286.32); | |
| 114 | + --sidebar-ring: oklch(0.705 0.015 286.067); | |
| 115 | + --destructive-foreground: var(--color-red-700); | |
| 116 | + --info: var(--color-blue-500); | |
| 117 | + --info-foreground: var(--color-blue-700); | |
| 118 | + --success: var(--color-emerald-500); | |
| 119 | + --success-foreground: var(--color-emerald-700); | |
| 120 | + --warning: var(--color-amber-500); | |
| 121 | + --warning-foreground: var(--color-amber-700); | |
| 122 | + | |
| 123 | + @variant dark { | |
| 124 | + color-scheme: dark; | |
| 125 | + --background: color-mix( | |
| 126 | + in srgb, | |
| 127 | + var(--color-neutral-950) 95%, | |
| 128 | + var(--color-white) | |
| 129 | + ); | |
| 130 | + --foreground: var(--color-neutral-100); | |
| 131 | + --card: color-mix(in srgb, var(--background) 98%, var(--color-white)); | |
| 132 | + --card-foreground: var(--color-neutral-100); | |
| 133 | + --popover: color-mix(in srgb, var(--background) 98%, var(--color-white)); | |
| 134 | + --popover-foreground: var(--color-neutral-100); | |
| 135 | + --primary: var(--color-neutral-100); | |
| 136 | + --primary-foreground: var(--color-neutral-800); | |
| 137 | + --secondary: --alpha(var(--color-white) / 4%); | |
| 138 | + --secondary-foreground: var(--color-neutral-100); | |
| 139 | + --muted: --alpha(var(--color-white) / 4%); | |
| 140 | + --muted-foreground: color-mix( | |
| 141 | + in srgb, | |
| 142 | + var(--color-neutral-500) 90%, | |
| 143 | + var(--color-white) | |
| 144 | + ); | |
| 145 | + --accent: --alpha(var(--color-white) / 4%); | |
| 146 | + --accent-foreground: var(--color-neutral-100); | |
| 147 | + --destructive: color-mix( | |
| 148 | + in srgb, | |
| 149 | + var(--color-red-500) 90%, | |
| 150 | + var(--color-white) | |
| 151 | + ); | |
| 152 | + --border: --alpha(var(--color-white) / 6%); | |
| 153 | + --input: --alpha(var(--color-white) / 8%); | |
| 154 | + --ring: var(--color-neutral-500); | |
| 155 | + --chart-1: oklch(0.488 0.243 264.376); | |
| 156 | + --chart-2: oklch(0.696 0.17 162.48); | |
| 157 | + --chart-3: oklch(0.769 0.188 70.08); | |
| 158 | + --chart-4: oklch(0.627 0.265 303.9); | |
| 159 | + --chart-5: oklch(0.645 0.246 16.439); | |
| 160 | + --sidebar: oklch(0.21 0.006 285.885); | |
| 161 | + --sidebar-foreground: oklch(0.985 0 0); | |
| 162 | + --sidebar-primary: oklch(0.488 0.243 264.376); | |
| 163 | + --sidebar-primary-foreground: oklch(0.985 0 0); | |
| 164 | + --sidebar-accent: oklch(0.274 0.006 286.033); | |
| 165 | + --sidebar-accent-foreground: oklch(0.985 0 0); | |
| 166 | + --sidebar-border: oklch(1 0 0 / 10%); | |
| 167 | + --sidebar-ring: oklch(0.552 0.016 285.938); | |
| 168 | + --destructive-foreground: var(--color-red-400); | |
| 169 | + --info: var(--color-blue-500); | |
| 170 | + --info-foreground: var(--color-blue-400); | |
| 171 | + --success: var(--color-emerald-500); | |
| 172 | + --success-foreground: var(--color-emerald-400); | |
| 173 | + --warning: var(--color-amber-500); | |
| 174 | + --warning-foreground: var(--color-amber-400); | |
| 175 | + } | |
| 5 | 176 | } |
| 6 | 177 | |
| 7 | 178 | body { |
| @@ -10,8 +181,6 @@ body { · 2 unmodified lines | ||
| 10 | 181 | margin: 0; |
| 11 | 182 | padding: 0; |
| 12 | 183 | min-height: 100vh; |
| 13 | − color: #e0e0e0; | |
| 14 | − background: #0c0c0c; | |
| 15 | 184 | } |
| 16 | 185 | |
| 17 | 186 | body::after { |
| @@ -55,25 +224,20 @@ input { · 37 unmodified lines | ||
| 55 | 224 | } |
| 56 | 225 | |
| 57 | 226 | ::-webkit-scrollbar-thumb { |
| 58 | − background: rgba(255, 255, 255, 0.1); | |
| 227 | + background: rgba(0, 0, 0, 0.15); | |
| 59 | 228 | border-radius: 3px; |
| 60 | 229 | } |
| 61 | 230 | |
| 62 | 231 | ::-webkit-scrollbar-thumb:hover { |
| 63 | − background: rgba(255, 255, 255, 0.18); | |
| 232 | + background: rgba(0, 0, 0, 0.25); | |
| 64 | 233 | } |
| 65 | 234 | |
| 66 | −/* Reasoning select — clickable label surface */ | |
| 67 | −label:has(> select#reasoning-effort) { | |
| 68 | − position: relative; | |
| 235 | +.dark ::-webkit-scrollbar-thumb { | |
| 236 | + background: rgba(255, 255, 255, 0.1); | |
| 69 | 237 | } |
| 70 | −label:has(> select#reasoning-effort) select { | |
| 71 | − position: absolute; | |
| 72 | − inset: 0; | |
| 73 | − opacity: 0; | |
| 74 | − cursor: pointer; | |
| 75 | − width: 100%; | |
| 76 | − height: 100%; | |
| 238 | + | |
| 239 | +.dark ::-webkit-scrollbar-thumb:hover { | |
| 240 | + background: rgba(255, 255, 255, 0.18); | |
| 77 | 241 | } |
| 78 | 242 | |
| 79 | 243 | /* Chat markdown rendering */ |
| @@ -108,34 +272,38 @@ label:has(> select#reasoning-effort) select { · 28 unmodified lines | ||
| 108 | 272 | } |
| 109 | 273 | |
| 110 | 274 | .chat-markdown a { |
| 111 | − color: #93c5fd; | |
| 275 | + color: var(--info-foreground); | |
| 112 | 276 | text-decoration: underline; |
| 113 | − text-decoration-color: rgba(147, 197, 253, 0.4); | |
| 277 | + text-decoration-color: color-mix( | |
| 278 | + in srgb, | |
| 279 | + var(--info-foreground) 40%, | |
| 280 | + transparent | |
| 281 | + ); | |
| 114 | 282 | } |
| 115 | 283 | |
| 116 | 284 | .chat-markdown a:hover { |
| 117 | − color: #bfdbfe; | |
| 285 | + opacity: 0.8; | |
| 118 | 286 | } |
| 119 | 287 | |
| 120 | 288 | .chat-markdown blockquote { |
| 121 | − border-left: 2px solid rgba(255, 255, 255, 0.2); | |
| 289 | + border-left: 2px solid var(--border); | |
| 122 | 290 | padding-left: 0.8rem; |
| 123 | − color: rgba(224, 224, 224, 0.75); | |
| 291 | + color: var(--muted-foreground); | |
| 124 | 292 | } |
| 125 | 293 | |
| 126 | 294 | .chat-markdown :not(pre) > code { |
| 127 | − border: 1px solid rgba(255, 255, 255, 0.12); | |
| 295 | + border: 1px solid var(--border); | |
| 128 | 296 | border-radius: 0.375rem; |
| 129 | − background: rgba(255, 255, 255, 0.07); | |
| 297 | + background: var(--muted); | |
| 130 | 298 | padding: 0.1rem 0.35rem; |
| 131 | − color: #f4f4f5; | |
| 299 | + color: var(--foreground); | |
| 132 | 300 | } |
| 133 | 301 | |
| 134 | 302 | .chat-markdown pre { |
| 135 | 303 | overflow-x: auto; |
| 136 | − border: 1px solid rgba(255, 255, 255, 0.12); | |
| 304 | + border: 1px solid var(--border); | |
| 137 | 305 | border-radius: 0.75rem; |
| 138 | − background: rgba(0, 0, 0, 0.3); | |
| 306 | + background: var(--muted); | |
| 139 | 307 | padding: 0.8rem 0.9rem; |
| 140 | 308 | } |
| 141 | 309 | |
| @@ -153,7 +321,7 @@ label:has(> select#reasoning-effort) select { · 11 unmodified lines | ||
| 153 | 321 | |
| 154 | 322 | .chat-markdown th, |
| 155 | 323 | .chat-markdown td { |
| 156 | − border: 1px solid rgba(255, 255, 255, 0.12); | |
| 324 | + border: 1px solid var(--border); | |
| 157 | 325 | padding: 0.35rem 0.45rem; |
| 158 | 326 | text-align: left; |
| 159 | 327 | } |
| @@ -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 | +} | |
| @@ -4,7 +4,11 @@ · 3 unmodified lines | ||
| 4 | 4 | "composite": true, |
| 5 | 5 | "jsx": "react-jsx", |
| 6 | 6 | "lib": ["ES2023", "DOM", "DOM.Iterable"], |
| 7 | − "types": ["vite/client"] | |
| 7 | + "exactOptionalPropertyTypes": false, | |
| 8 | + "types": ["vite/client"], | |
| 9 | + "paths": { | |
| 10 | + "#/*": ["./src/*"] | |
| 11 | + } | |
| 8 | 12 | }, |
| 9 | 13 | "include": ["src", "vite.config.ts"] |
| 10 | 14 | } |
| @@ -1,3 +1,4 @@ | ||
| 1 | +import path from "node:path"; | |
| 1 | 2 | import tailwindcss from "@tailwindcss/vite"; |
| 2 | 3 | import react from "@vitejs/plugin-react"; |
| 3 | 4 | import { defineConfig } from "vite"; |
| @@ -11,6 +12,11 @@ export default defineConfig({ · 7 unmodified lines | ||
| 11 | 12 | }), |
| 12 | 13 | tailwindcss(), |
| 13 | 14 | ], |
| 15 | + resolve: { | |
| 16 | + alias: { | |
| 17 | + "#": path.resolve(__dirname, "src"), | |
| 18 | + }, | |
| 19 | + }, | |
| 14 | 20 | server: { |
| 15 | 21 | port: Number(process.env.ELECTRON_RENDERER_PORT ?? 5173), |
| 16 | 22 | strictPort: true, |
| @@ -34,12 +34,17 @@ · 33 unmodified lines | ||
| 34 | 34 | "version": "0.0.0", |
| 35 | 35 | "dependencies": { |
| 36 | 36 | "@acme/contracts": "workspace:*", |
| 37 | + "@base-ui/react": "^1.1.0", | |
| 38 | + "class-variance-authority": "^0.7.1", | |
| 39 | + "clsx": "^2.1.1", | |
| 37 | 40 | "highlight.js": "^11.11.1", |
| 41 | + "lucide-react": "^0.563.0", | |
| 38 | 42 | "react": "^19.0.0", |
| 39 | 43 | "react-dom": "^19.0.0", |
| 40 | 44 | "react-markdown": "^10.1.0", |
| 41 | 45 | "rehype-highlight": "^7.0.2", |
| 42 | 46 | "remark-gfm": "^4.0.1", |
| 47 | + "tailwind-merge": "^3.4.0", | |
| 43 | 48 | "zod": "^3.24.1", |
| 44 | 49 | }, |
| 45 | 50 | "devDependencies": { |
| @@ -104,12 +109,18 @@ · 58 unmodified lines | ||
| 104 | 109 | |
| 105 | 110 | "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], |
| 106 | 111 | |
| 112 | + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], | |
| 113 | + | |
| 107 | 114 | "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], |
| 108 | 115 | |
| 109 | 116 | "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], |
| 110 | 117 | |
| 111 | 118 | "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], |
| 112 | 119 | |
| 120 | + "@base-ui/react": ["@base-ui/react@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@base-ui/utils": "0.2.4", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-ikcJRNj1mOiF2HZ5jQHrXoVoHcNHdBU5ejJljcBl+VTLoYXR6FidjTN86GjO6hyshi6TZFuNvv0dEOgaOFv6Lw=="], | |
| 121 | + | |
| 122 | + "@base-ui/utils": ["@base-ui/utils@0.2.4", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-smZwpMhjO29v+jrZusBSc5T+IJ3vBb9cjIiBjtKcvWmRj9Z4DWGVR3efr1eHR56/bqY5a4qyY9ElkOY5ljo3ng=="], | |
| 123 | + | |
| 113 | 124 | "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], |
| 114 | 125 | |
| 115 | 126 | "@electron/node-gyp": ["@electron/node-gyp@github:electron/node-gyp#06b29aa", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": "./bin/node-gyp.js" }, "electron-node-gyp-06b29aa"], |
| @@ -168,6 +179,14 @@ · 52 unmodified lines | ||
| 168 | 179 | |
| 169 | 180 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], |
| 170 | 181 | |
| 182 | + "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], | |
| 183 | + | |
| 184 | + "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], | |
| 185 | + | |
| 186 | + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="], | |
| 187 | + | |
| 188 | + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], | |
| 189 | + | |
| 171 | 190 | "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="], |
| 172 | 191 | |
| 173 | 192 | "@hapi/address": ["@hapi/address@5.1.1", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA=="], |
| @@ -182,6 +201,16 @@ · 8 unmodified lines | ||
| 182 | 201 | |
| 183 | 202 | "@hapi/topo": ["@hapi/topo@6.0.2", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg=="], |
| 184 | 203 | |
| 204 | + "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], | |
| 205 | + | |
| 206 | + "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="], | |
| 207 | + | |
| 208 | + "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], | |
| 209 | + | |
| 210 | + "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], | |
| 211 | + | |
| 212 | + "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], | |
| 213 | + | |
| 185 | 214 | "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], |
| 186 | 215 | |
| 187 | 216 | "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], |
| @@ -194,10 +223,18 @@ · 6 unmodified lines | ||
| 194 | 223 | |
| 195 | 224 | "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], |
| 196 | 225 | |
| 226 | + "@mswjs/interceptors": ["@mswjs/interceptors@0.41.2", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-7G0Uf0yK3f2bjElBLGHIQzgRgMESczOMyYVasq1XK8P5HaXtlW4eQhz9MBL+TQILZLaruq+ClGId+hH0w4jvWw=="], | |
| 227 | + | |
| 197 | 228 | "@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="], |
| 198 | 229 | |
| 199 | 230 | "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="], |
| 200 | 231 | |
| 232 | + "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], | |
| 233 | + | |
| 234 | + "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="], | |
| 235 | + | |
| 236 | + "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], | |
| 237 | + | |
| 201 | 238 | "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jmUfF7cNJPw57bEK7sMIqrYRgn4LH428tSgtgLTCtjuGuu1ShREyrkeB7y8HtkXRfhBs4lVY+HMLhqElJvZ6ww=="], |
| 202 | 239 | |
| 203 | 240 | "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-S6vlV8S7jbjzJOSjfVg2CimUC0r7/aHDLdUm/3+/B/SU/s1jV7ivqWkMv1/8EB43d1BBwT9JQ60ZMTkBqeXSFA=="], |
| @@ -358,6 +395,8 @@ · 154 unmodified lines | ||
| 358 | 395 | |
| 359 | 396 | "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], |
| 360 | 397 | |
| 398 | + "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], | |
| 399 | + | |
| 361 | 400 | "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], |
| 362 | 401 | |
| 363 | 402 | "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], |
| @@ -460,18 +499,24 @@ · 96 unmodified lines | ||
| 460 | 499 | |
| 461 | 500 | "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], |
| 462 | 501 | |
| 502 | + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], | |
| 503 | + | |
| 463 | 504 | "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], |
| 464 | 505 | |
| 465 | 506 | "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], |
| 466 | 507 | |
| 467 | 508 | "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], |
| 468 | 509 | |
| 510 | + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], | |
| 511 | + | |
| 469 | 512 | "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], |
| 470 | 513 | |
| 471 | 514 | "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], |
| 472 | 515 | |
| 473 | 516 | "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], |
| 474 | 517 | |
| 518 | + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], | |
| 519 | + | |
| 475 | 520 | "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], |
| 476 | 521 | |
| 477 | 522 | "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], |
| @@ -490,6 +535,8 @@ · 12 unmodified lines | ||
| 490 | 535 | |
| 491 | 536 | "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], |
| 492 | 537 | |
| 538 | + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], | |
| 539 | + | |
| 493 | 540 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], |
| 494 | 541 | |
| 495 | 542 | "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], |
| @@ -614,6 +661,8 @@ · 118 unmodified lines | ||
| 614 | 661 | |
| 615 | 662 | "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], |
| 616 | 663 | |
| 664 | + "graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="], | |
| 665 | + | |
| 617 | 666 | "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], |
| 618 | 667 | |
| 619 | 668 | "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], |
| @@ -632,6 +681,8 @@ · 12 unmodified lines | ||
| 632 | 681 | |
| 633 | 682 | "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], |
| 634 | 683 | |
| 684 | + "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], | |
| 685 | + | |
| 635 | 686 | "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], |
| 636 | 687 | |
| 637 | 688 | "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], |
| @@ -680,6 +731,8 @@ · 42 unmodified lines | ||
| 680 | 731 | |
| 681 | 732 | "is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="], |
| 682 | 733 | |
| 734 | + "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], | |
| 735 | + | |
| 683 | 736 | "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], |
| 684 | 737 | |
| 685 | 738 | "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], |
| @@ -754,6 +807,8 @@ · 68 unmodified lines | ||
| 754 | 807 | |
| 755 | 808 | "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], |
| 756 | 809 | |
| 810 | + "lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="], | |
| 811 | + | |
| 757 | 812 | "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], |
| 758 | 813 | |
| 759 | 814 | "make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="], |
| @@ -884,6 +939,10 @@ · 124 unmodified lines | ||
| 884 | 939 | |
| 885 | 940 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], |
| 886 | 941 | |
| 942 | + "msw": ["msw@2.12.9", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-NYbi51C6M3dujGmcmuGemu68jy12KqQPoVWGeroKToLGsBgrwG5ErM8WctoIIg49/EV49SEvYM9WSqO4G7kNeQ=="], | |
| 943 | + | |
| 944 | + "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], | |
| 945 | + | |
| 887 | 946 | "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], |
| 888 | 947 | |
| 889 | 948 | "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], |
| @@ -916,6 +975,8 @@ · 26 unmodified lines | ||
| 916 | 975 | |
| 917 | 976 | "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], |
| 918 | 977 | |
| 978 | + "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], | |
| 979 | + | |
| 919 | 980 | "oxfmt": ["oxfmt@0.28.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.28.0", "@oxfmt/darwin-x64": "0.28.0", "@oxfmt/linux-arm64-gnu": "0.28.0", "@oxfmt/linux-arm64-musl": "0.28.0", "@oxfmt/linux-x64-gnu": "0.28.0", "@oxfmt/linux-x64-musl": "0.28.0", "@oxfmt/win32-arm64": "0.28.0", "@oxfmt/win32-x64": "0.28.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-3+hhBqPE6Kp22KfJmnstrZbl+KdOVSEu1V0ABaFIg1rYLtrMgrupx9znnHgHLqKxAVHebjTdiCJDk30CXOt6cw=="], |
| 920 | 981 | |
| 921 | 982 | "oxlint": ["oxlint@1.43.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.43.0", "@oxlint/darwin-x64": "1.43.0", "@oxlint/linux-arm64-gnu": "1.43.0", "@oxlint/linux-arm64-musl": "1.43.0", "@oxlint/linux-x64-gnu": "1.43.0", "@oxlint/linux-x64-musl": "1.43.0", "@oxlint/win32-arm64": "1.43.0", "@oxlint/win32-x64": "1.43.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.11.2" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-xiqTCsKZch+R61DPCjyqUVP2MhkQlRRYxLRBeBDi+dtQJ90MOgdcjIktvDCgXz0bgtx94EQzHEndsizZjMX2OA=="], |
| @@ -930,6 +991,8 @@ · 8 unmodified lines | ||
| 930 | 991 | |
| 931 | 992 | "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], |
| 932 | 993 | |
| 994 | + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], | |
| 995 | + | |
| 933 | 996 | "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], |
| 934 | 997 | |
| 935 | 998 | "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], |
| @@ -994,6 +1057,8 @@ · 58 unmodified lines | ||
| 994 | 1057 | |
| 995 | 1058 | "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], |
| 996 | 1059 | |
| 1060 | + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], | |
| 1061 | + | |
| 997 | 1062 | "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], |
| 998 | 1063 | |
| 999 | 1064 | "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], |
| @@ -1004,6 +1069,8 @@ · 4 unmodified lines | ||
| 1004 | 1069 | |
| 1005 | 1070 | "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], |
| 1006 | 1071 | |
| 1072 | + "rettime": ["rettime@0.10.1", "", {}, "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw=="], | |
| 1073 | + | |
| 1007 | 1074 | "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], |
| 1008 | 1075 | |
| 1009 | 1076 | "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], |
| @@ -1052,8 +1119,12 @@ · 42 unmodified lines | ||
| 1052 | 1119 | |
| 1053 | 1120 | "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], |
| 1054 | 1121 | |
| 1122 | + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], | |
| 1123 | + | |
| 1055 | 1124 | "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], |
| 1056 | 1125 | |
| 1126 | + "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], | |
| 1127 | + | |
| 1057 | 1128 | "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], |
| 1058 | 1129 | |
| 1059 | 1130 | "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], |
| @@ -1074,6 +1145,12 @@ · 14 unmodified lines | ||
| 1074 | 1145 | |
| 1075 | 1146 | "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], |
| 1076 | 1147 | |
| 1148 | + "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], | |
| 1149 | + | |
| 1150 | + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], | |
| 1151 | + | |
| 1152 | + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], | |
| 1153 | + | |
| 1077 | 1154 | "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], |
| 1078 | 1155 | |
| 1079 | 1156 | "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], |
| @@ -1096,8 +1173,14 @@ · 16 unmodified lines | ||
| 1096 | 1173 | |
| 1097 | 1174 | "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], |
| 1098 | 1175 | |
| 1176 | + "tldts": ["tldts@7.0.23", "", { "dependencies": { "tldts-core": "^7.0.23" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw=="], | |
| 1177 | + | |
| 1178 | + "tldts-core": ["tldts-core@7.0.23", "", {}, "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ=="], | |
| 1179 | + | |
| 1099 | 1180 | "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], |
| 1100 | 1181 | |
| 1182 | + "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], | |
| 1183 | + | |
| 1101 | 1184 | "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], |
| 1102 | 1185 | |
| 1103 | 1186 | "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], |
| @@ -1124,7 +1207,7 @@ · 20 unmodified lines | ||
| 1124 | 1207 | |
| 1125 | 1208 | "turbo-windows-arm64": ["turbo-windows-arm64@2.8.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-afTUGKBRmOJU1smQSBnFGcbq0iabAPwh1uXu2BVk7BREg30/1gMnJh9DFEQTah+UD3n3ru8V55J83RQNFfqoyw=="], |
| 1126 | 1209 | |
| 1127 | − "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], | |
| 1210 | + "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="], | |
| 1128 | 1211 | |
| 1129 | 1212 | "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], |
| 1130 | 1213 | |
| @@ -1154,8 +1237,12 @@ · 23 unmodified lines | ||
| 1154 | 1237 | |
| 1155 | 1238 | "unixify": ["unixify@1.0.0", "", { "dependencies": { "normalize-path": "^2.1.1" } }, "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg=="], |
| 1156 | 1239 | |
| 1240 | + "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], | |
| 1241 | + | |
| 1157 | 1242 | "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], |
| 1158 | 1243 | |
| 1244 | + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], | |
| 1245 | + | |
| 1159 | 1246 | "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], |
| 1160 | 1247 | |
| 1161 | 1248 | "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], |
| @@ -1192,6 +1279,8 @@ · 30 unmodified lines | ||
| 1192 | 1279 | |
| 1193 | 1280 | "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], |
| 1194 | 1281 | |
| 1282 | + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], | |
| 1283 | + | |
| 1195 | 1284 | "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], |
| 1196 | 1285 | |
| 1197 | 1286 | "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], |
| @@ -1208,6 +1297,10 @@ · 10 unmodified lines | ||
| 1208 | 1297 | |
| 1209 | 1298 | "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], |
| 1210 | 1299 | |
| 1300 | + "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], | |
| 1301 | + | |
| 1302 | + "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], | |
| 1303 | + | |
| 1211 | 1304 | "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], |
| 1212 | 1305 | |
| 1213 | 1306 | "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], |
| @@ -1254,6 +1347,8 @@ · 40 unmodified lines | ||
| 1254 | 1347 | |
| 1255 | 1348 | "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], |
| 1256 | 1349 | |
| 1350 | + "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], | |
| 1351 | + | |
| 1257 | 1352 | "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], |
| 1258 | 1353 | |
| 1259 | 1354 | "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], |