@weichenlin/swell-ui 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3453 @@
1
+ import { Check, ChevronDown, CircleAlert, CircleCheck, CircleDashed, CircleX, Copy, Eye, EyeOff, Info, LoaderCircle, Menu, Moon, Search, Sun, TriangleAlert, X } from "lucide-react";
2
+ import { clsx } from "clsx";
3
+ import { extendTailwindMerge } from "tailwind-merge";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { cva } from "class-variance-authority";
6
+ import { Children, cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useId, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
7
+ import { createPortal } from "react-dom";
8
+ //#region src/lib/cn.ts
9
+ const tone = (name) => [
10
+ name,
11
+ `${name}-fg`,
12
+ `${name}-subtle-bg`,
13
+ `${name}-subtle-fg`,
14
+ `${name}-border`
15
+ ];
16
+ const twMerge = extendTailwindMerge({ extend: { theme: {
17
+ color: [
18
+ "bg",
19
+ "surface",
20
+ "surface-raised",
21
+ "surface-muted",
22
+ "fg",
23
+ "fg-strong",
24
+ "fg-muted",
25
+ "fg-subtle",
26
+ "border",
27
+ "border-strong",
28
+ "ring",
29
+ "ring-deep",
30
+ "primary",
31
+ "primary-fg",
32
+ "accent",
33
+ "inverse",
34
+ "inverse-fg",
35
+ "focus",
36
+ "input-bg",
37
+ "input-bg-hover",
38
+ "input-border",
39
+ "input-border-hover",
40
+ "input-placeholder",
41
+ "input-focus",
42
+ "input-highlight",
43
+ "input-halo",
44
+ "input-halo-invalid",
45
+ "input-halo-invalid-focus",
46
+ "control-checked",
47
+ "control-checked-fg",
48
+ "popover-bg",
49
+ "popover-border",
50
+ "popover-highlight",
51
+ "dialog-backdrop",
52
+ ...[
53
+ "success",
54
+ "warning",
55
+ "danger",
56
+ "info",
57
+ "neutral"
58
+ ].flatMap(tone)
59
+ ],
60
+ shadow: [
61
+ "input",
62
+ "input-lift",
63
+ "switch-thumb",
64
+ "popover",
65
+ "dialog",
66
+ "whisper"
67
+ ],
68
+ container: ["page"],
69
+ font: [
70
+ "sans",
71
+ "serif",
72
+ "mono"
73
+ ]
74
+ } } });
75
+ function cn(...inputs) {
76
+ return twMerge(clsx(inputs));
77
+ }
78
+ //#endregion
79
+ //#region src/components/badge/badge.tsx
80
+ const toneClasses$1 = {
81
+ neutral: {
82
+ subtle: ["border-neutral-border bg-neutral-subtle-bg text-neutral-subtle-fg", "text-neutral"],
83
+ outline: ["border-neutral-border bg-transparent text-neutral-subtle-fg", "text-neutral"],
84
+ solid: ["border-neutral bg-neutral text-neutral-fg", "text-neutral-fg"]
85
+ },
86
+ info: {
87
+ subtle: ["border-info-border bg-info-subtle-bg text-info-subtle-fg", "text-info"],
88
+ outline: ["border-info-border bg-transparent text-info-subtle-fg", "text-info"],
89
+ solid: ["border-info bg-info text-info-fg", "text-info-fg"]
90
+ },
91
+ success: {
92
+ subtle: ["border-success-border bg-success-subtle-bg text-success-subtle-fg", "text-success"],
93
+ outline: ["border-success-border bg-transparent text-success-subtle-fg", "text-success"],
94
+ solid: ["border-success bg-success text-success-fg", "text-success-fg"]
95
+ },
96
+ warning: {
97
+ subtle: ["border-warning-border bg-warning-subtle-bg text-warning-subtle-fg", "text-warning"],
98
+ outline: ["border-warning-border bg-transparent text-warning-subtle-fg", "text-warning"],
99
+ solid: ["border-warning bg-warning text-warning-fg", "text-warning-fg"]
100
+ },
101
+ danger: {
102
+ subtle: ["border-danger-border bg-danger-subtle-bg text-danger-subtle-fg", "text-danger"],
103
+ outline: ["border-danger-border bg-transparent text-danger-subtle-fg", "text-danger"],
104
+ solid: ["border-danger bg-danger text-danger-fg", "text-danger-fg"]
105
+ }
106
+ };
107
+ const sizeClasses$3 = {
108
+ sm: {
109
+ base: "h-5 px-1.5",
110
+ withIcon: "ps-1"
111
+ },
112
+ md: {
113
+ base: "h-6 px-2",
114
+ withIcon: "ps-1.5"
115
+ }
116
+ };
117
+ function Badge({ tone = "neutral", variant = "subtle", size = "md", icon, busy = false, context, className, children, ...props }) {
118
+ const [toneClass, iconClass] = toneClasses$1[tone][variant];
119
+ const hasIcon = busy || icon !== void 0 && icon !== null && icon !== false;
120
+ return /* @__PURE__ */ jsxs("span", {
121
+ ...props,
122
+ "data-slot": "badge",
123
+ className: cn("inline-flex items-center gap-1 rounded-md border text-xs leading-none font-medium whitespace-nowrap", "forced-colors:border-[CanvasText]", sizeClasses$3[size].base, hasIcon && sizeClasses$3[size].withIcon, toneClass, className),
124
+ children: [hasIcon && /* @__PURE__ */ jsx("span", {
125
+ "aria-hidden": "true",
126
+ "data-slot": "badge-icon",
127
+ className: cn("inline-flex shrink-0 items-center [&_svg]:size-3 [&_svg]:shrink-0", iconClass),
128
+ children: busy ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(LoaderCircle, { className: "animate-spin motion-reduce:hidden" }), /* @__PURE__ */ jsx(CircleDashed, { className: "hidden motion-reduce:block" })] }) : icon
129
+ }), /* @__PURE__ */ jsxs("span", {
130
+ "data-slot": "badge-label",
131
+ className: "min-w-0 truncate leading-4",
132
+ children: [context && /* @__PURE__ */ jsxs("span", {
133
+ className: "sr-only",
134
+ children: [context, ": "]
135
+ }), children]
136
+ })]
137
+ });
138
+ }
139
+ //#endregion
140
+ //#region src/hooks/use-press-feedback.ts
141
+ const GROW_MS = 450;
142
+ const FADE_MS = 300;
143
+ const OPACITY = .16;
144
+ const RELEASE_EVENTS = [
145
+ "pointerup",
146
+ "pointerleave",
147
+ "pointercancel",
148
+ "keyup",
149
+ "blur"
150
+ ];
151
+ /**
152
+ * Press feedback for buttons and button-styled links: toggles `data-pressed`
153
+ * on the host (styled as a scale) and, when `ripple` is on, spawns a
154
+ * Material-style ripple via the Web Animations API.
155
+ *
156
+ * Attach the returned ref to an absolutely positioned, clipped overlay inside
157
+ * the host. Listeners are bound natively to the overlay's parent so the host's
158
+ * own props/ref stay untouched. Hosts with aria-disabled="true" get no feedback.
159
+ */
160
+ function usePressFeedback(ripple = true) {
161
+ const ref = useRef(null);
162
+ useEffect(() => {
163
+ const layer = ref.current;
164
+ const host = layer?.parentElement;
165
+ if (!layer || !host) return;
166
+ const live = /* @__PURE__ */ new Set();
167
+ const activationKeys = host instanceof HTMLAnchorElement ? ["Enter"] : ["Enter", " "];
168
+ const isInert = () => host.getAttribute("aria-disabled") === "true";
169
+ const canRipple = () => ripple && typeof layer.animate === "function" && !(typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
170
+ const spawn = (x, y, rect) => {
171
+ const r = Math.hypot(Math.max(x, rect.width - x), Math.max(y, rect.height - y));
172
+ const el = document.createElement("span");
173
+ el.style.cssText = `position:absolute;left:${x - r}px;top:${y - r}px;width:${r * 2}px;height:${r * 2}px;border-radius:50%;background:currentColor;opacity:${OPACITY};pointer-events:none`;
174
+ layer.append(el);
175
+ const grow = el.animate([{ transform: "scale(0)" }, { transform: "scale(1)" }], {
176
+ duration: GROW_MS,
177
+ easing: "cubic-bezier(0.2, 0, 0, 1)",
178
+ fill: "forwards"
179
+ });
180
+ live.add({
181
+ el,
182
+ grow
183
+ });
184
+ };
185
+ const press = (x, y) => {
186
+ host.dataset.pressed = "";
187
+ if (!canRipple()) return;
188
+ const rect = layer.getBoundingClientRect();
189
+ spawn(x ?? rect.width / 2, y ?? rect.height / 2, rect);
190
+ };
191
+ const release = () => {
192
+ delete host.dataset.pressed;
193
+ for (const r of live) {
194
+ live.delete(r);
195
+ r.grow.finished.catch(() => {}).then(() => r.el.animate([{ opacity: 0 }], {
196
+ duration: FADE_MS,
197
+ easing: "linear",
198
+ fill: "forwards"
199
+ }).finished).catch(() => {}).finally(() => r.el.remove());
200
+ }
201
+ };
202
+ const onPointerDown = (e) => {
203
+ if (e.button !== 0 || e.isPrimary === false || isInert()) return;
204
+ const rect = layer.getBoundingClientRect();
205
+ press(e.clientX - rect.left, e.clientY - rect.top);
206
+ };
207
+ const onKeyDown = (e) => {
208
+ if (e.repeat || !activationKeys.includes(e.key) || isInert()) return;
209
+ press();
210
+ };
211
+ host.addEventListener("pointerdown", onPointerDown);
212
+ host.addEventListener("keydown", onKeyDown);
213
+ for (const type of RELEASE_EVENTS) host.addEventListener(type, release);
214
+ return () => {
215
+ host.removeEventListener("pointerdown", onPointerDown);
216
+ host.removeEventListener("keydown", onKeyDown);
217
+ for (const type of RELEASE_EVENTS) host.removeEventListener(type, release);
218
+ release();
219
+ };
220
+ }, [ripple]);
221
+ return ref;
222
+ }
223
+ //#endregion
224
+ //#region src/components/tooltip.tsx
225
+ const OPEN_DELAY_MS = 300;
226
+ const GAP_PX = 6;
227
+ const VIEWPORT_PADDING_PX = 8;
228
+ const opposite = {
229
+ top: "bottom",
230
+ bottom: "top",
231
+ left: "right",
232
+ right: "left"
233
+ };
234
+ function isFocusVisible$1(el) {
235
+ try {
236
+ return el.matches(":focus-visible");
237
+ } catch {
238
+ return true;
239
+ }
240
+ }
241
+ function clamp(value, min, max) {
242
+ return Math.max(min, Math.min(value, Math.max(min, max)));
243
+ }
244
+ /**
245
+ * Bubble placement in viewport coordinates. left/top are for the outer box, which
246
+ * includes the GAP_PX padding facing the trigger, so the box always touches the trigger.
247
+ */
248
+ function place(anchor, width, height, preferred) {
249
+ const vw = document.documentElement.clientWidth;
250
+ const vh = document.documentElement.clientHeight;
251
+ const room = {
252
+ top: anchor.top,
253
+ bottom: vh - anchor.bottom,
254
+ left: anchor.left,
255
+ right: vw - anchor.right
256
+ };
257
+ const need = (s) => (s === "top" || s === "bottom" ? height : width) + GAP_PX + VIEWPORT_PADDING_PX;
258
+ const side = room[preferred] < need(preferred) && room[opposite[preferred]] > room[preferred] ? opposite[preferred] : preferred;
259
+ if (side === "top" || side === "bottom") return {
260
+ side,
261
+ left: clamp(anchor.left + anchor.width / 2 - width / 2, VIEWPORT_PADDING_PX, vw - width - VIEWPORT_PADDING_PX),
262
+ top: side === "top" ? anchor.top - height - GAP_PX : anchor.bottom
263
+ };
264
+ return {
265
+ side,
266
+ top: clamp(anchor.top + anchor.height / 2 - height / 2, VIEWPORT_PADDING_PX, vh - height - VIEWPORT_PADDING_PX),
267
+ left: side === "left" ? anchor.left - width - GAP_PX : anchor.right
268
+ };
269
+ }
270
+ /**
271
+ * Hover/focus tooltip around any element. The bubble is portaled to `<body>` with
272
+ * fixed positioning, so scroll containers (tables, drawers) never clip it.
273
+ * Events are read on the wrapper, so aria-disabled triggers work; React still
274
+ * treats the portaled bubble as inside the wrapper, so the pointer can rest on it.
275
+ */
276
+ function Tooltip({ content, side = "top", describe = true, className, children }) {
277
+ const id = useId();
278
+ const [open, setOpen] = useState(false);
279
+ const [position, setPosition] = useState();
280
+ const anchorRef = useRef(null);
281
+ const bubbleRef = useRef(null);
282
+ const timer = useRef(void 0);
283
+ const active = content != null && content !== false && content !== "";
284
+ const clearTimer = () => clearTimeout(timer.current);
285
+ const show = () => {
286
+ clearTimer();
287
+ setOpen(true);
288
+ };
289
+ const hide = () => {
290
+ clearTimer();
291
+ setOpen(false);
292
+ };
293
+ useEffect(() => () => clearTimeout(timer.current), []);
294
+ const [wasActive, setWasActive] = useState(active);
295
+ if (active !== wasActive) {
296
+ setWasActive(active);
297
+ if (!active) setOpen(false);
298
+ }
299
+ useEffect(() => {
300
+ if (!open) return;
301
+ const onKeyDown = (e) => {
302
+ if (e.key === "Escape") setOpen(false);
303
+ };
304
+ document.addEventListener("keydown", onKeyDown);
305
+ return () => document.removeEventListener("keydown", onKeyDown);
306
+ }, [open]);
307
+ useLayoutEffect(() => {
308
+ if (!open) return;
309
+ const update = () => {
310
+ const anchor = anchorRef.current;
311
+ const bubble = bubbleRef.current;
312
+ if (!anchor || !bubble) return;
313
+ const next = place(anchor.getBoundingClientRect(), bubble.offsetWidth, bubble.offsetHeight, side);
314
+ setPosition({
315
+ ...next,
316
+ left: Math.round(next.left),
317
+ top: Math.round(next.top)
318
+ });
319
+ };
320
+ update();
321
+ window.addEventListener("scroll", update, true);
322
+ window.addEventListener("resize", update);
323
+ return () => {
324
+ window.removeEventListener("scroll", update, true);
325
+ window.removeEventListener("resize", update);
326
+ };
327
+ }, [
328
+ open,
329
+ side,
330
+ content
331
+ ]);
332
+ const describedBy = active && describe ? [children.props["aria-describedby"], id].filter(Boolean).join(" ") : children.props["aria-describedby"];
333
+ const placed = position?.side ?? side;
334
+ return /* @__PURE__ */ jsxs("span", {
335
+ ref: anchorRef,
336
+ className: cn("relative inline-flex", className),
337
+ onPointerEnter: () => {
338
+ if (!active) return;
339
+ clearTimer();
340
+ timer.current = setTimeout(() => setOpen(true), OPEN_DELAY_MS);
341
+ },
342
+ onPointerLeave: hide,
343
+ onPointerDown: hide,
344
+ onFocus: (e) => {
345
+ if (active && isFocusVisible$1(e.target)) show();
346
+ },
347
+ onBlur: (e) => {
348
+ if (!e.currentTarget.contains(e.relatedTarget)) hide();
349
+ },
350
+ children: [cloneElement(children, { "aria-describedby": describedBy }), active && createPortal(/* @__PURE__ */ jsx("span", {
351
+ id,
352
+ role: "tooltip",
353
+ "aria-hidden": describe ? void 0 : true,
354
+ hidden: !open,
355
+ "data-side": placed,
356
+ style: position && {
357
+ left: position.left,
358
+ top: position.top
359
+ },
360
+ className: cn("fixed top-0 left-0 z-50", {
361
+ top: "pb-1.5",
362
+ bottom: "pt-1.5",
363
+ left: "pr-1.5",
364
+ right: "pl-1.5"
365
+ }[placed]),
366
+ children: /* @__PURE__ */ jsx("span", {
367
+ ref: bubbleRef,
368
+ className: cn("block w-max max-w-xs rounded-md border border-border-strong bg-inverse px-2 py-1", "text-xs/snug font-medium text-inverse-fg shadow-whisper", "transition-[opacity,translate] duration-150 ease-out starting:opacity-0", {
369
+ top: "starting:translate-y-0.5",
370
+ bottom: "starting:-translate-y-0.5",
371
+ left: "starting:translate-x-0.5",
372
+ right: "starting:-translate-x-0.5"
373
+ }[placed], "motion-reduce:transition-none"),
374
+ children: content
375
+ })
376
+ }), document.body)]
377
+ });
378
+ }
379
+ //#endregion
380
+ //#region src/components/button/button.tsx
381
+ const buttonVariants = cva([
382
+ "relative inline-flex cursor-pointer items-center justify-center font-medium whitespace-nowrap",
383
+ "touch-manipulation select-none [-webkit-tap-highlight-color:transparent]",
384
+ "transition-[color,background-color,box-shadow,opacity,scale] duration-150 ease-out",
385
+ "data-pressed:scale-[0.97]",
386
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus",
387
+ "after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:border after:border-transparent",
388
+ "forced-colors:after:border-[ButtonText]",
389
+ "pointer-coarse:before:absolute pointer-coarse:before:top-1/2 pointer-coarse:before:left-1/2",
390
+ "pointer-coarse:before:size-full pointer-coarse:before:min-h-11 pointer-coarse:before:min-w-11",
391
+ "pointer-coarse:before:-translate-1/2"
392
+ ], {
393
+ variants: {
394
+ intent: {
395
+ primary: "bg-primary text-primary-fg rounded-lg shadow-[0_0_0_1px_var(--color-primary)] hover:not-aria-disabled:opacity-90",
396
+ secondary: "bg-surface-muted text-fg-strong rounded-lg shadow-[0_0_0_1px_var(--color-ring)] hover:not-aria-disabled:bg-ring",
397
+ surface: "bg-surface-raised text-fg rounded-xl hover:not-aria-disabled:bg-surface-muted",
398
+ dark: "bg-inverse text-inverse-fg rounded-lg shadow-[0_0_0_1px_var(--color-ring)] hover:not-aria-disabled:opacity-90",
399
+ ghost: "bg-transparent text-fg rounded-lg hover:not-aria-disabled:bg-surface-muted"
400
+ },
401
+ size: {
402
+ sm: "h-8.5 px-3 text-sm",
403
+ md: "h-9.5 px-4 text-sm",
404
+ lg: "h-10.5 px-5 text-base"
405
+ },
406
+ tone: {
407
+ default: "",
408
+ danger: ""
409
+ },
410
+ fullWidth: {
411
+ true: "w-full",
412
+ false: ""
413
+ },
414
+ square: {
415
+ true: "aspect-square px-0",
416
+ false: ""
417
+ }
418
+ },
419
+ compoundVariants: [
420
+ {
421
+ intent: "primary",
422
+ tone: "danger",
423
+ class: "bg-danger text-danger-fg shadow-[0_0_0_1px_var(--color-danger)]"
424
+ },
425
+ {
426
+ intent: "secondary",
427
+ tone: "danger",
428
+ class: "text-danger shadow-[0_0_0_1px_var(--color-danger)] hover:not-aria-disabled:bg-danger hover:not-aria-disabled:text-danger-fg"
429
+ },
430
+ {
431
+ intent: "surface",
432
+ tone: "danger",
433
+ class: "text-danger hover:not-aria-disabled:bg-danger hover:not-aria-disabled:text-danger-fg"
434
+ },
435
+ {
436
+ intent: "dark",
437
+ tone: "danger",
438
+ class: "bg-danger text-danger-fg shadow-[0_0_0_1px_var(--color-danger)]"
439
+ },
440
+ {
441
+ intent: "ghost",
442
+ tone: "danger",
443
+ class: "text-danger hover:not-aria-disabled:bg-danger/10"
444
+ }
445
+ ],
446
+ defaultVariants: {
447
+ intent: "primary",
448
+ size: "md",
449
+ tone: "default",
450
+ fullWidth: false,
451
+ square: false
452
+ }
453
+ });
454
+ const spinnerSize = {
455
+ sm: 16,
456
+ md: 18,
457
+ lg: 20
458
+ };
459
+ const iconSize$1 = {
460
+ sm: "[&_svg]:size-4",
461
+ md: "[&_svg]:size-4",
462
+ lg: "[&_svg]:size-[18px]"
463
+ };
464
+ const iconStartPadding$1 = {
465
+ sm: "ps-2",
466
+ md: "ps-3",
467
+ lg: "ps-4"
468
+ };
469
+ const iconEndPadding = {
470
+ sm: "pe-2",
471
+ md: "pe-3",
472
+ lg: "pe-4"
473
+ };
474
+ /** Press layer + content + spinner; shared by every button-styled host. */
475
+ function ButtonContent({ size, ripple, loading = false, iconLeft, iconRight, children }) {
476
+ const layerRef = usePressFeedback(ripple);
477
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
478
+ /* @__PURE__ */ jsx("span", {
479
+ ref: layerRef,
480
+ "data-ripple": ripple ? "" : void 0,
481
+ "aria-hidden": true,
482
+ className: "pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit] forced-colors:hidden"
483
+ }),
484
+ /* @__PURE__ */ jsxs("span", {
485
+ className: cn("relative inline-flex items-center gap-2 [&_svg]:shrink-0", iconSize$1[size], loading && "opacity-0"),
486
+ children: [
487
+ iconLeft && /* @__PURE__ */ jsx("span", {
488
+ "aria-hidden": true,
489
+ className: "inline-flex",
490
+ children: iconLeft
491
+ }),
492
+ children,
493
+ iconRight && /* @__PURE__ */ jsx("span", {
494
+ "aria-hidden": true,
495
+ className: "inline-flex",
496
+ children: iconRight
497
+ })
498
+ ]
499
+ }),
500
+ loading && /* @__PURE__ */ jsx("span", {
501
+ className: "absolute inset-0 flex items-center justify-center",
502
+ children: /* @__PURE__ */ jsx(LoaderCircle, {
503
+ className: "animate-spin forced-colors:text-[ButtonText]",
504
+ size: spinnerSize[size],
505
+ "aria-hidden": true
506
+ })
507
+ })
508
+ ] });
509
+ }
510
+ function iconPadding(size, iconLeft, iconRight) {
511
+ return [iconLeft ? iconStartPadding$1[size] : "", iconRight ? iconEndPadding[size] : ""];
512
+ }
513
+ function stateClass(disabled, loading) {
514
+ if (loading) return "cursor-progress opacity-70";
515
+ if (disabled) return "cursor-not-allowed opacity-50 forced-colors:opacity-100 forced-colors:text-[GrayText] forced-colors:after:border-[GrayText]";
516
+ }
517
+ function ButtonImpl({ className, intent, size, tone, fullWidth, square, loading = false, loadingText = "Loading", disabled = false, iconLeft, iconRight, ripple, tooltip, type = "button", onClick, children, ...props }) {
518
+ const s = size ?? "md";
519
+ const inert = disabled || loading;
520
+ const button = /* @__PURE__ */ jsx("button", {
521
+ ...props,
522
+ type,
523
+ "data-slot": "button",
524
+ "aria-disabled": inert || void 0,
525
+ "aria-busy": loading || void 0,
526
+ className: cn(buttonVariants({
527
+ intent,
528
+ size,
529
+ tone,
530
+ fullWidth,
531
+ square
532
+ }), iconPadding(s, iconLeft, iconRight), stateClass(disabled, loading), className),
533
+ onClick: (e) => {
534
+ if (inert) {
535
+ e.preventDefault();
536
+ return;
537
+ }
538
+ onClick?.(e);
539
+ },
540
+ children: /* @__PURE__ */ jsx(ButtonContent, {
541
+ size: s,
542
+ ripple: ripple ?? true,
543
+ loading,
544
+ iconLeft,
545
+ iconRight,
546
+ children
547
+ })
548
+ });
549
+ return /* @__PURE__ */ jsxs(Fragment, { children: [tooltip ? /* @__PURE__ */ jsx(Tooltip, {
550
+ ...tooltip,
551
+ className: fullWidth ? "flex w-full" : void 0,
552
+ children: button
553
+ }) : button, /* @__PURE__ */ jsx("span", {
554
+ "aria-live": "polite",
555
+ "aria-atomic": true,
556
+ className: "sr-only",
557
+ children: loading ? loadingText : ""
558
+ })] });
559
+ }
560
+ function IconOnlyButton({ icon, tooltip = true, tooltipSide, disabledReason, className, size, ...props }) {
561
+ const reason = props.disabled ? disabledReason : void 0;
562
+ const tip = reason ? {
563
+ content: reason,
564
+ side: tooltipSide
565
+ } : tooltip ? {
566
+ content: props["aria-label"],
567
+ side: tooltipSide,
568
+ describe: false
569
+ } : disabledReason !== void 0 ? { content: void 0 } : void 0;
570
+ return /* @__PURE__ */ jsx(ButtonImpl, {
571
+ ...props,
572
+ size,
573
+ tooltip: tip,
574
+ square: true,
575
+ className,
576
+ children: icon
577
+ });
578
+ }
579
+ function Button(props) {
580
+ if (props.icon !== void 0) return /* @__PURE__ */ jsx(IconOnlyButton, { ...props });
581
+ const { disabledReason, ...rest } = props;
582
+ const tooltip = disabledReason === void 0 ? void 0 : { content: rest.disabled ? disabledReason : void 0 };
583
+ return /* @__PURE__ */ jsx(ButtonImpl, {
584
+ ...rest,
585
+ tooltip
586
+ });
587
+ }
588
+ //#endregion
589
+ //#region src/components/card/card.tsx
590
+ const CardContext = createContext({
591
+ titleId: "",
592
+ setTitled: () => {}
593
+ });
594
+ /**
595
+ * Bordered surface for a group of related content. No padding of its own: CardHeader and
596
+ * CardContent add it, so full-bleed rows (lists, tables) can sit edge to edge.
597
+ */
598
+ function Card({ as: Comp = "div", className, ...props }) {
599
+ const titleId = useId();
600
+ const [titled, setTitled] = useState(false);
601
+ return /* @__PURE__ */ jsx(CardContext, {
602
+ value: {
603
+ titleId,
604
+ setTitled
605
+ },
606
+ children: /* @__PURE__ */ jsx(Comp, {
607
+ "aria-labelledby": (Comp === "section" || Comp === "article") && titled && !props["aria-label"] ? titleId : void 0,
608
+ ...props,
609
+ "data-slot": "card",
610
+ className: cn("rounded-xl border border-border-strong bg-surface", className)
611
+ })
612
+ });
613
+ }
614
+ function CardHeader({ title, description, action, headingLevel = 2, className, ...props }) {
615
+ const { titleId, setTitled } = useContext(CardContext);
616
+ useLayoutEffect(() => {
617
+ setTitled(true);
618
+ return () => setTitled(false);
619
+ }, [setTitled]);
620
+ const Heading = headingLevel === 2 ? "h2" : "h3";
621
+ return /* @__PURE__ */ jsxs("div", {
622
+ ...props,
623
+ "data-slot": "card-header",
624
+ className: cn("flex items-start justify-between gap-4 px-5 pt-5", className),
625
+ children: [/* @__PURE__ */ jsxs("div", {
626
+ className: "min-w-0",
627
+ children: [/* @__PURE__ */ jsx(Heading, {
628
+ id: titleId,
629
+ className: "font-sans text-sm font-medium",
630
+ children: title
631
+ }), description && /* @__PURE__ */ jsx("p", {
632
+ className: "text-xs text-fg-muted",
633
+ children: description
634
+ })]
635
+ }), action && /* @__PURE__ */ jsx("div", {
636
+ className: "shrink-0",
637
+ children: action
638
+ })]
639
+ });
640
+ }
641
+ function CardContent({ className, ...props }) {
642
+ return /* @__PURE__ */ jsx("div", {
643
+ ...props,
644
+ "data-slot": "card-content",
645
+ className: cn("p-5 [[data-slot=card-header]+&]:pt-4", className)
646
+ });
647
+ }
648
+ //#endregion
649
+ //#region src/components/input/field.tsx
650
+ const controlHeight = {
651
+ sm: "h-[34px]",
652
+ md: "h-[38px]",
653
+ lg: "h-[42px]"
654
+ };
655
+ const textSize = {
656
+ sm: "text-sm",
657
+ md: "text-sm",
658
+ lg: "text-base"
659
+ };
660
+ const iconSize = {
661
+ sm: "[&_svg]:size-4",
662
+ md: "[&_svg]:size-4",
663
+ lg: "[&_svg]:size-[18px]"
664
+ };
665
+ const inputPadding = {
666
+ sm: "px-3",
667
+ md: "px-3",
668
+ lg: "px-4"
669
+ };
670
+ const startSlotPadding = {
671
+ sm: "ps-2",
672
+ md: "ps-2.5",
673
+ lg: "ps-3"
674
+ };
675
+ const endSlotPadding = {
676
+ sm: "pe-2",
677
+ md: "pe-2.5",
678
+ lg: "pe-3"
679
+ };
680
+ const actionsPadding = {
681
+ sm: "pe-[3px]",
682
+ md: "pe-1",
683
+ lg: "pe-1"
684
+ };
685
+ /** Compact Button square that fits inside the field's inner height with an even inset. */
686
+ const compactButton = {
687
+ sm: "size-[26px]",
688
+ md: "size-7",
689
+ lg: "size-8"
690
+ };
691
+ const labelStart = {
692
+ sm: "start-2",
693
+ md: "start-2",
694
+ lg: "start-3"
695
+ };
696
+ const floatShift = {
697
+ sm: "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:-translate-y-[calc(50%+17px)]",
698
+ md: "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:-translate-y-[calc(50%+19px)]",
699
+ lg: "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:-translate-y-[calc(50%+21px)]"
700
+ };
701
+ const isText = (node) => typeof node === "string" || typeof node === "number";
702
+ const isPresent = (node) => node != null && node !== false && node !== "";
703
+ function Adornment({ node, side, size }) {
704
+ const text = isText(node);
705
+ return /* @__PURE__ */ jsx("span", {
706
+ "aria-hidden": text ? void 0 : true,
707
+ className: cn("flex shrink-0 items-center self-stretch text-fg-muted [&_svg]:shrink-0", text ? ["whitespace-nowrap select-none pointer-coarse:text-base", textSize[size]] : iconSize[size], side === "start" ? startSlotPadding[size] : endSlotPadding[size]),
708
+ children: node
709
+ });
710
+ }
711
+ const notchPadding = {
712
+ border: {
713
+ sm: "ps-2 pe-2",
714
+ md: "ps-2 pe-2",
715
+ lg: "ps-3 pe-1"
716
+ },
717
+ halo: {
718
+ sm: "ps-[9px] pe-[9px]",
719
+ md: "ps-[9px] pe-[9px]",
720
+ lg: "ps-[13px] pe-[5px]"
721
+ }
722
+ };
723
+ /**
724
+ * MUI-style notched outline: a fieldset draws the ring and an invisible legend holding the label
725
+ * text at its floated size cuts the gap. The legend collapses while the label rests in the field.
726
+ * A legend as tall as the border keeps the border at the fieldset's top edge.
727
+ */
728
+ function Notch({ ring, size, disabled, children }) {
729
+ const halo = ring === "halo";
730
+ return /* @__PURE__ */ jsx("fieldset", {
731
+ "aria-hidden": true,
732
+ className: cn("pointer-events-none absolute m-0 min-w-0 p-0", "transition-[border-color] duration-200 ease-in-out motion-reduce:transition-none", notchPadding[ring][size], halo ? ["-inset-1 rounded-[calc(var(--radius-lg)+3px)] border-[3px] border-(--field-halo)", "forced-colors:invisible forced-colors:group-has-[:is(input,select):is(:focus,:open)]/control:visible forced-colors:border-[Highlight]"] : ["-inset-px rounded-[inherit] border border-(--field-border)", disabled && "forced-colors:border-[GrayText]"]),
733
+ children: /* @__PURE__ */ jsx("legend", {
734
+ className: cn("invisible float-none block w-auto max-w-[0.01px] overflow-hidden p-0 text-xs whitespace-nowrap", halo ? "h-0.75" : "h-px", "transition-[max-width] duration-50 ease-out motion-reduce:transition-none", "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:max-w-full", "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:delay-50", "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:duration-100"),
735
+ children: /* @__PURE__ */ jsx("span", {
736
+ className: "px-1",
737
+ children
738
+ })
739
+ })
740
+ });
741
+ }
742
+ /** Keeps an internal ref (focus forwarding, clear, reset) alongside the caller's ref. */
743
+ function useMergedRef(ownRef, ref) {
744
+ return useCallback((node) => {
745
+ ownRef.current = node;
746
+ if (typeof ref === "function") return ref(node);
747
+ if (ref) ref.current = node;
748
+ }, [ownRef, ref]);
749
+ }
750
+ const focusControl = (control) => control.focus();
751
+ /**
752
+ * The frame every field shares (input family, Select): label above or floating with a notched
753
+ * outline, the bordered control with its border/halo/hover states, description and live region.
754
+ * State selectors match `input` or `select`, never the inner buttons, so a focused clear or copy
755
+ * button shows only its own ring. `:open` too: an open base-select picker moves focus onto its
756
+ * options, and the select then matches neither `:focus` nor `:focus-within`. Inside `:is()` an
757
+ * unsupported `:open` is dropped alone instead of invalidating the rule.
758
+ */
759
+ function FieldShell({ label, floating = false, helperText, error, size, id: idProp, required, disabled, readOnly, "aria-describedby": describedByProp, hasStart = false, className, status = "", controlRef, activate = focusControl, children }) {
760
+ const autoId = useId();
761
+ const id = idProp ?? `${autoId}-control`;
762
+ const descriptionId = `${autoId}-description`;
763
+ const invalid = Boolean(error);
764
+ const description = invalid ? error === true ? null : error : helperText;
765
+ const hasDescription = isPresent(description);
766
+ const describedBy = [describedByProp, hasDescription ? descriptionId : null].filter(Boolean).join(" ") || void 0;
767
+ const hasLabel = Boolean(label);
768
+ const floatingLabel = floating && hasLabel;
769
+ const asterisk = required ? /* @__PURE__ */ jsx("span", {
770
+ "aria-hidden": true,
771
+ className: "text-danger",
772
+ children: " *"
773
+ }) : null;
774
+ const forwardClick = (e) => {
775
+ const control = controlRef.current;
776
+ const target = e.target;
777
+ if (!control || control.contains(target) || target.closest("button")) return;
778
+ e.preventDefault();
779
+ activate(control);
780
+ };
781
+ const renderFloatingLabel = (restingWidth) => floatingLabel ? /* @__PURE__ */ jsxs("label", {
782
+ htmlFor: id,
783
+ className: cn("pointer-events-none absolute top-1/2 -translate-y-1/2 truncate px-1 leading-normal", "text-fg-muted pointer-coarse:text-base", "transition-[translate,font-size,color] duration-150 ease-out motion-reduce:transition-none", labelStart[size], textSize[size], restingWidth, floatShift[size], "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:max-w-[calc(100%-1rem)]", "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:text-xs", invalid ? "text-danger" : ["[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:group-has-[:is(input,select):not(:is(:focus,:open))]/control:text-fg-strong", "group-has-[:is(input,select):is(:focus,:open)]/control:text-input-focus"]),
784
+ children: [label, asterisk]
785
+ }) : null;
786
+ return /* @__PURE__ */ jsxs("div", {
787
+ "data-slot": "input-field",
788
+ className: cn("flex w-full flex-col gap-1.5", disabled && "opacity-50", className),
789
+ children: [
790
+ hasLabel && !floating && /* @__PURE__ */ jsxs("label", {
791
+ htmlFor: id,
792
+ className: "self-start text-sm font-medium text-fg",
793
+ children: [label, asterisk]
794
+ }),
795
+ /* @__PURE__ */ jsxs("div", {
796
+ "data-slot": "input-control",
797
+ "data-start": hasStart ? "" : void 0,
798
+ onMouseDown: forwardClick,
799
+ className: cn("group/control relative isolate flex items-center rounded-lg border", floatingLabel ? "bg-transparent" : "[--field-bg:var(--color-input-bg)] bg-(--field-bg)", "transition-[border-color,box-shadow,background-color] duration-200 ease-in-out motion-reduce:transition-none", controlHeight[size], invalid ? ["[--field-border:var(--color-danger)] [--field-halo:var(--color-input-halo-invalid)]", "has-[:is(input,select):is(:focus,:open)]:[--field-halo:var(--color-input-halo-invalid-focus)]"] : ["[--field-border:var(--color-input-border)] [--field-halo:transparent]", "has-[:is(input,select):is(:focus,:open)]:[--field-border:var(--color-input-focus)] has-[:is(input,select):is(:focus,:open)]:[--field-halo:var(--color-input-halo)]"], "[--field-lift:0_0_#0000]", !invalid && !disabled && !readOnly && [
800
+ "hover:not-has-[:is(input,select):is(:focus,:open)]:[--field-border:var(--color-input-border-hover)]",
801
+ !floatingLabel && "hover:not-has-[:is(input,select):is(:focus,:open)]:[--field-bg:var(--color-input-bg-hover)]",
802
+ "hover:not-has-[:is(input,select):is(:focus,:open)]:[--field-lift:var(--shadow-input-lift)]"
803
+ ], floatingLabel ? ["border-transparent forced-colors:border-[Canvas]", !readOnly && "[box-shadow:var(--field-lift)]"] : [
804
+ "border-(--field-border)",
805
+ readOnly ? "[box-shadow:0_0_0_3px_var(--field-halo)]" : "[box-shadow:var(--shadow-input),var(--field-lift),0_0_0_3px_var(--field-halo)]",
806
+ "has-[:is(input,select):is(:focus,:open)]:outline-hidden",
807
+ disabled && "forced-colors:border-[GrayText]"
808
+ ], readOnly && !floatingLabel && "[--field-bg:var(--color-bg)]", disabled && "cursor-not-allowed", "pointer-coarse:before:absolute pointer-coarse:before:inset-x-0 pointer-coarse:before:top-1/2", "pointer-coarse:before:-z-10 pointer-coarse:before:h-11 pointer-coarse:before:-translate-y-1/2"),
809
+ children: [floatingLabel && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Notch, {
810
+ size,
811
+ ring: "halo",
812
+ disabled,
813
+ children: [label, asterisk]
814
+ }), /* @__PURE__ */ jsxs(Notch, {
815
+ size,
816
+ ring: "border",
817
+ disabled,
818
+ children: [label, asterisk]
819
+ })] }), children({
820
+ id,
821
+ invalid,
822
+ describedBy,
823
+ floating: floatingLabel,
824
+ floatingLabel: renderFloatingLabel
825
+ })]
826
+ }),
827
+ hasDescription && /* @__PURE__ */ jsxs("p", {
828
+ id: descriptionId,
829
+ className: cn("m-0 flex items-start gap-1 text-xs/snug", invalid ? "text-danger" : "text-fg-muted"),
830
+ children: [invalid && /* @__PURE__ */ jsx(CircleAlert, {
831
+ "aria-hidden": true,
832
+ className: "mt-px size-3.5 shrink-0"
833
+ }), /* @__PURE__ */ jsx("span", { children: description })]
834
+ }),
835
+ /* @__PURE__ */ jsx("span", {
836
+ "aria-live": "polite",
837
+ "aria-atomic": true,
838
+ className: "sr-only",
839
+ children: status
840
+ })
841
+ ]
842
+ });
843
+ }
844
+ /** The input family's frame: adornments, actions and the input itself inside FieldShell. */
845
+ function FieldFrame({ label, floating = false, helperText, error, size = "md", start, end, mono = false, validating = false, validatingText = "Checking…", className, inputClassName, inputRef: inputRefProp, actions, status, ref, id, required, disabled, readOnly, placeholder, "aria-describedby": describedByProp, "aria-invalid": ariaInvalid, ...props }) {
846
+ const ownRef = useRef(null);
847
+ const inputRef = inputRefProp ?? ownRef;
848
+ const setRef = useMergedRef(inputRef, ref);
849
+ const hasStart = isPresent(start);
850
+ const hasEnd = isPresent(end);
851
+ return /* @__PURE__ */ jsx(FieldShell, {
852
+ label,
853
+ floating,
854
+ helperText,
855
+ error,
856
+ size,
857
+ id,
858
+ required,
859
+ disabled,
860
+ readOnly,
861
+ "aria-describedby": describedByProp,
862
+ hasStart,
863
+ className,
864
+ status: validating ? validatingText : status ?? "",
865
+ controlRef: inputRef,
866
+ children: (field) => /* @__PURE__ */ jsxs(Fragment, { children: [
867
+ hasStart ? /* @__PURE__ */ jsx(Adornment, {
868
+ node: start,
869
+ side: "start",
870
+ size
871
+ }) : null,
872
+ /* @__PURE__ */ jsxs("span", {
873
+ className: "@container flex min-w-0 flex-1 self-stretch rounded-[inherit]",
874
+ children: [field.floatingLabel("max-w-[calc(100cqw-1rem)]"), /* @__PURE__ */ jsx("input", {
875
+ ...props,
876
+ ref: setRef,
877
+ id: field.id,
878
+ required,
879
+ disabled,
880
+ readOnly,
881
+ placeholder: field.floating ? placeholder || " " : placeholder,
882
+ "aria-invalid": field.invalid ? true : ariaInvalid,
883
+ "aria-describedby": field.describedBy,
884
+ spellCheck: mono ? false : props.spellCheck,
885
+ autoCapitalize: mono ? "off" : props.autoCapitalize,
886
+ autoCorrect: mono ? "off" : props.autoCorrect,
887
+ className: cn("h-full min-w-0 flex-1 rounded-[inherit] bg-transparent text-fg outline-none", "placeholder:text-input-placeholder pointer-coarse:text-base", "disabled:cursor-not-allowed", "autofill:[transition:background-color_0s_600000s] autofill:[-webkit-text-fill-color:var(--color-fg)]", textSize[size], inputPadding[size], hasStart && (isText(start) ? "ps-1" : "ps-2"), !validating && hasEnd && isText(end) ? "pe-1" : (validating || hasEnd || actions) && "pe-2", mono && "font-mono", field.floating && ["placeholder:opacity-0", "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(input:not(:placeholder-shown)),:has(input:autofill),:has(input:-webkit-autofill),:has(select:focus),:has(select:open),:has(select):not(:has(option[value=\"\"]:checked)))_&]:placeholder:opacity-100"], inputClassName)
888
+ })]
889
+ }),
890
+ validating ? /* @__PURE__ */ jsx("span", {
891
+ className: cn("flex shrink-0 items-center self-stretch text-fg-muted", iconSize[size], endSlotPadding[size]),
892
+ children: /* @__PURE__ */ jsx(LoaderCircle, {
893
+ "aria-hidden": true,
894
+ className: "animate-spin motion-reduce:[animation-duration:2.5s]"
895
+ })
896
+ }) : hasEnd ? /* @__PURE__ */ jsx(Adornment, {
897
+ node: end,
898
+ side: "end",
899
+ size
900
+ }) : null,
901
+ actions ? /* @__PURE__ */ jsx("span", {
902
+ className: cn("flex shrink-0 items-center gap-0.5", actionsPadding[size]),
903
+ children: actions
904
+ }) : null
905
+ ] })
906
+ });
907
+ }
908
+ //#endregion
909
+ //#region src/components/checkbox/choice-row.tsx
910
+ /**
911
+ * The row Switch and Checkbox share: one `<label>` holding the control, its label, description
912
+ * and error, so the whole row toggles it natively. The label line is 24px tall, which is the
913
+ * WCAG 2.5.8 minimum target; touch gets a 44px band without changing the layout.
914
+ * Disabled styling reads `:disabled` off the input, so a disabled fieldset dims rows too.
915
+ */
916
+ function ChoiceRow({ slot, label, description, error, labelPosition = "end", gap, controlOffset, className, "aria-describedby": describedByProp, control }) {
917
+ const id = useId();
918
+ const labelId = `${id}-label`;
919
+ const descriptionId = `${id}-description`;
920
+ const errorId = `${id}-error`;
921
+ const hasLabel = isPresent(label);
922
+ const hasDescription = isPresent(description);
923
+ const invalid = Boolean(error);
924
+ const hasErrorText = invalid && error !== true && isPresent(error);
925
+ const describedBy = [
926
+ describedByProp,
927
+ hasDescription && descriptionId,
928
+ hasErrorText && errorId
929
+ ].filter(Boolean).join(" ") || void 0;
930
+ const text = hasLabel || hasDescription || hasErrorText ? /* @__PURE__ */ jsxs("span", {
931
+ className: "flex min-w-0 flex-col",
932
+ children: [
933
+ hasLabel && /* @__PURE__ */ jsx("span", {
934
+ id: labelId,
935
+ children: label
936
+ }),
937
+ hasDescription && /* @__PURE__ */ jsx("span", {
938
+ id: descriptionId,
939
+ className: "text-xs/snug text-fg-muted",
940
+ children: description
941
+ }),
942
+ hasErrorText && /* @__PURE__ */ jsxs("span", {
943
+ id: errorId,
944
+ className: cn("flex items-start gap-1 text-xs/snug text-danger", hasDescription && "mt-1"),
945
+ children: [/* @__PURE__ */ jsx(CircleAlert, {
946
+ "aria-hidden": true,
947
+ className: "mt-px size-3.5 shrink-0"
948
+ }), /* @__PURE__ */ jsx("span", { children: error })]
949
+ })
950
+ ]
951
+ }) : null;
952
+ const controlSlot = /* @__PURE__ */ jsx("span", {
953
+ className: cn("flex shrink-0", controlOffset),
954
+ children: control({
955
+ labelledBy: hasLabel ? labelId : void 0,
956
+ describedBy,
957
+ invalid
958
+ })
959
+ });
960
+ return /* @__PURE__ */ jsx("label", {
961
+ "data-slot": slot,
962
+ className: cn("relative isolate flex min-h-6 min-w-6 items-start text-sm/6 text-fg", "cursor-pointer has-[input:disabled]:cursor-not-allowed has-[input[aria-busy=true]]:cursor-progress", "has-[input:disabled]:opacity-50", "forced-colors:has-[input:disabled]:text-[GrayText] forced-colors:has-[input:disabled]:opacity-100", "pointer-coarse:before:absolute pointer-coarse:before:top-1/2 pointer-coarse:before:left-1/2", "pointer-coarse:before:-z-10 pointer-coarse:before:size-full pointer-coarse:before:min-h-11", "pointer-coarse:before:min-w-11 pointer-coarse:before:-translate-1/2", gap, labelPosition === "start" ? "w-full justify-between" : "w-fit max-w-full", className),
963
+ children: labelPosition === "start" ? /* @__PURE__ */ jsxs(Fragment, { children: [text, controlSlot] }) : /* @__PURE__ */ jsxs(Fragment, { children: [controlSlot, text] })
964
+ });
965
+ }
966
+ //#endregion
967
+ //#region src/components/checkbox/checkbox.tsx
968
+ /**
969
+ * Drawn from the short end, like a pen stroke. As wide as the dash (Geist's 8-unit check reads
970
+ * small next to it). Length ~14.1; the dash pattern rounds it up to 15.
971
+ */
972
+ const CHECK_PATH = "M5 10L8.5 13.5L15 7";
973
+ const markBase = ["pointer-events-none col-start-1 row-start-1 size-3.5 text-control-checked-fg", "forced-colors:forced-color-adjust-none forced-colors:text-[HighlightText] forced-colors:peer-disabled:text-[Canvas]"];
974
+ function Checkbox({ label, description, error, indeterminate = false, className, inputClassName, ref, checked, "aria-describedby": describedBy, "aria-invalid": ariaInvalid, ...props }) {
975
+ const ownRef = useRef(null);
976
+ const setRef = useMergedRef(ownRef, ref);
977
+ useLayoutEffect(() => {
978
+ if (ownRef.current) ownRef.current.indeterminate = indeterminate;
979
+ }, [indeterminate, checked]);
980
+ return /* @__PURE__ */ jsx(ChoiceRow, {
981
+ slot: "checkbox",
982
+ label,
983
+ description,
984
+ error,
985
+ gap: "gap-2",
986
+ controlOffset: "mt-1",
987
+ className,
988
+ "aria-describedby": describedBy,
989
+ control: (row) => /* @__PURE__ */ jsxs("span", {
990
+ className: cn("relative inline-grid size-4 place-items-center", "transition-[scale] duration-100 ease-out has-[input:enabled:active]:scale-[0.92]"),
991
+ children: [
992
+ /* @__PURE__ */ jsx("input", {
993
+ ...props,
994
+ ref: setRef,
995
+ type: "checkbox",
996
+ checked,
997
+ "aria-labelledby": props["aria-labelledby"] ?? row.labelledBy,
998
+ "aria-describedby": row.describedBy,
999
+ "aria-invalid": row.invalid ? true : ariaInvalid,
1000
+ className: cn("peer col-start-1 row-start-1 m-0 size-4 cursor-pointer appearance-none rounded-sm border outline-hidden", "disabled:cursor-not-allowed", "border-(--checkbox-border) bg-(--checkbox-bg) [box-shadow:0_0_0_3px_var(--checkbox-halo)]", "transition-[background-color,border-color,box-shadow] duration-200 ease-in-out", "[--checkbox-bg:var(--color-input-bg)] [--checkbox-halo:transparent]", "checked:[--checkbox-bg:var(--color-control-checked)] indeterminate:[--checkbox-bg:var(--color-control-checked)]", row.invalid ? [
1001
+ "[--checkbox-border:var(--color-danger)] [--checkbox-halo:var(--color-input-halo-invalid)]",
1002
+ "focus-visible:[--checkbox-halo:var(--color-input-halo-invalid-focus)]",
1003
+ "not-forced-colors:checked:focus-visible:outline-danger not-forced-colors:indeterminate:focus-visible:outline-danger"
1004
+ ] : [
1005
+ "[--checkbox-border:var(--color-input-border)]",
1006
+ "checked:[--checkbox-border:var(--color-control-checked)] indeterminate:[--checkbox-border:var(--color-control-checked)]",
1007
+ "enabled:not-checked:not-indeterminate:hover:[--checkbox-border:var(--color-input-border-hover)]",
1008
+ "enabled:not-checked:not-indeterminate:hover:[--checkbox-bg:var(--color-input-bg-hover)]",
1009
+ "focus-visible:[--checkbox-border:var(--color-input-focus)] focus-visible:[--checkbox-halo:var(--color-input-halo)]",
1010
+ "not-forced-colors:checked:focus-visible:outline-control-checked not-forced-colors:indeterminate:focus-visible:outline-control-checked"
1011
+ ], "checked:focus-visible:[--checkbox-halo:transparent] indeterminate:focus-visible:[--checkbox-halo:transparent]", "checked:focus-visible:outline-solid checked:focus-visible:outline-2 checked:focus-visible:outline-offset-2", "indeterminate:focus-visible:outline-solid indeterminate:focus-visible:outline-2 indeterminate:focus-visible:outline-offset-2", "forced-colors:forced-color-adjust-none forced-colors:[box-shadow:none]", "forced-colors:border-[CanvasText] forced-colors:bg-[Canvas]", "forced-colors:checked:border-[Highlight] forced-colors:checked:bg-[Highlight]", "forced-colors:indeterminate:border-[Highlight] forced-colors:indeterminate:bg-[Highlight]", "forced-colors:disabled:border-[GrayText] forced-colors:disabled:checked:bg-[GrayText] forced-colors:disabled:indeterminate:bg-[GrayText]", "forced-colors:focus-visible:outline-[CanvasText]", inputClassName)
1012
+ }),
1013
+ /* @__PURE__ */ jsx("svg", {
1014
+ "aria-hidden": true,
1015
+ viewBox: "0 0 20 20",
1016
+ fill: "none",
1017
+ stroke: "currentColor",
1018
+ strokeWidth: 2,
1019
+ strokeLinecap: "round",
1020
+ strokeLinejoin: "round",
1021
+ className: cn(markBase, "[stroke-dasharray:15] [stroke-dashoffset:15] opacity-0", "[transition:opacity_80ms_ease-out,stroke-dashoffset_0s_80ms]", "peer-[:checked:not(:indeterminate)]:[stroke-dashoffset:0] peer-[:checked:not(:indeterminate)]:opacity-100", "peer-[:checked:not(:indeterminate)]:[transition:stroke-dashoffset_150ms_ease-out]"),
1022
+ children: /* @__PURE__ */ jsx("path", { d: CHECK_PATH })
1023
+ }),
1024
+ /* @__PURE__ */ jsx("svg", {
1025
+ "aria-hidden": true,
1026
+ viewBox: "0 0 20 20",
1027
+ fill: "none",
1028
+ stroke: "currentColor",
1029
+ strokeWidth: 2,
1030
+ strokeLinecap: "round",
1031
+ className: cn(markBase, "scale-x-0 opacity-0 [transition:opacity_80ms_ease-out,scale_0s_80ms]", "peer-indeterminate:scale-x-100 peer-indeterminate:opacity-100", "peer-indeterminate:[transition:scale_150ms_ease-out]"),
1032
+ children: /* @__PURE__ */ jsx("path", { d: "M5 10H15" })
1033
+ })
1034
+ ]
1035
+ })
1036
+ });
1037
+ }
1038
+ //#endregion
1039
+ //#region src/components/checkbox/checkbox-group.tsx
1040
+ /** A `<fieldset>` + `<legend>` around related checkboxes; holds no value of its own. */
1041
+ function CheckboxGroup({ legend, description, error, orientation = "vertical", className, "aria-describedby": describedByProp, children, ...props }) {
1042
+ const id = useId();
1043
+ const descriptionId = `${id}-description`;
1044
+ const errorId = `${id}-error`;
1045
+ const hasDescription = isPresent(description);
1046
+ const hasError = Boolean(error) && error !== true && isPresent(error);
1047
+ const describedBy = [
1048
+ describedByProp,
1049
+ hasDescription && descriptionId,
1050
+ hasError && errorId
1051
+ ].filter(Boolean).join(" ") || void 0;
1052
+ return /* @__PURE__ */ jsxs("fieldset", {
1053
+ ...props,
1054
+ "data-slot": "checkbox-group",
1055
+ "aria-describedby": describedBy,
1056
+ className: cn("group/checkbox-group m-0 min-w-0 border-0 p-0", className),
1057
+ children: [/* @__PURE__ */ jsx("legend", {
1058
+ className: "float-start mb-1.5 w-full p-0 text-sm font-medium text-fg group-disabled/checkbox-group:opacity-50",
1059
+ children: legend
1060
+ }), /* @__PURE__ */ jsxs("div", {
1061
+ className: "clear-both flex flex-col",
1062
+ children: [
1063
+ hasDescription && /* @__PURE__ */ jsx("p", {
1064
+ id: descriptionId,
1065
+ className: "m-0 -mt-1 mb-2.5 text-xs/snug text-fg-muted group-disabled/checkbox-group:opacity-50",
1066
+ children: description
1067
+ }),
1068
+ /* @__PURE__ */ jsx("div", {
1069
+ "data-orientation": orientation,
1070
+ className: cn("flex", orientation === "vertical" ? "flex-col gap-2" : "flex-row flex-wrap gap-x-6 gap-y-2"),
1071
+ children
1072
+ }),
1073
+ hasError && /* @__PURE__ */ jsxs("p", {
1074
+ id: errorId,
1075
+ className: "m-0 mt-2 flex items-start gap-1 text-xs/snug text-danger",
1076
+ children: [/* @__PURE__ */ jsx(CircleAlert, {
1077
+ "aria-hidden": true,
1078
+ className: "mt-px size-3.5 shrink-0"
1079
+ }), /* @__PURE__ */ jsx("span", { children: error })]
1080
+ })
1081
+ ]
1082
+ })]
1083
+ });
1084
+ }
1085
+ //#endregion
1086
+ //#region src/components/modal/modal-shell.tsx
1087
+ const ModalContext = createContext(null);
1088
+ /** Parts of one wrapper must not work inside the other: a DialogHeader in a Drawer throws too. */
1089
+ function useModalContext(part, kind, owner) {
1090
+ const context = useContext(ModalContext);
1091
+ if (!context || context.kind !== kind) throw new Error(`${part} must be rendered inside <${owner}>`);
1092
+ return context;
1093
+ }
1094
+ /** Registers a rendered title (and description) so the dialog's aria-* point at them. */
1095
+ function useAnnounceHeading({ setHasTitle, setHasDescription }, hasDescription) {
1096
+ useLayoutEffect(() => {
1097
+ setHasTitle(true);
1098
+ return () => setHasTitle(false);
1099
+ }, [setHasTitle]);
1100
+ useLayoutEffect(() => {
1101
+ if (!hasDescription) return;
1102
+ setHasDescription(true);
1103
+ return () => setHasDescription(false);
1104
+ }, [hasDescription, setHasDescription]);
1105
+ }
1106
+ const lockHolders = /* @__PURE__ */ new Set();
1107
+ let unlockedStyle = {
1108
+ overflow: "",
1109
+ paddingInlineEnd: ""
1110
+ };
1111
+ function lockScroll(el) {
1112
+ if (lockHolders.has(el)) return;
1113
+ lockHolders.add(el);
1114
+ if (lockHolders.size > 1) return;
1115
+ const root = document.documentElement;
1116
+ unlockedStyle = {
1117
+ overflow: root.style.overflow,
1118
+ paddingInlineEnd: root.style.paddingInlineEnd
1119
+ };
1120
+ root.style.paddingInlineEnd = `${window.innerWidth - root.clientWidth}px`;
1121
+ root.style.overflow = "hidden";
1122
+ }
1123
+ function unlockScroll(el) {
1124
+ if (!lockHolders.delete(el) || lockHolders.size > 0) return;
1125
+ const root = document.documentElement;
1126
+ root.style.overflow = unlockedStyle.overflow;
1127
+ root.style.paddingInlineEnd = unlockedStyle.paddingInlineEnd;
1128
+ }
1129
+ const BOUNCE_MS = 200;
1130
+ const bounces = /* @__PURE__ */ new WeakMap();
1131
+ function bounce(el, keyframes) {
1132
+ if (!keyframes || typeof el.animate !== "function" || typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
1133
+ bounces.get(el)?.cancel();
1134
+ bounces.set(el, el.animate(keyframes(), { duration: BOUNCE_MS }));
1135
+ }
1136
+ /**
1137
+ * Native `<dialog>` + showModal(): top layer (no portal), inert page, focus containment and
1138
+ * restore, and Esc all come from the platform. Controlled: `open` drives it and every close
1139
+ * request goes through `onOpenChange`. `children` is the wrapper's panel, mounted only while open
1140
+ * or exiting.
1141
+ */
1142
+ function ModalShell({ kind, open, onOpenChange, alert = false, dismissible = true, bounceKeyframes, className, children, ref, onCancel, onClose, onPointerDown, onMouseDown, onClick, "aria-labelledby": labelledByProp, "aria-describedby": describedByProp, ...props }) {
1143
+ const dialogRef = useRef(null);
1144
+ const setRef = useMergedRef(dialogRef, ref);
1145
+ const titleId = useId();
1146
+ const descriptionId = useId();
1147
+ const [hasTitle, setHasTitle] = useState(false);
1148
+ const [hasDescription, setHasDescription] = useState(false);
1149
+ const [present, setPresent] = useState(open);
1150
+ if (open && !present) setPresent(true);
1151
+ const [closeCount, setCloseCount] = useState(0);
1152
+ const latest = useRef({
1153
+ open,
1154
+ onOpenChange,
1155
+ alert,
1156
+ dismissible,
1157
+ bounceKeyframes
1158
+ });
1159
+ useInsertionEffect(() => {
1160
+ latest.current = {
1161
+ open,
1162
+ onOpenChange,
1163
+ alert,
1164
+ dismissible,
1165
+ bounceKeyframes
1166
+ };
1167
+ });
1168
+ const requestClose = useCallback((reason) => {
1169
+ const el = dialogRef.current;
1170
+ const { open, onOpenChange, alert, dismissible, bounceKeyframes } = latest.current;
1171
+ if (!el || !open || el.hasAttribute("data-closing")) return;
1172
+ if (!dismissible || alert && reason === "backdrop") {
1173
+ bounce(el, bounceKeyframes);
1174
+ return;
1175
+ }
1176
+ onOpenChange(false, reason);
1177
+ }, []);
1178
+ const showOnAttach = useCallback((node) => {
1179
+ const el = node?.parentElement;
1180
+ if (!open || !(el instanceof HTMLDialogElement)) return;
1181
+ el.removeAttribute("data-closing");
1182
+ if (!el.open) {
1183
+ lockScroll(el);
1184
+ el.showModal();
1185
+ }
1186
+ }, [open]);
1187
+ useLayoutEffect(() => {
1188
+ const el = dialogRef.current;
1189
+ if (open || !el?.open) return;
1190
+ el.setAttribute("data-closing", "");
1191
+ let cancelled = false;
1192
+ Promise.allSettled(el.getAnimations().map((animation) => animation.finished)).then(() => {
1193
+ if (!cancelled && el.open && el.hasAttribute("data-closing")) el.close();
1194
+ });
1195
+ return () => {
1196
+ cancelled = true;
1197
+ };
1198
+ }, [open]);
1199
+ const handleCancel = (event) => {
1200
+ onCancel?.(event);
1201
+ event.preventDefault();
1202
+ requestClose("escape");
1203
+ };
1204
+ const handleClose = (event) => {
1205
+ onClose?.(event);
1206
+ const el = event.currentTarget;
1207
+ if (el.open) return;
1208
+ el.removeAttribute("data-closing");
1209
+ unlockScroll(el);
1210
+ setPresent(false);
1211
+ setCloseCount((count) => count + 1);
1212
+ if (latest.current.open) latest.current.onOpenChange(false, "escape");
1213
+ };
1214
+ const pressedBackdrop = useRef(false);
1215
+ const handlePointerDown = (event) => {
1216
+ onPointerDown?.(event);
1217
+ pressedBackdrop.current = event.target === event.currentTarget;
1218
+ };
1219
+ const handleMouseDown = (event) => {
1220
+ onMouseDown?.(event);
1221
+ if (event.target === event.currentTarget) event.preventDefault();
1222
+ };
1223
+ const handleClick = (event) => {
1224
+ onClick?.(event);
1225
+ const onBackdrop = pressedBackdrop.current && event.target === event.currentTarget;
1226
+ pressedBackdrop.current = false;
1227
+ if (onBackdrop) requestClose("backdrop");
1228
+ };
1229
+ useEffect(() => {
1230
+ const el = dialogRef.current;
1231
+ if (!el) return;
1232
+ if (el.open) lockScroll(el);
1233
+ return () => unlockScroll(el);
1234
+ }, []);
1235
+ const rendered = open || present;
1236
+ return /* @__PURE__ */ jsx(ModalContext, {
1237
+ value: {
1238
+ kind,
1239
+ titleId,
1240
+ descriptionId,
1241
+ alert,
1242
+ dismissible,
1243
+ setHasTitle,
1244
+ setHasDescription,
1245
+ requestClose
1246
+ },
1247
+ children: /* @__PURE__ */ jsx("dialog", {
1248
+ ...props,
1249
+ ref: setRef,
1250
+ "data-slot": kind,
1251
+ role: alert ? "alertdialog" : void 0,
1252
+ "aria-labelledby": labelledByProp ?? (hasTitle ? titleId : void 0),
1253
+ "aria-describedby": describedByProp ?? (hasDescription ? descriptionId : void 0),
1254
+ onCancel: handleCancel,
1255
+ onClose: handleClose,
1256
+ onPointerDown: handlePointerDown,
1257
+ onMouseDown: handleMouseDown,
1258
+ onClick: handleClick,
1259
+ className,
1260
+ children: rendered && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1261
+ hidden: true,
1262
+ ref: showOnAttach
1263
+ }, closeCount), children] })
1264
+ })
1265
+ });
1266
+ }
1267
+ //#endregion
1268
+ //#region src/components/dialog/dialog.tsx
1269
+ function useDialogContext(part) {
1270
+ return useModalContext(part, "dialog", "Dialog");
1271
+ }
1272
+ const sizeClasses$2 = {
1273
+ sm: "max-w-[384px]",
1274
+ md: "max-w-[480px]",
1275
+ lg: "max-w-[640px]"
1276
+ };
1277
+ const motionClasses$1 = [
1278
+ "transition-[opacity,translate,scale] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)]",
1279
+ "starting:open:translate-y-1 starting:open:scale-96 starting:open:opacity-0",
1280
+ "data-closing:pointer-events-none data-closing:scale-98 data-closing:opacity-0",
1281
+ "data-closing:duration-120 data-closing:ease-[cubic-bezier(0.2,0,1,0.9)]",
1282
+ "backdrop:bg-dialog-backdrop backdrop:transition-opacity backdrop:duration-200 backdrop:ease-[cubic-bezier(0.16,1,0.3,1)]",
1283
+ "starting:open:backdrop:opacity-0",
1284
+ "data-closing:backdrop:opacity-0 data-closing:backdrop:duration-120",
1285
+ "data-closing:backdrop:ease-[cubic-bezier(0.2,0,1,0.9)]"
1286
+ ];
1287
+ const sheetClasses = [
1288
+ "max-sm:mx-0 max-sm:mt-auto max-sm:mb-0 max-sm:w-full max-sm:max-w-none",
1289
+ "max-sm:max-h-[calc(100dvh-32px)]",
1290
+ "max-sm:duration-250 max-sm:ease-[cubic-bezier(0.33,1,0.68,1)]",
1291
+ "max-sm:starting:open:translate-y-full max-sm:starting:open:scale-100 max-sm:starting:open:opacity-100",
1292
+ "max-sm:data-closing:translate-y-full max-sm:data-closing:scale-100 max-sm:data-closing:opacity-100",
1293
+ "max-sm:data-closing:duration-180",
1294
+ "max-sm:backdrop:duration-250 max-sm:backdrop:ease-[cubic-bezier(0.33,1,0.68,1)]",
1295
+ "max-sm:data-closing:backdrop:duration-180"
1296
+ ];
1297
+ function bounceKeyframes() {
1298
+ const sheet = typeof window.matchMedia === "function" && window.matchMedia("(width < 40rem)").matches;
1299
+ const rest = sheet ? { translate: "0 0" } : { scale: "1" };
1300
+ const peak = sheet ? { translate: "0 -6px" } : { scale: "1.02" };
1301
+ return [
1302
+ {
1303
+ ...rest,
1304
+ easing: "ease-out"
1305
+ },
1306
+ {
1307
+ ...peak,
1308
+ easing: "ease-out"
1309
+ },
1310
+ rest
1311
+ ];
1312
+ }
1313
+ /**
1314
+ * Modal dialog on the native `<dialog>` + showModal() (behavior in ModalShell). Controlled: `open`
1315
+ * drives it and every close request goes through `onOpenChange`.
1316
+ */
1317
+ function Dialog({ size = "md", className, children, ...props }) {
1318
+ return /* @__PURE__ */ jsx(ModalShell, {
1319
+ ...props,
1320
+ kind: "dialog",
1321
+ bounceKeyframes,
1322
+ className: cn("m-auto w-[calc(100%-2rem)] max-h-[calc(100dvh-64px)] overflow-visible border-0 bg-transparent p-0 text-fg", "overscroll-contain outline-none", sizeClasses$2[size], motionClasses$1, sheetClasses, className),
1323
+ children: /* @__PURE__ */ jsx("div", {
1324
+ "data-slot": "dialog-panel",
1325
+ className: cn("flex max-h-[inherit] flex-col rounded-xl border border-popover-border bg-popover-bg shadow-dialog", "forced-colors:border-[CanvasText]", "max-sm:rounded-b-none max-sm:border-b-0 max-sm:pb-[env(safe-area-inset-bottom)]"),
1326
+ children
1327
+ })
1328
+ });
1329
+ }
1330
+ function DialogHeader({ title, description, closeButton, className, ...props }) {
1331
+ const context = useDialogContext("DialogHeader");
1332
+ const hasDescription = description != null && description !== false && description !== "";
1333
+ const showClose = closeButton ?? !context.alert;
1334
+ useAnnounceHeading(context, hasDescription);
1335
+ return /* @__PURE__ */ jsxs("div", {
1336
+ ...props,
1337
+ "data-slot": "dialog-header",
1338
+ className: cn("flex shrink-0 items-start gap-4 px-6 pt-6 pb-4", className),
1339
+ children: [/* @__PURE__ */ jsxs("div", {
1340
+ className: "min-w-0 flex-1",
1341
+ children: [/* @__PURE__ */ jsx("h2", {
1342
+ id: context.titleId,
1343
+ className: "m-0 text-xl/tight font-medium text-fg",
1344
+ children: title
1345
+ }), hasDescription && /* @__PURE__ */ jsx("p", {
1346
+ id: context.descriptionId,
1347
+ className: "mt-1.5 mb-0 text-sm text-fg-muted",
1348
+ children: description
1349
+ })]
1350
+ }), showClose && /* @__PURE__ */ jsx(Button, {
1351
+ intent: "ghost",
1352
+ size: "sm",
1353
+ icon: /* @__PURE__ */ jsx(X, {}),
1354
+ "aria-label": "Close",
1355
+ tooltip: false,
1356
+ disabled: !context.dismissible,
1357
+ className: "-me-2 -mt-1 shrink-0",
1358
+ onClick: () => context.requestClose("close-button")
1359
+ })]
1360
+ });
1361
+ }
1362
+ /** Scrolls when the content is taller than the space left between header and footer. */
1363
+ function DialogBody({ className, ...props }) {
1364
+ return /* @__PURE__ */ jsx("div", {
1365
+ ...props,
1366
+ "data-slot": "dialog-body",
1367
+ className: cn("min-h-0 overflow-y-auto overscroll-contain px-6 py-1 text-sm text-fg", className)
1368
+ });
1369
+ }
1370
+ /** Actions, right-aligned: Cancel first, the primary action last (rightmost). */
1371
+ function DialogFooter({ className, ...props }) {
1372
+ return /* @__PURE__ */ jsx("div", {
1373
+ ...props,
1374
+ "data-slot": "dialog-footer",
1375
+ className: cn("flex shrink-0 flex-wrap items-center justify-end gap-2 px-6 pt-5 pb-6", "max-sm:*:data-[slot=button]:flex-1", className)
1376
+ });
1377
+ }
1378
+ //#endregion
1379
+ //#region src/components/drawer/drawer.tsx
1380
+ function useDrawerContext(part) {
1381
+ return useModalContext(part, "drawer", "Drawer");
1382
+ }
1383
+ const sizeClasses$1 = {
1384
+ sm: "w-[280px]",
1385
+ md: "w-[384px]",
1386
+ lg: "w-[480px]"
1387
+ };
1388
+ const sideClasses = {
1389
+ left: ["mr-auto ml-0", "motion-safe:starting:open:-translate-x-full motion-safe:data-closing:-translate-x-full"],
1390
+ right: ["mr-0 ml-auto", "motion-safe:starting:open:translate-x-full motion-safe:data-closing:translate-x-full"]
1391
+ };
1392
+ const motionClasses = [
1393
+ "transition-[translate,opacity] duration-200 ease-[cubic-bezier(0.4,0,0.2,1)]",
1394
+ "motion-reduce:starting:open:opacity-0 motion-reduce:data-closing:opacity-0",
1395
+ "data-closing:pointer-events-none",
1396
+ "backdrop:bg-dialog-backdrop backdrop:transition-opacity backdrop:duration-200 backdrop:ease-[cubic-bezier(0.4,0,0.2,1)]",
1397
+ "starting:open:backdrop:opacity-0 data-closing:backdrop:opacity-0"
1398
+ ];
1399
+ const panelSideClasses = {
1400
+ left: "border-r",
1401
+ right: "border-l"
1402
+ };
1403
+ /**
1404
+ * Modal side panel on the native `<dialog>` (behavior shared with Dialog through ModalShell).
1405
+ * Controlled through `open` / `onOpenChange(open, reason)`; Esc, the backdrop, DrawerHeader's close
1406
+ * button and DrawerClose all ask through `onOpenChange`. Needs a name: `aria-label` /
1407
+ * `aria-labelledby`, or a DrawerHeader title.
1408
+ */
1409
+ function Drawer({ side = "right", size = "md", className, children, ref, ...props }) {
1410
+ const dialogRef = useRef(null);
1411
+ const setRef = useMergedRef(dialogRef, ref);
1412
+ useEffect(() => {
1413
+ if (process.env.NODE_ENV === "production" || !props.open) return;
1414
+ const timer = setTimeout(() => {
1415
+ const el = dialogRef.current;
1416
+ if (!el?.open) return;
1417
+ if (el.getAttribute("aria-label")?.trim() || el.getAttribute("aria-labelledby")) return;
1418
+ console.warn("Drawer needs an accessible name: pass aria-label or render a DrawerHeader title.");
1419
+ });
1420
+ return () => clearTimeout(timer);
1421
+ }, [props.open]);
1422
+ return /* @__PURE__ */ jsx(ModalShell, {
1423
+ ...props,
1424
+ ref: setRef,
1425
+ kind: "drawer",
1426
+ "data-side": side,
1427
+ "data-size": size,
1428
+ className: cn("my-0 h-dvh max-h-none max-w-[calc(100vw-56px)] overflow-visible border-0 bg-transparent p-0 text-fg", "overscroll-contain outline-none", sizeClasses$1[size], sideClasses[side], motionClasses, className),
1429
+ children: /* @__PURE__ */ jsx("div", {
1430
+ "data-slot": "drawer-panel",
1431
+ className: cn("flex h-full flex-col border-popover-border bg-popover-bg shadow-dialog", panelSideClasses[side], "forced-colors:border-[CanvasText]"),
1432
+ children
1433
+ })
1434
+ });
1435
+ }
1436
+ /** Title, optional description and the close button, pinned above the body. */
1437
+ function DrawerHeader({ title, description, className, ...props }) {
1438
+ const context = useDrawerContext("DrawerHeader");
1439
+ const hasDescription = description != null && description !== false && description !== "";
1440
+ useAnnounceHeading(context, hasDescription);
1441
+ return /* @__PURE__ */ jsxs("div", {
1442
+ ...props,
1443
+ "data-slot": "drawer-header",
1444
+ className: cn("flex shrink-0 items-start gap-4 px-6 pt-6 pb-4", className),
1445
+ children: [/* @__PURE__ */ jsxs("div", {
1446
+ className: "min-w-0 flex-1",
1447
+ children: [/* @__PURE__ */ jsx("h2", {
1448
+ id: context.titleId,
1449
+ className: "m-0 text-xl/tight font-medium wrap-anywhere text-fg",
1450
+ children: title
1451
+ }), hasDescription && /* @__PURE__ */ jsx("p", {
1452
+ id: context.descriptionId,
1453
+ className: "mt-1.5 mb-0 text-sm wrap-anywhere text-fg-muted",
1454
+ children: description
1455
+ })]
1456
+ }), /* @__PURE__ */ jsx(Button, {
1457
+ intent: "ghost",
1458
+ size: "sm",
1459
+ icon: /* @__PURE__ */ jsx(X, {}),
1460
+ "aria-label": "Close",
1461
+ tooltip: false,
1462
+ className: "-me-2 -mt-1 shrink-0",
1463
+ onClick: () => context.requestClose("close-button")
1464
+ })]
1465
+ });
1466
+ }
1467
+ /** Fills the space between header and footer and scrolls on its own; they stay put. */
1468
+ function DrawerBody({ className, ...props }) {
1469
+ return /* @__PURE__ */ jsx("div", {
1470
+ ...props,
1471
+ "data-slot": "drawer-body",
1472
+ className: cn("min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 py-1 text-sm text-fg", className)
1473
+ });
1474
+ }
1475
+ /** Actions, right-aligned: Cancel first, the primary action last (rightmost). */
1476
+ function DrawerFooter({ className, ...props }) {
1477
+ return /* @__PURE__ */ jsx("div", {
1478
+ ...props,
1479
+ "data-slot": "drawer-footer",
1480
+ className: cn("flex shrink-0 flex-wrap items-center justify-end gap-2 px-6 pt-5 pb-6", className)
1481
+ });
1482
+ }
1483
+ /** A ghost icon button that closes the drawer, for custom headers. */
1484
+ function DrawerClose({ size = "sm", icon, ...props }) {
1485
+ const { requestClose } = useDrawerContext("DrawerClose");
1486
+ return /* @__PURE__ */ jsx(Button, {
1487
+ ...props,
1488
+ intent: "ghost",
1489
+ size,
1490
+ icon: icon ?? /* @__PURE__ */ jsx(X, {}),
1491
+ tooltip: false,
1492
+ onClick: () => requestClose("close-button")
1493
+ });
1494
+ }
1495
+ //#endregion
1496
+ //#region src/components/input/copy-input.tsx
1497
+ const RESET_MS = 1500;
1498
+ function CopyInput({ value, mono = true, size = "md", onFocus, onClick, ...props }) {
1499
+ const inputRef = useRef(null);
1500
+ const [result, setResult] = useState("idle");
1501
+ const timer = useRef(void 0);
1502
+ useEffect(() => () => clearTimeout(timer.current), []);
1503
+ const copy = async () => {
1504
+ clearTimeout(timer.current);
1505
+ try {
1506
+ await navigator.clipboard.writeText(value);
1507
+ setResult("copied");
1508
+ } catch {
1509
+ setResult("failed");
1510
+ }
1511
+ timer.current = setTimeout(() => setResult("idle"), RESET_MS);
1512
+ };
1513
+ return /* @__PURE__ */ jsx(FieldFrame, {
1514
+ ...props,
1515
+ value,
1516
+ readOnly: true,
1517
+ mono,
1518
+ size,
1519
+ inputRef,
1520
+ status: result === "copied" ? "Copied" : result === "failed" ? "Copy failed" : "",
1521
+ onFocus: (e) => {
1522
+ e.currentTarget.select();
1523
+ onFocus?.(e);
1524
+ },
1525
+ onClick: (e) => {
1526
+ e.currentTarget.select();
1527
+ onClick?.(e);
1528
+ },
1529
+ actions: props.disabled ? null : /* @__PURE__ */ jsx(Button, {
1530
+ icon: result === "copied" ? /* @__PURE__ */ jsx(Check, {}) : /* @__PURE__ */ jsx(Copy, {}),
1531
+ "aria-label": "Copy",
1532
+ intent: "ghost",
1533
+ size,
1534
+ className: compactButton[size],
1535
+ onClick: copy
1536
+ })
1537
+ });
1538
+ }
1539
+ //#endregion
1540
+ //#region src/components/input/input.tsx
1541
+ function Input({ type = "text", ...props }) {
1542
+ return /* @__PURE__ */ jsx(FieldFrame, {
1543
+ ...props,
1544
+ type
1545
+ });
1546
+ }
1547
+ //#endregion
1548
+ //#region src/components/input/password-input.tsx
1549
+ function PasswordInput({ autoComplete = "current-password", size = "md", ...props }) {
1550
+ const inputRef = useRef(null);
1551
+ const [visible, setVisible] = useState(false);
1552
+ return /* @__PURE__ */ jsx(FieldFrame, {
1553
+ ...props,
1554
+ size,
1555
+ type: visible ? "text" : "password",
1556
+ autoComplete,
1557
+ inputRef,
1558
+ actions: props.disabled ? null : /* @__PURE__ */ jsx(Button, {
1559
+ icon: visible ? /* @__PURE__ */ jsx(EyeOff, {}) : /* @__PURE__ */ jsx(Eye, {}),
1560
+ "aria-label": "Show password",
1561
+ "aria-pressed": visible,
1562
+ intent: "ghost",
1563
+ size,
1564
+ className: compactButton[size],
1565
+ onMouseDown: (e) => e.preventDefault(),
1566
+ onClick: () => setVisible((v) => !v)
1567
+ })
1568
+ });
1569
+ }
1570
+ //#endregion
1571
+ //#region src/components/input/search-input.tsx
1572
+ function setNativeValue(input, value) {
1573
+ Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set?.call(input, value);
1574
+ input.dispatchEvent(new Event("input", { bubbles: true }));
1575
+ }
1576
+ function SearchInput({ onClear, onChange, onKeyDown, inputClassName, size = "md", ...props }) {
1577
+ const inputRef = useRef(null);
1578
+ const controlled = props.value !== void 0;
1579
+ const [filled, setFilled] = useState(() => Boolean(props.defaultValue));
1580
+ const canClear = (controlled ? String(props.value) !== "" : filled) && !props.disabled && !props.readOnly;
1581
+ const clear = () => {
1582
+ const input = inputRef.current;
1583
+ if (!input) return;
1584
+ setNativeValue(input, "");
1585
+ onClear?.();
1586
+ input.focus();
1587
+ };
1588
+ return /* @__PURE__ */ jsx(FieldFrame, {
1589
+ ...props,
1590
+ size,
1591
+ type: "search",
1592
+ inputRef,
1593
+ start: /* @__PURE__ */ jsx(Search, {}),
1594
+ inputClassName: cn("[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none", inputClassName),
1595
+ onChange: (e) => {
1596
+ if (!controlled) setFilled(e.currentTarget.value !== "");
1597
+ onChange?.(e);
1598
+ },
1599
+ onKeyDown: (e) => {
1600
+ onKeyDown?.(e);
1601
+ if (e.key !== "Escape" || e.defaultPrevented || e.currentTarget.value === "") return;
1602
+ if (props.readOnly) return;
1603
+ e.preventDefault();
1604
+ e.stopPropagation();
1605
+ clear();
1606
+ },
1607
+ actions: canClear ? /* @__PURE__ */ jsx(Button, {
1608
+ icon: /* @__PURE__ */ jsx(X, {}),
1609
+ "aria-label": "Clear search",
1610
+ intent: "ghost",
1611
+ size,
1612
+ className: compactButton[size],
1613
+ onMouseDown: (e) => e.preventDefault(),
1614
+ onClick: clear
1615
+ }) : null
1616
+ });
1617
+ }
1618
+ //#endregion
1619
+ //#region src/components/meter/meter.tsx
1620
+ const toneClasses = {
1621
+ neutral: "bg-neutral-border",
1622
+ info: "bg-info",
1623
+ success: "bg-success",
1624
+ warning: "bg-warning",
1625
+ danger: "bg-danger"
1626
+ };
1627
+ /**
1628
+ * Stacked proportion bar with a legend. The bar is decorative; the legend list carries the
1629
+ * numbers for assistive tech, so nothing is conveyed by color alone.
1630
+ */
1631
+ function Meter({ label, summary, segments, className, ...props }) {
1632
+ const labelId = useId();
1633
+ const shown = segments.filter((s) => s.value > 0);
1634
+ return /* @__PURE__ */ jsxs("div", {
1635
+ ...props,
1636
+ "data-slot": "meter",
1637
+ className: cn("text-xs", className),
1638
+ children: [
1639
+ /* @__PURE__ */ jsxs("div", {
1640
+ className: "flex justify-between gap-3",
1641
+ children: [/* @__PURE__ */ jsx("span", {
1642
+ id: labelId,
1643
+ className: "font-medium",
1644
+ children: label
1645
+ }), summary && /* @__PURE__ */ jsx("span", {
1646
+ className: "text-fg-muted",
1647
+ children: summary
1648
+ })]
1649
+ }),
1650
+ /* @__PURE__ */ jsx("div", {
1651
+ "aria-hidden": true,
1652
+ className: "mt-2 flex h-2 gap-0.5 overflow-hidden rounded-full bg-surface-muted",
1653
+ children: shown.map((s) => /* @__PURE__ */ jsx("span", {
1654
+ className: cn("min-w-1", toneClasses[s.tone]),
1655
+ style: { flex: s.value }
1656
+ }, s.label))
1657
+ }),
1658
+ shown.length > 0 && /* @__PURE__ */ jsx("ul", {
1659
+ "aria-labelledby": labelId,
1660
+ className: "mt-2 flex flex-wrap gap-x-3.5 gap-y-1 text-fg-muted",
1661
+ children: shown.map((s) => /* @__PURE__ */ jsxs("li", {
1662
+ className: "flex items-center gap-1.5",
1663
+ children: [
1664
+ /* @__PURE__ */ jsx("span", {
1665
+ "aria-hidden": true,
1666
+ className: cn("size-2 rounded-xs", toneClasses[s.tone])
1667
+ }),
1668
+ /* @__PURE__ */ jsx("span", {
1669
+ className: "font-medium text-fg",
1670
+ children: s.value
1671
+ }),
1672
+ s.label
1673
+ ]
1674
+ }, s.label))
1675
+ })
1676
+ ]
1677
+ });
1678
+ }
1679
+ //#endregion
1680
+ //#region src/components/select/select.tsx
1681
+ const selectPadding = {
1682
+ sm: "px-3",
1683
+ md: "px-3",
1684
+ lg: "px-4"
1685
+ };
1686
+ const iconStartPadding = {
1687
+ sm: "ps-8",
1688
+ md: "ps-[34px]",
1689
+ lg: "ps-[38px]"
1690
+ };
1691
+ const chevronEndPadding = {
1692
+ sm: "pe-8",
1693
+ md: "pe-[34px]",
1694
+ lg: "pe-[38px]"
1695
+ };
1696
+ const labelRestingWidth = {
1697
+ sm: "max-w-[calc(100%-40px)]",
1698
+ md: "max-w-[calc(100%-42px)]",
1699
+ lg: "max-w-[calc(100%-50px)]"
1700
+ };
1701
+ const isGroup = (item) => "options" in item;
1702
+ const pickerClasses = cn("supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[appearance:base-select]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:[appearance:base-select]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker-icon]:hidden", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:[position-area:block-end_span-inline-end]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:[position-try-fallbacks:flip-block]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:mt-1", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:min-w-[anchor-size(width)]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:max-h-[max(10rem,calc(100%-12px))]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:overflow-y-auto", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:rounded-xl", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:border", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:border-popover-border", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:bg-popover-bg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:p-1", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:shadow-popover", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:text-fg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:origin-top", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:opacity-0", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:scale-96", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:transition-[opacity,scale,display,overlay]", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:transition-discrete", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:duration-80", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::picker(select)]:ease-out", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:opacity-100", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:scale-100", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:duration-120", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:starting:opacity-0", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:open:[&::picker(select)]:starting:scale-96", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:motion-reduce:[&::picker(select)]:transition-none");
1703
+ function optionClasses(size, mono) {
1704
+ return cn("bg-popover-bg text-fg checked:font-medium disabled:text-fg-subtle", textSize[size], mono && "font-mono", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:flex", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:cursor-default", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:items-center", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:gap-2", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:rounded-lg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:bg-transparent", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:px-2", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:py-1.5", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:not-disabled:hover:bg-popover-highlight", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:focus-visible:bg-popover-highlight", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:focus-visible:outline-hidden", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:disabled:text-fg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:disabled:opacity-50", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::checkmark]:order-1", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:[&::checkmark]:ms-auto");
1705
+ }
1706
+ function openPicker(select) {
1707
+ select.focus();
1708
+ try {
1709
+ select.showPicker?.();
1710
+ } catch {}
1711
+ }
1712
+ /**
1713
+ * Single-choice select: a real `<select>` (native keyboard, typeahead, forms, validation and
1714
+ * autofill), customized through `appearance: base-select` where supported. Same frame as Input.
1715
+ */
1716
+ function Select({ options, placeholder, helperText, error, size = "md", start, mono = false, className, selectClassName, label, floating = false, id, required, disabled, value, defaultValue, onChange, ref, "aria-describedby": describedByProp, "aria-invalid": ariaInvalid, ...props }) {
1717
+ const selectRef = useRef(null);
1718
+ const setRef = useMergedRef(selectRef, ref);
1719
+ const controlled = value !== void 0;
1720
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? "");
1721
+ const current = controlled ? value : uncontrolledValue;
1722
+ useEffect(() => {
1723
+ const form = selectRef.current?.form;
1724
+ if (!form) return;
1725
+ let timer;
1726
+ const onReset = () => {
1727
+ timer = setTimeout(() => {
1728
+ if (selectRef.current) setUncontrolledValue(selectRef.current.value);
1729
+ });
1730
+ };
1731
+ form.addEventListener("reset", onReset);
1732
+ return () => {
1733
+ form.removeEventListener("reset", onReset);
1734
+ clearTimeout(timer);
1735
+ };
1736
+ }, [props.form]);
1737
+ const leading = options.flatMap((item) => isGroup(item) ? item.options : [item]).find((option) => option.value === current)?.icon ?? start;
1738
+ const hasLeading = isPresent(leading);
1739
+ const optionClass = optionClasses(size, mono);
1740
+ const renderOption = (option) => /* @__PURE__ */ jsxs("option", {
1741
+ value: option.value,
1742
+ disabled: option.disabled,
1743
+ className: optionClass,
1744
+ children: [isPresent(option.icon) && /* @__PURE__ */ jsx("span", {
1745
+ "aria-hidden": true,
1746
+ className: "flex shrink-0 text-fg-muted [&_svg]:size-4 [&_svg]:shrink-0",
1747
+ children: option.icon
1748
+ }), /* @__PURE__ */ jsx("span", { children: option.label })]
1749
+ }, option.value);
1750
+ return /* @__PURE__ */ jsx(FieldShell, {
1751
+ label,
1752
+ floating,
1753
+ helperText,
1754
+ error,
1755
+ size,
1756
+ id,
1757
+ required,
1758
+ disabled,
1759
+ "aria-describedby": describedByProp,
1760
+ hasStart: hasLeading,
1761
+ className,
1762
+ controlRef: selectRef,
1763
+ activate: openPicker,
1764
+ children: (field) => /* @__PURE__ */ jsxs(Fragment, { children: [
1765
+ field.floatingLabel(labelRestingWidth[size]),
1766
+ /* @__PURE__ */ jsxs("select", {
1767
+ ...props,
1768
+ ref: setRef,
1769
+ id: field.id,
1770
+ required,
1771
+ disabled,
1772
+ ...controlled ? { value } : { defaultValue: defaultValue ?? "" },
1773
+ onChange: (e) => {
1774
+ if (!controlled) setUncontrolledValue(e.currentTarget.value);
1775
+ onChange?.(e);
1776
+ },
1777
+ "aria-invalid": field.invalid ? true : ariaInvalid,
1778
+ "aria-describedby": field.describedBy,
1779
+ className: cn("size-full min-w-0 appearance-none items-center truncate rounded-[inherit] bg-transparent text-fg outline-none", "pointer-coarse:text-base disabled:cursor-not-allowed", "[&:has(option[value=\"\"]:checked)]:text-input-placeholder", textSize[size], selectPadding[size], hasLeading && iconStartPadding[size], chevronEndPadding[size], mono && "font-mono", pickerClasses, selectClassName),
1780
+ children: [/* @__PURE__ */ jsx("option", {
1781
+ value: "",
1782
+ disabled: true,
1783
+ hidden: true,
1784
+ children: field.floating ? "" : placeholder ?? ""
1785
+ }), options.map((item) => isGroup(item) ? /* @__PURE__ */ jsx("optgroup", {
1786
+ label: item.label,
1787
+ className: cn("bg-popover-bg font-sans text-fg", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:text-xs", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:font-medium", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:text-fg-muted", "supports-[appearance:base-select]:[@media(hover:hover)_and_(pointer:fine)]:not-first-of-type:mt-1"),
1788
+ children: item.options.map(renderOption)
1789
+ }, `group:${item.label}`) : renderOption(item))]
1790
+ }),
1791
+ hasLeading && /* @__PURE__ */ jsx("span", {
1792
+ "aria-hidden": true,
1793
+ className: cn("pointer-events-none absolute inset-y-0 inset-s-0 flex items-center text-fg-muted [&_svg]:shrink-0", iconSize[size], startSlotPadding[size]),
1794
+ children: leading
1795
+ }),
1796
+ /* @__PURE__ */ jsx("span", {
1797
+ "aria-hidden": true,
1798
+ className: cn("pointer-events-none absolute inset-y-0 inset-e-0 flex items-center text-fg-muted", iconSize[size], endSlotPadding[size]),
1799
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: "transition-transform duration-150 ease-out group-has-[select:open]/control:rotate-180 motion-reduce:transition-none" })
1800
+ })
1801
+ ] })
1802
+ });
1803
+ }
1804
+ //#endregion
1805
+ //#region src/lib/slot.tsx
1806
+ function setRef(ref, node) {
1807
+ if (typeof ref === "function") return ref(node);
1808
+ if (ref) ref.current = node;
1809
+ }
1810
+ function composeRefs(...refs) {
1811
+ return (node) => {
1812
+ const cleanups = refs.map((ref) => setRef(ref, node));
1813
+ return () => {
1814
+ for (const [i, ref] of refs.entries()) {
1815
+ const cleanup = cleanups[i];
1816
+ if (typeof cleanup === "function") cleanup();
1817
+ else setRef(ref, null);
1818
+ }
1819
+ };
1820
+ };
1821
+ }
1822
+ function mergeProps(slot, child) {
1823
+ const merged = {
1824
+ ...slot,
1825
+ ...child
1826
+ };
1827
+ for (const key of Object.keys(slot)) {
1828
+ const own = slot[key];
1829
+ const theirs = child[key];
1830
+ if (/^on[A-Z]/.test(key) && typeof own === "function" && typeof theirs === "function") merged[key] = (...args) => {
1831
+ theirs(...args);
1832
+ own(...args);
1833
+ };
1834
+ else if (key === "className") merged.className = cn(own, theirs);
1835
+ else if (key === "style") merged.style = {
1836
+ ...own,
1837
+ ...theirs
1838
+ };
1839
+ }
1840
+ return merged;
1841
+ }
1842
+ /** Applies `wrap` to an element's children, or to what a render-prop child returns. */
1843
+ function wrapChildren(children, wrap) {
1844
+ if (typeof children !== "function") return wrap(children);
1845
+ return (...args) => wrap(children(...args));
1846
+ }
1847
+ /** Renders its only child with these props merged in (Radix Slot style). */
1848
+ function Slot({ children, ref, ...props }) {
1849
+ const child = Children.only(children);
1850
+ const childProps = isValidElement(child) ? child.props : {};
1851
+ const childRef = childProps.ref;
1852
+ const composedRef = useMemo(() => composeRefs(ref, childRef), [ref, childRef]);
1853
+ if (!isValidElement(child)) return null;
1854
+ return cloneElement(child, {
1855
+ ...mergeProps(props, childProps),
1856
+ ref: composedRef
1857
+ });
1858
+ }
1859
+ //#endregion
1860
+ //#region src/components/tabs/shared.tsx
1861
+ const listBase = [
1862
+ "relative isolate max-w-full overflow-x-auto overscroll-x-contain scroll-px-6",
1863
+ "[mask-image:linear-gradient(to_right,transparent,#000_var(--tabs-fade-left,0px),#000_calc(100%-var(--tabs-fade-right,0px)),transparent)]",
1864
+ "[scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
1865
+ "before:pointer-events-none before:absolute before:left-0 before:-z-1 before:opacity-0",
1866
+ "before:w-(--tabs-indicator-w) data-indicator:before:opacity-100",
1867
+ "before:duration-200 before:ease-[cubic-bezier(0.16,1,0.3,1)]",
1868
+ "data-instant:before:transition-none data-instant:after:transition-none"
1869
+ ];
1870
+ const listVariant = {
1871
+ line: [
1872
+ "flex items-center shadow-[inset_0_-1px_0_var(--color-border-strong)]",
1873
+ "before:bottom-0 before:h-0.5 before:bg-fg before:[translate:var(--tabs-indicator-x)_0]",
1874
+ "before:transition-[translate,width]",
1875
+ "forced-colors:before:bg-[Highlight] forced-colors:before:[forced-color-adjust:none]",
1876
+ "after:pointer-events-none after:absolute after:top-0 after:left-0 after:-z-1 after:rounded-lg",
1877
+ "after:bg-fg/7 after:opacity-0 data-hover:after:opacity-100",
1878
+ "after:h-(--tabs-hover-h) after:w-(--tabs-hover-w)",
1879
+ "after:[translate:var(--tabs-hover-x)_var(--tabs-hover-y)]",
1880
+ "after:transition-[translate,width,height,opacity] after:duration-150",
1881
+ "after:ease-[cubic-bezier(0.33,1,0.68,1)]",
1882
+ "data-hover-enter:after:transition-opacity"
1883
+ ],
1884
+ segmented: [
1885
+ "inline-flex w-fit rounded-md p-1 shadow-[inset_0_0_0_1px_var(--color-ring)]",
1886
+ "before:top-0 before:h-(--tabs-indicator-h) before:rounded-sm before:bg-fg/8",
1887
+ "before:[translate:var(--tabs-indicator-x)_var(--tabs-indicator-y)]",
1888
+ "before:transition-[translate,width,height]",
1889
+ "forced-colors:before:border-2 forced-colors:before:border-[Highlight]"
1890
+ ]
1891
+ };
1892
+ const listSize = {
1893
+ line: {
1894
+ sm: "h-10",
1895
+ md: "h-12"
1896
+ },
1897
+ segmented: {
1898
+ sm: "h-8",
1899
+ md: "h-9"
1900
+ }
1901
+ };
1902
+ function listClasses(variant, size) {
1903
+ return [
1904
+ ...listBase,
1905
+ ...listVariant[variant],
1906
+ listSize[variant][size],
1907
+ variant === "segmented" ? "pointer-coarse:h-13" : "pointer-coarse:h-12"
1908
+ ];
1909
+ }
1910
+ const itemBase = [
1911
+ "relative inline-flex shrink-0 cursor-pointer items-center justify-center",
1912
+ "text-sm whitespace-nowrap text-fg-muted no-underline select-none",
1913
+ "touch-manipulation [-webkit-tap-highlight-color:transparent]",
1914
+ "transition-colors duration-150 ease-[cubic-bezier(0.4,0,0.2,1)]",
1915
+ "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus",
1916
+ "pointer-coarse:min-h-11 pointer-coarse:min-w-11",
1917
+ "not-data-disabled:not-aria-disabled:hover:text-fg data-selected:text-fg aria-[current=page]:text-fg",
1918
+ "data-disabled:cursor-not-allowed data-disabled:opacity-50",
1919
+ "aria-disabled:cursor-not-allowed aria-disabled:opacity-50",
1920
+ "forced-colors:data-disabled:text-[GrayText] forced-colors:data-disabled:opacity-100",
1921
+ "forced-colors:aria-disabled:text-[GrayText] forced-colors:aria-disabled:opacity-100"
1922
+ ];
1923
+ const itemVariant = {
1924
+ line: ["rounded-lg data-selected:font-medium aria-[current=page]:font-medium"],
1925
+ segmented: ["h-full rounded-sm font-medium"]
1926
+ };
1927
+ const itemSize = {
1928
+ line: {
1929
+ sm: "h-7 px-2.5",
1930
+ md: "h-8 px-3"
1931
+ },
1932
+ segmented: {
1933
+ sm: "px-2.5",
1934
+ md: "px-3"
1935
+ }
1936
+ };
1937
+ function itemClasses$1(variant, size) {
1938
+ return [
1939
+ ...itemBase,
1940
+ ...itemVariant[variant],
1941
+ itemSize[variant][size]
1942
+ ];
1943
+ }
1944
+ /** Icon, label and badge, laid out the same in both components. */
1945
+ function TabContent({ icon, badge, children }) {
1946
+ return /* @__PURE__ */ jsxs("span", {
1947
+ "data-slot": "tab-content",
1948
+ className: "inline-flex items-center gap-2",
1949
+ children: [
1950
+ icon != null && icon !== false && /* @__PURE__ */ jsx("span", {
1951
+ "aria-hidden": true,
1952
+ "data-slot": "tab-icon",
1953
+ className: "inline-flex shrink-0 [&_svg]:size-4",
1954
+ children: icon
1955
+ }),
1956
+ /* @__PURE__ */ jsxs("span", {
1957
+ className: "grid justify-items-center",
1958
+ children: [/* @__PURE__ */ jsx("span", {
1959
+ className: "col-start-1 row-start-1",
1960
+ children
1961
+ }), /* @__PURE__ */ jsx("span", {
1962
+ "aria-hidden": true,
1963
+ "data-slot": "tab-label-sizer",
1964
+ className: "invisible col-start-1 row-start-1 font-medium",
1965
+ children
1966
+ })]
1967
+ }),
1968
+ badge != null && badge !== false && /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx("span", {
1969
+ "data-slot": "tab-badge",
1970
+ className: "inline-flex",
1971
+ children: badge
1972
+ })] })
1973
+ ]
1974
+ });
1975
+ }
1976
+ function boxIn(list, el) {
1977
+ const lr = list.getBoundingClientRect();
1978
+ const r = el.getBoundingClientRect();
1979
+ const scale = list.offsetWidth ? lr.width / list.offsetWidth : 1;
1980
+ return {
1981
+ x: (r.left - lr.left) / scale - list.clientLeft + list.scrollLeft,
1982
+ y: (r.top - lr.top) / scale - list.clientTop + list.scrollTop,
1983
+ w: r.width / scale,
1984
+ h: r.height / scale
1985
+ };
1986
+ }
1987
+ function setBox(list, name, { x, y, w, h }) {
1988
+ list.style.setProperty(`--tabs-${name}-x`, `${x}px`);
1989
+ list.style.setProperty(`--tabs-${name}-y`, `${y}px`);
1990
+ list.style.setProperty(`--tabs-${name}-w`, `${w}px`);
1991
+ list.style.setProperty(`--tabs-${name}-h`, `${h}px`);
1992
+ }
1993
+ const isSelected = (item) => item.dataset.selected !== void 0 || item.getAttribute("aria-current") === "page";
1994
+ const isDisabled = (item) => item.dataset.disabled !== void 0 || item.getAttribute("aria-disabled") === "true";
1995
+ /** This list's own items; a nested tab list keeps its items to itself. */
1996
+ function ownItems(list) {
1997
+ return [...list.querySelectorAll("[data-slot=\"tab\"]")].filter((item) => item.closest("[data-slot=\"tab-list\"]") === list);
1998
+ }
1999
+ const LINE_BAR_OUTSET = 2;
2000
+ const EDGE_FADE = 24;
2001
+ function setEdgeFade(list) {
2002
+ const max = list.scrollWidth - list.clientWidth;
2003
+ const left = getComputedStyle(list).direction === "rtl" ? max + list.scrollLeft : list.scrollLeft;
2004
+ const fade = (distance) => Math.min(Math.max(Math.round(distance), 0), EDGE_FADE);
2005
+ list.style.setProperty("--tabs-fade-left", `${fade(left)}px`);
2006
+ list.style.setProperty("--tabs-fade-right", `${fade(max - left)}px`);
2007
+ }
2008
+ function useTabListMotion(variant, { revealSelected = false } = {}) {
2009
+ const listRef = useRef(null);
2010
+ const hovered = useRef(null);
2011
+ useLayoutEffect(() => {
2012
+ const list = listRef.current;
2013
+ if (!list) return;
2014
+ let revealed;
2015
+ const place = (instant) => {
2016
+ setEdgeFade(list);
2017
+ const item = ownItems(list).find(isSelected);
2018
+ if (!item) {
2019
+ delete list.dataset.indicator;
2020
+ return;
2021
+ }
2022
+ if (revealSelected && item !== revealed) {
2023
+ revealed = item;
2024
+ revealInList(list, item);
2025
+ }
2026
+ const content = item.querySelector("[data-slot=\"tab-content\"]");
2027
+ const box = boxIn(list, variant === "line" && content ? content : item);
2028
+ if (variant === "line") {
2029
+ box.x -= LINE_BAR_OUTSET;
2030
+ box.w += 4;
2031
+ }
2032
+ if (instant || list.dataset.indicator === void 0) {
2033
+ list.dataset.instant = "";
2034
+ setBox(list, "indicator", box);
2035
+ list.dataset.indicator = "";
2036
+ list.offsetWidth;
2037
+ delete list.dataset.instant;
2038
+ } else setBox(list, "indicator", box);
2039
+ };
2040
+ place(true);
2041
+ const onScroll = () => setEdgeFade(list);
2042
+ list.addEventListener("scroll", onScroll, { passive: true });
2043
+ const resizes = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(() => place(true));
2044
+ resizes?.observe(list);
2045
+ for (const item of ownItems(list)) resizes?.observe(item);
2046
+ const mutations = new MutationObserver(() => {
2047
+ place(false);
2048
+ for (const item of ownItems(list)) resizes?.observe(item);
2049
+ });
2050
+ mutations.observe(list, {
2051
+ subtree: true,
2052
+ childList: true,
2053
+ attributes: true,
2054
+ attributeFilter: ["data-selected", "aria-current"]
2055
+ });
2056
+ return () => {
2057
+ list.removeEventListener("scroll", onScroll);
2058
+ mutations.disconnect();
2059
+ resizes?.disconnect();
2060
+ };
2061
+ }, [variant, revealSelected]);
2062
+ const onPointerMove = (e) => {
2063
+ const list = listRef.current;
2064
+ if (variant !== "line" || !list || e.pointerType && e.pointerType !== "mouse") return;
2065
+ const item = e.target.closest("[data-slot=\"tab\"]");
2066
+ if (!item || isDisabled(item) || !ownItems(list).includes(item)) return;
2067
+ const entering = list.dataset.hover === void 0;
2068
+ if (item === hovered.current && !entering) return;
2069
+ hovered.current = item;
2070
+ if (entering) list.dataset.hoverEnter = "";
2071
+ else delete list.dataset.hoverEnter;
2072
+ setBox(list, "hover", boxIn(list, item));
2073
+ list.dataset.hover = "";
2074
+ };
2075
+ const onPointerLeave = () => {
2076
+ const list = listRef.current;
2077
+ if (!list) return;
2078
+ hovered.current = null;
2079
+ delete list.dataset.hover;
2080
+ };
2081
+ return {
2082
+ listRef,
2083
+ onPointerMove,
2084
+ onPointerLeave
2085
+ };
2086
+ }
2087
+ function revealInList(list, el) {
2088
+ const pad = Number.parseFloat(getComputedStyle(list).scrollPaddingInlineStart) || 0;
2089
+ const start = el.getBoundingClientRect().left - list.getBoundingClientRect().left;
2090
+ const end = start + el.offsetWidth;
2091
+ if (start < pad) list.scrollLeft += start - pad;
2092
+ else if (end > list.clientWidth - pad) list.scrollLeft += end - list.clientWidth + pad;
2093
+ }
2094
+ function useRevealWhenSelected(ref, selected) {
2095
+ useLayoutEffect(() => {
2096
+ const el = ref.current;
2097
+ const list = el?.closest("[data-slot=\"tab-list\"]");
2098
+ if (selected && el && list) revealInList(list, el);
2099
+ }, [ref, selected]);
2100
+ }
2101
+ function revealOnFocus(el) {
2102
+ el.scrollIntoView?.({
2103
+ block: "nearest",
2104
+ inline: "nearest"
2105
+ });
2106
+ }
2107
+ //#endregion
2108
+ //#region src/components/sidebar/sidebar.tsx
2109
+ /** At and above this width the sidebar is part of the layout; below it, a drawer. */
2110
+ const WIDE_QUERY = "(min-width: 960px)";
2111
+ const SidebarContext = createContext(null);
2112
+ function useSidebarContext(part) {
2113
+ const context = useContext(SidebarContext);
2114
+ if (!context) throw new Error(`${part} must be rendered inside <SidebarProvider>`);
2115
+ return context;
2116
+ }
2117
+ const PanelContext = createContext(null);
2118
+ function usePanelContext(part) {
2119
+ const context = useContext(PanelContext);
2120
+ if (!context) throw new Error(`${part} must be rendered inside <Sidebar>`);
2121
+ return context;
2122
+ }
2123
+ /**
2124
+ * Drawer open state shared by Sidebar and SidebarTrigger, which usually sit in different parts of
2125
+ * the app shell. One per sidebar; no shortcuts, no persistence.
2126
+ */
2127
+ function SidebarProvider({ children }) {
2128
+ const [open, setOpen] = useState(false);
2129
+ const drawerId = useId();
2130
+ useEffect(() => {
2131
+ if (!open || typeof window.matchMedia !== "function") return;
2132
+ const wide = window.matchMedia(WIDE_QUERY);
2133
+ const onChange = () => {
2134
+ if (wide.matches) setOpen(false);
2135
+ };
2136
+ wide.addEventListener("change", onChange);
2137
+ return () => wide.removeEventListener("change", onChange);
2138
+ }, [open]);
2139
+ return /* @__PURE__ */ jsx(SidebarContext, {
2140
+ value: {
2141
+ open,
2142
+ setOpen,
2143
+ drawerId
2144
+ },
2145
+ children
2146
+ });
2147
+ }
2148
+ /**
2149
+ * App navigation. From 960px up it is docked at the start of the layout: 240px, full height, only
2150
+ * the body scrolls. Below 960px it is hidden and SidebarTrigger opens the same content in a drawer.
2151
+ * Needs a SidebarProvider around both.
2152
+ */
2153
+ function Sidebar({ className, children, ...props }) {
2154
+ const { open, setOpen, drawerId } = useSidebarContext("Sidebar");
2155
+ const label = props["aria-label"] ? { "aria-label": props["aria-label"] } : { "aria-labelledby": props["aria-labelledby"] };
2156
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(PanelContext, {
2157
+ value: { drawer: false },
2158
+ children: /* @__PURE__ */ jsx("nav", {
2159
+ ...props,
2160
+ "data-slot": "sidebar",
2161
+ className: cn("sticky top-0 hidden h-dvh w-60 shrink-0 flex-col border-e border-border-strong bg-bg", "min-[960px]:flex", className),
2162
+ children
2163
+ })
2164
+ }), /* @__PURE__ */ jsx(Drawer, {
2165
+ ...label,
2166
+ id: drawerId,
2167
+ open,
2168
+ onOpenChange: setOpen,
2169
+ side: "left",
2170
+ size: "sm",
2171
+ children: /* @__PURE__ */ jsx(PanelContext, {
2172
+ value: { drawer: true },
2173
+ children: /* @__PURE__ */ jsx("nav", {
2174
+ ...label,
2175
+ "data-slot": "sidebar",
2176
+ "data-drawer": "",
2177
+ className: "flex min-h-0 flex-1 flex-col",
2178
+ children
2179
+ })
2180
+ })
2181
+ })] });
2182
+ }
2183
+ /** Menu button that opens the drawer; only shown below 960px. Focus returns here on close. */
2184
+ function SidebarTrigger({ "aria-label": ariaLabel = "Open navigation", className, onClick, ...props }) {
2185
+ const { open, setOpen, drawerId } = useSidebarContext("SidebarTrigger");
2186
+ return /* @__PURE__ */ jsx(Button, {
2187
+ ...props,
2188
+ intent: "ghost",
2189
+ icon: /* @__PURE__ */ jsx(Menu, {}),
2190
+ "aria-label": ariaLabel,
2191
+ "aria-expanded": open,
2192
+ "aria-controls": drawerId,
2193
+ tooltip: false,
2194
+ className: cn("min-[960px]:hidden", className),
2195
+ onClick: (e) => {
2196
+ onClick?.(e);
2197
+ if (!e.defaultPrevented) setOpen(true);
2198
+ }
2199
+ });
2200
+ }
2201
+ /** Brand row, pinned above the body. In the drawer it also holds the close button. */
2202
+ function SidebarHeader({ className, children, ...props }) {
2203
+ const { drawer } = usePanelContext("SidebarHeader");
2204
+ return /* @__PURE__ */ jsxs("div", {
2205
+ ...props,
2206
+ "data-slot": "sidebar-header",
2207
+ className: cn("flex h-14 shrink-0 items-center gap-2 px-3", className),
2208
+ children: [children, drawer && /* @__PURE__ */ jsx(DrawerClose, {
2209
+ "aria-label": "Close navigation",
2210
+ className: "ms-auto shrink-0"
2211
+ })]
2212
+ });
2213
+ }
2214
+ function useHoverPill() {
2215
+ const ref = useRef(null);
2216
+ const hovered = useRef(null);
2217
+ const onPointerMove = (e) => {
2218
+ const body = ref.current;
2219
+ if (!body || e.pointerType && e.pointerType !== "mouse") return;
2220
+ const item = e.target.closest("[data-slot=\"sidebar-item\"]");
2221
+ if (!item || item.closest("[data-slot=\"sidebar-body\"]") !== body) return;
2222
+ const entering = body.dataset.hover === void 0;
2223
+ if (item === hovered.current && !entering) return;
2224
+ hovered.current = item;
2225
+ if (entering) body.dataset.hoverEnter = "";
2226
+ else delete body.dataset.hoverEnter;
2227
+ const { x, y, w, h } = boxIn(body, item);
2228
+ body.style.setProperty("--sidebar-hover-x", `${x}px`);
2229
+ body.style.setProperty("--sidebar-hover-y", `${y}px`);
2230
+ body.style.setProperty("--sidebar-hover-w", `${w}px`);
2231
+ body.style.setProperty("--sidebar-hover-h", `${h}px`);
2232
+ body.dataset.hover = "";
2233
+ };
2234
+ const onPointerLeave = () => {
2235
+ const body = ref.current;
2236
+ if (!body) return;
2237
+ hovered.current = null;
2238
+ delete body.dataset.hover;
2239
+ };
2240
+ return {
2241
+ ref,
2242
+ onPointerMove,
2243
+ onPointerLeave
2244
+ };
2245
+ }
2246
+ const bodyClasses = [
2247
+ "relative isolate flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto overscroll-contain px-3 py-2",
2248
+ "after:pointer-events-none after:absolute after:top-0 after:left-0 after:-z-1 after:rounded-md",
2249
+ "after:bg-fg/5 after:opacity-0 data-hover:after:opacity-100",
2250
+ "after:h-(--sidebar-hover-h) after:w-(--sidebar-hover-w)",
2251
+ "after:[translate:var(--sidebar-hover-x)_var(--sidebar-hover-y)]",
2252
+ "after:transition-[translate,width,height,opacity] after:duration-150",
2253
+ "after:ease-[cubic-bezier(0.33,1,0.68,1)]",
2254
+ "data-hover-enter:after:transition-opacity"
2255
+ ];
2256
+ /** The groups, between header and footer; the only part that scrolls. */
2257
+ function SidebarBody({ className, ref, onPointerMove, onPointerLeave, ...props }) {
2258
+ const hover = useHoverPill();
2259
+ const mergedRef = useMergedRef(hover.ref, ref);
2260
+ return /* @__PURE__ */ jsx("div", {
2261
+ ...props,
2262
+ ref: mergedRef,
2263
+ "data-slot": "sidebar-body",
2264
+ className: cn(bodyClasses, className),
2265
+ onPointerMove: (e) => {
2266
+ onPointerMove?.(e);
2267
+ hover.onPointerMove(e);
2268
+ },
2269
+ onPointerLeave: (e) => {
2270
+ onPointerLeave?.(e);
2271
+ hover.onPointerLeave();
2272
+ }
2273
+ });
2274
+ }
2275
+ /** Pinned below the body. */
2276
+ function SidebarFooter({ className, ...props }) {
2277
+ return /* @__PURE__ */ jsx("div", {
2278
+ ...props,
2279
+ "data-slot": "sidebar-footer",
2280
+ className: cn("shrink-0 p-3", className)
2281
+ });
2282
+ }
2283
+ function SidebarGroup({ label, className, children, ...props }) {
2284
+ const labelId = useId();
2285
+ const hasLabel = label != null && label !== false && label !== "";
2286
+ return /* @__PURE__ */ jsxs("div", {
2287
+ ...props,
2288
+ "data-slot": "sidebar-group",
2289
+ className: cn("flex flex-col", className),
2290
+ children: [hasLabel && /* @__PURE__ */ jsx("div", {
2291
+ id: labelId,
2292
+ "data-slot": "sidebar-group-label",
2293
+ className: "px-2 pt-2 pb-1 text-xs/4 text-fg-muted",
2294
+ children: label
2295
+ }), /* @__PURE__ */ jsx("ul", {
2296
+ "aria-labelledby": hasLabel ? labelId : void 0,
2297
+ className: "flex flex-col gap-px",
2298
+ children
2299
+ })]
2300
+ });
2301
+ }
2302
+ const itemClasses = [
2303
+ "flex h-9 w-full items-center gap-2 rounded-md ps-2.5 pe-2",
2304
+ "text-sm leading-5 font-medium text-fg-muted no-underline select-none",
2305
+ "touch-manipulation [-webkit-tap-highlight-color:transparent]",
2306
+ "transition-[color,background-color] duration-150 ease-[cubic-bezier(0.4,0,0.2,1)]",
2307
+ "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus",
2308
+ "hover:text-fg aria-[current=page]:bg-fg/8 aria-[current=page]:text-fg",
2309
+ "active:bg-fg/8 aria-[current=page]:active:bg-fg/12",
2310
+ "forced-colors:aria-[current=page]:bg-[Highlight] forced-colors:aria-[current=page]:text-[HighlightText]",
2311
+ "forced-colors:aria-[current=page]:[forced-color-adjust:none]"
2312
+ ];
2313
+ /** Wraps runs of plain text in the truncating label span; icons and badges pass through. */
2314
+ function withLabel(children) {
2315
+ const out = [];
2316
+ let text = [];
2317
+ const flush = () => {
2318
+ if (text.length === 0) return;
2319
+ out.push(/* @__PURE__ */ jsx("span", {
2320
+ className: "min-w-0 flex-1 truncate",
2321
+ children: text
2322
+ }, `label-${out.length}`));
2323
+ text = [];
2324
+ };
2325
+ for (const child of Children.toArray(children)) if (typeof child === "string" || typeof child === "number") text.push(child);
2326
+ else {
2327
+ flush();
2328
+ out.push(child);
2329
+ }
2330
+ flush();
2331
+ return out;
2332
+ }
2333
+ /**
2334
+ * A sidebar link. Plain `<a>` by default; with `asChild`, your router's Link. Mark the current one
2335
+ * aria-current="page". Inside the drawer, a click the link handles in place (preventDefault, as
2336
+ * router links do) closes the drawer; a modified click opens a new tab and leaves it open.
2337
+ */
2338
+ function SidebarItem({ asChild = false, className, children, ...props }) {
2339
+ const { setOpen } = useSidebarContext("SidebarItem");
2340
+ const { drawer } = usePanelContext("SidebarItem");
2341
+ const itemProps = {
2342
+ ...props,
2343
+ "data-slot": "sidebar-item",
2344
+ className: cn(itemClasses, className)
2345
+ };
2346
+ let item;
2347
+ if (asChild) {
2348
+ const child = Children.only(children);
2349
+ item = isValidElement(child) ? /* @__PURE__ */ jsx(Slot, {
2350
+ ...itemProps,
2351
+ children: cloneElement(child, { children: wrapChildren(child.props.children, withLabel) })
2352
+ }) : null;
2353
+ } else item = /* @__PURE__ */ jsx("a", {
2354
+ ...itemProps,
2355
+ children: withLabel(children)
2356
+ });
2357
+ return /* @__PURE__ */ jsx("li", {
2358
+ className: "flex",
2359
+ onClick: (e) => {
2360
+ if (drawer && e.defaultPrevented) setOpen(false);
2361
+ },
2362
+ children: item
2363
+ });
2364
+ }
2365
+ /** Decorative 16px leading icon for a SidebarItem (hidden from assistive tech); takes the text color. */
2366
+ function SidebarIcon({ className, ...props }) {
2367
+ return /* @__PURE__ */ jsx("span", {
2368
+ "aria-hidden": true,
2369
+ ...props,
2370
+ "data-slot": "sidebar-item-icon",
2371
+ className: cn("flex shrink-0 [&_svg]:size-4", className)
2372
+ });
2373
+ }
2374
+ /** Count at the end of a SidebarItem, as a small neutral Badge; part of the name ("Projects 8"). */
2375
+ function SidebarBadge({ className, ...props }) {
2376
+ return /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx(Badge, {
2377
+ size: "sm",
2378
+ ...props,
2379
+ className: cn("shrink-0", className)
2380
+ })] });
2381
+ }
2382
+ //#endregion
2383
+ //#region src/components/switch/switch.tsx
2384
+ const sizeClasses = {
2385
+ sm: {
2386
+ track: "h-3.5 w-7 [--switch-thumb:10px] [--switch-travel:14px] [--switch-stretch:2px]",
2387
+ offset: "mt-[5px]",
2388
+ icon: "[&_svg]:size-2"
2389
+ },
2390
+ md: {
2391
+ track: "h-5 w-9 [--switch-thumb:16px] [--switch-travel:16px] [--switch-stretch:4px]",
2392
+ offset: "mt-0.5",
2393
+ icon: "[&_svg]:size-3"
2394
+ }
2395
+ };
2396
+ function Switch({ label, description, error, size = "md", loading = false, labelPosition = "end", className, inputClassName, disabled, onChange, "aria-describedby": describedBy, "aria-invalid": ariaInvalid, ...props }) {
2397
+ const s = sizeClasses[size];
2398
+ const pressable = !disabled && !loading;
2399
+ const handleChange = (e) => {
2400
+ if (loading) {
2401
+ e.currentTarget.checked = !e.currentTarget.checked;
2402
+ return;
2403
+ }
2404
+ onChange?.(e);
2405
+ };
2406
+ return /* @__PURE__ */ jsx(ChoiceRow, {
2407
+ slot: "switch",
2408
+ label,
2409
+ description,
2410
+ error,
2411
+ labelPosition,
2412
+ gap: "gap-3",
2413
+ controlOffset: s.offset,
2414
+ className,
2415
+ "aria-describedby": describedBy,
2416
+ control: (row) => /* @__PURE__ */ jsxs("span", {
2417
+ className: cn("relative inline-flex", s.track),
2418
+ children: [/* @__PURE__ */ jsx("input", {
2419
+ ...props,
2420
+ type: "checkbox",
2421
+ role: "switch",
2422
+ disabled,
2423
+ "aria-busy": loading || void 0,
2424
+ "aria-labelledby": props["aria-labelledby"] ?? row.labelledBy,
2425
+ "aria-describedby": row.describedBy,
2426
+ "aria-invalid": row.invalid ? true : ariaInvalid,
2427
+ onChange: handleChange,
2428
+ className: cn("peer m-0 size-full cursor-pointer appearance-none rounded-full border border-transparent outline-hidden", "disabled:cursor-not-allowed aria-busy:cursor-progress", "bg-(--switch-track) [box-shadow:0_0_0_1px_var(--switch-ring),0_0_0_4px_var(--switch-halo)]", "transition-[background-color,border-color,box-shadow] duration-150 ease-out", "[--switch-track:var(--color-input-border)] checked:[--switch-track:var(--color-control-checked)]", pressable && "not-checked:hover:[--switch-track:var(--color-input-border-hover)]", row.invalid ? ["[--switch-ring:var(--color-danger)] [--switch-halo:var(--color-input-halo-invalid)]", "focus-visible:[--switch-halo:var(--color-input-halo-invalid-focus)]"] : [
2429
+ "[--switch-ring:transparent] [--switch-halo:transparent]",
2430
+ "focus-visible:[--switch-ring:var(--color-input-focus)] focus-visible:[--switch-halo:var(--color-input-halo)]",
2431
+ "not-forced-colors:checked:focus-visible:outline-control-checked"
2432
+ ], "checked:focus-visible:[--switch-ring:transparent] checked:focus-visible:[--switch-halo:transparent]", "checked:focus-visible:outline-solid checked:focus-visible:outline-2 checked:focus-visible:outline-offset-2", row.invalid && "not-forced-colors:checked:focus-visible:outline-danger", "forced-colors:forced-color-adjust-none forced-colors:[box-shadow:none]", "forced-colors:border-[CanvasText] forced-colors:bg-[Canvas]", "forced-colors:checked:border-[Highlight] forced-colors:checked:bg-[Highlight]", "forced-colors:disabled:border-[GrayText] forced-colors:disabled:checked:bg-[GrayText]", "forced-colors:focus-visible:outline-[CanvasText]", inputClassName)
2433
+ }), /* @__PURE__ */ jsx("span", {
2434
+ "aria-hidden": true,
2435
+ className: cn("pointer-events-none absolute inset-s-0.5 top-0.5 grid place-items-center rounded-full", "h-(--switch-thumb) w-(--switch-thumb-w) bg-control-checked-fg text-control-checked shadow-switch-thumb", "[--switch-dir:1] rtl:[--switch-dir:-1] [translate:calc(var(--switch-x)*var(--switch-dir))_0]", "[--switch-x:0px] [--switch-thumb-w:var(--switch-thumb)] peer-checked:[--switch-x:var(--switch-travel)]", "transition-[translate,width] duration-150 ease-in-out", pressable && ["peer-active:[--switch-thumb-w:calc(var(--switch-thumb)+var(--switch-stretch))]", "peer-checked:peer-active:[--switch-x:calc(var(--switch-travel)-var(--switch-stretch))]"], "peer-disabled:shadow-none", "forced-colors:forced-color-adjust-none forced-colors:shadow-none", "forced-colors:bg-[CanvasText] forced-colors:text-[Canvas]", "forced-colors:peer-checked:bg-[HighlightText] forced-colors:peer-checked:text-[Highlight]", "forced-colors:peer-disabled:bg-[GrayText] forced-colors:peer-disabled:peer-checked:bg-[Canvas]", s.icon),
2436
+ children: loading && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(LoaderCircle, { className: "animate-spin motion-reduce:hidden" }), /* @__PURE__ */ jsx(CircleDashed, { className: "hidden motion-reduce:block" })] })
2437
+ })]
2438
+ })
2439
+ });
2440
+ }
2441
+ //#endregion
2442
+ //#region src/components/tabs/tab-nav.tsx
2443
+ const TabNavContext = createContext(null);
2444
+ /**
2445
+ * Tabs that change the URL: a `<nav>` of links, the current one marked (by the router, usually)
2446
+ * with aria-current="page". Same look as Tabs, but link semantics: Tab moves between links, Enter
2447
+ * follows one, and each can be opened in a new tab. For in-page panels, use Tabs.
2448
+ */
2449
+ function TabNav({ variant = "line", size = "md", className, listClassName, children, ...props }) {
2450
+ const { listRef, onPointerMove, onPointerLeave } = useTabListMotion(variant, { revealSelected: true });
2451
+ return /* @__PURE__ */ jsx(TabNavContext.Provider, {
2452
+ value: {
2453
+ variant,
2454
+ size
2455
+ },
2456
+ children: /* @__PURE__ */ jsx("nav", {
2457
+ ...props,
2458
+ "data-slot": "tab-nav",
2459
+ className,
2460
+ children: /* @__PURE__ */ jsx("ul", {
2461
+ ref: listRef,
2462
+ "data-slot": "tab-list",
2463
+ "data-variant": variant,
2464
+ className: cn(listClasses(variant, size), listClassName),
2465
+ onPointerMove,
2466
+ onPointerLeave,
2467
+ children
2468
+ })
2469
+ })
2470
+ });
2471
+ }
2472
+ /**
2473
+ * A link styled as a tab. Plain `<a>` by default; with `asChild`, your router's Link (for an index
2474
+ * route, make it current only on an exact match, or it stays current on every child route).
2475
+ */
2476
+ function TabNavLink({ asChild = false, icon, badge, className, onFocus, children, ...props }) {
2477
+ const context = useContext(TabNavContext);
2478
+ if (!context) throw new Error("TabNavLink must be rendered inside <TabNav>");
2479
+ const itemProps = {
2480
+ ...props,
2481
+ "data-slot": "tab",
2482
+ className: cn(itemClasses$1(context.variant, context.size), className),
2483
+ onFocus: (e) => {
2484
+ onFocus?.(e);
2485
+ revealOnFocus(e.currentTarget);
2486
+ }
2487
+ };
2488
+ const content = (label) => /* @__PURE__ */ jsx(TabContent, {
2489
+ icon,
2490
+ badge,
2491
+ children: label
2492
+ });
2493
+ let item;
2494
+ if (asChild) {
2495
+ const child = Children.only(children);
2496
+ item = isValidElement(child) ? /* @__PURE__ */ jsx(Slot, {
2497
+ ...itemProps,
2498
+ children: cloneElement(child, { children: wrapChildren(child.props.children, content) })
2499
+ }) : null;
2500
+ } else item = /* @__PURE__ */ jsx("a", {
2501
+ ...itemProps,
2502
+ children: content(children)
2503
+ });
2504
+ return /* @__PURE__ */ jsx("li", {
2505
+ className: "flex shrink-0",
2506
+ children: item
2507
+ });
2508
+ }
2509
+ //#endregion
2510
+ //#region src/components/tabs/tabs.tsx
2511
+ const TabsContext = createContext(null);
2512
+ function useTabsContext(part) {
2513
+ const context = useContext(TabsContext);
2514
+ if (!context) throw new Error(`${part} must be rendered inside <Tabs>`);
2515
+ return context;
2516
+ }
2517
+ const partId = (baseId, part, value) => `${baseId}-${part}-${value.replace(/\s+/g, "-")}`;
2518
+ /**
2519
+ * In-page tabs (WAI-ARIA APG tabs pattern): a tablist that switches panels without changing the
2520
+ * URL. For tabs that navigate between routes, use TabNav.
2521
+ */
2522
+ function Tabs({ value, defaultValue, onValueChange, activationMode = "automatic", variant = "line", size = "md", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, children, ...props }) {
2523
+ const id = useId();
2524
+ const [inner, setInner] = useState(defaultValue);
2525
+ const controlled = value !== void 0;
2526
+ const selected = controlled ? value : inner;
2527
+ const select = (next) => {
2528
+ if (next === selected) return;
2529
+ if (!controlled) setInner(next);
2530
+ onValueChange?.(next);
2531
+ };
2532
+ return /* @__PURE__ */ jsx(TabsContext.Provider, {
2533
+ value: {
2534
+ baseId: id,
2535
+ selected,
2536
+ select,
2537
+ activationMode,
2538
+ variant,
2539
+ size,
2540
+ label: {
2541
+ "aria-label": ariaLabel,
2542
+ "aria-labelledby": ariaLabelledby
2543
+ }
2544
+ },
2545
+ children: /* @__PURE__ */ jsx("div", {
2546
+ ...props,
2547
+ "data-slot": "tabs",
2548
+ children
2549
+ })
2550
+ });
2551
+ }
2552
+ const keyMoves = {
2553
+ ArrowRight: (i, last, rtl) => rtl ? i === 0 ? last : i - 1 : i === last ? 0 : i + 1,
2554
+ ArrowLeft: (i, last, rtl) => rtl ? i === last ? 0 : i + 1 : i === 0 ? last : i - 1,
2555
+ Home: () => 0,
2556
+ End: (_, last) => last
2557
+ };
2558
+ function TabList({ className, ref, onKeyDown, onPointerMove, onPointerLeave, children, ...props }) {
2559
+ const { select, activationMode, variant, size, label } = useTabsContext("TabList");
2560
+ const motion = useTabListMotion(variant);
2561
+ const mergedRef = useMergedRef(motion.listRef, ref);
2562
+ const handleKeyDown = (e) => {
2563
+ onKeyDown?.(e);
2564
+ const move = keyMoves[e.key];
2565
+ if (e.defaultPrevented || !move || e.altKey || e.ctrlKey || e.metaKey) return;
2566
+ const list = e.currentTarget;
2567
+ const tabs = [...list.querySelectorAll("[role=\"tab\"]:not(:disabled)")].filter((tab) => tab.closest("[role=\"tablist\"]") === list);
2568
+ const index = tabs.indexOf(e.target);
2569
+ if (index === -1) return;
2570
+ e.preventDefault();
2571
+ const rtl = getComputedStyle(list).direction === "rtl";
2572
+ const next = tabs[move(index, tabs.length - 1, rtl)];
2573
+ next.focus();
2574
+ if (activationMode === "automatic" && next.dataset.value !== void 0) select(next.dataset.value);
2575
+ };
2576
+ return /* @__PURE__ */ jsx("div", {
2577
+ ...label,
2578
+ ...props,
2579
+ ref: mergedRef,
2580
+ role: "tablist",
2581
+ "data-slot": "tab-list",
2582
+ "data-variant": variant,
2583
+ className: cn(listClasses(variant, size), className),
2584
+ onKeyDown: handleKeyDown,
2585
+ onPointerMove: (e) => {
2586
+ onPointerMove?.(e);
2587
+ motion.onPointerMove(e);
2588
+ },
2589
+ onPointerLeave: (e) => {
2590
+ onPointerLeave?.(e);
2591
+ motion.onPointerLeave();
2592
+ },
2593
+ children
2594
+ });
2595
+ }
2596
+ function Tab({ value, icon, badge, disabled = false, className, ref, onClick, onFocus, children, ...props }) {
2597
+ const context = useTabsContext("Tab");
2598
+ const isSelected = context.selected === value;
2599
+ const ownRef = useRef(null);
2600
+ const mergedRef = useMergedRef(ownRef, ref);
2601
+ useRevealWhenSelected(ownRef, isSelected);
2602
+ return /* @__PURE__ */ jsx("button", {
2603
+ ...props,
2604
+ ref: mergedRef,
2605
+ type: "button",
2606
+ role: "tab",
2607
+ id: partId(context.baseId, "tab", value),
2608
+ "aria-controls": partId(context.baseId, "panel", value),
2609
+ "aria-selected": isSelected,
2610
+ tabIndex: isSelected ? 0 : -1,
2611
+ disabled,
2612
+ "data-slot": "tab",
2613
+ "data-value": value,
2614
+ "data-selected": isSelected || void 0,
2615
+ "data-disabled": disabled || void 0,
2616
+ className: cn(itemClasses$1(context.variant, context.size), className),
2617
+ onClick: (e) => {
2618
+ onClick?.(e);
2619
+ if (!e.defaultPrevented) context.select(value);
2620
+ },
2621
+ onFocus: (e) => {
2622
+ onFocus?.(e);
2623
+ revealOnFocus(e.currentTarget);
2624
+ },
2625
+ children: /* @__PURE__ */ jsx(TabContent, {
2626
+ icon,
2627
+ badge,
2628
+ children
2629
+ })
2630
+ });
2631
+ }
2632
+ function TabPanel({ value, keepMounted = false, className, ...props }) {
2633
+ const { baseId, selected } = useTabsContext("TabPanel");
2634
+ const isSelected = selected === value;
2635
+ if (!isSelected && !keepMounted) return null;
2636
+ return /* @__PURE__ */ jsx("div", {
2637
+ tabIndex: 0,
2638
+ ...props,
2639
+ role: "tabpanel",
2640
+ id: partId(baseId, "panel", value),
2641
+ "aria-labelledby": partId(baseId, "tab", value),
2642
+ hidden: !isSelected,
2643
+ "data-slot": "tab-panel",
2644
+ className: cn("focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus", className)
2645
+ });
2646
+ }
2647
+ //#endregion
2648
+ //#region src/components/theme-switcher/theme-switcher.tsx
2649
+ const systemThemeQuery = "(prefers-color-scheme: dark)";
2650
+ function subscribeSystemTheme(onChange) {
2651
+ const query = window.matchMedia?.(systemThemeQuery);
2652
+ query?.addEventListener("change", onChange);
2653
+ return () => query?.removeEventListener("change", onChange);
2654
+ }
2655
+ const systemIsDark = () => window.matchMedia?.(systemThemeQuery).matches ?? false;
2656
+ const serverIsDark = () => false;
2657
+ /** Toggle the visible theme in one click, including when the initial choice follows the system. */
2658
+ function ThemeSwitcher({ value, onChange, onClick, className, "aria-label": ariaLabel, ...props }) {
2659
+ const systemDark = useSyncExternalStore(subscribeSystemTheme, systemIsDark, serverIsDark);
2660
+ const dark = value === "system" ? systemDark : value === "dark";
2661
+ const next = dark ? "light" : "dark";
2662
+ const label = ariaLabel ?? (dark ? "Switch to light theme" : "Switch to dark theme");
2663
+ return /* @__PURE__ */ jsx(Button, {
2664
+ ...props,
2665
+ intent: "ghost",
2666
+ icon: dark ? /* @__PURE__ */ jsx(Sun, {}) : /* @__PURE__ */ jsx(Moon, {}),
2667
+ "aria-label": label,
2668
+ title: label,
2669
+ tooltip: false,
2670
+ "data-slot": "theme-switcher",
2671
+ className: cn("size-11 shrink-0 text-fg-muted hover:text-fg [&_svg]:size-5", className),
2672
+ onClick: (event) => {
2673
+ onClick?.(event);
2674
+ if (!event.defaultPrevented) onChange(next);
2675
+ }
2676
+ });
2677
+ }
2678
+ //#endregion
2679
+ //#region src/components/theme-switcher/use-theme.ts
2680
+ const validTheme = (value) => value === "light" || value === "dark" ? value : "system";
2681
+ /** Persisted color theme; `system` clears data-theme so prefers-color-scheme decides. */
2682
+ function useTheme({ storageKey = "swell-ui-theme" } = {}) {
2683
+ const [theme, setTheme] = useState(() => {
2684
+ try {
2685
+ return validTheme(localStorage.getItem(storageKey));
2686
+ } catch {
2687
+ return "system";
2688
+ }
2689
+ });
2690
+ useEffect(() => {
2691
+ if (theme === "system") delete document.documentElement.dataset.theme;
2692
+ else document.documentElement.dataset.theme = theme;
2693
+ try {
2694
+ localStorage.setItem(storageKey, theme);
2695
+ } catch {}
2696
+ }, [theme, storageKey]);
2697
+ return {
2698
+ theme,
2699
+ setTheme
2700
+ };
2701
+ }
2702
+ //#endregion
2703
+ //#region src/components/toast/store.ts
2704
+ function isActionButton(action) {
2705
+ return typeof action === "object" && action !== null && !isValidElement(action) && !(Symbol.iterator in action) && "label" in action;
2706
+ }
2707
+ function resolveDuration(record) {
2708
+ if (record.duration !== void 0) return record.duration;
2709
+ if (record.type === "danger" || record.type === "loading") return Number.POSITIVE_INFINITY;
2710
+ return record.action != null && record.action !== false ? 1e4 : 5e3;
2711
+ }
2712
+ let toasts = [];
2713
+ const listeners = /* @__PURE__ */ new Set();
2714
+ let nextId = 0;
2715
+ let nextKey = 0;
2716
+ function emit(next) {
2717
+ toasts = next;
2718
+ for (const listener of listeners) listener();
2719
+ }
2720
+ function subscribe(listener) {
2721
+ listeners.add(listener);
2722
+ return () => {
2723
+ listeners.delete(listener);
2724
+ };
2725
+ }
2726
+ function getSnapshot() {
2727
+ return toasts;
2728
+ }
2729
+ const serverSnapshot = [];
2730
+ function getServerSnapshot() {
2731
+ return serverSnapshot;
2732
+ }
2733
+ function findLive(id) {
2734
+ return toasts.find((t) => t.id === id && !t.removed);
2735
+ }
2736
+ function create(options) {
2737
+ const id = options.id ?? `toast-${++nextId}`;
2738
+ const { id: _id, type = "neutral", dismissible = true, ...rest } = options;
2739
+ const live = findLive(id);
2740
+ if (live) {
2741
+ const next = {
2742
+ ...rest,
2743
+ id,
2744
+ type,
2745
+ dismissible,
2746
+ key: live.key,
2747
+ version: live.version + 1,
2748
+ removed: false
2749
+ };
2750
+ emit(toasts.map((t) => t === live ? next : t));
2751
+ return id;
2752
+ }
2753
+ emit([{
2754
+ ...rest,
2755
+ id,
2756
+ type,
2757
+ dismissible,
2758
+ key: ++nextKey,
2759
+ version: 0,
2760
+ removed: false
2761
+ }, ...toasts]);
2762
+ return id;
2763
+ }
2764
+ function update(id, patch) {
2765
+ const live = findLive(id);
2766
+ if (!live) return;
2767
+ const { type = live.type, dismissible = live.dismissible, ...rest } = patch;
2768
+ emit(toasts.map((t) => t === live ? {
2769
+ ...live,
2770
+ ...rest,
2771
+ type,
2772
+ dismissible,
2773
+ version: live.version + 1
2774
+ } : t));
2775
+ }
2776
+ function dismiss(id, swipeOut) {
2777
+ const hit = (t) => !t.removed && (id === void 0 || t.id === id);
2778
+ if (!toasts.some(hit)) return;
2779
+ if (listeners.size === 0) {
2780
+ emit(toasts.filter((t) => !hit(t)));
2781
+ return;
2782
+ }
2783
+ emit(toasts.map((t) => hit(t) ? {
2784
+ ...t,
2785
+ removed: true,
2786
+ swipeOut
2787
+ } : t));
2788
+ }
2789
+ /** Called by the Toaster once a dismissed toast's exit has finished. */
2790
+ function removeToast(key) {
2791
+ if (toasts.some((t) => t.key === key)) emit(toasts.filter((t) => t.key !== key));
2792
+ }
2793
+ function isStepOptions(step) {
2794
+ return typeof step === "object" && step !== null && !isValidElement(step) && !(Symbol.iterator in step) && "title" in step;
2795
+ }
2796
+ function stepPatch(step, type) {
2797
+ const base = {
2798
+ type,
2799
+ description: void 0,
2800
+ action: void 0,
2801
+ duration: void 0
2802
+ };
2803
+ return isStepOptions(step) ? {
2804
+ ...base,
2805
+ ...step
2806
+ } : {
2807
+ ...base,
2808
+ title: step
2809
+ };
2810
+ }
2811
+ function shortcut(type) {
2812
+ return (title, options) => create({
2813
+ ...options,
2814
+ title,
2815
+ type
2816
+ });
2817
+ }
2818
+ /**
2819
+ * Imperative toasts, callable from anywhere (event handlers, mutations, loaders) once a
2820
+ * `<Toaster />` is mounted. Every call returns the toast's id. `toasterId` routes a toast to a
2821
+ * `<Toaster id>` instead.
2822
+ */
2823
+ const toast = Object.assign((options) => create(options), {
2824
+ success: shortcut("success"),
2825
+ info: shortcut("info"),
2826
+ warning: shortcut("warning"),
2827
+ /** Renders as the `danger` type. Stays until dismissed. */
2828
+ error: shortcut("danger"),
2829
+ /** Spinner; stays until updated or dismissed. */
2830
+ loading: shortcut("loading"),
2831
+ /** Merges into a toast still on screen and restarts its timer. No-op for unknown ids. */
2832
+ update,
2833
+ /** Plays the exit of one toast, or of all of them without an id. */
2834
+ dismiss: (id) => dismiss(id),
2835
+ /**
2836
+ * Loading toast that turns into success or danger in place when the promise settles. Steps
2837
+ * are a title or full options; success/error may be functions of the result / the error.
2838
+ */
2839
+ promise(promise, steps) {
2840
+ const id = create({
2841
+ id: steps.id,
2842
+ toasterId: steps.toasterId,
2843
+ ...stepPatch(steps.loading, "loading")
2844
+ });
2845
+ const settle = (step, type) => update(id, stepPatch(step, type));
2846
+ (typeof promise === "function" ? promise() : promise).then((data) => settle(typeof steps.success === "function" ? steps.success(data) : steps.success, "success"), (error) => settle(typeof steps.error === "function" ? steps.error(error) : steps.error, "danger"));
2847
+ return id;
2848
+ }
2849
+ });
2850
+ //#endregion
2851
+ //#region src/components/toast/toaster.tsx
2852
+ const GAP = 14;
2853
+ const SCALE_STEP = .05;
2854
+ const SWIPE_DISTANCE = 45;
2855
+ const SWIPE_VELOCITY = .11;
2856
+ const EXIT_FALLBACK_MS = 600;
2857
+ const ENTER_MS = 400;
2858
+ const MOBILE_QUERY = "(width <= 600px)";
2859
+ const MOBILE_VISIBLE = 2;
2860
+ const CARD_BORDERS = 2;
2861
+ const owners = [];
2862
+ const ownerListeners = /* @__PURE__ */ new Set();
2863
+ function subscribeOwners(listener) {
2864
+ ownerListeners.add(listener);
2865
+ return () => {
2866
+ ownerListeners.delete(listener);
2867
+ };
2868
+ }
2869
+ function useIsActiveToaster(id) {
2870
+ const [owner] = useState(() => Symbol("toaster"));
2871
+ useLayoutEffect(() => {
2872
+ const entry = {
2873
+ owner,
2874
+ id
2875
+ };
2876
+ owners.push(entry);
2877
+ for (const listener of ownerListeners) listener();
2878
+ return () => {
2879
+ owners.splice(owners.indexOf(entry), 1);
2880
+ for (const listener of ownerListeners) listener();
2881
+ };
2882
+ }, [owner, id]);
2883
+ return useSyncExternalStore(subscribeOwners, () => owners.find((entry) => entry.id === id)?.owner === owner, () => false);
2884
+ }
2885
+ function useMediaQuery(query) {
2886
+ return useSyncExternalStore((onChange) => {
2887
+ if (typeof window.matchMedia !== "function") return () => {};
2888
+ const mql = window.matchMedia(query);
2889
+ mql.addEventListener("change", onChange);
2890
+ return () => mql.removeEventListener("change", onChange);
2891
+ }, () => typeof window.matchMedia === "function" && window.matchMedia(query).matches, () => false);
2892
+ }
2893
+ function useDocumentHidden() {
2894
+ return useSyncExternalStore((onChange) => {
2895
+ document.addEventListener("visibilitychange", onChange);
2896
+ return () => document.removeEventListener("visibilitychange", onChange);
2897
+ }, () => document.visibilityState === "hidden", () => false);
2898
+ }
2899
+ function isFocusVisible(el) {
2900
+ if (!el) return false;
2901
+ try {
2902
+ return el.matches(":focus-visible");
2903
+ } catch {
2904
+ return true;
2905
+ }
2906
+ }
2907
+ function showPopover(el) {
2908
+ if (!el?.isConnected || typeof el.showPopover !== "function") return;
2909
+ let open = false;
2910
+ try {
2911
+ open = el.matches(":popover-open");
2912
+ } catch {}
2913
+ try {
2914
+ if (!open) el.showPopover();
2915
+ } catch {}
2916
+ }
2917
+ function modalDialogs() {
2918
+ try {
2919
+ return [...document.querySelectorAll("dialog:modal")].filter((dialog) => !dialog.hasAttribute("data-closing"));
2920
+ } catch {
2921
+ return [];
2922
+ }
2923
+ }
2924
+ function useToastHost(enabled) {
2925
+ const [host, setHost] = useState(null);
2926
+ useLayoutEffect(() => {
2927
+ if (!enabled) return;
2928
+ const el = document.createElement("div");
2929
+ el.dataset.slot = "toast-host";
2930
+ el.style.display = "contents";
2931
+ document.body.append(el);
2932
+ setHost(el);
2933
+ return () => {
2934
+ el.remove();
2935
+ setHost(null);
2936
+ };
2937
+ }, [enabled]);
2938
+ return host;
2939
+ }
2940
+ function useFollowModalDialogs(host, viewportRef) {
2941
+ useLayoutEffect(() => {
2942
+ const stack = [];
2943
+ const relocate = () => {
2944
+ const open = modalDialogs();
2945
+ for (let i = stack.length - 1; i >= 0; i--) if (!open.includes(stack[i])) stack.splice(i, 1);
2946
+ for (const dialog of open) if (!stack.includes(dialog)) stack.push(dialog);
2947
+ const target = stack.at(-1) ?? document.body;
2948
+ const viewport = viewportRef.current;
2949
+ if (host.parentElement !== target) {
2950
+ viewport?.setAttribute("data-moving", "");
2951
+ target.append(host);
2952
+ showPopover(viewport);
2953
+ viewport?.offsetWidth;
2954
+ viewport?.removeAttribute("data-moving");
2955
+ } else showPopover(viewport);
2956
+ };
2957
+ relocate();
2958
+ const observer = new MutationObserver(relocate);
2959
+ observer.observe(document.documentElement, {
2960
+ subtree: true,
2961
+ attributes: true,
2962
+ attributeFilter: ["open", "data-closing"]
2963
+ });
2964
+ return () => observer.disconnect();
2965
+ }, [host, viewportRef]);
2966
+ }
2967
+ /**
2968
+ * The single place toasts render: mount one near the app root (inside the router, so a `<Link>`
2969
+ * action has its context). Toasts themselves come from `toast()` and friends.
2970
+ */
2971
+ function Toaster(props) {
2972
+ const host = useToastHost(useIsActiveToaster(props.id));
2973
+ if (!host) return null;
2974
+ return createPortal(/* @__PURE__ */ jsx(Viewport, {
2975
+ ...props,
2976
+ host
2977
+ }), host);
2978
+ }
2979
+ const positionClasses = {
2980
+ "bottom-right": "top-auto bottom-6 left-auto right-6 max-[601px]:bottom-4",
2981
+ "bottom-left": "top-auto bottom-6 left-6 right-auto max-[601px]:bottom-4",
2982
+ "bottom-center": "top-auto bottom-6 left-1/2 right-auto -translate-x-1/2 max-[601px]:bottom-4",
2983
+ "top-right": "top-6 bottom-auto left-auto right-6 max-[601px]:top-4",
2984
+ "top-left": "top-6 bottom-auto left-6 right-auto max-[601px]:top-4",
2985
+ "top-center": "top-6 bottom-auto left-1/2 right-auto -translate-x-1/2 max-[601px]:top-4"
2986
+ };
2987
+ function Viewport({ id, position = "bottom-right", visibleToasts = 3, host }) {
2988
+ const all = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
2989
+ const toasts = useMemo(() => all.filter((t) => t.toasterId === id), [all, id]);
2990
+ const viewportRef = useRef(null);
2991
+ const statusRef = useRef(null);
2992
+ const alertRef = useRef(null);
2993
+ const returnFocus = useRef(null);
2994
+ const [heights, setHeights] = useState({});
2995
+ const [hovered, setHovered] = useState(false);
2996
+ const [focusWithin, setFocusWithin] = useState(false);
2997
+ const [interacting, setInteracting] = useState(false);
2998
+ const mobile = useMediaQuery(MOBILE_QUERY);
2999
+ const documentHidden = useDocumentHidden();
3000
+ useFollowModalDialogs(host, viewportRef);
3001
+ const live = toasts.filter((t) => !t.removed);
3002
+ const limit = Math.max(1, mobile ? Math.min(visibleToasts, MOBILE_VISIBLE) : visibleToasts);
3003
+ const expanded = live.length > 0 && (hovered || focusWithin);
3004
+ const paused = expanded || interacting || documentHidden;
3005
+ const frontHeight = live.length ? heights[live[0].key] : void 0;
3006
+ const layouts = /* @__PURE__ */ new Map();
3007
+ let before = 0;
3008
+ live.forEach((t, index) => {
3009
+ layouts.set(t.key, {
3010
+ index,
3011
+ offset: before + index * GAP,
3012
+ visible: index < limit
3013
+ });
3014
+ before += heights[t.key] ?? 0;
3015
+ });
3016
+ const onHeight = useCallback((key, height) => {
3017
+ setHeights((prev) => {
3018
+ if (prev[key] === height) return prev;
3019
+ const next = { ...prev };
3020
+ if (height === void 0) delete next[key];
3021
+ else next[key] = height;
3022
+ return next;
3023
+ });
3024
+ }, []);
3025
+ const pending = useRef({
3026
+ polite: [],
3027
+ assertive: [],
3028
+ frame: 0
3029
+ });
3030
+ const announce = useCallback((text, assertive) => {
3031
+ const queue = pending.current;
3032
+ const list = assertive ? queue.assertive : queue.polite;
3033
+ const region = assertive ? alertRef.current : statusRef.current;
3034
+ if (!region || list.includes(text)) return;
3035
+ list.push(text);
3036
+ region.textContent = "";
3037
+ if (queue.frame) return;
3038
+ queue.frame = requestAnimationFrame(() => {
3039
+ queue.frame = 0;
3040
+ if (statusRef.current && queue.polite.length) statusRef.current.textContent = queue.polite.join(" ");
3041
+ if (alertRef.current && queue.assertive.length) alertRef.current.textContent = queue.assertive.join(" ");
3042
+ queue.polite = [];
3043
+ queue.assertive = [];
3044
+ });
3045
+ }, []);
3046
+ useEffect(() => {
3047
+ const queue = pending.current;
3048
+ return () => cancelAnimationFrame(queue.frame);
3049
+ }, []);
3050
+ const restoreFocus = useCallback(() => {
3051
+ const target = returnFocus.current;
3052
+ returnFocus.current = null;
3053
+ if (target?.isConnected && target !== document.body) target.focus({ preventScroll: true });
3054
+ if (viewportRef.current?.contains(document.activeElement)) document.activeElement.blur();
3055
+ }, []);
3056
+ const onFocusLost = useCallback((index) => {
3057
+ const viewport = viewportRef.current;
3058
+ if (viewport && isFocusVisible(document.activeElement)) {
3059
+ const cards = [...viewport.querySelectorAll("li[data-slot=\"toast\"]:not([data-removed]):not([inert])")].sort((a, b) => Number(a.dataset.index) - Number(b.dataset.index));
3060
+ const next = cards.find((card) => Number(card.dataset.index) >= index) ?? cards.at(-1);
3061
+ if (next) {
3062
+ next.focus({ preventScroll: true });
3063
+ return;
3064
+ }
3065
+ }
3066
+ restoreFocus();
3067
+ }, [restoreFocus]);
3068
+ useEffect(() => {
3069
+ const onKeyDown = (event) => {
3070
+ const f6 = event.key === "F6" && !event.altKey && !event.ctrlKey && !event.metaKey;
3071
+ const target = event.target instanceof HTMLElement ? event.target : null;
3072
+ const typing = !!target && (target.isContentEditable || target.matches("input, textarea, select"));
3073
+ const altT = event.code === "KeyT" && event.altKey && !event.ctrlKey && !event.metaKey && !typing;
3074
+ const viewport = viewportRef.current;
3075
+ if (!f6 && !altT || event.defaultPrevented || !viewport) return;
3076
+ if (viewport.contains(document.activeElement)) {
3077
+ event.preventDefault();
3078
+ restoreFocus();
3079
+ return;
3080
+ }
3081
+ const newest = viewport.querySelector("li[data-slot=\"toast\"][data-index=\"0\"]:not([data-removed])");
3082
+ if (!newest) return;
3083
+ event.preventDefault();
3084
+ returnFocus.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
3085
+ newest.focus({ preventScroll: true });
3086
+ };
3087
+ document.addEventListener("keydown", onKeyDown);
3088
+ return () => document.removeEventListener("keydown", onKeyDown);
3089
+ }, [restoreFocus]);
3090
+ const handleKeyDown = (event) => {
3091
+ if (event.key !== "Escape") return;
3092
+ const card = event.target.closest("li[data-slot=\"toast\"]");
3093
+ if (!card || card.dataset.dismissible === void 0 || !card.dataset.toastId) return;
3094
+ event.preventDefault();
3095
+ event.stopPropagation();
3096
+ dismiss(card.dataset.toastId);
3097
+ };
3098
+ const handleFocus = (event) => {
3099
+ const from = event.relatedTarget;
3100
+ if (!event.currentTarget.contains(from) && returnFocus.current === null) returnFocus.current = from instanceof HTMLElement ? from : null;
3101
+ setFocusWithin(isFocusVisible(event.target));
3102
+ };
3103
+ const handleBlur = (event) => {
3104
+ if (event.currentTarget.contains(event.relatedTarget)) return;
3105
+ setFocusWithin(false);
3106
+ returnFocus.current = null;
3107
+ };
3108
+ const handlePointerEnter = (event) => {
3109
+ if (event.pointerType !== "touch") setHovered(true);
3110
+ };
3111
+ const handlePointerLeave = (event) => {
3112
+ if (event.pointerType !== "touch") setHovered(false);
3113
+ };
3114
+ const [y, x] = position.split("-");
3115
+ const directions = x === "center" ? [y] : [y, x];
3116
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3117
+ /* @__PURE__ */ jsx("section", {
3118
+ ref: viewportRef,
3119
+ popover: "manual",
3120
+ "aria-label": "Notifications (F6)",
3121
+ tabIndex: -1,
3122
+ "data-slot": "toast-viewport",
3123
+ "data-position": position,
3124
+ "data-expanded": expanded || void 0,
3125
+ onKeyDown: handleKeyDown,
3126
+ onFocus: handleFocus,
3127
+ onBlur: handleBlur,
3128
+ onPointerDown: () => setInteracting(true),
3129
+ onPointerUp: () => setInteracting(false),
3130
+ onPointerCancel: () => setInteracting(false),
3131
+ className: cn("group/viewport fixed m-0 h-auto w-95 overflow-visible border-0 bg-transparent p-0 text-fg outline-none", "max-[601px]:inset-x-4 max-[601px]:w-auto max-[601px]:translate-x-0", positionClasses[position]),
3132
+ children: /* @__PURE__ */ jsx("ol", {
3133
+ className: "relative m-0 list-none p-0",
3134
+ onPointerEnter: handlePointerEnter,
3135
+ onPointerLeave: handlePointerLeave,
3136
+ children: toasts.map((record) => /* @__PURE__ */ jsx(ToastCard, {
3137
+ record,
3138
+ layout: layouts.get(record.key),
3139
+ total: live.length,
3140
+ expanded,
3141
+ paused,
3142
+ frontHeight,
3143
+ ownHeight: heights[record.key],
3144
+ edge: y,
3145
+ directions,
3146
+ onHeight,
3147
+ onFocusLost,
3148
+ announce
3149
+ }, record.key))
3150
+ })
3151
+ }),
3152
+ /* @__PURE__ */ jsx("div", {
3153
+ ref: statusRef,
3154
+ role: "status",
3155
+ "data-slot": "toast-announcer",
3156
+ className: "sr-only"
3157
+ }),
3158
+ /* @__PURE__ */ jsx("div", {
3159
+ ref: alertRef,
3160
+ role: "alert",
3161
+ "data-slot": "toast-announcer",
3162
+ className: "sr-only"
3163
+ })
3164
+ ] });
3165
+ }
3166
+ const typeClasses = {
3167
+ neutral: "border-popover-border bg-popover-bg text-fg",
3168
+ loading: "border-popover-border bg-popover-bg text-fg",
3169
+ success: "border-success-border bg-success-subtle-bg text-success-subtle-fg",
3170
+ info: "border-info-border bg-info-subtle-bg text-info-subtle-fg",
3171
+ warning: "border-warning-border bg-warning-subtle-bg text-warning-subtle-fg",
3172
+ danger: "border-danger-border bg-danger-subtle-bg text-danger-subtle-fg"
3173
+ };
3174
+ const icons = [
3175
+ [
3176
+ "success",
3177
+ "text-success",
3178
+ /* @__PURE__ */ jsx(CircleCheck, {}, "success")
3179
+ ],
3180
+ [
3181
+ "info",
3182
+ "text-info",
3183
+ /* @__PURE__ */ jsx(Info, {}, "info")
3184
+ ],
3185
+ [
3186
+ "warning",
3187
+ "text-warning",
3188
+ /* @__PURE__ */ jsx(TriangleAlert, {}, "warning")
3189
+ ],
3190
+ [
3191
+ "danger",
3192
+ "text-danger",
3193
+ /* @__PURE__ */ jsx(CircleX, {}, "danger")
3194
+ ]
3195
+ ];
3196
+ const iconLayer = "inline-flex opacity-0 transition-opacity duration-200 data-active:opacity-100";
3197
+ function ToastIcon({ type }) {
3198
+ const loading = type === "loading";
3199
+ return /* @__PURE__ */ jsxs("span", {
3200
+ "aria-hidden": true,
3201
+ "data-slot": "toast-icon",
3202
+ className: cn("mt-0.5 grid size-4 shrink-0 [&_svg]:size-4 *:col-start-1 *:row-start-1", type === "neutral" && "hidden"),
3203
+ children: [icons.map(([iconType, color, icon]) => /* @__PURE__ */ jsx("span", {
3204
+ "data-active": iconType === type || void 0,
3205
+ className: cn(iconLayer, color),
3206
+ children: icon
3207
+ }, iconType)), /* @__PURE__ */ jsxs("span", {
3208
+ "data-active": loading || void 0,
3209
+ className: cn(iconLayer, "text-fg-muted"),
3210
+ children: [/* @__PURE__ */ jsx(LoaderCircle, { className: cn(loading && "animate-spin", "motion-reduce:hidden") }), /* @__PURE__ */ jsx(CircleDashed, { className: "hidden motion-reduce:block" })]
3211
+ })]
3212
+ });
3213
+ }
3214
+ const enterFrom = {
3215
+ bottom: "starting:[translate:0_100%]",
3216
+ top: "starting:[translate:0_-100%]"
3217
+ };
3218
+ function ToastCard({ record, layout, total, expanded, paused, frontHeight, ownHeight, edge, directions, onHeight, onFocusLost, announce }) {
3219
+ const { id, key, type, title, description, action, dismissible, version, removed, swipeOut } = record;
3220
+ const liRef = useRef(null);
3221
+ const bodyRef = useRef(null);
3222
+ const titleRef = useRef(null);
3223
+ const descriptionRef = useRef(null);
3224
+ const titleId = useId();
3225
+ const descriptionId = useId();
3226
+ const [entered, setEntered] = useState(false);
3227
+ const [kept, setKept] = useState(layout ?? {
3228
+ index: 0,
3229
+ offset: 0,
3230
+ visible: true
3231
+ });
3232
+ if (layout && (layout.index !== kept.index || layout.offset !== kept.offset || layout.visible !== kept.visible)) setKept(layout);
3233
+ const { index, offset, visible } = layout ?? kept;
3234
+ const stacked = !expanded && index > 0;
3235
+ const sign = edge === "bottom" ? -1 : 1;
3236
+ const height = stacked ? frontHeight : ownHeight;
3237
+ useLayoutEffect(() => {
3238
+ const body = bodyRef.current;
3239
+ if (!body) return;
3240
+ const measure = () => onHeight(key, body.offsetHeight + CARD_BORDERS);
3241
+ measure();
3242
+ const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(measure);
3243
+ observer?.observe(body);
3244
+ return () => {
3245
+ observer?.disconnect();
3246
+ onHeight(key, void 0);
3247
+ };
3248
+ }, [key, onHeight]);
3249
+ useEffect(() => {
3250
+ const timer = setTimeout(() => setEntered(true), ENTER_MS);
3251
+ return () => clearTimeout(timer);
3252
+ }, []);
3253
+ useLayoutEffect(() => {
3254
+ const titleText = titleRef.current?.textContent?.trim() ?? "";
3255
+ const descriptionText = descriptionRef.current?.textContent?.trim() ?? "";
3256
+ const text = titleText && descriptionText && !/[.!?…:]$/.test(titleText) ? `${titleText}. ${descriptionText}` : `${titleText} ${descriptionText}`.trim();
3257
+ if (text) announce(text, type === "danger");
3258
+ }, [
3259
+ version,
3260
+ type,
3261
+ announce
3262
+ ]);
3263
+ const duration = resolveDuration(record);
3264
+ const timer = useRef({
3265
+ version: -1,
3266
+ duration: 0,
3267
+ remaining: 0
3268
+ });
3269
+ const holding = paused || !visible;
3270
+ useEffect(() => {
3271
+ const t = timer.current;
3272
+ if (t.version !== version || t.duration !== duration) {
3273
+ t.version = version;
3274
+ t.duration = duration;
3275
+ t.remaining = duration;
3276
+ }
3277
+ if (holding || removed || !Number.isFinite(t.remaining)) return;
3278
+ const start = Date.now();
3279
+ const handle = setTimeout(() => dismiss(id), Math.max(0, t.remaining));
3280
+ return () => {
3281
+ clearTimeout(handle);
3282
+ t.remaining -= Date.now() - start;
3283
+ };
3284
+ }, [
3285
+ id,
3286
+ version,
3287
+ duration,
3288
+ holding,
3289
+ removed
3290
+ ]);
3291
+ useLayoutEffect(() => {
3292
+ if (!removed) return;
3293
+ const li = liRef.current;
3294
+ if (li?.contains(document.activeElement)) onFocusLost(index);
3295
+ let done = false;
3296
+ const finish = () => {
3297
+ if (done) return;
3298
+ done = true;
3299
+ removeToast(key);
3300
+ };
3301
+ const fallback = setTimeout(finish, EXIT_FALLBACK_MS);
3302
+ const animations = typeof li?.getAnimations === "function" ? li.getAnimations() : void 0;
3303
+ if (animations) Promise.allSettled(animations.map((a) => a.finished)).then(finish);
3304
+ return () => {
3305
+ done = true;
3306
+ clearTimeout(fallback);
3307
+ };
3308
+ }, [
3309
+ removed,
3310
+ key,
3311
+ index,
3312
+ onFocusLost
3313
+ ]);
3314
+ const drag = useRef(null);
3315
+ const swipeable = dismissible && !removed;
3316
+ const setSwipe = (li, dx, dy) => {
3317
+ li.style.setProperty("--toast-swipe-x", `${dx}px`);
3318
+ li.style.setProperty("--toast-swipe-y", `${dy}px`);
3319
+ };
3320
+ const handlePointerDown = (event) => {
3321
+ if (!swipeable || event.button !== 0) return;
3322
+ if (event.target.closest("button, a, input, textarea, select, [data-no-swipe]")) return;
3323
+ drag.current = {
3324
+ x: event.clientX,
3325
+ y: event.clientY,
3326
+ time: Date.now(),
3327
+ amount: 0
3328
+ };
3329
+ event.currentTarget.setPointerCapture?.(event.pointerId);
3330
+ };
3331
+ const handlePointerMove = (event) => {
3332
+ const d = drag.current;
3333
+ const li = event.currentTarget;
3334
+ if (!d) return;
3335
+ const dx = event.clientX - d.x;
3336
+ const dy = event.clientY - d.y;
3337
+ if (!d.axis) {
3338
+ if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) return;
3339
+ if (window.getSelection()?.toString()) {
3340
+ drag.current = null;
3341
+ return;
3342
+ }
3343
+ d.axis = Math.abs(dx) > Math.abs(dy) ? "x" : "y";
3344
+ li.setAttribute("data-swiping", "");
3345
+ }
3346
+ const delta = d.axis === "x" ? dx : dy;
3347
+ const toward = d.axis === "x" ? delta > 0 ? "right" : "left" : delta > 0 ? "bottom" : "top";
3348
+ const allowed = directions.includes(toward);
3349
+ d.amount = allowed ? delta : delta / (1.5 + Math.abs(delta) / 20);
3350
+ d.direction = allowed ? toward : void 0;
3351
+ if (d.axis === "x") setSwipe(li, d.amount, 0);
3352
+ else setSwipe(li, 0, d.amount);
3353
+ };
3354
+ const endDrag = (event, cancelled) => {
3355
+ const d = drag.current;
3356
+ drag.current = null;
3357
+ const li = event.currentTarget;
3358
+ if (!d?.axis) return;
3359
+ li.removeAttribute("data-swiping");
3360
+ const velocity = Math.abs(d.amount) / Math.max(1, Date.now() - d.time);
3361
+ if (!cancelled && d.direction && (Math.abs(d.amount) >= SWIPE_DISTANCE || velocity > SWIPE_VELOCITY)) {
3362
+ dismiss(id, d.direction);
3363
+ return;
3364
+ }
3365
+ setSwipe(li, 0, 0);
3366
+ };
3367
+ const touchAction = !swipeable ? void 0 : directions.length === 1 ? "touch-pan-x" : "touch-none";
3368
+ const style = {
3369
+ "--toast-y": `${sign * (expanded ? offset : index * GAP)}px`,
3370
+ "--toast-scale": expanded ? 1 : Math.max(0, 1 - index * SCALE_STEP),
3371
+ zIndex: total - index
3372
+ };
3373
+ const hasAction = action != null && action !== false;
3374
+ return /* @__PURE__ */ jsx("li", {
3375
+ ref: liRef,
3376
+ tabIndex: 0,
3377
+ "aria-labelledby": titleId,
3378
+ "aria-describedby": description != null && description !== false ? descriptionId : void 0,
3379
+ inert: removed || !visible,
3380
+ "data-slot": "toast",
3381
+ "data-toast-id": id,
3382
+ "data-type": type,
3383
+ "data-index": index,
3384
+ "data-front": index === 0 || void 0,
3385
+ "data-stacked": stacked || void 0,
3386
+ "data-visible": visible,
3387
+ "data-removed": removed || void 0,
3388
+ "data-swipe-out": removed ? swipeOut : void 0,
3389
+ "data-dismissible": dismissible || void 0,
3390
+ "data-entered": entered || void 0,
3391
+ style,
3392
+ onPointerDown: handlePointerDown,
3393
+ onPointerMove: handlePointerMove,
3394
+ onPointerUp: (event) => endDrag(event, false),
3395
+ onPointerCancel: (event) => endDrag(event, true),
3396
+ className: cn("group/toast absolute inset-x-0 rounded-xl outline-none select-none", edge === "bottom" ? "bottom-0 origin-top" : "top-0 origin-bottom", "transform-[translateY(var(--toast-y))_scale(var(--toast-scale))]", "[translate:var(--toast-swipe-x,0px)_var(--toast-swipe-y,0px)]", "transition-[transform,translate,scale,opacity] duration-350 ease-[cubic-bezier(0.25,0.75,0.6,0.98)]", enterFrom[edge], "starting:opacity-0", "after:absolute after:inset-x-0 after:h-3.75", edge === "bottom" ? "after:bottom-full" : "after:top-full", "data-[visible=false]:pointer-events-none data-[visible=false]:opacity-0", "data-removed:pointer-events-none data-removed:scale-99 data-removed:opacity-0 data-removed:duration-150", "data-removed:data-swipe-out:duration-200 data-removed:data-swipe-out:ease-out", "data-[swipe-out=right]:[translate:calc(100%+24px)_0]", "data-[swipe-out=left]:[translate:calc(-100%-24px)_0]", "data-[swipe-out=bottom]:[translate:0_calc(100%+24px)]", "data-[swipe-out=top]:[translate:0_calc(-100%-24px)]", "data-swiping:transition-none", "group-data-moving/viewport:data-entered:transition-none", "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus", touchAction),
3397
+ children: /* @__PURE__ */ jsx("div", {
3398
+ "data-slot": "toast-card",
3399
+ style: { height },
3400
+ className: cn("overflow-hidden rounded-[inherit] border shadow-popover", "forced-colors:border-[CanvasText]", "[transition-property:height,background-color,border-color,color]", "duration-[350ms,200ms,200ms,200ms] ease-[cubic-bezier(0.25,0.75,0.6,0.98)]", typeClasses[type]),
3401
+ children: /* @__PURE__ */ jsxs("div", {
3402
+ ref: bodyRef,
3403
+ "data-slot": "toast-body",
3404
+ className: cn("relative p-4 transition-opacity duration-200", "group-data-stacked/toast:pointer-events-none group-data-stacked/toast:opacity-0"),
3405
+ children: [
3406
+ /* @__PURE__ */ jsxs("div", {
3407
+ className: cn("flex items-start gap-3", dismissible && "pe-9.5"),
3408
+ children: [/* @__PURE__ */ jsx(ToastIcon, { type }), /* @__PURE__ */ jsxs("div", {
3409
+ className: "min-w-0 flex-1",
3410
+ children: [/* @__PURE__ */ jsx("div", {
3411
+ ref: titleRef,
3412
+ id: titleId,
3413
+ "data-slot": "toast-title",
3414
+ className: "text-sm/5 font-medium",
3415
+ children: title
3416
+ }), description != null && description !== false && /* @__PURE__ */ jsx("div", {
3417
+ ref: descriptionRef,
3418
+ id: descriptionId,
3419
+ "data-slot": "toast-description",
3420
+ className: "mt-0.5 text-sm/5 text-fg-muted",
3421
+ children: description
3422
+ })]
3423
+ })]
3424
+ }),
3425
+ hasAction && /* @__PURE__ */ jsx("div", {
3426
+ "data-slot": "toast-action",
3427
+ className: "mt-3 flex justify-end gap-2",
3428
+ children: isActionButton(action) ? /* @__PURE__ */ jsx(Button, {
3429
+ intent: "secondary",
3430
+ size: "sm",
3431
+ onClick: (event) => {
3432
+ action.onClick?.(event);
3433
+ if (!event.defaultPrevented) dismiss(id);
3434
+ },
3435
+ children: action.label
3436
+ }) : action
3437
+ }),
3438
+ dismissible && /* @__PURE__ */ jsx(Button, {
3439
+ intent: "ghost",
3440
+ size: "sm",
3441
+ icon: /* @__PURE__ */ jsx(X, {}),
3442
+ "aria-label": "Close",
3443
+ tooltip: false,
3444
+ className: "absolute top-2.25 right-2 text-current hover:not-aria-disabled:bg-current/10",
3445
+ onClick: () => dismiss(id)
3446
+ })
3447
+ ]
3448
+ })
3449
+ })
3450
+ });
3451
+ }
3452
+ //#endregion
3453
+ export { Badge, Button, Card, CardContent, CardHeader, Checkbox, CheckboxGroup, CopyInput, Dialog, DialogBody, DialogFooter, DialogHeader, Drawer, DrawerBody, DrawerClose, DrawerFooter, DrawerHeader, Input, Meter, PasswordInput, SearchInput, Select, Sidebar, SidebarBadge, SidebarBody, SidebarFooter, SidebarGroup, SidebarHeader, SidebarIcon, SidebarItem, SidebarProvider, SidebarTrigger, Switch, Tab, TabList, TabNav, TabNavLink, TabPanel, Tabs, ThemeSwitcher, Toaster, Tooltip, cn, toast, useTheme };