@weichenlin/swell-ui 0.2.0 → 0.4.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 CHANGED
@@ -1,9 +1,9 @@
1
- import { Check, ChevronDown, CircleAlert, CircleCheck, CircleDashed, CircleX, Copy, Eye, EyeOff, Info, LoaderCircle, Menu, Moon, Search, Sun, TriangleAlert, X } from "lucide-react";
1
+ import { ArrowDown, ArrowUp, ArrowUpDown, Check, ChevronDown, ChevronLeft, ChevronRight, CircleAlert, CircleCheck, CircleDashed, CircleX, Copy, Eye, EyeOff, Info, LoaderCircle, Menu, Moon, Search, Sun, TriangleAlert, X } from "lucide-react";
2
+ import { Children, Fragment, cloneElement, createContext, isValidElement, use, useCallback, useContext, useEffect, useEffectEvent, useId, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
3
  import { clsx } from "clsx";
3
4
  import { extendTailwindMerge } from "tailwind-merge";
4
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
6
  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
7
  import { createPortal } from "react-dom";
8
8
  //#region src/lib/cn.ts
9
9
  const tone = (name) => [
@@ -45,6 +45,8 @@ const twMerge = extendTailwindMerge({ extend: { theme: {
45
45
  "input-halo-invalid-focus",
46
46
  "control-checked",
47
47
  "control-checked-fg",
48
+ "switch-handle",
49
+ "switch-handle-checked",
48
50
  "popover-bg",
49
51
  "popover-border",
50
52
  "popover-highlight",
@@ -76,64 +78,592 @@ function cn(...inputs) {
76
78
  return twMerge(clsx(inputs));
77
79
  }
78
80
  //#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
- }
81
+ //#region src/components/input/field.tsx
82
+ const controlHeight = {
83
+ sm: "h-[34px]",
84
+ md: "h-[38px]",
85
+ lg: "h-[42px]"
106
86
  };
107
- const sizeClasses$3 = {
108
- sm: {
109
- base: "h-5 px-1.5",
110
- withIcon: "ps-1"
87
+ const textSize = {
88
+ sm: "text-sm",
89
+ md: "text-sm",
90
+ lg: "text-base"
91
+ };
92
+ const iconSize$1 = {
93
+ sm: "[&_svg]:size-4",
94
+ md: "[&_svg]:size-4",
95
+ lg: "[&_svg]:size-[18px]"
96
+ };
97
+ const inputPadding = {
98
+ sm: "px-3",
99
+ md: "px-3",
100
+ lg: "px-4"
101
+ };
102
+ const startSlotPadding = {
103
+ sm: "ps-2",
104
+ md: "ps-2.5",
105
+ lg: "ps-3"
106
+ };
107
+ const endSlotPadding = {
108
+ sm: "pe-2",
109
+ md: "pe-2.5",
110
+ lg: "pe-3"
111
+ };
112
+ const actionsPadding = {
113
+ sm: "pe-[3px]",
114
+ md: "pe-1",
115
+ lg: "pe-1"
116
+ };
117
+ /** Compact Button square that fits inside the field's inner height with an even inset. */
118
+ const compactButton = {
119
+ sm: "size-[26px]",
120
+ md: "size-7",
121
+ lg: "size-8"
122
+ };
123
+ const labelStart = {
124
+ sm: "start-2",
125
+ md: "start-2",
126
+ lg: "start-3"
127
+ };
128
+ const floatShift = {
129
+ sm: "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(textarea:focus),:has(textarea:not(:placeholder-shown)),: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%+var(--field-label-shift,17px))]",
130
+ md: "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(textarea:focus),:has(textarea:not(:placeholder-shown)),: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%+var(--field-label-shift,19px))]",
131
+ lg: "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(textarea:focus),:has(textarea:not(:placeholder-shown)),: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%+var(--field-label-shift,21px))]"
132
+ };
133
+ const isText = (node) => typeof node === "string" || typeof node === "number";
134
+ const isPresent = (node) => node != null && node !== false && node !== "";
135
+ function Adornment({ node, side, size }) {
136
+ const text = isText(node);
137
+ return /* @__PURE__ */ jsx("span", {
138
+ "aria-hidden": text ? void 0 : true,
139
+ 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$1[size], side === "start" ? startSlotPadding[size] : endSlotPadding[size]),
140
+ children: node
141
+ });
142
+ }
143
+ const notchPadding = {
144
+ border: {
145
+ sm: "ps-2 pe-2",
146
+ md: "ps-2 pe-2",
147
+ lg: "ps-3 pe-1"
111
148
  },
112
- md: {
113
- base: "h-6 px-2",
114
- withIcon: "ps-1.5"
149
+ halo: {
150
+ sm: "ps-[9px] pe-[9px]",
151
+ md: "ps-[9px] pe-[9px]",
152
+ lg: "ps-[13px] pe-[5px]"
115
153
  }
116
154
  };
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", {
155
+ /**
156
+ * MUI-style notched outline: a fieldset draws the ring and an invisible legend holding the label
157
+ * text at its floated size cuts the gap. The legend collapses while the label rests in the field.
158
+ * A legend as tall as the border keeps the border at the fieldset's top edge.
159
+ */
160
+ function Notch({ ring, size, disabled, children }) {
161
+ const halo = ring === "halo";
162
+ return /* @__PURE__ */ jsx("fieldset", {
163
+ "aria-hidden": true,
164
+ 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,textarea):is(:focus,:open)]/control:visible forced-colors:border-[Highlight]"] : ["-inset-px rounded-[inherit] border border-(--field-border)", disabled && "forced-colors:border-[GrayText]"]),
165
+ children: /* @__PURE__ */ jsx("legend", {
166
+ 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(textarea:focus),:has(textarea:not(:placeholder-shown)),: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(textarea:focus),:has(textarea:not(:placeholder-shown)),: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(textarea:focus),:has(textarea:not(:placeholder-shown)),: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"),
167
+ children: /* @__PURE__ */ jsx("span", {
168
+ className: "px-1",
169
+ children
170
+ })
171
+ })
172
+ });
173
+ }
174
+ /** Keeps an internal ref (focus forwarding, clear, reset) alongside the caller's ref. */
175
+ function useMergedRef(ownRef, ref) {
176
+ return useCallback((node) => {
177
+ ownRef.current = node;
178
+ if (typeof ref === "function") return ref(node);
179
+ if (ref) ref.current = node;
180
+ }, [ownRef, ref]);
181
+ }
182
+ const focusControl = (control) => control.focus();
183
+ /**
184
+ * The frame every field shares (input family, Select): label above or floating with a notched
185
+ * outline, the bordered control with its border/halo/hover states, description and live region.
186
+ * State selectors match `input`, `textarea` or `select`, never the inner buttons, so a focused clear or copy
187
+ * button shows only its own ring. `:open` too: an open base-select picker moves focus onto its
188
+ * options, and the select then matches neither `:focus` nor `:focus-within`. Inside `:is()` an
189
+ * unsupported `:open` is dropped alone instead of invalidating the rule.
190
+ */
191
+ function FieldShell({ label, floating = false, helperText, error, size, id: idProp, required, disabled, readOnly, "aria-describedby": describedByProp, hasStart = false, multiline = false, footer, className, status = "", controlRef, activate = focusControl, children }) {
192
+ const autoId = useId();
193
+ const id = idProp ?? `${autoId}-control`;
194
+ const descriptionId = `${autoId}-description`;
195
+ const invalid = Boolean(error);
196
+ const description = invalid ? error === true ? null : error : helperText;
197
+ const hasDescription = isPresent(description);
198
+ const describedBy = [describedByProp, hasDescription ? descriptionId : null].filter(Boolean).join(" ") || void 0;
199
+ const hasLabel = Boolean(label);
200
+ const floatingLabel = floating && hasLabel;
201
+ const asterisk = required ? /* @__PURE__ */ jsx("span", {
202
+ "aria-hidden": true,
203
+ className: "text-danger",
204
+ children: " *"
205
+ }) : null;
206
+ const forwardClick = (e) => {
207
+ const control = controlRef.current;
208
+ const target = e.target;
209
+ if (!control || control.contains(target) || target.closest("button")) return;
210
+ e.preventDefault();
211
+ activate(control);
212
+ };
213
+ const renderFloatingLabel = (restingWidth) => floatingLabel ? /* @__PURE__ */ jsxs("label", {
214
+ htmlFor: id,
215
+ 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", multiline && {
216
+ sm: "top-[18px]",
217
+ md: "top-5",
218
+ lg: "top-6"
219
+ }[size], labelStart[size], textSize[size], restingWidth, floatShift[size], "[[data-slot=input-control]:is([data-start],:has(input:focus),:has(textarea:focus),:has(textarea:not(:placeholder-shown)),: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(textarea:focus),:has(textarea:not(:placeholder-shown)),: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(textarea:focus),:has(textarea:not(:placeholder-shown)),: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,textarea):not(:is(:focus,:open))]/control:text-fg-strong", "group-has-[:is(input,select,textarea):is(:focus,:open)]/control:text-input-focus"]),
220
+ children: [label, asterisk]
221
+ }) : null;
222
+ return /* @__PURE__ */ jsxs("div", {
223
+ "data-slot": "input-field",
224
+ className: cn("flex w-full flex-col gap-1.5", disabled && "opacity-50", className),
225
+ children: [
226
+ hasLabel && !floating && /* @__PURE__ */ jsxs("label", {
227
+ htmlFor: id,
228
+ className: "self-start text-sm font-medium text-fg",
229
+ children: [label, asterisk]
230
+ }),
231
+ /* @__PURE__ */ jsxs("div", {
232
+ "data-slot": "input-control",
233
+ "data-start": hasStart ? "" : void 0,
234
+ onMouseDown: forwardClick,
235
+ 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", multiline ? {
236
+ sm: "[--field-label-shift:19px]",
237
+ md: "[--field-label-shift:21px]",
238
+ lg: "[--field-label-shift:25px]"
239
+ }[size] : controlHeight[size], invalid ? ["[--field-border:var(--color-danger)] [--field-halo:var(--color-input-halo-invalid)]", "has-[:is(input,select,textarea):is(:focus,:open)]:[--field-halo:var(--color-input-halo-invalid-focus)]"] : ["[--field-border:var(--color-input-border)] [--field-halo:transparent]", "has-[:is(input,select,textarea):is(:focus,:open)]:[--field-border:var(--color-input-focus)] has-[:is(input,select,textarea):is(:focus,:open)]:[--field-halo:var(--color-input-halo)]"], "[--field-lift:0_0_#0000]", !invalid && !disabled && !readOnly && [
240
+ "hover:not-has-[:is(input,select,textarea):is(:focus,:open)]:[--field-border:var(--color-input-border-hover)]",
241
+ !floatingLabel && "hover:not-has-[:is(input,select,textarea):is(:focus,:open)]:[--field-bg:var(--color-input-bg-hover)]",
242
+ "hover:not-has-[:is(input,select,textarea):is(:focus,:open)]:[--field-lift:var(--shadow-input-lift)]"
243
+ ], floatingLabel ? ["border-transparent forced-colors:border-[Canvas]", !readOnly && "[box-shadow:var(--field-lift)]"] : [
244
+ "border-(--field-border)",
245
+ 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)]",
246
+ "has-[:is(input,select,textarea):is(:focus,:open)]:outline-hidden",
247
+ disabled && "forced-colors:border-[GrayText]"
248
+ ], 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"),
249
+ children: [floatingLabel && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Notch, {
250
+ size,
251
+ ring: "halo",
252
+ disabled,
253
+ children: [label, asterisk]
254
+ }), /* @__PURE__ */ jsxs(Notch, {
255
+ size,
256
+ ring: "border",
257
+ disabled,
258
+ children: [label, asterisk]
259
+ })] }), children({
260
+ id,
261
+ invalid,
262
+ describedBy,
263
+ floating: floatingLabel,
264
+ floatingLabel: renderFloatingLabel
265
+ })]
266
+ }),
267
+ (hasDescription || isPresent(footer)) && /* @__PURE__ */ jsxs("div", {
268
+ className: "flex items-start justify-between gap-3",
269
+ children: [hasDescription && /* @__PURE__ */ jsxs("p", {
270
+ id: descriptionId,
271
+ className: cn("m-0 flex min-w-0 flex-1 items-start gap-1 text-xs/snug", invalid ? "text-danger" : "text-fg-muted"),
272
+ children: [invalid && /* @__PURE__ */ jsx(CircleAlert, {
273
+ "aria-hidden": true,
274
+ className: "mt-px size-3.5 shrink-0"
275
+ }), /* @__PURE__ */ jsx("span", { children: description })]
276
+ }), footer]
277
+ }),
278
+ /* @__PURE__ */ jsx("span", {
279
+ "aria-live": "polite",
280
+ "aria-atomic": true,
281
+ className: "sr-only",
282
+ children: status
283
+ })
284
+ ]
285
+ });
286
+ }
287
+ /** The input family's frame: adornments, actions and the input itself inside FieldShell. */
288
+ 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 }) {
289
+ const ownRef = useRef(null);
290
+ const inputRef = inputRefProp ?? ownRef;
291
+ const setRef = useMergedRef(inputRef, ref);
292
+ const hasStart = isPresent(start);
293
+ const hasEnd = isPresent(end);
294
+ return /* @__PURE__ */ jsx(FieldShell, {
295
+ label,
296
+ floating,
297
+ helperText,
298
+ error,
299
+ size,
300
+ id,
301
+ required,
302
+ disabled,
303
+ readOnly,
304
+ "aria-describedby": describedByProp,
305
+ hasStart,
306
+ className,
307
+ status: validating ? validatingText : status ?? "",
308
+ controlRef: inputRef,
309
+ children: (field) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
310
+ hasStart ? /* @__PURE__ */ jsx(Adornment, {
311
+ node: start,
312
+ side: "start",
313
+ size
314
+ }) : null,
315
+ /* @__PURE__ */ jsxs("span", {
316
+ className: "@container flex min-w-0 flex-1 self-stretch rounded-[inherit]",
317
+ children: [field.floatingLabel("max-w-[calc(100cqw-1rem)]"), /* @__PURE__ */ jsx("input", {
318
+ ...props,
319
+ ref: setRef,
320
+ id: field.id,
321
+ required,
322
+ disabled,
323
+ readOnly,
324
+ placeholder: field.floating ? placeholder || " " : placeholder,
325
+ "aria-invalid": field.invalid ? true : ariaInvalid,
326
+ "aria-describedby": field.describedBy,
327
+ spellCheck: mono ? false : props.spellCheck,
328
+ autoCapitalize: mono ? "off" : props.autoCapitalize,
329
+ autoCorrect: mono ? "off" : props.autoCorrect,
330
+ 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(textarea:focus),:has(textarea:not(:placeholder-shown)),: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)
331
+ })]
332
+ }),
333
+ validating ? /* @__PURE__ */ jsx("span", {
334
+ className: cn("flex shrink-0 items-center self-stretch text-fg-muted", iconSize$1[size], endSlotPadding[size]),
335
+ children: /* @__PURE__ */ jsx(LoaderCircle, {
336
+ "aria-hidden": true,
337
+ className: "animate-spin motion-reduce:[animation-duration:2.5s]"
338
+ })
339
+ }) : hasEnd ? /* @__PURE__ */ jsx(Adornment, {
340
+ node: end,
341
+ side: "end",
342
+ size
343
+ }) : null,
344
+ actions ? /* @__PURE__ */ jsx("span", {
345
+ className: cn("flex shrink-0 items-center gap-0.5", actionsPadding[size]),
346
+ children: actions
347
+ }) : null
348
+ ] })
349
+ });
350
+ }
351
+ //#endregion
352
+ //#region src/components/accordion/use-accordion-motion.ts
353
+ /** Animate measured heights; never cap content with an arbitrary max-height. */
354
+ function useAccordionMotion(open, itemRef, restoreFocus) {
355
+ const panelRef = useRef(null);
356
+ const innerRef = useRef(null);
357
+ const previousOpen = useRef(open);
358
+ const [present, setPresent] = useState(open);
359
+ useLayoutEffect(() => {
360
+ const panel = panelRef.current;
361
+ const inner = innerRef.current;
362
+ if (!panel || !inner) return;
363
+ const changed = previousOpen.current !== open;
364
+ previousOpen.current = open;
365
+ let active = true;
366
+ let animation;
367
+ let observer;
368
+ let target = -1;
369
+ let settled = false;
370
+ const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)");
371
+ if (!open && panel.contains(panel.ownerDocument.activeElement) && itemRef.current) restoreFocus(itemRef.current);
372
+ panel.inert = !open;
373
+ panel.toggleAttribute("inert", !open);
374
+ if (open) panel.removeAttribute("aria-hidden");
375
+ else panel.setAttribute("aria-hidden", "true");
376
+ const finish = () => {
377
+ if (!active) return;
378
+ settled = true;
379
+ observer?.disconnect();
380
+ animation?.cancel();
381
+ animation = void 0;
382
+ panel.style.removeProperty("height");
383
+ panel.style.removeProperty("overflow");
384
+ panel.hidden = !open;
385
+ setPresent(open);
386
+ };
387
+ const run = () => {
388
+ if (!active || settled) return;
389
+ const nextTarget = open ? inner.getBoundingClientRect().height : 0;
390
+ if (nextTarget === target && animation) return;
391
+ target = nextTarget;
392
+ const from = panel.getBoundingClientRect().height;
393
+ animation?.cancel();
394
+ panel.style.height = `${from}px`;
395
+ const current = panel.animate([{ height: `${from}px` }, { height: `${target}px` }], {
396
+ duration: open ? 200 : 160,
397
+ easing: "cubic-bezier(.16,1,.3,1)",
398
+ fill: "both"
399
+ });
400
+ animation = current;
401
+ current.finished.then(() => {
402
+ if (active && animation === current) finish();
403
+ }, () => {});
404
+ };
405
+ if (!changed || reduced?.matches || typeof panel.animate !== "function") finish();
406
+ else {
407
+ if (open && !panel.style.height) panel.style.height = "0px";
408
+ panel.hidden = false;
409
+ panel.style.overflow = "hidden";
410
+ setPresent(true);
411
+ run();
412
+ if (open && typeof ResizeObserver !== "undefined") {
413
+ observer = new ResizeObserver(run);
414
+ observer.observe(inner);
415
+ }
416
+ }
417
+ const onReducedMotion = () => {
418
+ if (reduced?.matches) finish();
419
+ };
420
+ reduced?.addEventListener("change", onReducedMotion);
421
+ return () => {
422
+ active = false;
423
+ observer?.disconnect();
424
+ reduced?.removeEventListener("change", onReducedMotion);
425
+ if (animation) {
426
+ panel.style.height = `${panel.getBoundingClientRect().height}px`;
427
+ animation.cancel();
428
+ }
429
+ };
430
+ }, [
431
+ open,
432
+ itemRef,
433
+ restoreFocus
434
+ ]);
435
+ return {
436
+ panelRef,
437
+ innerRef,
438
+ present
439
+ };
440
+ }
441
+ //#endregion
442
+ //#region src/components/accordion/accordion.tsx
443
+ const AccordionContext = createContext(null);
444
+ const ItemContext = createContext(null);
445
+ function useAccordionContext(part) {
446
+ const context = useContext(AccordionContext);
447
+ if (!context) throw new Error(`${part} must be rendered inside <Accordion>`);
448
+ return context;
449
+ }
450
+ function useItemContext(part) {
451
+ const context = useContext(ItemContext);
452
+ if (!context) throw new Error(`${part} must be rendered inside <AccordionItem>`);
453
+ return context;
454
+ }
455
+ const valuesOf = (value) => typeof value === "string" ? [value] : [...new Set(value ?? [])];
456
+ const ownTriggers = (root) => [...root.querySelectorAll("[data-slot=\"accordion-trigger\"]")].filter((node) => node.closest("[data-slot=\"accordion\"]") === root);
457
+ /** Vertical disclosure group. Changing type requires a new React key. */
458
+ function Accordion({ type = "single", value, defaultValue, onValueChange, collapsible = true, variant = "line", headingLevel = 3, disabled = false, className, children, ref, onKeyDown, ...props }) {
459
+ const [initialType] = useState(type);
460
+ if (initialType !== type) throw new Error("Changing Accordion type requires a new key");
461
+ const controlled = value !== void 0;
462
+ const [internal, setInternal] = useState(() => valuesOf(defaultValue));
463
+ const selected = controlled ? valuesOf(value) : internal;
464
+ const rootRef = useRef(null);
465
+ const mergedRef = useMergedRef(rootRef, ref);
466
+ const items = useRef(/* @__PURE__ */ new Map());
467
+ const internalRef = useRef(internal);
468
+ useLayoutEffect(() => {
469
+ internalRef.current = internal;
470
+ }, [internal]);
471
+ const restoreFocus = useCallback((item, removing = false) => {
472
+ const root = rootRef.current;
473
+ if (!root) return;
474
+ const triggers = ownTriggers(root);
475
+ const own = triggers.find((node) => node.closest("[data-slot=\"accordion-item\"]") === item);
476
+ const index = own ? triggers.indexOf(own) : -1;
477
+ const next = (!removing && own && !own.disabled ? own : void 0) ?? triggers.slice(index + 1).find((node) => !node.disabled) ?? triggers.slice(0, Math.max(index, 0)).reverse().find((node) => !node.disabled);
478
+ if (next) next.focus({ preventScroll: true });
479
+ else {
480
+ const previous = root.getAttribute("tabindex");
481
+ root.tabIndex = -1;
482
+ root.focus({ preventScroll: true });
483
+ if (previous === null) root.removeAttribute("tabindex");
484
+ else root.setAttribute("tabindex", previous);
485
+ }
486
+ }, []);
487
+ const register = useCallback((itemValue, node) => {
488
+ const members = items.current;
489
+ if (members.has(itemValue)) throw new Error(`Accordion item values must be unique: "${itemValue}"`);
490
+ members.set(itemValue, node);
491
+ const reconcile = () => {
492
+ queueMicrotask(() => {
493
+ if (controlled || !rootRef.current) return;
494
+ if (internalRef.current.every((entry) => members.has(entry))) return;
495
+ setInternal((current) => {
496
+ const next = current.filter((entry) => members.has(entry));
497
+ return next.length === current.length ? current : next;
498
+ });
499
+ });
500
+ };
501
+ reconcile();
502
+ return () => {
503
+ members.delete(itemValue);
504
+ const focused = node.ownerDocument.activeElement;
505
+ if (focused && node.contains(focused)) {
506
+ const root = rootRef.current;
507
+ const triggers = root ? ownTriggers(root) : [];
508
+ const index = triggers.findIndex((trigger) => node.contains(trigger));
509
+ const candidates = [...triggers.slice(index + 1), ...triggers.slice(0, index).reverse()];
510
+ queueMicrotask(() => {
511
+ const active = node.ownerDocument.activeElement;
512
+ if (node.isConnected || !rootRef.current || active !== node.ownerDocument.body && active !== focused) return;
513
+ const next = candidates.find((trigger) => trigger.isConnected && !trigger.disabled);
514
+ if (next) next.focus({ preventScroll: true });
515
+ else restoreFocus(node, true);
516
+ });
517
+ }
518
+ reconcile();
519
+ };
520
+ }, [controlled, restoreFocus]);
521
+ const toggle = (itemValue) => {
522
+ const open = selected.includes(itemValue);
523
+ if (disabled || type === "single" && open && !collapsible) return;
524
+ const next = open ? selected.filter((entry) => entry !== itemValue) : type === "single" ? [itemValue] : [...selected, itemValue];
525
+ if (!controlled) setInternal(next);
526
+ if (type === "multiple") onValueChange?.(next);
527
+ else onValueChange?.(next[0] ?? null);
528
+ };
529
+ return /* @__PURE__ */ jsx(AccordionContext, {
530
+ value: {
531
+ selected,
532
+ variant,
533
+ headingLevel,
534
+ disabled,
535
+ collapsible: type === "multiple" || collapsible,
536
+ toggle,
537
+ register,
538
+ restoreFocus
539
+ },
540
+ children: /* @__PURE__ */ jsx(ItemContext, {
541
+ value: null,
542
+ children: /* @__PURE__ */ jsx("div", {
543
+ ...props,
544
+ ref: mergedRef,
545
+ "data-slot": "accordion",
546
+ "data-variant": variant,
547
+ "data-disabled": disabled || void 0,
548
+ className: cn("min-w-0", variant === "outline" && "flex flex-col gap-2", className),
549
+ onKeyDown: (event) => {
550
+ onKeyDown?.(event);
551
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
552
+ if (![
553
+ "ArrowDown",
554
+ "ArrowUp",
555
+ "Home",
556
+ "End"
557
+ ].includes(event.key)) return;
558
+ const triggers = ownTriggers(event.currentTarget).filter((node) => !node.disabled);
559
+ const index = triggers.indexOf(event.target);
560
+ if (index === -1) return;
561
+ event.preventDefault();
562
+ triggers[event.key === "Home" ? 0 : event.key === "End" ? triggers.length - 1 : Math.max(0, Math.min(triggers.length - 1, index + (event.key === "ArrowDown" ? 1 : -1)))]?.focus();
563
+ },
564
+ children
565
+ })
566
+ })
567
+ });
568
+ }
569
+ function AccordionItem({ value, disabled = false, className, children, ref, ...props }) {
570
+ const group = useAccordionContext("AccordionItem");
571
+ if (!value.trim()) throw new Error("AccordionItem value must be a non-empty string");
572
+ const id = useId();
573
+ const itemRef = useRef(null);
574
+ const mergedRef = useMergedRef(itemRef, ref);
575
+ const register = group.register;
576
+ useLayoutEffect(() => {
577
+ if (itemRef.current) return register(value, itemRef.current);
578
+ }, [register, value]);
579
+ const open = group.selected.includes(value);
580
+ const isDisabled = group.disabled || disabled;
581
+ return /* @__PURE__ */ jsx(ItemContext, {
582
+ value: {
583
+ id,
584
+ value,
585
+ open,
586
+ disabled: isDisabled,
587
+ itemRef
588
+ },
589
+ children: /* @__PURE__ */ jsx("div", {
590
+ ...props,
591
+ ref: mergedRef,
592
+ "data-slot": "accordion-item",
593
+ "data-state": open ? "open" : "closed",
594
+ "data-disabled": isDisabled || void 0,
595
+ className: cn("min-w-0 border-input-border", group.variant === "outline" ? "rounded-xl border bg-surface" : "border-b", className),
596
+ children
597
+ })
598
+ });
599
+ }
600
+ function AccordionTrigger({ children, description, className, onClick, "aria-describedby": describedBy, ...props }) {
601
+ const group = useAccordionContext("AccordionTrigger");
602
+ const item = useItemContext("AccordionTrigger");
603
+ const Heading = `h${group.headingLevel}`;
604
+ const hasDescription = isPresent(description);
605
+ const locked = item.open && !group.collapsible;
606
+ return /* @__PURE__ */ jsx(Heading, {
607
+ "data-slot": "accordion-heading",
608
+ className: "m-0 text-inherit",
609
+ children: /* @__PURE__ */ jsxs("button", {
610
+ ...props,
611
+ id: `${item.id}-trigger`,
612
+ type: "button",
613
+ disabled: item.disabled,
614
+ "aria-expanded": item.open,
615
+ "aria-controls": `${item.id}-content`,
616
+ "aria-disabled": locked || void 0,
617
+ "aria-labelledby": props["aria-label"] ? void 0 : `${item.id}-title`,
618
+ "aria-describedby": [describedBy, hasDescription && `${item.id}-description`].filter(Boolean).join(" ") || void 0,
619
+ "data-slot": "accordion-trigger",
620
+ "data-state": item.open ? "open" : "closed",
621
+ className: cn("group/accordion-trigger flex min-h-12 w-full cursor-pointer items-start gap-3 rounded-sm py-3 text-start text-sm/6 font-medium text-fg outline-none", "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus", "disabled:cursor-not-allowed disabled:opacity-50 aria-disabled:cursor-default", "forced-colors:disabled:text-[GrayText]", group.variant === "outline" && "rounded-xl px-4", className),
622
+ onClick: (event) => {
623
+ onClick?.(event);
624
+ if (!event.defaultPrevented && !item.disabled && !locked) group.toggle(item.value);
625
+ },
626
+ children: [/* @__PURE__ */ jsxs("span", {
627
+ className: "min-w-0 flex-1 wrap-anywhere",
628
+ children: [/* @__PURE__ */ jsx("span", {
629
+ id: `${item.id}-title`,
630
+ className: "underline-offset-4 group-enabled/accordion-trigger:group-hover/accordion-trigger:underline",
631
+ children
632
+ }), hasDescription && /* @__PURE__ */ jsx("span", {
633
+ id: `${item.id}-description`,
634
+ className: "mt-0.5 block text-xs/4.5 font-normal text-fg-muted",
635
+ children: description
636
+ })]
637
+ }), /* @__PURE__ */ jsx(ChevronDown, {
638
+ "aria-hidden": true,
639
+ className: cn("mt-1 size-4 shrink-0 text-fg-muted transition-transform duration-200 ease-out motion-reduce:transition-none", item.open && "rotate-180")
640
+ })]
641
+ })
642
+ });
643
+ }
644
+ function AccordionContent({ keepMounted = true, region = false, className, contentClassName, children, ref, ...props }) {
645
+ const group = useAccordionContext("AccordionContent");
646
+ const item = useItemContext("AccordionContent");
647
+ const { panelRef, innerRef, present } = useAccordionMotion(item.open, item.itemRef, group.restoreFocus);
648
+ const mergedRef = useMergedRef(panelRef, ref);
649
+ return /* @__PURE__ */ jsx("div", {
121
650
  ...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
- })]
651
+ ref: mergedRef,
652
+ id: `${item.id}-content`,
653
+ role: region ? "region" : void 0,
654
+ "aria-labelledby": region ? `${item.id}-title` : void 0,
655
+ hidden: !item.open && !present,
656
+ inert: !item.open && !present,
657
+ "aria-hidden": !item.open && !present ? true : void 0,
658
+ "data-slot": "accordion-content",
659
+ "data-state": item.open ? "open" : "closed",
660
+ className: cn("min-w-0", contentClassName),
661
+ children: /* @__PURE__ */ jsx("div", {
662
+ ref: innerRef,
663
+ "data-slot": "accordion-content-inner",
664
+ className: cn("flow-root min-w-0 pb-4 text-sm/5.5 wrap-anywhere text-fg-muted", group.variant === "outline" ? "ps-4 pe-11" : "pe-7", className),
665
+ children: (keepMounted || item.open || present) && children
666
+ })
137
667
  });
138
668
  }
139
669
  //#endregion
@@ -221,11 +751,66 @@ function usePressFeedback(ripple = true) {
221
751
  return ref;
222
752
  }
223
753
  //#endregion
754
+ //#region src/lib/slot.tsx
755
+ function setRef(ref, node) {
756
+ if (typeof ref === "function") return ref(node);
757
+ if (ref) ref.current = node;
758
+ }
759
+ function composeRefs(...refs) {
760
+ return (node) => {
761
+ const cleanups = refs.map((ref) => setRef(ref, node));
762
+ return () => {
763
+ for (const [i, ref] of refs.entries()) {
764
+ const cleanup = cleanups[i];
765
+ if (typeof cleanup === "function") cleanup();
766
+ else setRef(ref, null);
767
+ }
768
+ };
769
+ };
770
+ }
771
+ function mergeProps(slot, child) {
772
+ const merged = {
773
+ ...slot,
774
+ ...child
775
+ };
776
+ for (const key of Object.keys(slot)) {
777
+ const own = slot[key];
778
+ const theirs = child[key];
779
+ if (/^on[A-Z]/.test(key) && typeof own === "function" && typeof theirs === "function") merged[key] = (...args) => {
780
+ theirs(...args);
781
+ own(...args);
782
+ };
783
+ else if (key === "className") merged.className = cn(own, theirs);
784
+ else if (key === "style") merged.style = {
785
+ ...own,
786
+ ...theirs
787
+ };
788
+ }
789
+ return merged;
790
+ }
791
+ /** Applies `wrap` to an element's children, or to what a render-prop child returns. */
792
+ function wrapChildren(children, wrap) {
793
+ if (typeof children !== "function") return wrap(children);
794
+ return (...args) => wrap(children(...args));
795
+ }
796
+ /** Renders its only child with these props merged in (Radix Slot style). */
797
+ function Slot({ children, ref, ...props }) {
798
+ const child = Children.only(children);
799
+ const childProps = isValidElement(child) ? child.props : {};
800
+ const childRef = childProps.ref;
801
+ const composedRef = useMemo(() => composeRefs(ref, childRef), [ref, childRef]);
802
+ if (!isValidElement(child)) return null;
803
+ return cloneElement(child, {
804
+ ...mergeProps(props, childProps),
805
+ ref: composedRef
806
+ });
807
+ }
808
+ //#endregion
224
809
  //#region src/components/tooltip.tsx
225
810
  const OPEN_DELAY_MS = 300;
226
811
  const GAP_PX = 6;
227
812
  const VIEWPORT_PADDING_PX = 8;
228
- const opposite = {
813
+ const opposite$1 = {
229
814
  top: "bottom",
230
815
  bottom: "top",
231
816
  left: "right",
@@ -238,7 +823,7 @@ function isFocusVisible$1(el) {
238
823
  return true;
239
824
  }
240
825
  }
241
- function clamp(value, min, max) {
826
+ function clamp$2(value, min, max) {
242
827
  return Math.max(min, Math.min(value, Math.max(min, max)));
243
828
  }
244
829
  /**
@@ -255,15 +840,15 @@ function place(anchor, width, height, preferred) {
255
840
  right: vw - anchor.right
256
841
  };
257
842
  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;
843
+ const side = room[preferred] < need(preferred) && room[opposite$1[preferred]] > room[preferred] ? opposite$1[preferred] : preferred;
259
844
  if (side === "top" || side === "bottom") return {
260
845
  side,
261
- left: clamp(anchor.left + anchor.width / 2 - width / 2, VIEWPORT_PADDING_PX, vw - width - VIEWPORT_PADDING_PX),
846
+ left: clamp$2(anchor.left + anchor.width / 2 - width / 2, VIEWPORT_PADDING_PX, vw - width - VIEWPORT_PADDING_PX),
262
847
  top: side === "top" ? anchor.top - height - GAP_PX : anchor.bottom
263
848
  };
264
849
  return {
265
850
  side,
266
- top: clamp(anchor.top + anchor.height / 2 - height / 2, VIEWPORT_PADDING_PX, vh - height - VIEWPORT_PADDING_PX),
851
+ top: clamp$2(anchor.top + anchor.height / 2 - height / 2, VIEWPORT_PADDING_PX, vh - height - VIEWPORT_PADDING_PX),
267
852
  left: side === "left" ? anchor.left - width - GAP_PX : anchor.right
268
853
  };
269
854
  }
@@ -456,7 +1041,7 @@ const spinnerSize = {
456
1041
  md: 18,
457
1042
  lg: 20
458
1043
  };
459
- const iconSize$1 = {
1044
+ const iconSize = {
460
1045
  sm: "[&_svg]:size-4",
461
1046
  md: "[&_svg]:size-4",
462
1047
  lg: "[&_svg]:size-[18px]"
@@ -474,7 +1059,7 @@ const iconEndPadding = {
474
1059
  /** Press layer + content + spinner; shared by every button-styled host. */
475
1060
  function ButtonContent({ size, ripple, loading = false, iconLeft, iconRight, children }) {
476
1061
  const layerRef = usePressFeedback(ripple);
477
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1062
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
478
1063
  /* @__PURE__ */ jsx("span", {
479
1064
  ref: layerRef,
480
1065
  "data-ripple": ripple ? "" : void 0,
@@ -482,7 +1067,7 @@ function ButtonContent({ size, ripple, loading = false, iconLeft, iconRight, chi
482
1067
  className: "pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit] forced-colors:hidden"
483
1068
  }),
484
1069
  /* @__PURE__ */ jsxs("span", {
485
- className: cn("relative inline-flex items-center gap-2 [&_svg]:shrink-0", iconSize$1[size], loading && "opacity-0"),
1070
+ className: cn("relative inline-flex items-center gap-2 [&_svg]:shrink-0", iconSize[size], loading && "opacity-0"),
486
1071
  children: [
487
1072
  iconLeft && /* @__PURE__ */ jsx("span", {
488
1073
  "aria-hidden": true,
@@ -514,395 +1099,292 @@ function stateClass(disabled, loading) {
514
1099
  if (loading) return "cursor-progress opacity-70";
515
1100
  if (disabled) return "cursor-not-allowed opacity-50 forced-colors:opacity-100 forced-colors:text-[GrayText] forced-colors:after:border-[GrayText]";
516
1101
  }
517
- function ButtonImpl({ className, intent, size, tone, fullWidth, square, loading = false, loadingText = "Loading", disabled = false, iconLeft, iconRight, ripple, tooltip, type = "button", onClick, children, ...props }) {
1102
+ function blockActivation(e) {
1103
+ e.preventDefault();
1104
+ e.stopPropagation();
1105
+ }
1106
+ function ButtonImpl({ asChild = false, className, intent, size, tone, fullWidth, square, loading = false, loadingText = "Loading", disabled = false, iconLeft, iconRight, ripple, tooltip, type = "button", onClick, onClickCapture, onAuxClickCapture, children, ...props }) {
518
1107
  const s = size ?? "md";
519
1108
  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,
1109
+ const classes = cn(buttonVariants({
1110
+ intent,
572
1111
  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
1112
+ tone,
1113
+ fullWidth,
1114
+ square
1115
+ }), iconPadding(s, iconLeft, iconRight), stateClass(disabled, loading), className);
1116
+ const content = (inner) => /* @__PURE__ */ jsx(ButtonContent, {
1117
+ size: s,
1118
+ ripple: ripple ?? true,
1119
+ loading,
1120
+ iconLeft,
1121
+ iconRight,
1122
+ children: inner
586
1123
  });
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,
1124
+ if (asChild) {
1125
+ const child = Children.only(children);
1126
+ if (!isValidElement(child)) return null;
1127
+ const link = /* @__PURE__ */ jsx(Slot, {
608
1128
  ...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
- })]
1129
+ "data-slot": "button",
1130
+ "aria-disabled": disabled || void 0,
1131
+ className: classes,
1132
+ onClick,
1133
+ onClickCapture: disabled ? blockActivation : onClickCapture,
1134
+ onAuxClickCapture: disabled ? blockActivation : onAuxClickCapture,
1135
+ children: cloneElement(child, { children: wrapChildren(child.props.children, content) })
1136
+ });
1137
+ return tooltip ? /* @__PURE__ */ jsx(Tooltip, {
1138
+ ...tooltip,
1139
+ className: fullWidth ? "flex w-full" : void 0,
1140
+ children: link
1141
+ }) : link;
1142
+ }
1143
+ const button = /* @__PURE__ */ jsx("button", {
1144
+ ...props,
1145
+ type,
1146
+ "data-slot": "button",
1147
+ "aria-disabled": inert || void 0,
1148
+ "aria-busy": loading || void 0,
1149
+ className: classes,
1150
+ onClickCapture,
1151
+ onAuxClickCapture,
1152
+ onClick: (e) => {
1153
+ if (inert) {
1154
+ e.preventDefault();
1155
+ return;
1156
+ }
1157
+ onClick?.(e);
1158
+ },
1159
+ children: content(children)
639
1160
  });
1161
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [tooltip ? /* @__PURE__ */ jsx(Tooltip, {
1162
+ ...tooltip,
1163
+ className: fullWidth ? "flex w-full" : void 0,
1164
+ children: button
1165
+ }) : button, /* @__PURE__ */ jsx("span", {
1166
+ "aria-live": "polite",
1167
+ "aria-atomic": true,
1168
+ className: "sr-only",
1169
+ children: loading ? loadingText : ""
1170
+ })] });
640
1171
  }
641
- function CardContent({ className, ...props }) {
642
- return /* @__PURE__ */ jsx("div", {
1172
+ function IconOnlyButton({ icon, tooltip = true, tooltipSide, disabledReason, className, size, children, ...props }) {
1173
+ const reason = props.disabled ? disabledReason : void 0;
1174
+ const tip = reason ? {
1175
+ content: reason,
1176
+ side: tooltipSide
1177
+ } : tooltip ? {
1178
+ content: props["aria-label"],
1179
+ side: tooltipSide,
1180
+ describe: false
1181
+ } : disabledReason !== void 0 ? { content: void 0 } : void 0;
1182
+ const child = Children.toArray(children)[0];
1183
+ return /* @__PURE__ */ jsx(ButtonImpl, {
643
1184
  ...props,
644
- "data-slot": "card-content",
645
- className: cn("p-5 [[data-slot=card-header]+&]:pt-4", className)
1185
+ size,
1186
+ tooltip: tip,
1187
+ square: true,
1188
+ className,
1189
+ children: props.asChild && isValidElement(child) ? cloneElement(child, void 0, icon) : icon
646
1190
  });
647
1191
  }
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
1192
+ function Button(props) {
1193
+ if (props.icon !== void 0) return /* @__PURE__ */ jsx(IconOnlyButton, { ...props });
1194
+ const { disabledReason, ...rest } = props;
1195
+ const tooltip = disabledReason === void 0 ? void 0 : { content: rest.disabled ? disabledReason : void 0 };
1196
+ return /* @__PURE__ */ jsx(ButtonImpl, {
1197
+ ...rest,
1198
+ tooltip
709
1199
  });
710
1200
  }
711
- const notchPadding = {
712
- border: {
713
- sm: "ps-2 pe-2",
714
- md: "ps-2 pe-2",
715
- lg: "ps-3 pe-1"
1201
+ //#endregion
1202
+ //#region src/components/alert/alert.tsx
1203
+ const tones = {
1204
+ neutral: {
1205
+ Icon: Info,
1206
+ iconClass: "text-neutral"
716
1207
  },
717
- halo: {
718
- sm: "ps-[9px] pe-[9px]",
719
- md: "ps-[9px] pe-[9px]",
720
- lg: "ps-[13px] pe-[5px]"
1208
+ info: {
1209
+ Icon: Info,
1210
+ iconClass: "text-info"
1211
+ },
1212
+ success: {
1213
+ Icon: CircleCheck,
1214
+ iconClass: "text-success"
1215
+ },
1216
+ warning: {
1217
+ Icon: TriangleAlert,
1218
+ iconClass: "text-warning"
1219
+ },
1220
+ danger: {
1221
+ Icon: CircleAlert,
1222
+ iconClass: "text-danger"
721
1223
  }
722
1224
  };
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
- })
1225
+ /** A persistent message shown next to the content it describes. Use a live region for updates that need announcing. */
1226
+ function Alert({ tone = "neutral", title, action, onDismiss, dismissLabel = "Dismiss alert", className, children, ...props }) {
1227
+ const { Icon, iconClass } = tones[tone];
1228
+ return /* @__PURE__ */ jsxs("div", {
1229
+ ...props,
1230
+ "data-slot": "alert",
1231
+ "data-tone": tone,
1232
+ className: cn("@container/alert flex items-start gap-3 rounded-xl border border-ring-deep px-4 py-3", "text-sm text-fg forced-colors:border-[CanvasText]", className),
1233
+ children: [/* @__PURE__ */ jsxs("div", {
1234
+ className: cn("grid min-w-0 flex-1 grid-cols-[1rem_minmax(0,1fr)] items-start gap-x-2.5 gap-y-3", action && "@md/alert:grid-cols-[1rem_minmax(0,1fr)_auto]"),
1235
+ children: [
1236
+ /* @__PURE__ */ jsx(Icon, {
1237
+ "aria-hidden": "true",
1238
+ "data-slot": "alert-icon",
1239
+ className: cn("mt-0.5 size-4", iconClass)
1240
+ }),
1241
+ /* @__PURE__ */ jsxs("div", {
1242
+ className: "min-w-0 space-y-0.5 wrap-anywhere",
1243
+ children: [title != null && /* @__PURE__ */ jsx("div", {
1244
+ "data-slot": "alert-title",
1245
+ className: "font-medium",
1246
+ children: title
1247
+ }), children != null && /* @__PURE__ */ jsx("div", {
1248
+ "data-slot": "alert-description",
1249
+ className: title != null ? "text-fg-muted" : void 0,
1250
+ children
1251
+ })]
1252
+ }),
1253
+ action && /* @__PURE__ */ jsx("div", {
1254
+ "data-slot": "alert-action",
1255
+ className: "col-start-2 justify-self-start @md/alert:col-start-3 @md/alert:self-center",
1256
+ children: action
1257
+ })
1258
+ ]
1259
+ }), onDismiss && /* @__PURE__ */ jsx(Button, {
1260
+ "aria-label": dismissLabel,
1261
+ className: "-my-1.5 -me-1.5 shrink-0 text-fg-muted @md/alert:self-center",
1262
+ icon: /* @__PURE__ */ jsx(X, {}),
1263
+ intent: "ghost",
1264
+ size: "sm",
1265
+ onClick: onDismiss
1266
+ })]
740
1267
  });
741
1268
  }
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,
1269
+ //#endregion
1270
+ //#region src/components/badge/badge.tsx
1271
+ const toneClasses$1 = {
1272
+ neutral: {
1273
+ subtle: ["border-neutral-border bg-neutral-subtle-bg text-neutral-subtle-fg", "text-neutral"],
1274
+ outline: ["border-neutral-border bg-transparent text-neutral-subtle-fg", "text-neutral"],
1275
+ solid: ["border-neutral bg-neutral text-neutral-fg", "text-neutral-fg"]
1276
+ },
1277
+ info: {
1278
+ subtle: ["border-info-border bg-info-subtle-bg text-info-subtle-fg", "text-info"],
1279
+ outline: ["border-info-border bg-transparent text-info-subtle-fg", "text-info"],
1280
+ solid: ["border-info bg-info text-info-fg", "text-info-fg"]
1281
+ },
1282
+ success: {
1283
+ subtle: ["border-success-border bg-success-subtle-bg text-success-subtle-fg", "text-success"],
1284
+ outline: ["border-success-border bg-transparent text-success-subtle-fg", "text-success"],
1285
+ solid: ["border-success bg-success text-success-fg", "text-success-fg"]
1286
+ },
1287
+ warning: {
1288
+ subtle: ["border-warning-border bg-warning-subtle-bg text-warning-subtle-fg", "text-warning"],
1289
+ outline: ["border-warning-border bg-transparent text-warning-subtle-fg", "text-warning"],
1290
+ solid: ["border-warning bg-warning text-warning-fg", "text-warning-fg"]
1291
+ },
1292
+ danger: {
1293
+ subtle: ["border-danger-border bg-danger-subtle-bg text-danger-subtle-fg", "text-danger"],
1294
+ outline: ["border-danger-border bg-transparent text-danger-subtle-fg", "text-danger"],
1295
+ solid: ["border-danger bg-danger text-danger-fg", "text-danger-fg"]
1296
+ }
1297
+ };
1298
+ const sizeClasses$3 = {
1299
+ sm: {
1300
+ base: "h-5 px-1.5",
1301
+ withIcon: "ps-1"
1302
+ },
1303
+ md: {
1304
+ base: "h-6 px-2",
1305
+ withIcon: "ps-1.5"
1306
+ }
1307
+ };
1308
+ function Badge({ tone = "neutral", variant = "subtle", size = "md", icon, busy = false, context, className, children, ...props }) {
1309
+ const [toneClass, iconClass] = toneClasses$1[tone][variant];
1310
+ const hasIcon = busy || icon !== void 0 && icon !== null && icon !== false;
1311
+ return /* @__PURE__ */ jsxs("span", {
1312
+ ...props,
1313
+ "data-slot": "badge",
1314
+ 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),
1315
+ children: [hasIcon && /* @__PURE__ */ jsx("span", {
1316
+ "aria-hidden": "true",
1317
+ "data-slot": "badge-icon",
1318
+ className: cn("inline-flex shrink-0 items-center [&_svg]:size-3 [&_svg]:shrink-0", iconClass),
1319
+ children: busy ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(LoaderCircle, { className: "animate-spin motion-reduce:hidden" }), /* @__PURE__ */ jsx(CircleDashed, { className: "hidden motion-reduce:block" })] }) : icon
1320
+ }), /* @__PURE__ */ jsxs("span", {
1321
+ "data-slot": "badge-label",
1322
+ className: "min-w-0 truncate leading-4",
1323
+ children: [context && /* @__PURE__ */ jsxs("span", {
838
1324
  className: "sr-only",
839
- children: status
840
- })
841
- ]
1325
+ children: [context, ": "]
1326
+ }), children]
1327
+ })]
842
1328
  });
843
1329
  }
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
- ] })
1330
+ //#endregion
1331
+ //#region src/components/card/card.tsx
1332
+ const CardContext = createContext({
1333
+ titleId: "",
1334
+ setTitled: () => {}
1335
+ });
1336
+ /**
1337
+ * Bordered surface for a group of related content. No padding of its own: CardHeader and
1338
+ * CardContent add it, so full-bleed rows (lists, tables) can sit edge to edge.
1339
+ */
1340
+ function Card({ as: Comp = "div", className, ...props }) {
1341
+ const titleId = useId();
1342
+ const [titled, setTitled] = useState(false);
1343
+ return /* @__PURE__ */ jsx(CardContext, {
1344
+ value: {
1345
+ titleId,
1346
+ setTitled
1347
+ },
1348
+ children: /* @__PURE__ */ jsx(Comp, {
1349
+ "aria-labelledby": (Comp === "section" || Comp === "article") && titled && !props["aria-label"] ? titleId : void 0,
1350
+ ...props,
1351
+ "data-slot": "card",
1352
+ className: cn("rounded-xl border border-border-strong bg-surface", className)
1353
+ })
1354
+ });
1355
+ }
1356
+ function CardHeader({ title, description, action, headingLevel = 2, className, ...props }) {
1357
+ const { titleId, setTitled } = useContext(CardContext);
1358
+ useLayoutEffect(() => {
1359
+ setTitled(true);
1360
+ return () => setTitled(false);
1361
+ }, [setTitled]);
1362
+ const Heading = headingLevel === 2 ? "h2" : "h3";
1363
+ return /* @__PURE__ */ jsxs("div", {
1364
+ ...props,
1365
+ "data-slot": "card-header",
1366
+ className: cn("flex items-start justify-between gap-4 px-5 pt-5", className),
1367
+ children: [/* @__PURE__ */ jsxs("div", {
1368
+ className: "min-w-0",
1369
+ children: [/* @__PURE__ */ jsx(Heading, {
1370
+ id: titleId,
1371
+ className: "font-sans text-sm font-medium",
1372
+ children: title
1373
+ }), description && /* @__PURE__ */ jsx("p", {
1374
+ className: "text-xs text-fg-muted",
1375
+ children: description
1376
+ })]
1377
+ }), action && /* @__PURE__ */ jsx("div", {
1378
+ className: "shrink-0",
1379
+ children: action
1380
+ })]
1381
+ });
1382
+ }
1383
+ function CardContent({ className, ...props }) {
1384
+ return /* @__PURE__ */ jsx("div", {
1385
+ ...props,
1386
+ "data-slot": "card-content",
1387
+ className: cn("p-5 [[data-slot=card-header]+&]:pt-4", className)
906
1388
  });
907
1389
  }
908
1390
  //#endregion
@@ -960,7 +1442,7 @@ function ChoiceRow({ slot, label, description, error, labelPosition = "end", gap
960
1442
  return /* @__PURE__ */ jsx("label", {
961
1443
  "data-slot": slot,
962
1444
  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] })
1445
+ children: labelPosition === "start" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [text, controlSlot] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [controlSlot, text] })
964
1446
  });
965
1447
  }
966
1448
  //#endregion
@@ -1034,52 +1516,561 @@ function Checkbox({ label, description, error, indeterminate = false, className,
1034
1516
  ]
1035
1517
  })
1036
1518
  });
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, {
1519
+ }
1520
+ //#endregion
1521
+ //#region src/components/checkbox/checkbox-group.tsx
1522
+ /** A `<fieldset>` + `<legend>` around related checkboxes; holds no value of its own. */
1523
+ function CheckboxGroup({ legend, description, error, orientation = "vertical", className, "aria-describedby": describedByProp, children, ...props }) {
1524
+ const id = useId();
1525
+ const descriptionId = `${id}-description`;
1526
+ const errorId = `${id}-error`;
1527
+ const hasDescription = isPresent(description);
1528
+ const hasError = Boolean(error) && error !== true && isPresent(error);
1529
+ const describedBy = [
1530
+ describedByProp,
1531
+ hasDescription && descriptionId,
1532
+ hasError && errorId
1533
+ ].filter(Boolean).join(" ") || void 0;
1534
+ return /* @__PURE__ */ jsxs("fieldset", {
1535
+ ...props,
1536
+ "data-slot": "checkbox-group",
1537
+ "aria-describedby": describedBy,
1538
+ className: cn("group/checkbox-group m-0 min-w-0 border-0 p-0", className),
1539
+ children: [/* @__PURE__ */ jsx("legend", {
1540
+ className: "float-start mb-1.5 w-full p-0 text-sm font-medium text-fg group-disabled/checkbox-group:opacity-50",
1541
+ children: legend
1542
+ }), /* @__PURE__ */ jsxs("div", {
1543
+ className: "clear-both flex flex-col",
1544
+ children: [
1545
+ hasDescription && /* @__PURE__ */ jsx("p", {
1546
+ id: descriptionId,
1547
+ className: "m-0 -mt-1 mb-2.5 text-xs/snug text-fg-muted group-disabled/checkbox-group:opacity-50",
1548
+ children: description
1549
+ }),
1550
+ /* @__PURE__ */ jsx("div", {
1551
+ "data-orientation": orientation,
1552
+ className: cn("flex", orientation === "vertical" ? "flex-col gap-2" : "flex-row flex-wrap gap-x-6 gap-y-2"),
1553
+ children
1554
+ }),
1555
+ hasError && /* @__PURE__ */ jsxs("p", {
1556
+ id: errorId,
1557
+ className: "m-0 mt-2 flex items-start gap-1 text-xs/snug text-danger",
1558
+ children: [/* @__PURE__ */ jsx(CircleAlert, {
1559
+ "aria-hidden": true,
1560
+ className: "mt-px size-3.5 shrink-0"
1561
+ }), /* @__PURE__ */ jsx("span", { children: error })]
1562
+ })
1563
+ ]
1564
+ })]
1565
+ });
1566
+ }
1567
+ //#endregion
1568
+ //#region src/components/combobox/types.ts
1569
+ const isGroup$1 = (item) => "options" in item;
1570
+ const normalize = (text) => text.trim().normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLocaleLowerCase();
1571
+ const defaultFilter = (option, query) => normalize([option.label, ...option.keywords ?? []].join(" ")).includes(normalize(query));
1572
+ //#endregion
1573
+ //#region src/components/combobox/use-combobox-position.ts
1574
+ const clamp$1 = (n, min, max) => Math.max(min, Math.min(n, Math.max(min, max)));
1575
+ /** Top-layer positioning only: unlike Menu, a combobox never transfers focus into the list. */
1576
+ function useComboboxPosition(open, inputRef, contentRef) {
1577
+ useLayoutEffect(() => {
1578
+ const content = contentRef.current;
1579
+ const anchor = inputRef.current?.closest("[data-slot=\"input-control\"]");
1580
+ if (!open || !content || !anchor) return;
1581
+ content.showPopover?.();
1582
+ const update = () => {
1583
+ const rect = anchor.getBoundingClientRect();
1584
+ const viewport = window.visualViewport;
1585
+ const left = viewport?.offsetLeft ?? 0;
1586
+ const top = viewport?.offsetTop ?? 0;
1587
+ const width = viewport?.width ?? document.documentElement.clientWidth;
1588
+ const height = viewport?.height ?? document.documentElement.clientHeight;
1589
+ content.style.width = `${Math.min(rect.width, Math.max(0, width - 16))}px`;
1590
+ const below = top + height - rect.bottom - 12;
1591
+ const above = rect.top - top - 12;
1592
+ const side = below < Math.min(content.scrollHeight, 280) && above > below ? "top" : "bottom";
1593
+ content.style.maxHeight = `${Math.max(0, Math.min(280, side === "top" ? above : below))}px`;
1594
+ const w = content.offsetWidth;
1595
+ const h = content.offsetHeight;
1596
+ const rtl = getComputedStyle(anchor).direction === "rtl";
1597
+ content.style.left = `${Math.round(clamp$1(rtl ? rect.right - w : rect.left, left + 8, left + width - w - 8))}px`;
1598
+ content.style.top = `${Math.round(clamp$1(side === "top" ? rect.top - h - 4 : rect.bottom + 4, top + 8, top + height - h - 8))}px`;
1599
+ content.dataset.side = side;
1600
+ };
1601
+ update();
1602
+ const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(update);
1603
+ observer?.observe(anchor);
1604
+ observer?.observe(content);
1605
+ const mutation = new MutationObserver(update);
1606
+ mutation.observe(content, {
1607
+ childList: true,
1608
+ subtree: true,
1609
+ characterData: true
1610
+ });
1611
+ window.addEventListener("resize", update);
1612
+ window.addEventListener("scroll", update, true);
1613
+ window.visualViewport?.addEventListener("resize", update);
1614
+ window.visualViewport?.addEventListener("scroll", update);
1615
+ return () => {
1616
+ observer?.disconnect();
1617
+ mutation.disconnect();
1618
+ window.removeEventListener("resize", update);
1619
+ window.removeEventListener("scroll", update, true);
1620
+ window.visualViewport?.removeEventListener("resize", update);
1621
+ window.visualViewport?.removeEventListener("scroll", update);
1622
+ content.hidePopover?.();
1623
+ };
1624
+ }, [
1625
+ open,
1626
+ inputRef,
1627
+ contentRef
1628
+ ]);
1629
+ }
1630
+ //#endregion
1631
+ //#region src/components/combobox/combobox.tsx
1632
+ const resultsText = (count) => `${count} ${count === 1 ? "option" : "options"} available.`;
1633
+ function Combobox({ options, value, defaultValue = null, onValueChange, inputValue, onInputValueChange, open: openProp, defaultOpen = false, onOpenChange, filter = defaultFilter, selectedOption, loading = false, loadingText = "Loading…", searchError, onRetry, retryText = "Retry", emptyText = "No matching options.", clearable = true, openOnFocus = false, clearLabel = "Clear selection", openLabel = "Open options", closeLabel = "Close options", requiredText = "Choose an option from the list.", unresolvedText = "The selected option is unavailable.", getResultsText = resultsText, label, floating, helperText, error, size = "md", start, mono, inputClassName, contentClassName, className, name, form, disabled, readOnly, required, ref, id: idProp, onClick, onFocus, onBlur, onKeyDown, onCompositionStart, onCompositionEnd, onInvalid, ...props }) {
1634
+ const autoId = useId();
1635
+ const id = idProp ?? `${autoId}-input`;
1636
+ const listId = `${autoId}-list`;
1637
+ const rootRef = useRef(null);
1638
+ const inputRef = useRef(null);
1639
+ const contentRef = useRef(null);
1640
+ const hiddenRef = useRef(null);
1641
+ const composing = useRef(false);
1642
+ const skipFocusOpen = useRef(false);
1643
+ const [internalValue, setInternalValue] = useState(defaultValue);
1644
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
1645
+ const [draft, setDraft] = useState(null);
1646
+ const [dirty, setDirty] = useState(false);
1647
+ const [compositionText, setCompositionText] = useState(null);
1648
+ const [active, setActive] = useState(null);
1649
+ const [nativeInvalid, setNativeInvalid] = useState(false);
1650
+ const selectedValue = value === void 0 ? internalValue : value;
1651
+ const open = !disabled && !readOnly && (openProp ?? internalOpen);
1652
+ const flat = useMemo(() => {
1653
+ const items = options.flatMap((item) => isGroup$1(item) ? [...item.options] : [item]);
1654
+ const seen = /* @__PURE__ */ new Set();
1655
+ for (const item of items) {
1656
+ if (!item.value || seen.has(item.value)) throw new Error("Combobox option values must be unique, non-empty strings");
1657
+ seen.add(item.value);
1658
+ }
1659
+ return items;
1660
+ }, [options]);
1661
+ const resolved = flat.find((item) => item.value === selectedValue) ?? (selectedOption?.value === selectedValue ? selectedOption : null);
1662
+ const [cached, setCached] = useState(resolved);
1663
+ const selected = resolved ?? (cached?.value === selectedValue ? cached : null);
1664
+ const [initialOption, setInitialOption] = useState(resolved?.value === defaultValue ? resolved : null);
1665
+ if (resolved?.value === defaultValue && resolved !== initialOption) setInitialOption(resolved);
1666
+ const text = compositionText !== null ? draft ?? inputValue ?? selected?.label ?? "" : inputValue ?? draft ?? selected?.label ?? "";
1667
+ const query = dirty ? compositionText ?? text : "";
1668
+ const blocked = loading || Boolean(searchError);
1669
+ const [previousResults, setPreviousResults] = useState({
1670
+ options,
1671
+ blocked,
1672
+ filter
1673
+ });
1674
+ if (previousResults.options !== options || previousResults.blocked !== blocked || previousResults.filter !== filter) {
1675
+ setPreviousResults({
1676
+ options,
1677
+ blocked,
1678
+ filter
1679
+ });
1680
+ setActive(null);
1681
+ }
1682
+ const visible = flat.filter((item) => filter === false || !query || filter(item, query));
1683
+ const activeItem = !blocked && active?.options === options && active.query === query ? visible.find((item) => item.value === active.value && !item.disabled) : void 0;
1684
+ const optionId = (item) => `${autoId}-option-${encodeURIComponent(item.value)}`;
1685
+ const activeId = open && activeItem ? optionId(activeItem) : void 0;
1686
+ const invalidMessage = selectedValue !== null && !selected ? unresolvedText : required && selectedValue === null ? requiredText : "";
1687
+ const fieldError = error || (nativeInvalid && invalidMessage ? invalidMessage : void 0);
1688
+ const count = visible.filter((item) => !item.disabled).length;
1689
+ const status = open ? searchError ? typeof searchError === "string" ? searchError : "Unable to load options." : loading ? loadingText : getResultsText(count) : "";
1690
+ const [previousSelection, setPreviousSelection] = useState({
1691
+ value: selectedValue,
1692
+ label: selected?.label
1693
+ });
1694
+ if (resolved && resolved !== cached) setCached(resolved);
1695
+ if (previousSelection.value !== selectedValue || previousSelection.label !== selected?.label) {
1696
+ setPreviousSelection({
1697
+ value: selectedValue,
1698
+ label: selected?.label
1699
+ });
1700
+ setDraft(null);
1701
+ setDirty(false);
1702
+ setActive(null);
1703
+ }
1704
+ if (nativeInvalid && !invalidMessage) setNativeInvalid(false);
1705
+ useLayoutEffect(() => {
1706
+ inputRef.current?.setCustomValidity(disabled || readOnly ? "" : invalidMessage);
1707
+ }, [
1708
+ invalidMessage,
1709
+ disabled,
1710
+ readOnly
1711
+ ]);
1712
+ useComboboxPosition(open, inputRef, contentRef);
1713
+ useLayoutEffect(() => {
1714
+ if (activeId) document.getElementById(activeId)?.scrollIntoView?.({ block: "nearest" });
1715
+ }, [activeId]);
1716
+ const updateText = (next, reason, fallback = false) => {
1717
+ setDraft(fallback ? null : next);
1718
+ if (next !== text || reason === "input") onInputValueChange?.(next, { reason });
1719
+ };
1720
+ const changeOpen = (next, reason) => {
1721
+ if (openProp === void 0) setInternalOpen(next);
1722
+ if (next !== open) onOpenChange?.(next, { reason });
1723
+ };
1724
+ const close = (reason) => {
1725
+ changeOpen(false, reason);
1726
+ updateText(selected?.label ?? "", "revert", true);
1727
+ setDirty(false);
1728
+ setActive(null);
1729
+ };
1730
+ const show = (reason, direction) => {
1731
+ if (disabled || readOnly) return;
1732
+ changeOpen(true, reason);
1733
+ setDirty(false);
1734
+ const items = flat.filter((item) => !item.disabled);
1735
+ const initial = items.find((item) => item.value === selectedValue) ?? (direction === 1 ? items[0] : direction === -1 ? items.at(-1) : void 0);
1736
+ setActive(initial ? {
1737
+ value: initial.value,
1738
+ options,
1739
+ query: ""
1740
+ } : null);
1741
+ };
1742
+ const focusInput = () => {
1743
+ skipFocusOpen.current = true;
1744
+ inputRef.current?.focus({ preventScroll: true });
1745
+ skipFocusOpen.current = false;
1746
+ };
1747
+ const clear = (reason) => {
1748
+ if (value === void 0) setInternalValue(null);
1749
+ if (selectedValue !== null) onValueChange?.(null, {
1750
+ reason: "clear",
1751
+ option: null
1752
+ });
1753
+ updateText("", reason);
1754
+ setDirty(true);
1755
+ setActive(null);
1756
+ changeOpen(true, "clear");
1757
+ };
1758
+ const edit = (next) => {
1759
+ if (disabled || readOnly) return;
1760
+ if (next === "") clear("input");
1761
+ else {
1762
+ updateText(next, "input");
1763
+ setDirty(true);
1764
+ setActive(null);
1765
+ changeOpen(true, "input");
1766
+ }
1767
+ };
1768
+ const choose = (item) => {
1769
+ if (blocked || disabled || readOnly || item.disabled) return;
1770
+ setCached(item);
1771
+ if (value === void 0) setInternalValue(item.value);
1772
+ if (selectedValue !== item.value) onValueChange?.(item.value, {
1773
+ reason: "select",
1774
+ option: item
1775
+ });
1776
+ updateText(item.label, "selection", true);
1777
+ setDirty(false);
1778
+ setActive(null);
1779
+ changeOpen(false, "select");
1780
+ focusInput();
1781
+ };
1782
+ const dismiss = useEffectEvent(close);
1783
+ useLayoutEffect(() => {
1784
+ if (!open) return;
1785
+ const outside = (event) => {
1786
+ if (!event.composedPath().includes(rootRef.current)) dismiss("outside");
1787
+ };
1788
+ document.addEventListener("pointerdown", outside, true);
1789
+ return () => {
1790
+ document.removeEventListener("pointerdown", outside, true);
1791
+ };
1792
+ }, [open]);
1793
+ const wasOpen = useRef(open);
1794
+ const settle = useEffectEvent(() => {
1795
+ updateText(selected?.label ?? "", "revert", true);
1796
+ setDirty(false);
1797
+ setActive(null);
1798
+ });
1799
+ useLayoutEffect(() => {
1800
+ if (wasOpen.current && !open) settle();
1801
+ wasOpen.current = open;
1802
+ }, [open]);
1803
+ const reset = useEffectEvent(() => {
1804
+ const nextValue = value === void 0 ? defaultValue : value;
1805
+ const nextOption = flat.find((item) => item.value === nextValue) ?? (selectedOption?.value === nextValue ? selectedOption : null) ?? (cached?.value === nextValue ? cached : null) ?? (initialOption?.value === nextValue ? initialOption : null);
1806
+ if (nextOption) setCached(nextOption);
1807
+ const nextText = inputValue ?? nextOption?.label ?? "";
1808
+ if (value === void 0) setInternalValue(nextValue);
1809
+ if (openProp === void 0) setInternalOpen(false);
1810
+ updateText(nextText, "reset", true);
1811
+ setDirty(false);
1812
+ setCompositionText(null);
1813
+ composing.current = false;
1814
+ setActive(null);
1815
+ setNativeInvalid(false);
1816
+ if (inputRef.current) inputRef.current.value = nextText;
1817
+ if (hiddenRef.current) hiddenRef.current.value = nextValue ?? "";
1818
+ });
1819
+ useLayoutEffect(() => {
1820
+ const owner = inputRef.current?.form;
1821
+ if (!owner) return;
1822
+ let timer;
1823
+ const handleReset = (event) => {
1824
+ clearTimeout(timer);
1825
+ timer = setTimeout(() => {
1826
+ if (!event.defaultPrevented) reset();
1827
+ }, 0);
1828
+ };
1829
+ owner.addEventListener("reset", handleReset);
1830
+ return () => {
1831
+ clearTimeout(timer);
1832
+ owner.removeEventListener("reset", handleReset);
1833
+ };
1834
+ }, [form]);
1835
+ const renderOption = (item) => /* @__PURE__ */ jsxs("div", {
1836
+ id: optionId(item),
1837
+ role: "option",
1838
+ "aria-selected": item.value === selectedValue,
1839
+ "aria-disabled": item.disabled || void 0,
1840
+ "aria-labelledby": `${optionId(item)}-label`,
1841
+ "aria-describedby": item.description ? `${optionId(item)}-description` : void 0,
1842
+ "data-slot": "combobox-option",
1843
+ "data-active": activeItem?.value === item.value ? "" : void 0,
1844
+ className: cn("flex min-h-8.5 cursor-default items-center gap-2 rounded-lg px-2 py-1.5 text-sm/5 text-fg outline-none select-none pointer-coarse:min-h-11", "data-active:bg-popover-highlight data-active:outline-1 data-active:-outline-offset-1 data-active:outline-input-border", "aria-disabled:cursor-not-allowed aria-disabled:opacity-50 forced-colors:data-active:outline-[Highlight]"),
1845
+ onPointerMove: (event) => {
1846
+ if (event.pointerType !== "touch" && !item.disabled) setActive({
1847
+ value: item.value,
1848
+ options,
1849
+ query
1850
+ });
1851
+ },
1852
+ onMouseDown: (event) => event.preventDefault(),
1853
+ onClick: () => choose(item),
1854
+ children: [
1855
+ item.icon && /* @__PURE__ */ jsx("span", {
1856
+ "aria-hidden": true,
1857
+ className: "shrink-0 text-fg-muted [&_svg]:size-4",
1858
+ children: item.icon
1859
+ }),
1860
+ /* @__PURE__ */ jsxs("span", {
1861
+ className: "min-w-0 flex-1 wrap-anywhere",
1862
+ children: [/* @__PURE__ */ jsx("span", {
1863
+ id: `${optionId(item)}-label`,
1864
+ children: item.label
1865
+ }), item.description && /* @__PURE__ */ jsx("span", {
1866
+ id: `${optionId(item)}-description`,
1867
+ className: "mt-0.5 block text-xs/snug text-fg-muted",
1868
+ children: item.description
1869
+ })]
1870
+ }),
1871
+ /* @__PURE__ */ jsx(Check, {
1872
+ "aria-hidden": true,
1873
+ className: cn("size-4 shrink-0", item.value !== selectedValue && "invisible")
1874
+ })
1875
+ ]
1876
+ }, item.value);
1877
+ return /* @__PURE__ */ jsxs("div", {
1878
+ ref: rootRef,
1879
+ onBlur: (event) => {
1880
+ if ((open || dirty) && !rootRef.current?.contains(event.relatedTarget)) close("blur");
1881
+ },
1882
+ "data-slot": "combobox",
1883
+ "data-state": open ? "open" : "closed",
1884
+ className: cn("w-full", className),
1885
+ children: [
1886
+ /* @__PURE__ */ jsx(FieldFrame, {
1887
+ ...props,
1888
+ ref,
1889
+ inputRef,
1890
+ id,
1891
+ type: "text",
1892
+ form,
1893
+ label,
1894
+ floating,
1895
+ helperText,
1896
+ error: fieldError,
1897
+ required,
1898
+ disabled,
1899
+ readOnly,
1900
+ size,
1901
+ start,
1902
+ mono,
1903
+ value: text,
1904
+ role: "combobox",
1905
+ "aria-autocomplete": "list",
1906
+ "aria-expanded": open,
1907
+ "aria-controls": open ? listId : void 0,
1908
+ "aria-activedescendant": activeId,
1909
+ autoComplete: props.autoComplete ?? "off",
1910
+ status,
1911
+ inputClassName,
1912
+ className: "pointer-coarse:**:data-[slot=input-control]:min-h-11",
1913
+ onChange: (event) => {
1914
+ if (composing.current) setDraft(event.currentTarget.value);
1915
+ else edit(event.currentTarget.value);
1916
+ },
1917
+ onCompositionStart: (event) => {
1918
+ onCompositionStart?.(event);
1919
+ composing.current = true;
1920
+ setCompositionText(query);
1921
+ },
1922
+ onCompositionEnd: (event) => {
1923
+ onCompositionEnd?.(event);
1924
+ composing.current = false;
1925
+ setCompositionText(null);
1926
+ edit(event.currentTarget.value);
1927
+ },
1928
+ onClick: (event) => {
1929
+ onClick?.(event);
1930
+ if (!event.defaultPrevented && !open) show("trigger");
1931
+ },
1932
+ onFocus: (event) => {
1933
+ onFocus?.(event);
1934
+ if (!event.defaultPrevented && openOnFocus && !open && !skipFocusOpen.current) show("input");
1935
+ },
1936
+ onBlur,
1937
+ onInvalid: (event) => {
1938
+ setNativeInvalid(true);
1939
+ onInvalid?.(event);
1940
+ },
1941
+ onKeyDown: (event) => {
1942
+ onKeyDown?.(event);
1943
+ if (event.defaultPrevented || disabled || readOnly || composing.current || event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229) return;
1944
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1945
+ event.preventDefault();
1946
+ const direction = event.key === "ArrowDown" ? 1 : -1;
1947
+ if (!open) {
1948
+ show("keyboard", direction);
1949
+ return;
1950
+ }
1951
+ if (blocked) return;
1952
+ const enabled = visible.filter((item) => !item.disabled);
1953
+ const index = enabled.findIndex((item) => item.value === activeItem?.value);
1954
+ const next = index < 0 ? direction === 1 ? enabled[0] : enabled.at(-1) : enabled[index + direction];
1955
+ if (next) setActive({
1956
+ value: next.value,
1957
+ options,
1958
+ query
1959
+ });
1960
+ } else if (event.key === "Enter" && open) {
1961
+ event.preventDefault();
1962
+ if (activeItem) choose(activeItem);
1963
+ } else if (event.key === "Escape" && open) {
1964
+ event.preventDefault();
1965
+ event.stopPropagation();
1966
+ close("escape");
1967
+ } else if (event.key === "Tab" && !(searchError && onRetry)) close("blur");
1968
+ },
1969
+ actions: !readOnly && /* @__PURE__ */ jsxs(Fragment$1, { children: [clearable && (text !== "" || selectedValue !== null) && /* @__PURE__ */ jsx(Button, {
1970
+ type: "button",
1971
+ icon: /* @__PURE__ */ jsx(X, {}),
1972
+ "aria-label": clearLabel,
1973
+ intent: "ghost",
1974
+ size,
1975
+ disabled,
1976
+ className: cn(compactButton[size], "pointer-coarse:size-11 pointer-coarse:before:hidden"),
1977
+ onMouseDown: (event) => event.preventDefault(),
1978
+ onClick: () => {
1979
+ clear("clear");
1980
+ focusInput();
1981
+ }
1982
+ }), /* @__PURE__ */ jsx(Button, {
1983
+ type: "button",
1984
+ icon: /* @__PURE__ */ jsx(ChevronDown, {}),
1985
+ tabIndex: -1,
1986
+ "aria-label": open ? closeLabel : openLabel,
1987
+ "aria-expanded": open,
1988
+ "aria-controls": open ? listId : void 0,
1989
+ disabled,
1990
+ intent: "ghost",
1991
+ size,
1992
+ className: cn(compactButton[size], "pointer-coarse:size-11 pointer-coarse:before:hidden"),
1993
+ onMouseDown: (event) => event.preventDefault(),
1994
+ onClick: () => {
1995
+ focusInput();
1996
+ if (open) close("trigger");
1997
+ else show("trigger");
1998
+ }
1999
+ })] })
2000
+ }),
2001
+ name && /* @__PURE__ */ jsx("input", {
2002
+ ref: hiddenRef,
2003
+ type: "hidden",
2004
+ name,
2005
+ form,
2006
+ disabled,
2007
+ value: selectedValue ?? ""
2008
+ }),
2009
+ open && /* @__PURE__ */ jsxs("div", {
2010
+ ref: contentRef,
2011
+ popover: "manual",
2012
+ "data-slot": "combobox-content",
2013
+ className: cn("fixed inset-auto z-50 m-0 box-border max-h-70 overflow-y-auto rounded-xl border border-popover-border bg-popover-bg p-1 font-sans text-sm text-fg shadow-popover outline-none scrollbar-subtle", "transition-opacity duration-150 starting:opacity-0 motion-reduce:transition-none", contentClassName),
2014
+ onKeyDown: (event) => {
2015
+ if (event.key === "Escape") {
2016
+ event.preventDefault();
2017
+ event.stopPropagation();
2018
+ close("escape");
2019
+ focusInput();
2020
+ }
2021
+ },
2022
+ children: [/* @__PURE__ */ jsx("div", {
2023
+ id: listId,
2024
+ role: "listbox",
2025
+ "aria-label": label ?? props["aria-label"],
2026
+ "aria-busy": loading || void 0,
2027
+ children: !blocked && options.map((item, index) => {
2028
+ if (!isGroup$1(item)) return visible.includes(item) ? renderOption(item) : null;
2029
+ const members = item.options.filter((option) => visible.includes(option));
2030
+ if (!members.length) return null;
2031
+ return /* @__PURE__ */ jsxs("div", {
2032
+ role: "group",
2033
+ "aria-labelledby": `${autoId}-group-${index}`,
2034
+ "data-slot": "combobox-group",
2035
+ className: "not-first:mt-1 not-first:border-t not-first:border-popover-border not-first:pt-1",
2036
+ children: [/* @__PURE__ */ jsx("div", {
2037
+ id: `${autoId}-group-${index}`,
2038
+ className: "px-2 pt-1.5 pb-1 text-xs font-medium text-fg-muted",
2039
+ children: item.label
2040
+ }), members.map(renderOption)]
2041
+ }, `${item.label}-${index}`);
2042
+ })
2043
+ }), searchError ? /* @__PURE__ */ jsxs("div", {
2044
+ className: "flex items-start gap-2 p-3 text-xs text-danger",
2045
+ children: [
2046
+ /* @__PURE__ */ jsx(CircleAlert, {
2047
+ "aria-hidden": true,
2048
+ className: "mt-px size-3.5 shrink-0"
2049
+ }),
2050
+ /* @__PURE__ */ jsx("span", {
2051
+ className: "flex-1",
2052
+ children: searchError
2053
+ }),
2054
+ onRetry && /* @__PURE__ */ jsx(Button, {
2055
+ type: "button",
2056
+ size: "sm",
2057
+ intent: "secondary",
2058
+ onClick: onRetry,
2059
+ children: retryText
2060
+ })
2061
+ ]
2062
+ }) : loading ? /* @__PURE__ */ jsxs("div", {
2063
+ className: "flex items-center justify-center gap-2 p-4 text-sm text-fg-muted",
2064
+ children: [/* @__PURE__ */ jsx(LoaderCircle, {
1077
2065
  "aria-hidden": true,
1078
- className: "mt-px size-3.5 shrink-0"
1079
- }), /* @__PURE__ */ jsx("span", { children: error })]
1080
- })
1081
- ]
1082
- })]
2066
+ className: "size-4 animate-spin motion-reduce:[animation-duration:2.5s]"
2067
+ }), loadingText]
2068
+ }) : visible.length === 0 ? /* @__PURE__ */ jsx("div", {
2069
+ className: "p-4 text-center text-sm text-fg-muted",
2070
+ children: emptyText
2071
+ }) : null]
2072
+ })
2073
+ ]
1083
2074
  });
1084
2075
  }
1085
2076
  //#endregion
@@ -1257,7 +2248,7 @@ function ModalShell({ kind, open, onOpenChange, alert = false, dismissible = tru
1257
2248
  onMouseDown: handleMouseDown,
1258
2249
  onClick: handleClick,
1259
2250
  className,
1260
- children: rendered && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2251
+ children: rendered && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
1261
2252
  hidden: true,
1262
2253
  ref: showOnAttach
1263
2254
  }, closeCount), children] })
@@ -1493,6 +2484,326 @@ function DrawerClose({ size = "sm", icon, ...props }) {
1493
2484
  });
1494
2485
  }
1495
2486
  //#endregion
2487
+ //#region src/components/dropdown-menu/dropdown-menu.tsx
2488
+ const MenuContext = createContext(null);
2489
+ function useMenu() {
2490
+ const value = use(MenuContext);
2491
+ if (!value) throw new Error("DropdownMenu parts must be inside DropdownMenu");
2492
+ return value;
2493
+ }
2494
+ function DropdownMenu({ children, open: controlled, defaultOpen = false, onOpenChange }) {
2495
+ const [internal, setInternal] = useState(defaultOpen);
2496
+ const id = useId();
2497
+ const trigger = useRef(null);
2498
+ const content = useRef(null);
2499
+ const initialFocus = useRef("first");
2500
+ const reason = useRef("trigger");
2501
+ return /* @__PURE__ */ jsx(MenuContext, {
2502
+ value: {
2503
+ open: controlled ?? internal,
2504
+ triggerId: `${id}-trigger`,
2505
+ contentId: `${id}-menu`,
2506
+ trigger,
2507
+ content,
2508
+ initialFocus,
2509
+ reason,
2510
+ change(next, why, focus = "first") {
2511
+ if (next) initialFocus.current = focus;
2512
+ reason.current = why;
2513
+ if (controlled === void 0) setInternal(next);
2514
+ onOpenChange?.(next, why);
2515
+ }
2516
+ },
2517
+ children
2518
+ });
2519
+ }
2520
+ function DropdownMenuTrigger({ asChild = false, ref, onClick, onClickCapture, onKeyDown, disabled, ...props }) {
2521
+ const menu = useMenu();
2522
+ const mergedRef = useMergedRef(menu.trigger, ref);
2523
+ return /* @__PURE__ */ jsx(asChild ? Slot : "button", {
2524
+ ...props,
2525
+ ref: mergedRef,
2526
+ id: menu.triggerId,
2527
+ type: "button",
2528
+ "data-slot": "dropdown-menu-trigger",
2529
+ "data-state": menu.open ? "open" : "closed",
2530
+ disabled: asChild ? disabled : void 0,
2531
+ "aria-haspopup": "menu",
2532
+ "aria-expanded": menu.open,
2533
+ "aria-controls": menu.open ? menu.contentId : void 0,
2534
+ "aria-disabled": disabled || props["aria-disabled"] || void 0,
2535
+ onClickCapture: (event) => {
2536
+ if (disabled) {
2537
+ event.preventDefault();
2538
+ event.stopPropagation();
2539
+ return;
2540
+ }
2541
+ onClickCapture?.(event);
2542
+ },
2543
+ onClick: (event) => {
2544
+ onClick?.(event);
2545
+ if (disabled || event.defaultPrevented || event.currentTarget.getAttribute("aria-disabled") === "true") return;
2546
+ menu.change(!menu.open, "trigger");
2547
+ },
2548
+ onKeyDown: (event) => {
2549
+ onKeyDown?.(event);
2550
+ if (disabled || event.defaultPrevented || event.currentTarget.getAttribute("aria-disabled") === "true") return;
2551
+ if (["ArrowDown", "ArrowUp"].includes(event.key)) {
2552
+ event.preventDefault();
2553
+ menu.change(true, "trigger", event.key === "ArrowUp" ? "last" : "first");
2554
+ }
2555
+ }
2556
+ });
2557
+ }
2558
+ const opposite = {
2559
+ top: "bottom",
2560
+ bottom: "top",
2561
+ left: "right",
2562
+ right: "left"
2563
+ };
2564
+ const clamp = (value, low, high) => Math.max(low, Math.min(value, Math.max(low, high)));
2565
+ function items(content) {
2566
+ return Array.from(content.querySelectorAll("[role=\"menuitem\"]")).filter((item) => !item.hidden && item.getAttribute("aria-hidden") !== "true");
2567
+ }
2568
+ function DropdownMenuContent(props) {
2569
+ const { open } = useMenu();
2570
+ return open ? /* @__PURE__ */ jsx(OpenContent, { ...props }) : null;
2571
+ }
2572
+ function OpenContent({ side = "bottom", align = "end", sideOffset = 6, className, ref, onKeyDown, style, ...props }) {
2573
+ const menu = useMenu();
2574
+ const { content: contentRef, trigger: triggerRef, initialFocus, reason: closeReason } = menu;
2575
+ const mergedRef = useMergedRef(contentRef, ref);
2576
+ const query = useRef({
2577
+ text: "",
2578
+ time: 0
2579
+ });
2580
+ const dismiss = useEffectEvent((reason) => menu.change(false, reason));
2581
+ useLayoutEffect(() => {
2582
+ const content = contentRef.current;
2583
+ const trigger = triggerRef.current;
2584
+ if (!content || !trigger) return;
2585
+ content.showPopover?.();
2586
+ const entries = items(content);
2587
+ ((initialFocus.current === "last" ? entries.at(-1) : entries[0]) ?? content).focus({ preventScroll: true });
2588
+ const outside = (event) => {
2589
+ const path = event.composedPath();
2590
+ if (!path.includes(content) && !path.includes(trigger)) dismiss("outside");
2591
+ };
2592
+ const focusOutside = (event) => {
2593
+ if (closeReason.current === "tab") return;
2594
+ if (!content.contains(event.target) && !trigger.contains(event.target)) dismiss("outside");
2595
+ };
2596
+ const reasonRef = closeReason;
2597
+ document.addEventListener("pointerdown", outside, true);
2598
+ document.addEventListener("focusin", focusOutside);
2599
+ return () => {
2600
+ document.removeEventListener("pointerdown", outside, true);
2601
+ document.removeEventListener("focusin", focusOutside);
2602
+ const restore = content.contains(document.activeElement) && !["outside", "tab"].includes(reasonRef.current);
2603
+ content.hidePopover?.();
2604
+ if (restore && trigger.isConnected) trigger.focus({ preventScroll: true });
2605
+ };
2606
+ }, [
2607
+ contentRef,
2608
+ triggerRef,
2609
+ initialFocus,
2610
+ closeReason
2611
+ ]);
2612
+ useLayoutEffect(() => {
2613
+ const content = contentRef.current;
2614
+ const trigger = triggerRef.current;
2615
+ if (!content || !trigger) return;
2616
+ const update = () => {
2617
+ const rect = trigger.getBoundingClientRect();
2618
+ const viewport = window.visualViewport;
2619
+ const left = viewport?.offsetLeft ?? 0;
2620
+ const top = viewport?.offsetTop ?? 0;
2621
+ const width = viewport?.width ?? document.documentElement.clientWidth;
2622
+ const height = viewport?.height ?? document.documentElement.clientHeight;
2623
+ const gap = Math.max(0, sideOffset);
2624
+ content.style.maxWidth = `${Math.max(0, width - 16)}px`;
2625
+ content.style.maxHeight = `${Math.max(0, height - 16)}px`;
2626
+ const room = {
2627
+ top: rect.top - top,
2628
+ bottom: top + height - rect.bottom,
2629
+ left: rect.left - left,
2630
+ right: left + width - rect.right
2631
+ };
2632
+ const vertical = side === "top" || side === "bottom";
2633
+ const needed = (vertical ? content.scrollHeight : content.offsetWidth) + gap + 8;
2634
+ const actual = room[side] < needed && room[opposite[side]] > room[side] ? opposite[side] : side;
2635
+ if (vertical) content.style.maxHeight = `${Math.max(0, room[actual] - gap - 8)}px`;
2636
+ else content.style.maxWidth = `${Math.max(0, room[actual] - gap - 8)}px`;
2637
+ const w = content.offsetWidth;
2638
+ const h = content.offsetHeight;
2639
+ const rtl = getComputedStyle(trigger).direction === "rtl";
2640
+ const fraction = {
2641
+ start: 0,
2642
+ center: .5,
2643
+ end: 1
2644
+ }[align];
2645
+ const horizontalFraction = rtl ? 1 - fraction : fraction;
2646
+ const x = vertical ? rect.left + (rect.width - w) * horizontalFraction : actual === "left" ? rect.left - w - gap : rect.right + gap;
2647
+ const y = vertical ? actual === "top" ? rect.top - h - gap : rect.bottom + gap : rect.top + (rect.height - h) * fraction;
2648
+ content.style.left = `${Math.round(clamp(x, left + 8, left + width - w - 8))}px`;
2649
+ content.style.top = `${Math.round(clamp(y, top + 8, top + height - h - 8))}px`;
2650
+ content.dataset.side = actual;
2651
+ };
2652
+ update();
2653
+ const observer = new ResizeObserver(update);
2654
+ observer.observe(trigger);
2655
+ observer.observe(content);
2656
+ window.addEventListener("resize", update);
2657
+ window.addEventListener("scroll", update, true);
2658
+ window.visualViewport?.addEventListener("resize", update);
2659
+ window.visualViewport?.addEventListener("scroll", update);
2660
+ return () => {
2661
+ observer.disconnect();
2662
+ window.removeEventListener("resize", update);
2663
+ window.removeEventListener("scroll", update, true);
2664
+ window.visualViewport?.removeEventListener("resize", update);
2665
+ window.visualViewport?.removeEventListener("scroll", update);
2666
+ };
2667
+ }, [
2668
+ side,
2669
+ align,
2670
+ sideOffset,
2671
+ contentRef,
2672
+ triggerRef
2673
+ ]);
2674
+ return /* @__PURE__ */ jsx("div", {
2675
+ ...props,
2676
+ ref: mergedRef,
2677
+ id: menu.contentId,
2678
+ role: "menu",
2679
+ tabIndex: -1,
2680
+ "aria-labelledby": props["aria-label"] ? void 0 : menu.triggerId,
2681
+ popover: "manual",
2682
+ "data-slot": "dropdown-menu-content",
2683
+ "data-state": "open",
2684
+ style,
2685
+ className: cn("fixed inset-auto z-50 m-0 box-border w-max min-w-50 overflow-y-auto rounded-lg border border-popover-border bg-popover-bg p-1 font-sans text-sm text-fg shadow-popover outline-none", "transition-opacity duration-150 starting:opacity-0 motion-reduce:transition-none", className),
2686
+ onKeyDown: (event) => {
2687
+ onKeyDown?.(event);
2688
+ if (event.defaultPrevented) return;
2689
+ const entries = items(event.currentTarget);
2690
+ const index = entries.indexOf(document.activeElement);
2691
+ if (event.key === "Escape") {
2692
+ event.preventDefault();
2693
+ event.stopPropagation();
2694
+ menu.change(false, "escape");
2695
+ } else if (event.key === "Tab") {
2696
+ event.preventDefault();
2697
+ const trigger = menu.trigger.current;
2698
+ if (!trigger) return;
2699
+ const scope = trigger.closest("dialog[open]") ?? document.body;
2700
+ const tabbables = Array.from(scope.querySelectorAll("a[href], button, input, select, textarea, [tabindex]")).filter((el) => el.tabIndex >= 0 && !el.matches(":disabled") && !el.closest("[inert]") && !event.currentTarget.contains(el) && el.getClientRects().length > 0);
2701
+ const target = tabbables[tabbables.indexOf(trigger) + (event.shiftKey ? -1 : 1)];
2702
+ menu.change(false, "tab");
2703
+ (target ?? trigger).focus();
2704
+ } else if ([
2705
+ "ArrowDown",
2706
+ "ArrowUp",
2707
+ "Home",
2708
+ "End"
2709
+ ].includes(event.key)) {
2710
+ event.preventDefault();
2711
+ entries[event.key === "Home" ? 0 : event.key === "End" ? entries.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + entries.length) % entries.length]?.focus();
2712
+ } else if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey && event.key !== " ") {
2713
+ event.preventDefault();
2714
+ const now = Date.now();
2715
+ query.current.text = (now - query.current.time > 500 ? "" : query.current.text) + event.key.toLocaleLowerCase();
2716
+ query.current.time = now;
2717
+ const text = query.current.text;
2718
+ const term = Array.from(text).every((char) => char === text[0]) ? text[0] : text;
2719
+ [...entries.slice(index + 1), ...entries.slice(0, index + 1)].find((item) => (item.dataset.textValue ?? item.textContent ?? "").trim().toLocaleLowerCase().startsWith(term))?.focus();
2720
+ }
2721
+ }
2722
+ });
2723
+ }
2724
+ function DropdownMenuItem({ asChild = false, iconLeft, tone = "default", textValue, disabled, onSelect, onClick, onClickCapture, onAuxClickCapture, onKeyDown, onPointerMove, children, className, ...props }) {
2725
+ const menu = useMenu();
2726
+ const Component = asChild ? Slot : "button";
2727
+ const renderContent = (label) => /* @__PURE__ */ jsxs(Fragment$1, { children: [iconLeft && /* @__PURE__ */ jsx("span", {
2728
+ "aria-hidden": true,
2729
+ children: iconLeft
2730
+ }), label] });
2731
+ const child = asChild ? Children.only(children) : null;
2732
+ const content = isValidElement(child) ? cloneElement(child, { children: wrapChildren(child.props.children, renderContent) }) : renderContent(children);
2733
+ return /* @__PURE__ */ jsx(Component, {
2734
+ ...props,
2735
+ type: asChild ? void 0 : "button",
2736
+ role: "menuitem",
2737
+ tabIndex: -1,
2738
+ "aria-disabled": disabled || void 0,
2739
+ "data-slot": "dropdown-menu-item",
2740
+ "data-tone": tone,
2741
+ "data-text-value": textValue,
2742
+ className: cn("flex min-h-8 w-full cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm/snug outline-none select-none pointer-coarse:min-h-10", "focus:bg-popover-highlight focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus", "aria-disabled:cursor-not-allowed aria-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0", tone === "danger" ? "text-danger-subtle-fg focus:bg-danger-subtle-bg" : "text-fg [&_svg]:text-fg-muted", className),
2743
+ onClickCapture: (event) => {
2744
+ if (disabled) {
2745
+ event.preventDefault();
2746
+ event.stopPropagation();
2747
+ return;
2748
+ }
2749
+ onClickCapture?.(event);
2750
+ },
2751
+ onAuxClickCapture: (event) => {
2752
+ if (disabled) {
2753
+ event.preventDefault();
2754
+ event.stopPropagation();
2755
+ return;
2756
+ }
2757
+ onAuxClickCapture?.(event);
2758
+ },
2759
+ onPointerMove: (event) => {
2760
+ onPointerMove?.(event);
2761
+ if (!event.defaultPrevented && event.pointerType !== "touch") event.currentTarget.focus({ preventScroll: true });
2762
+ },
2763
+ onKeyDown: (event) => {
2764
+ onKeyDown?.(event);
2765
+ if (event.defaultPrevented) return;
2766
+ if (event.key === " " || event.key === "Enter") {
2767
+ event.preventDefault();
2768
+ event.currentTarget.click();
2769
+ }
2770
+ },
2771
+ onClick: (event) => {
2772
+ if (disabled) {
2773
+ event.preventDefault();
2774
+ return;
2775
+ }
2776
+ onClick?.(event);
2777
+ const selection = new Event("select", { cancelable: true });
2778
+ onSelect?.(selection);
2779
+ if (!selection.defaultPrevented) menu.change(false, "item-select");
2780
+ },
2781
+ children: content
2782
+ });
2783
+ }
2784
+ function DropdownMenuGroup({ className, ...props }) {
2785
+ return /* @__PURE__ */ jsx("div", {
2786
+ ...props,
2787
+ role: "group",
2788
+ "data-slot": "dropdown-menu-group",
2789
+ className
2790
+ });
2791
+ }
2792
+ function DropdownMenuLabel({ className, ...props }) {
2793
+ return /* @__PURE__ */ jsx("div", {
2794
+ ...props,
2795
+ "data-slot": "dropdown-menu-label",
2796
+ className: cn("px-2 py-1.5 text-xs text-fg-muted", className)
2797
+ });
2798
+ }
2799
+ function DropdownMenuSeparator({ className, ...props }) {
2800
+ return /* @__PURE__ */ jsx("hr", {
2801
+ ...props,
2802
+ "data-slot": "dropdown-menu-separator",
2803
+ className: cn("my-1 h-px border-0 bg-popover-border", className)
2804
+ });
2805
+ }
2806
+ //#endregion
1496
2807
  //#region src/components/input/copy-input.tsx
1497
2808
  const RESET_MS = 1500;
1498
2809
  function CopyInput({ value, mono = true, size = "md", onFocus, onClick, ...props }) {
@@ -1677,6 +2988,197 @@ function Meter({ label, summary, segments, className, ...props }) {
1677
2988
  });
1678
2989
  }
1679
2990
  //#endregion
2991
+ //#region src/components/radio-group/radio-group.tsx
2992
+ const RadioContext = createContext(null);
2993
+ function RadioGroup({ name, legend, description, error, value, defaultValue, onValueChange, variant = "default", orientation = "vertical", itemsClassName, children, className, disabled, required, form, ref, "aria-invalid": ariaInvalid, "aria-describedby": describedByProp, ...props }) {
2994
+ const id = useId();
2995
+ const descriptionId = `${id}-description`;
2996
+ const errorId = `${id}-error`;
2997
+ const legendId = `${id}-legend`;
2998
+ const invalid = Boolean(error) || ariaInvalid === true || ariaInvalid === "true";
2999
+ const hasDescription = isPresent(description);
3000
+ const hasError = Boolean(error) && error !== true && isPresent(error);
3001
+ const describedBy = [
3002
+ describedByProp,
3003
+ hasDescription && descriptionId,
3004
+ hasError && errorId
3005
+ ].filter(Boolean).join(" ") || void 0;
3006
+ const controlled = value !== void 0;
3007
+ const [internalValue, setInternalValue] = useState(defaultValue ?? null);
3008
+ const selected = controlled ? value : internalValue;
3009
+ const groupRef = useRef(null);
3010
+ const mergedRef = useMergedRef(groupRef, ref);
3011
+ const inputs = useRef(/* @__PURE__ */ new Set());
3012
+ const currentSelection = useRef(selected);
3013
+ useLayoutEffect(() => {
3014
+ currentSelection.current = selected;
3015
+ }, [selected]);
3016
+ const register = useCallback((node) => {
3017
+ const members = inputs.current;
3018
+ if ([...members].some((input) => input.value === node.value)) throw new Error(`Radio values must be unique within a RadioGroup: "${node.value}"`);
3019
+ members.add(node);
3020
+ const reconcile = () => {
3021
+ queueMicrotask(() => {
3022
+ if (controlled || !groupRef.current) return;
3023
+ setInternalValue((current) => [...members].some((input) => input.value === current) ? current : null);
3024
+ });
3025
+ };
3026
+ reconcile();
3027
+ return () => {
3028
+ members.delete(node);
3029
+ reconcile();
3030
+ };
3031
+ }, [controlled]);
3032
+ useLayoutEffect(() => {
3033
+ const owner = groupRef.current?.form;
3034
+ if (!owner) return;
3035
+ let timer;
3036
+ const reset = (event) => {
3037
+ clearTimeout(timer);
3038
+ timer = setTimeout(() => {
3039
+ if (event.defaultPrevented) return;
3040
+ const next = controlled ? currentSelection.current : defaultValue ?? null;
3041
+ const members = [...inputs.current];
3042
+ if (!controlled) setInternalValue(members.some((input) => input.value === next) ? next : null);
3043
+ for (const input of members) input.checked = input.value === next;
3044
+ }, 0);
3045
+ };
3046
+ owner.addEventListener("reset", reset);
3047
+ return () => {
3048
+ clearTimeout(timer);
3049
+ owner.removeEventListener("reset", reset);
3050
+ };
3051
+ }, [
3052
+ controlled,
3053
+ defaultValue,
3054
+ form
3055
+ ]);
3056
+ return /* @__PURE__ */ jsx(RadioContext, {
3057
+ value: {
3058
+ name,
3059
+ form,
3060
+ selected,
3061
+ disabled,
3062
+ required,
3063
+ invalid,
3064
+ describedBy,
3065
+ variant,
3066
+ orientation,
3067
+ register,
3068
+ select: (event) => {
3069
+ if (!event.currentTarget.checked) return;
3070
+ const next = event.currentTarget.value;
3071
+ if (!controlled) setInternalValue(next);
3072
+ onValueChange?.(next, event);
3073
+ }
3074
+ },
3075
+ children: /* @__PURE__ */ jsxs("fieldset", {
3076
+ ...props,
3077
+ ref: mergedRef,
3078
+ name,
3079
+ form,
3080
+ disabled,
3081
+ "data-slot": "radio-group",
3082
+ "data-variant": variant,
3083
+ role: "radiogroup",
3084
+ "aria-labelledby": props["aria-labelledby"] ?? legendId,
3085
+ "aria-describedby": describedBy,
3086
+ "aria-invalid": invalid ? true : ariaInvalid,
3087
+ "aria-required": required || void 0,
3088
+ "aria-orientation": orientation,
3089
+ className: cn("group/radio-group m-0 min-w-0 border-0 p-0", className),
3090
+ children: [/* @__PURE__ */ jsxs("legend", {
3091
+ id: legendId,
3092
+ className: "float-start mb-1.5 w-full p-0 text-sm font-medium text-fg group-disabled/radio-group:opacity-50",
3093
+ children: [legend, required && /* @__PURE__ */ jsx("span", {
3094
+ "aria-hidden": true,
3095
+ className: "text-danger",
3096
+ children: " *"
3097
+ })]
3098
+ }), /* @__PURE__ */ jsxs("div", {
3099
+ className: "clear-both flex flex-col",
3100
+ children: [
3101
+ hasDescription && /* @__PURE__ */ jsx("p", {
3102
+ id: descriptionId,
3103
+ className: "m-0 -mt-1 mb-2.5 text-xs/snug text-fg-muted group-disabled/radio-group:opacity-50",
3104
+ children: description
3105
+ }),
3106
+ /* @__PURE__ */ jsx("div", {
3107
+ "data-slot": "radio-group-items",
3108
+ "data-orientation": orientation,
3109
+ className: cn("flex gap-2", orientation === "vertical" ? "flex-col" : "flex-row flex-wrap gap-x-6", variant === "card" && orientation === "horizontal" && "gap-3", itemsClassName),
3110
+ children
3111
+ }),
3112
+ hasError && /* @__PURE__ */ jsxs("p", {
3113
+ id: errorId,
3114
+ className: "m-0 mt-2 flex items-start gap-1 text-xs/snug text-danger",
3115
+ children: [/* @__PURE__ */ jsx(CircleAlert, {
3116
+ "aria-hidden": true,
3117
+ className: "mt-px size-3.5 shrink-0"
3118
+ }), /* @__PURE__ */ jsx("span", { children: error })]
3119
+ })
3120
+ ]
3121
+ })]
3122
+ })
3123
+ });
3124
+ }
3125
+ function Radio({ value, label, description, disabled, className, inputClassName, ref, onChange, "aria-describedby": describedBy, ...props }) {
3126
+ const group = useContext(RadioContext);
3127
+ if (!group) throw new Error("Radio must be rendered inside <RadioGroup>");
3128
+ if (value === "") throw new Error("Radio value must be a non-empty string");
3129
+ const ownRef = useRef(null);
3130
+ const mergedRef = useMergedRef(ownRef, ref);
3131
+ const register = group.register;
3132
+ useLayoutEffect(() => {
3133
+ if (ownRef.current) return register(ownRef.current);
3134
+ }, [register, value]);
3135
+ const card = group.variant === "card";
3136
+ const invalid = group.invalid;
3137
+ return /* @__PURE__ */ jsx(ChoiceRow, {
3138
+ slot: "radio",
3139
+ label,
3140
+ description,
3141
+ gap: card ? "gap-3" : "gap-2",
3142
+ controlOffset: "mt-1",
3143
+ "aria-describedby": [describedBy, group.describedBy].filter(Boolean).join(" ") || void 0,
3144
+ className: cn("group/radio wrap-anywhere motion-reduce:**:transition-none", "pointer-coarse:min-h-11 pointer-coarse:py-2.5", card && [
3145
+ "w-full rounded-lg border bg-surface p-3 pointer-coarse:p-3",
3146
+ "transition-[border-color,background-color] duration-150 ease-out",
3147
+ "[&>span:last-child>span:first-child]:font-medium",
3148
+ "has-[input:checked]:bg-[color-mix(in_oklab,var(--color-control-checked)_4%,var(--color-surface))]",
3149
+ "has-[input:focus-visible]:outline-solid has-[input:focus-visible]:outline-2 has-[input:focus-visible]:outline-offset-2",
3150
+ invalid ? "border-danger has-[input:focus-visible]:outline-danger" : "border-input-border has-[input:checked]:border-control-checked has-[input:enabled]:hover:border-input-border-hover has-[input:focus-visible]:outline-control-checked",
3151
+ "forced-colors:has-[input:checked]:border-[Highlight] forced-colors:has-[input:focus-visible]:outline-[Highlight]",
3152
+ group.orientation === "horizontal" && "flex-[1_1_14rem]"
3153
+ ], className),
3154
+ control: (row) => /* @__PURE__ */ jsxs("span", {
3155
+ className: "relative inline-grid size-4 place-items-center transition-[scale] duration-100 ease-out has-[input:enabled:active]:scale-[0.92]",
3156
+ children: [/* @__PURE__ */ jsx("input", {
3157
+ ...props,
3158
+ ref: mergedRef,
3159
+ "data-slot": "radio-input",
3160
+ type: "radio",
3161
+ name: group.name,
3162
+ form: group.form,
3163
+ value,
3164
+ checked: group.selected === value,
3165
+ disabled: group.disabled || disabled,
3166
+ required: group.required,
3167
+ "aria-labelledby": props["aria-labelledby"] ?? row.labelledBy,
3168
+ "aria-describedby": row.describedBy,
3169
+ onChange: (event) => {
3170
+ onChange?.(event);
3171
+ group.select(event);
3172
+ },
3173
+ className: cn("peer col-start-1 row-start-1 m-0 size-4 cursor-pointer appearance-none rounded-full border outline-hidden disabled:cursor-not-allowed", "bg-input-bg checked:bg-control-checked", "transition-[border-color,background-color,box-shadow] duration-150 ease-out", invalid ? "border-danger [box-shadow:0_0_0_3px_var(--color-input-halo-invalid)]" : "border-input-border checked:border-control-checked group-hover/radio:enabled:not-checked:border-input-border-hover", !card && [invalid ? "focus-visible:[box-shadow:0_0_0_3px_var(--color-input-halo-invalid-focus)] not-forced-colors:focus-visible:outline-danger" : "focus-visible:border-input-focus focus-visible:[box-shadow:0_0_0_3px_var(--color-input-halo)] not-forced-colors:focus-visible:outline-control-checked", "checked:focus-visible:shadow-none checked:focus-visible:outline-solid checked:focus-visible:outline-2 checked:focus-visible:outline-offset-2"], card && "focus-visible:outline-none", "forced-colors:forced-color-adjust-none forced-colors: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]", !card && "forced-colors:focus-visible:outline-[Highlight]", inputClassName)
3174
+ }), /* @__PURE__ */ jsx("span", {
3175
+ "aria-hidden": true,
3176
+ className: "pointer-events-none col-start-1 row-start-1 size-1.5 scale-0 rounded-full bg-control-checked-fg transition-[scale] duration-150 ease-out peer-checked:scale-100 forced-colors:forced-color-adjust-none forced-colors:bg-[HighlightText] forced-colors:peer-disabled:bg-[Canvas]"
3177
+ })]
3178
+ })
3179
+ });
3180
+ }
3181
+ //#endregion
1680
3182
  //#region src/components/select/select.tsx
1681
3183
  const selectPadding = {
1682
3184
  sm: "px-3",
@@ -1761,7 +3263,7 @@ function Select({ options, placeholder, helperText, error, size = "md", start, m
1761
3263
  className,
1762
3264
  controlRef: selectRef,
1763
3265
  activate: openPicker,
1764
- children: (field) => /* @__PURE__ */ jsxs(Fragment, { children: [
3266
+ children: (field) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
1765
3267
  field.floatingLabel(labelRestingWidth[size]),
1766
3268
  /* @__PURE__ */ jsxs("select", {
1767
3269
  ...props,
@@ -1790,73 +3292,18 @@ function Select({ options, placeholder, helperText, error, size = "md", start, m
1790
3292
  }),
1791
3293
  hasLeading && /* @__PURE__ */ jsx("span", {
1792
3294
  "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]),
3295
+ className: cn("pointer-events-none absolute inset-y-0 inset-s-0 flex items-center text-fg-muted [&_svg]:shrink-0", iconSize$1[size], startSlotPadding[size]),
1794
3296
  children: leading
1795
3297
  }),
1796
3298
  /* @__PURE__ */ jsx("span", {
1797
3299
  "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]),
3300
+ className: cn("pointer-events-none absolute inset-y-0 inset-e-0 flex items-center text-fg-muted", iconSize$1[size], endSlotPadding[size]),
1799
3301
  children: /* @__PURE__ */ jsx(ChevronDown, { className: "transition-transform duration-150 ease-out group-has-[select:open]/control:rotate-180 motion-reduce:transition-none" })
1800
3302
  })
1801
3303
  ] })
1802
3304
  });
1803
3305
  }
1804
3306
  //#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
3307
  //#region src/components/tabs/shared.tsx
1861
3308
  const listBase = [
1862
3309
  "relative isolate max-w-full overflow-x-auto overscroll-x-contain scroll-px-6",
@@ -1965,7 +3412,7 @@ function TabContent({ icon, badge, children }) {
1965
3412
  children
1966
3413
  })]
1967
3414
  }),
1968
- badge != null && badge !== false && /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx("span", {
3415
+ badge != null && badge !== false && /* @__PURE__ */ jsxs(Fragment$1, { children: [" ", /* @__PURE__ */ jsx("span", {
1969
3416
  "data-slot": "tab-badge",
1970
3417
  className: "inline-flex",
1971
3418
  children: badge
@@ -2153,7 +3600,7 @@ function SidebarProvider({ children }) {
2153
3600
  function Sidebar({ className, children, ...props }) {
2154
3601
  const { open, setOpen, drawerId } = useSidebarContext("Sidebar");
2155
3602
  const label = props["aria-label"] ? { "aria-label": props["aria-label"] } : { "aria-labelledby": props["aria-labelledby"] };
2156
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(PanelContext, {
3603
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(PanelContext, {
2157
3604
  value: { drawer: false },
2158
3605
  children: /* @__PURE__ */ jsx("nav", {
2159
3606
  ...props,
@@ -2373,13 +3820,586 @@ function SidebarIcon({ className, ...props }) {
2373
3820
  }
2374
3821
  /** Count at the end of a SidebarItem, as a small neutral Badge; part of the name ("Projects 8"). */
2375
3822
  function SidebarBadge({ className, ...props }) {
2376
- return /* @__PURE__ */ jsxs(Fragment, { children: [" ", /* @__PURE__ */ jsx(Badge, {
3823
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [" ", /* @__PURE__ */ jsx(Badge, {
2377
3824
  size: "sm",
2378
3825
  ...props,
2379
3826
  className: cn("shrink-0", className)
2380
3827
  })] });
2381
3828
  }
2382
3829
  //#endregion
3830
+ //#region src/components/slider/scale.ts
3831
+ const fail = (message) => {
3832
+ throw new Error(`Slider: ${message}`);
3833
+ };
3834
+ const close = (value, integer) => Math.abs(value - integer) <= 1e-7;
3835
+ const decimals = (value) => {
3836
+ const [digits, exponent = "0"] = String(value).toLowerCase().split("e");
3837
+ return Math.max(0, (digits.split(".")[1]?.length ?? 0) - Number(exponent));
3838
+ };
3839
+ /** Work in integer ticks, not repeated floating point additions. */
3840
+ function createScale(min, max, step, gap) {
3841
+ if (![
3842
+ min,
3843
+ max,
3844
+ step,
3845
+ gap
3846
+ ].every(Number.isFinite)) fail("bounds, step and gap must be finite");
3847
+ if (min >= max || step <= 0) fail("min must be less than max and step must be positive");
3848
+ if (!Number.isInteger(gap) || gap < 0) fail("minStepsBetweenThumbs must be a non-negative integer");
3849
+ const factor = 10 ** Math.max(decimals(min), decimals(max), decimals(step));
3850
+ const start = Math.round(min * factor), end = Math.round(max * factor), increment = Math.round(step * factor);
3851
+ if (![
3852
+ start,
3853
+ end,
3854
+ increment,
3855
+ end - start
3856
+ ].every(Number.isSafeInteger) || increment < 1) fail("bounds and step exceed safe numeric precision");
3857
+ const count = (end - start) / increment;
3858
+ if (!Number.isSafeInteger(count)) fail("(max - min) must be evenly divisible by step");
3859
+ if (gap > count) fail("minimum thumb distance exceeds the available range");
3860
+ const fromTick = (tick) => (start + tick * increment) / factor;
3861
+ const toTick = (value) => (value * factor - start) / increment;
3862
+ const validate = (values, length) => {
3863
+ if (values.length !== length) fail(`expected ${length} value${length === 1 ? "" : "s"}`);
3864
+ const ticks = values.map((value) => {
3865
+ if (!Number.isFinite(value)) fail("values must be finite");
3866
+ const tick = toTick(value), rounded = Math.round(tick);
3867
+ if (!close(tick, rounded) || rounded < 0 || rounded > count) fail("values must be on a step within min and max");
3868
+ return rounded;
3869
+ });
3870
+ if (ticks.length === 2 && ticks[1] - ticks[0] < gap) fail("range values must be ordered and satisfy the minimum distance");
3871
+ return ticks.map(fromTick);
3872
+ };
3873
+ const normalize = (values) => {
3874
+ const ticks = values.map((value) => Math.max(0, Math.min(count, Math.round(toTick(value)))));
3875
+ if (ticks.length === 2) {
3876
+ ticks[0] = Math.min(ticks[0], count - gap);
3877
+ ticks[1] = Math.max(ticks[1], ticks[0] + gap);
3878
+ }
3879
+ return ticks.map(fromTick);
3880
+ };
3881
+ const limits = (values, index) => values.length === 1 ? [0, count] : index === 0 ? [0, Math.round(toTick(values[1])) - gap] : [Math.round(toTick(values[0])) + gap, count];
3882
+ const move = (values, index, tick) => {
3883
+ const [low, high] = limits(values, index);
3884
+ return values.map((value, i) => i === index ? fromTick(Math.max(low, Math.min(high, Math.round(tick)))) : value);
3885
+ };
3886
+ return {
3887
+ min,
3888
+ max,
3889
+ step,
3890
+ gap,
3891
+ count,
3892
+ fromTick,
3893
+ toTick,
3894
+ validate,
3895
+ normalize,
3896
+ limits,
3897
+ move,
3898
+ key: `${min}/${max}/${step}/${gap}`
3899
+ };
3900
+ }
3901
+ const sameValues = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
3902
+ //#endregion
3903
+ //#region src/components/slider/use-slider.ts
3904
+ const keys = [
3905
+ "ArrowRight",
3906
+ "ArrowLeft",
3907
+ "ArrowUp",
3908
+ "ArrowDown",
3909
+ "Home",
3910
+ "End",
3911
+ "PageUp",
3912
+ "PageDown"
3913
+ ];
3914
+ function useSlider(options) {
3915
+ const { scale, value, defaultValue, disabled, form } = options;
3916
+ const controlled = value !== void 0;
3917
+ const [internal, setInternal] = useState(() => [...defaultValue]);
3918
+ const values = controlled ? value : scale.normalize(internal);
3919
+ if (!controlled && !sameValues(internal, values)) setInternal([...values]);
3920
+ const [dragging, setDragging] = useState(false);
3921
+ const [activeIndex, setActiveIndex] = useState(0);
3922
+ const inputs = useRef([]);
3923
+ const controlRef = useRef(null);
3924
+ const railRef = useRef(null);
3925
+ const session = useRef(null);
3926
+ const lastIndex = useRef(0);
3927
+ const mounted = useRef(false);
3928
+ const generation = useRef(0);
3929
+ const latest = useRef({
3930
+ ...options,
3931
+ values,
3932
+ controlled
3933
+ });
3934
+ function release() {
3935
+ const current = session.current;
3936
+ session.current = null;
3937
+ if (!current) return null;
3938
+ current.removeListeners?.();
3939
+ if (current.pointerId !== void 0 && current.control?.hasPointerCapture?.(current.pointerId)) current.control.releasePointerCapture(current.pointerId);
3940
+ if (mounted.current) setDragging(false);
3941
+ return current;
3942
+ }
3943
+ function canUse(index) {
3944
+ return !latest.current.disabled && !!inputs.current[index] && !inputs.current[index].matches(":disabled");
3945
+ }
3946
+ function publish(next, details) {
3947
+ const state = latest.current;
3948
+ if (sameValues(state.values, next)) return;
3949
+ if (session.current) session.current.expected = next;
3950
+ if (!state.controlled) {
3951
+ latest.current = {
3952
+ ...state,
3953
+ values: next
3954
+ };
3955
+ setInternal(next);
3956
+ }
3957
+ state.onValueChange?.(next, details);
3958
+ }
3959
+ function commit(start, expected, details, key, defer = false) {
3960
+ const token = generation.current;
3961
+ const notify = () => {
3962
+ const state = latest.current;
3963
+ if (!mounted.current || token !== generation.current || state.scale.key !== key || !canUse(details.thumbIndex)) return;
3964
+ if (!sameValues(state.values, expected) || sameValues(start, state.values)) return;
3965
+ state.onValueCommit?.([...state.values], details);
3966
+ };
3967
+ if (defer) queueMicrotask(notify);
3968
+ else notify();
3969
+ }
3970
+ function finish(canceled = false) {
3971
+ const current = release();
3972
+ if (!current) return;
3973
+ if (canceled) {
3974
+ generation.current++;
3975
+ publish(current.start, {
3976
+ thumbIndex: current.index,
3977
+ reason: "cancel"
3978
+ });
3979
+ } else commit(current.start, current.expected, {
3980
+ thumbIndex: current.index,
3981
+ reason: current.reason
3982
+ }, current.scaleKey);
3983
+ }
3984
+ function begin(index, reason) {
3985
+ const state = latest.current;
3986
+ const current = {
3987
+ index,
3988
+ reason,
3989
+ start: [...state.values],
3990
+ expected: [...state.values],
3991
+ scaleKey: state.scale.key,
3992
+ keys: /* @__PURE__ */ new Set(),
3993
+ offset: 0,
3994
+ startX: 0,
3995
+ pending: false
3996
+ };
3997
+ session.current = current;
3998
+ return current;
3999
+ }
4000
+ function move(index, tick, reason) {
4001
+ if (!canUse(index)) {
4002
+ release();
4003
+ return;
4004
+ }
4005
+ const state = latest.current;
4006
+ publish(state.scale.move(state.values, index, tick), {
4007
+ thumbIndex: index,
4008
+ reason
4009
+ });
4010
+ }
4011
+ function direction() {
4012
+ const node = controlRef.current;
4013
+ return node ? getComputedStyle(node).direction || node.closest("[dir]")?.getAttribute("dir") || "ltr" : "ltr";
4014
+ }
4015
+ function tickAt(clientX) {
4016
+ const rect = railRef.current?.getBoundingClientRect();
4017
+ if (!rect || rect.width <= 0) return null;
4018
+ let ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
4019
+ if (direction() === "rtl") ratio = 1 - ratio;
4020
+ return ratio * latest.current.scale.count;
4021
+ }
4022
+ function focus(index) {
4023
+ lastIndex.current = index;
4024
+ setActiveIndex(index);
4025
+ inputs.current[index]?.focus({ preventScroll: true });
4026
+ }
4027
+ function onPointerDown(event) {
4028
+ if (event.defaultPrevented || event.button !== 0 || event.isPrimary === false || session.current?.reason === "pointer") return;
4029
+ if (!canUse(0)) return;
4030
+ const tick = tickAt(event.clientX);
4031
+ if (tick === null) return;
4032
+ if (session.current) finish();
4033
+ const state = latest.current;
4034
+ const distances = state.values.map((v) => Math.abs(state.scale.toTick(v) - tick));
4035
+ const index = distances.length === 1 ? 0 : distances[0] === distances[1] ? lastIndex.current : distances[0] < distances[1] ? 0 : 1;
4036
+ event.preventDefault();
4037
+ focus(index);
4038
+ const current = begin(index, "pointer");
4039
+ current.pointerId = event.pointerId;
4040
+ current.control = event.currentTarget;
4041
+ current.startX = event.clientX;
4042
+ const rect = railRef.current.getBoundingClientRect();
4043
+ const ratio = state.scale.toTick(state.values[index]) / state.scale.count;
4044
+ const center = rect.left + (direction() === "rtl" ? 1 - ratio : ratio) * rect.width;
4045
+ const grabbed = Math.abs(event.clientX - center) <= 22;
4046
+ current.offset = grabbed ? event.clientX - center : 0;
4047
+ current.pending = state.values.length === 2 && state.values[0] === state.values[1] && grabbed;
4048
+ setDragging(true);
4049
+ const owner = event.currentTarget.ownerDocument.defaultView;
4050
+ const pointerMove = (next) => {
4051
+ if (session.current !== current || next.pointerId !== current.pointerId) return;
4052
+ const target = tickAt(next.clientX - current.offset);
4053
+ if (target === null) return;
4054
+ if (current.pending) {
4055
+ if (Math.abs(next.clientX - current.startX) < 2) return;
4056
+ current.index = target < latest.current.scale.toTick(current.start[0]) ? 0 : 1;
4057
+ current.pending = false;
4058
+ focus(current.index);
4059
+ }
4060
+ move(current.index, target, "pointer");
4061
+ };
4062
+ const pointerEnd = (next) => {
4063
+ if (session.current === current && next.pointerId === current.pointerId) finish(next.type !== "pointerup");
4064
+ };
4065
+ owner.addEventListener("pointermove", pointerMove);
4066
+ owner.addEventListener("pointerup", pointerEnd);
4067
+ owner.addEventListener("pointercancel", pointerEnd);
4068
+ current.control.addEventListener("lostpointercapture", pointerEnd);
4069
+ current.removeListeners = () => {
4070
+ owner.removeEventListener("pointermove", pointerMove);
4071
+ owner.removeEventListener("pointerup", pointerEnd);
4072
+ owner.removeEventListener("pointercancel", pointerEnd);
4073
+ current.control?.removeEventListener("lostpointercapture", pointerEnd);
4074
+ };
4075
+ event.currentTarget.setPointerCapture?.(event.pointerId);
4076
+ if (!grabbed) move(index, tick, "pointer");
4077
+ }
4078
+ function onKeyDown(event, index) {
4079
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || !keys.includes(event.key) || !canUse(index)) return;
4080
+ event.preventDefault();
4081
+ if (session.current?.reason === "pointer") return;
4082
+ (session.current ?? begin(index, "keyboard")).keys.add(event.key);
4083
+ const state = latest.current;
4084
+ const right = direction() === "rtl" ? -1 : 1;
4085
+ const deltas = {
4086
+ ArrowRight: right,
4087
+ ArrowLeft: -right,
4088
+ ArrowUp: 1,
4089
+ ArrowDown: -1,
4090
+ PageUp: 10,
4091
+ PageDown: -10
4092
+ };
4093
+ const [low, high] = state.scale.limits(state.values, index);
4094
+ move(index, event.key === "Home" ? low : event.key === "End" ? high : state.scale.toTick(state.values[index]) + deltas[event.key], "keyboard");
4095
+ }
4096
+ function onKeyUp(event) {
4097
+ const current = session.current;
4098
+ if (current?.reason !== "keyboard" || !current.keys.has(event.key)) return;
4099
+ current.keys.delete(event.key);
4100
+ if (!current.keys.size) finish();
4101
+ }
4102
+ function onInput(value, index) {
4103
+ if (session.current || !canUse(index) || !Number.isFinite(value)) return;
4104
+ const state = latest.current, start = [...state.values];
4105
+ const next = state.scale.move(start, index, state.scale.toTick(value));
4106
+ publish(next, {
4107
+ thumbIndex: index,
4108
+ reason: "input"
4109
+ });
4110
+ commit(start, next, {
4111
+ thumbIndex: index,
4112
+ reason: "input"
4113
+ }, state.scale.key, true);
4114
+ }
4115
+ useLayoutEffect(() => {
4116
+ latest.current = {
4117
+ ...options,
4118
+ values,
4119
+ controlled
4120
+ };
4121
+ const current = session.current;
4122
+ if (current && (current.scaleKey !== scale.key || disabled || !sameValues(values, current.expected))) {
4123
+ generation.current++;
4124
+ release();
4125
+ }
4126
+ });
4127
+ const cancelRef = useRef(() => {});
4128
+ useLayoutEffect(() => {
4129
+ cancelRef.current = () => finish(true);
4130
+ });
4131
+ useLayoutEffect(() => {
4132
+ mounted.current = true;
4133
+ const owner = controlRef.current?.ownerDocument.defaultView;
4134
+ const blur = () => cancelRef.current();
4135
+ owner?.addEventListener("blur", blur);
4136
+ return () => {
4137
+ mounted.current = false;
4138
+ generation.current++;
4139
+ release();
4140
+ owner?.removeEventListener("blur", blur);
4141
+ };
4142
+ }, []);
4143
+ useLayoutEffect(() => {
4144
+ const owner = inputs.current[0]?.form;
4145
+ if (!owner) return;
4146
+ let timer;
4147
+ const reset = (event) => {
4148
+ clearTimeout(timer);
4149
+ timer = setTimeout(() => {
4150
+ if (event.defaultPrevented) return;
4151
+ generation.current++;
4152
+ release();
4153
+ const state = latest.current;
4154
+ const next = state.controlled ? state.values : state.defaultValue;
4155
+ if (!state.controlled) {
4156
+ latest.current = {
4157
+ ...state,
4158
+ values: next
4159
+ };
4160
+ setInternal([...next]);
4161
+ }
4162
+ inputs.current.forEach((input, index) => {
4163
+ if (!input) return;
4164
+ const [low, high] = state.scale.limits(next, index);
4165
+ input.min = String(state.scale.fromTick(low));
4166
+ input.max = String(state.scale.fromTick(high));
4167
+ input.value = String(next[index]);
4168
+ });
4169
+ }, 0);
4170
+ };
4171
+ owner.addEventListener("reset", reset);
4172
+ return () => {
4173
+ clearTimeout(timer);
4174
+ owner.removeEventListener("reset", reset);
4175
+ };
4176
+ }, [form]);
4177
+ return {
4178
+ values,
4179
+ dragging,
4180
+ activeIndex,
4181
+ inputs,
4182
+ controlRef,
4183
+ railRef,
4184
+ onPointerDown,
4185
+ onKeyDown,
4186
+ onKeyUp,
4187
+ onInput,
4188
+ onFocus: (index) => {
4189
+ lastIndex.current = index;
4190
+ setActiveIndex(index);
4191
+ },
4192
+ onBlur: () => {
4193
+ if (session.current?.reason === "keyboard") finish(!controlRef.current?.ownerDocument.hasFocus());
4194
+ }
4195
+ };
4196
+ }
4197
+ //#endregion
4198
+ //#region src/components/slider/slider.tsx
4199
+ function SliderControl({ values, defaults, names, thumbLabels, range = false, minStepsBetweenThumbs = 0, min = 0, max = 100, step = 1, disabled = false, form, label, helperText, error, showValue = true, formatValue = String, getValueText, marks, change, commit, className, ref, onPointerDown, onKeyDown, onKeyUp, onBlur, onFocus, "aria-label": ariaLabel, "aria-describedby": describedBy, "aria-invalid": ariaInvalid, ...props }) {
4200
+ const id = useId();
4201
+ const scale = createScale(min, max, step, minStepsBetweenThumbs);
4202
+ const length = range ? 2 : 1;
4203
+ const initial = scale.validate(defaults ?? (range ? [min, max] : [min]), length);
4204
+ const selected = values === void 0 ? void 0 : scale.validate(values, length);
4205
+ const hasLabel = isPresent(label);
4206
+ if (!hasLabel && !ariaLabel?.trim()) throw new Error("Slider requires a label or aria-label");
4207
+ if (range && (!thumbLabels || thumbLabels.length !== 2 || thumbLabels.some((text) => !text.trim()))) throw new Error("RangeSlider requires two non-empty thumbLabels");
4208
+ if (names && (names.length !== length || names.some((name) => !name.trim()))) throw new Error("Slider requires one non-empty name per thumb");
4209
+ if (marks) {
4210
+ const seen = /* @__PURE__ */ new Set();
4211
+ for (const mark of marks) {
4212
+ const normalized = scale.validate([mark.value], 1)[0];
4213
+ if (seen.has(normalized)) throw new Error("Slider mark values must be unique");
4214
+ seen.add(normalized);
4215
+ }
4216
+ }
4217
+ const { inputs, controlRef, railRef, ...slider } = useSlider({
4218
+ scale,
4219
+ value: selected,
4220
+ defaultValue: initial,
4221
+ disabled,
4222
+ form,
4223
+ onValueChange: change,
4224
+ onValueCommit: commit
4225
+ });
4226
+ const mergedRef = useMergedRef(useRef(null), ref);
4227
+ const invalid = Boolean(error) || ariaInvalid === true || ariaInvalid === "true";
4228
+ const message = error && error !== true ? error : helperText;
4229
+ const description = [describedBy, isPresent(message) && `${id}-description`].filter(Boolean).join(" ") || void 0;
4230
+ const percentages = slider.values.map((value) => scale.toTick(value) / scale.count * 100);
4231
+ const start = range ? percentages[0] : 0;
4232
+ const end = percentages.at(-1);
4233
+ return /* @__PURE__ */ jsxs("div", {
4234
+ ...props,
4235
+ ref: mergedRef,
4236
+ role: "group",
4237
+ "aria-label": hasLabel ? void 0 : ariaLabel,
4238
+ "aria-labelledby": hasLabel ? `${id}-label` : void 0,
4239
+ "data-slot": "slider",
4240
+ "data-range": range || void 0,
4241
+ "data-disabled": disabled || void 0,
4242
+ "data-invalid": invalid || void 0,
4243
+ "data-dragging": slider.dragging || void 0,
4244
+ className: cn("min-w-0 has-[input:disabled]:opacity-50 forced-colors:has-[input:disabled]:opacity-100", className),
4245
+ onPointerDown: (event) => {
4246
+ onPointerDown?.(event);
4247
+ if (!event.defaultPrevented && controlRef.current?.contains(event.target)) slider.onPointerDown(event);
4248
+ },
4249
+ onKeyDown: (event) => {
4250
+ onKeyDown?.(event);
4251
+ const index = inputs.current.indexOf(event.target);
4252
+ if (!event.defaultPrevented && index !== -1) slider.onKeyDown(event, index);
4253
+ },
4254
+ onKeyUp: (event) => {
4255
+ onKeyUp?.(event);
4256
+ if (inputs.current.includes(event.target)) slider.onKeyUp(event);
4257
+ },
4258
+ onBlur: (event) => {
4259
+ onBlur?.(event);
4260
+ if (inputs.current.includes(event.target)) slider.onBlur();
4261
+ },
4262
+ onFocus: (event) => {
4263
+ onFocus?.(event);
4264
+ const index = inputs.current.indexOf(event.target);
4265
+ if (index !== -1) slider.onFocus(index);
4266
+ },
4267
+ children: [
4268
+ (hasLabel || showValue) && /* @__PURE__ */ jsxs("div", {
4269
+ className: "mb-0.5 flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1",
4270
+ children: [hasLabel && /* @__PURE__ */ jsx("span", {
4271
+ id: `${id}-label`,
4272
+ className: "min-w-0 text-sm font-medium wrap-anywhere text-fg",
4273
+ children: label
4274
+ }), showValue && /* @__PURE__ */ jsx("span", {
4275
+ "data-slot": "slider-value",
4276
+ "aria-hidden": true,
4277
+ className: "ms-auto text-sm font-medium wrap-anywhere text-fg tabular-nums",
4278
+ children: /* @__PURE__ */ jsx("bdi", {
4279
+ dir: "ltr",
4280
+ children: slider.values.map(formatValue).join(" – ")
4281
+ })
4282
+ })]
4283
+ }),
4284
+ /* @__PURE__ */ jsx("div", {
4285
+ ref: controlRef,
4286
+ "data-slot": "slider-control",
4287
+ className: cn("group/slider-control relative h-11 touch-pan-y touch-pinch-zoom select-none", disabled ? "cursor-not-allowed" : "cursor-pointer"),
4288
+ children: /* @__PURE__ */ jsxs("div", {
4289
+ ref: railRef,
4290
+ className: "pointer-events-none absolute inset-x-5.5 top-0 h-full",
4291
+ children: [
4292
+ /* @__PURE__ */ jsx("div", {
4293
+ "aria-hidden": true,
4294
+ "data-slot": "slider-track",
4295
+ className: "absolute inset-x-0 top-5 h-1 rounded-full border border-input-border bg-surface-muted forced-colors:border-[CanvasText] forced-colors:bg-[Canvas]"
4296
+ }),
4297
+ /* @__PURE__ */ jsx("div", {
4298
+ "aria-hidden": true,
4299
+ "data-slot": "slider-range",
4300
+ className: cn("absolute top-5 h-1 rounded-full forced-colors:forced-color-adjust-none forced-colors:bg-[Highlight]", invalid ? "bg-danger" : "bg-control-checked", disabled && "forced-colors:bg-[GrayText]"),
4301
+ style: {
4302
+ insetInlineStart: `${start}%`,
4303
+ width: `${end - start}%`
4304
+ }
4305
+ }),
4306
+ slider.values.map((value, i) => {
4307
+ const index = i;
4308
+ const [low, high] = scale.limits(slider.values, index);
4309
+ return /* @__PURE__ */ jsxs(Fragment, { children: [range && /* @__PURE__ */ jsx("span", {
4310
+ id: `${id}-thumb-label-${index}`,
4311
+ className: "sr-only",
4312
+ children: thumbLabels[index]
4313
+ }), /* @__PURE__ */ jsxs("span", {
4314
+ className: "absolute top-0 size-11 -translate-x-1/2 rtl:translate-x-1/2",
4315
+ style: {
4316
+ insetInlineStart: `${percentages[index]}%`,
4317
+ zIndex: slider.activeIndex === index ? 2 : 1
4318
+ },
4319
+ children: [/* @__PURE__ */ jsx("input", {
4320
+ ref: (node) => {
4321
+ inputs.current[index] = node;
4322
+ },
4323
+ type: "range",
4324
+ "data-slot": "slider-input",
4325
+ name: names?.[index],
4326
+ form,
4327
+ min: scale.fromTick(low),
4328
+ max: scale.fromTick(high),
4329
+ step,
4330
+ value,
4331
+ disabled,
4332
+ "aria-label": hasLabel ? void 0 : [ariaLabel, thumbLabels?.[index]].filter(Boolean).join(" "),
4333
+ "aria-labelledby": hasLabel ? [`${id}-label`, range && `${id}-thumb-label-${index}`].filter(Boolean).join(" ") : void 0,
4334
+ "aria-describedby": description,
4335
+ "aria-invalid": invalid ? true : ariaInvalid,
4336
+ "aria-valuetext": getValueText?.(value, index) ?? formatValue(value),
4337
+ className: "peer pointer-events-none absolute inset-0 m-0 size-full opacity-0",
4338
+ onChange: (event) => slider.onInput(event.currentTarget.valueAsNumber, index)
4339
+ }), /* @__PURE__ */ jsx("span", {
4340
+ "aria-hidden": true,
4341
+ "data-slot": "slider-thumb",
4342
+ "data-active": slider.dragging && slider.activeIndex === index || void 0,
4343
+ className: cn("pointer-events-none absolute inset-s-3.25 top-3.25 size-4.5 rounded-full border bg-surface-raised", "peer-focus-visible:outline-solid peer-focus-visible:outline-2 peer-focus-visible:outline-offset-3 peer-focus-visible:outline-focus", "transition-[border-color,box-shadow] duration-100 ease-out motion-reduce:transition-none", "data-active:[box-shadow:0_0_0_4px_var(--color-input-halo)]", invalid ? "border-danger" : "border-control-checked peer-enabled:group-hover/slider-control:border-input-border-hover", "forced-colors:forced-color-adjust-none forced-colors:border-[ButtonText] forced-colors:bg-[ButtonFace] forced-colors:shadow-none", "forced-colors:peer-focus-visible:outline-[Highlight] forced-colors:peer-disabled:border-[GrayText]")
4344
+ })]
4345
+ })] }, index);
4346
+ })
4347
+ ]
4348
+ })
4349
+ }),
4350
+ marks?.length ? /* @__PURE__ */ jsx("div", {
4351
+ "aria-hidden": true,
4352
+ "data-slot": "slider-marks",
4353
+ className: "relative mx-5.5 min-h-6 text-xs text-fg-muted tabular-nums",
4354
+ children: marks.map((mark) => {
4355
+ const fraction = scale.toTick(mark.value) / scale.count;
4356
+ return /* @__PURE__ */ jsxs("span", {
4357
+ className: "absolute top-0 w-0",
4358
+ style: { insetInlineStart: `${fraction * 100}%` },
4359
+ children: [/* @__PURE__ */ jsx("span", { className: "block h-1 w-px bg-input-border forced-colors:bg-[CanvasText]" }), /* @__PURE__ */ jsx("span", {
4360
+ className: cn("mt-0.5 inline-block whitespace-nowrap", fraction === 1 ? "-translate-x-full rtl:translate-x-full" : fraction !== 0 && "-translate-x-1/2 rtl:translate-x-1/2"),
4361
+ children: mark.label
4362
+ })]
4363
+ }, mark.value);
4364
+ })
4365
+ }) : /* @__PURE__ */ jsxs("div", {
4366
+ "aria-hidden": true,
4367
+ className: "mx-5.5 flex justify-between gap-2 text-xs text-fg-muted tabular-nums",
4368
+ children: [/* @__PURE__ */ jsx("span", { children: formatValue(min) }), /* @__PURE__ */ jsx("span", { children: formatValue(max) })]
4369
+ }),
4370
+ isPresent(message) && /* @__PURE__ */ jsxs("p", {
4371
+ id: `${id}-description`,
4372
+ className: cn("mt-2 mb-0 flex items-start gap-1 text-xs/relaxed", invalid ? "text-danger" : "text-fg-muted"),
4373
+ children: [invalid && /* @__PURE__ */ jsx(CircleAlert, {
4374
+ "aria-hidden": true,
4375
+ className: "mt-0.5 size-3.5 shrink-0"
4376
+ }), /* @__PURE__ */ jsx("span", { children: message })]
4377
+ })
4378
+ ]
4379
+ });
4380
+ }
4381
+ function Slider({ value, defaultValue, name, onValueChange, onValueCommit, ...props }) {
4382
+ return /* @__PURE__ */ jsx(SliderControl, {
4383
+ ...props,
4384
+ values: value === void 0 ? void 0 : [value],
4385
+ defaults: defaultValue === void 0 ? void 0 : [defaultValue],
4386
+ names: name === void 0 ? void 0 : [name],
4387
+ change: (next, details) => onValueChange?.(next[0], details),
4388
+ commit: (next, details) => onValueCommit?.(next[0], details)
4389
+ });
4390
+ }
4391
+ function RangeSlider({ value, defaultValue, name, onValueChange, onValueCommit, ...props }) {
4392
+ return /* @__PURE__ */ jsx(SliderControl, {
4393
+ ...props,
4394
+ range: true,
4395
+ values: value,
4396
+ defaults: defaultValue,
4397
+ names: name,
4398
+ change: (next, details) => onValueChange?.([next[0], next[1]], details),
4399
+ commit: (next, details) => onValueCommit?.([next[0], next[1]], details)
4400
+ });
4401
+ }
4402
+ //#endregion
2383
4403
  //#region src/components/switch/switch.tsx
2384
4404
  const sizeClasses = {
2385
4405
  sm: {
@@ -2432,13 +4452,240 @@ function Switch({ label, description, error, size = "md", loading = false, label
2432
4452
  ], "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
4453
  }), /* @__PURE__ */ jsx("span", {
2434
4454
  "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" })] })
4455
+ 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-switch-handle peer-checked:bg-switch-handle-checked 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),
4456
+ children: loading && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(LoaderCircle, { className: "animate-spin motion-reduce:hidden" }), /* @__PURE__ */ jsx(CircleDashed, { className: "hidden motion-reduce:block" })] })
2437
4457
  })]
2438
4458
  })
2439
4459
  });
2440
4460
  }
2441
4461
  //#endregion
4462
+ //#region src/components/table/table.tsx
4463
+ const labels = {
4464
+ search: "Search rows",
4465
+ clearSelection: "Clear selection",
4466
+ selectPage: "Select all rows on this page",
4467
+ selectRow: (id) => `Select row ${id}`,
4468
+ selectionCount: (count) => `${count} selected`,
4469
+ scrollRegion: "Table",
4470
+ pagination: "Pagination",
4471
+ previousPage: "Previous page",
4472
+ nextPage: "Next page",
4473
+ page: (page, count) => `${page} / ${count}`,
4474
+ range: (first, last, total) => total === 0 ? "0 rows" : `${first}–${last} of ${total}`,
4475
+ emptyState: "No rows yet",
4476
+ noResults: "No matching rows"
4477
+ };
4478
+ const alignClasses = {
4479
+ start: "text-start",
4480
+ center: "text-center",
4481
+ end: "text-end"
4482
+ };
4483
+ const sortIcons = {
4484
+ asc: ArrowUp,
4485
+ desc: ArrowDown
4486
+ };
4487
+ function Table({ data, columns, getRowId, title, emptyState, noResults, filtered = false, search, sorting, pagination, selection, className, tableClassName, labels: labelOverrides, ...props }) {
4488
+ const titleId = useId();
4489
+ const scrollRef = useRef(null);
4490
+ const searchRef = useRef(null);
4491
+ const emptyRef = useRef(null);
4492
+ const text = {
4493
+ ...labels,
4494
+ ...labelOverrides
4495
+ };
4496
+ const tableName = props["aria-labelledby"] ?? (title != null && !props["aria-label"] ? titleId : void 0);
4497
+ const showEmpty = data.length === 0 && !filtered && !search?.value.trim() && (!pagination || pagination.rowCount === 0);
4498
+ const showSearch = !showEmpty && search != null;
4499
+ const hasControls = showSearch || !!selection?.ids.size;
4500
+ const hasToolbar = title != null || hasControls;
4501
+ const rows = data.map((row) => ({
4502
+ row,
4503
+ id: getRowId(row),
4504
+ canSelect: selection?.canSelectRow?.(row) ?? true
4505
+ }));
4506
+ const selectableIds = selection ? rows.filter((row) => row.canSelect).map((row) => row.id) : [];
4507
+ const selectedOnPage = selectableIds.filter((id) => selection?.ids.has(id)).length;
4508
+ const allOnPageSelected = selectableIds.length > 0 && selectedOnPage === selectableIds.length;
4509
+ const updateSelection = (ids, checked) => {
4510
+ if (!selection) return;
4511
+ const next = new Set(selection.ids);
4512
+ for (const id of ids) if (checked) next.add(id);
4513
+ else next.delete(id);
4514
+ selection.onChange(next);
4515
+ };
4516
+ return /* @__PURE__ */ jsxs("div", {
4517
+ "data-slot": "table-root",
4518
+ className: cn("w-full min-w-0 bg-surface text-fg", className),
4519
+ children: [hasToolbar && /* @__PURE__ */ jsxs("div", {
4520
+ "data-slot": "table-toolbar",
4521
+ className: "flex flex-wrap items-center justify-between gap-3 border-b border-border px-4 py-3",
4522
+ children: [title != null && /* @__PURE__ */ jsx("span", {
4523
+ id: titleId,
4524
+ className: "text-sm font-semibold",
4525
+ children: title
4526
+ }), hasControls && /* @__PURE__ */ jsxs("div", {
4527
+ className: "ms-auto flex min-w-0 flex-wrap items-center justify-end gap-x-3 gap-y-2",
4528
+ children: [
4529
+ selection && /* @__PURE__ */ jsx("output", {
4530
+ className: "text-xs text-fg-muted",
4531
+ children: selection.ids.size > 0 && text.selectionCount(selection.ids.size)
4532
+ }),
4533
+ !!selection?.ids.size && /* @__PURE__ */ jsx(Button, {
4534
+ intent: "ghost",
4535
+ size: "sm",
4536
+ ripple: false,
4537
+ onClick: () => {
4538
+ selection.onChange(/* @__PURE__ */ new Set());
4539
+ (searchRef.current ?? scrollRef.current ?? emptyRef.current)?.focus();
4540
+ },
4541
+ children: text.clearSelection
4542
+ }),
4543
+ showSearch && /* @__PURE__ */ jsx(SearchInput, {
4544
+ ref: searchRef,
4545
+ "aria-label": text.search,
4546
+ placeholder: `${text.search}…`,
4547
+ size: "sm",
4548
+ className: "w-48 max-w-full",
4549
+ value: search.value,
4550
+ onChange: (event) => search.onChange(event.currentTarget.value)
4551
+ })
4552
+ ]
4553
+ })]
4554
+ }), showEmpty ? /* @__PURE__ */ jsx("div", {
4555
+ ref: emptyRef,
4556
+ "data-slot": "table-empty",
4557
+ tabIndex: -1,
4558
+ className: "grid min-h-48 place-items-center px-4 py-8 text-center text-sm text-fg-muted outline-hidden focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus",
4559
+ children: emptyState === void 0 ? text.emptyState : emptyState
4560
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("section", {
4561
+ ref: scrollRef,
4562
+ "data-slot": "table-scroll",
4563
+ "aria-labelledby": tableName,
4564
+ "aria-label": tableName ? void 0 : props["aria-label"] ?? text.scrollRegion,
4565
+ tabIndex: 0,
4566
+ className: "relative w-full overflow-x-auto outline-hidden focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus",
4567
+ children: /* @__PURE__ */ jsxs("table", {
4568
+ ...props,
4569
+ "aria-labelledby": tableName,
4570
+ "data-slot": "table",
4571
+ className: cn("w-full border-separate border-spacing-0 text-sm", tableClassName),
4572
+ children: [/* @__PURE__ */ jsx("thead", {
4573
+ "data-slot": "table-header",
4574
+ children: /* @__PURE__ */ jsxs("tr", { children: [selection && /* @__PURE__ */ jsx("th", {
4575
+ scope: "col",
4576
+ className: "w-12 border-b border-border px-3",
4577
+ children: /* @__PURE__ */ jsx(Checkbox, {
4578
+ "aria-label": text.selectPage,
4579
+ checked: allOnPageSelected,
4580
+ indeterminate: selectedOnPage > 0 && !allOnPageSelected,
4581
+ disabled: selectableIds.length === 0,
4582
+ onChange: (event) => updateSelection(selectableIds, event.currentTarget.checked),
4583
+ className: "mx-auto justify-center"
4584
+ })
4585
+ }), columns.map((column) => {
4586
+ const direction = sorting?.value?.id === column.id ? sorting.value.direction : void 0;
4587
+ const SortIcon = direction ? sortIcons[direction] : ArrowUpDown;
4588
+ const align = column.align ?? (column.numeric ? "end" : "start");
4589
+ return /* @__PURE__ */ jsx("th", {
4590
+ scope: "col",
4591
+ "aria-sort": direction ? direction === "asc" ? "ascending" : "descending" : void 0,
4592
+ className: cn("h-10 border-b border-border px-4 text-xs font-medium whitespace-nowrap text-fg-muted", alignClasses[align], column.headerClassName),
4593
+ children: sorting && column.sortable ? /* @__PURE__ */ jsxs("button", {
4594
+ type: "button",
4595
+ className: cn("inline-flex min-h-8 cursor-pointer items-center gap-1.5 rounded-sm hover:text-fg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus pointer-coarse:min-h-11", direction && "text-fg"),
4596
+ onClick: () => sorting.onChange(direction === "desc" ? null : {
4597
+ id: column.id,
4598
+ direction: direction === "asc" ? "desc" : "asc"
4599
+ }),
4600
+ children: [column.header, /* @__PURE__ */ jsx(SortIcon, {
4601
+ "aria-hidden": true,
4602
+ className: "size-3.5 shrink-0"
4603
+ })]
4604
+ }) : column.header
4605
+ }, column.id);
4606
+ })] })
4607
+ }), /* @__PURE__ */ jsxs("tbody", {
4608
+ "data-slot": "table-body",
4609
+ className: "[&>tr:not(:last-child)>*]:border-b [&>tr>*]:border-border",
4610
+ children: [rows.map(({ row, id, canSelect }) => {
4611
+ const selected = selection?.ids.has(id) ?? false;
4612
+ return /* @__PURE__ */ jsxs("tr", {
4613
+ "data-slot": "table-row",
4614
+ "data-selected": selected || void 0,
4615
+ className: "hover:bg-surface-muted data-selected:bg-surface-muted",
4616
+ children: [selection && /* @__PURE__ */ jsx("td", {
4617
+ className: "w-12 px-3",
4618
+ children: /* @__PURE__ */ jsx(Checkbox, {
4619
+ "aria-label": text.selectRow(id),
4620
+ checked: selected,
4621
+ disabled: !canSelect,
4622
+ onChange: (event) => updateSelection([id], event.currentTarget.checked),
4623
+ className: "mx-auto justify-center"
4624
+ })
4625
+ }), columns.map((column) => {
4626
+ const Cell = column.rowHeader ? "th" : "td";
4627
+ const align = column.align ?? (column.numeric ? "end" : "start");
4628
+ return /* @__PURE__ */ jsx(Cell, {
4629
+ scope: column.rowHeader ? "row" : void 0,
4630
+ className: cn("h-13 px-4 py-2 align-middle", alignClasses[align], column.numeric && "tabular-nums", column.rowHeader && "font-medium", column.className),
4631
+ children: column.cell(row)
4632
+ }, column.id);
4633
+ })]
4634
+ }, id);
4635
+ }), rows.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx("td", {
4636
+ colSpan: Math.max(1, columns.length + Number(Boolean(selection))),
4637
+ children: /* @__PURE__ */ jsx("div", {
4638
+ "data-slot": "table-no-results",
4639
+ className: "sticky inset-s-0 w-max max-w-[min(100%,18rem)] px-4 py-12 text-sm text-fg-muted",
4640
+ children: noResults === void 0 ? /* @__PURE__ */ jsx("output", { children: text.noResults }) : noResults
4641
+ })
4642
+ }) })]
4643
+ })]
4644
+ })
4645
+ }), pagination && /* @__PURE__ */ jsx(TablePagination, {
4646
+ ...pagination,
4647
+ visibleCount: rows.length,
4648
+ text
4649
+ })] })]
4650
+ });
4651
+ }
4652
+ function TablePagination({ pageIndex, pageSize, rowCount, visibleCount, onPageChange, text }) {
4653
+ const count = Math.ceil(rowCount / pageSize);
4654
+ const first = visibleCount ? pageIndex * pageSize + 1 : 0;
4655
+ const last = first ? first + visibleCount - 1 : 0;
4656
+ const canGoBack = pageIndex > 0;
4657
+ const canGoForward = pageIndex + 1 < count;
4658
+ return /* @__PURE__ */ jsxs("nav", {
4659
+ "data-slot": "table-pagination",
4660
+ "aria-label": text.pagination,
4661
+ className: "flex min-h-12 flex-wrap items-center justify-between gap-x-3 gap-y-1 border-t border-border px-4 py-1 text-xs text-fg-muted tabular-nums",
4662
+ children: [/* @__PURE__ */ jsx("output", { children: text.range(first, last, rowCount) }), /* @__PURE__ */ jsxs("span", {
4663
+ className: "flex items-center gap-2",
4664
+ children: [
4665
+ /* @__PURE__ */ jsx(Button, {
4666
+ icon: /* @__PURE__ */ jsx(ChevronLeft, { className: "rtl:rotate-180" }),
4667
+ "aria-label": text.previousPage,
4668
+ intent: "ghost",
4669
+ size: "sm",
4670
+ ripple: false,
4671
+ disabled: !canGoBack,
4672
+ onClick: () => onPageChange(pageIndex - 1)
4673
+ }),
4674
+ /* @__PURE__ */ jsx("span", { children: count === 0 ? "—" : text.page(pageIndex + 1, count) }),
4675
+ /* @__PURE__ */ jsx(Button, {
4676
+ icon: /* @__PURE__ */ jsx(ChevronRight, { className: "rtl:rotate-180" }),
4677
+ "aria-label": text.nextPage,
4678
+ intent: "ghost",
4679
+ size: "sm",
4680
+ ripple: false,
4681
+ disabled: !canGoForward,
4682
+ onClick: () => onPageChange(pageIndex + 1)
4683
+ })
4684
+ ]
4685
+ })]
4686
+ });
4687
+ }
4688
+ //#endregion
2442
4689
  //#region src/components/tabs/tab-nav.tsx
2443
4690
  const TabNavContext = createContext(null);
2444
4691
  /**
@@ -2645,6 +4892,155 @@ function TabPanel({ value, keepMounted = false, className, ...props }) {
2645
4892
  });
2646
4893
  }
2647
4894
  //#endregion
4895
+ //#region src/components/textarea/use-auto-resize.ts
4896
+ /** Measure the native control so wrapping, fonts and IME text keep their browser behavior. */
4897
+ function useAutoResize(ref, enabled, minRows, maxRows) {
4898
+ const resize = useCallback(() => {
4899
+ const node = ref.current;
4900
+ if (!enabled || !node || node.getClientRects().length === 0) return;
4901
+ const style = getComputedStyle(node);
4902
+ const lineHeight = Number.parseFloat(style.lineHeight);
4903
+ if (!Number.isFinite(lineHeight)) return;
4904
+ const padding = Number.parseFloat(style.paddingTop) + Number.parseFloat(style.paddingBottom);
4905
+ const border = Number.parseFloat(style.borderTopWidth) + Number.parseFloat(style.borderBottomWidth);
4906
+ const min = minRows * lineHeight + padding;
4907
+ const max = maxRows == null ? Infinity : maxRows * lineHeight + padding;
4908
+ const scrollTop = node.scrollTop;
4909
+ node.style.overflowY = "hidden";
4910
+ node.style.height = "0px";
4911
+ const contentHeight = node.scrollHeight;
4912
+ const height = Math.max(min, Math.min(max, contentHeight));
4913
+ node.style.height = `${height + (style.boxSizing === "border-box" ? border : -padding)}px`;
4914
+ node.style.overflowY = contentHeight > max ? "auto" : "hidden";
4915
+ node.scrollTop = scrollTop;
4916
+ }, [
4917
+ enabled,
4918
+ ref,
4919
+ minRows,
4920
+ maxRows
4921
+ ]);
4922
+ useLayoutEffect(resize);
4923
+ useLayoutEffect(() => {
4924
+ if (!enabled) return;
4925
+ const node = ref.current;
4926
+ if (!node) return;
4927
+ let width = node.getBoundingClientRect().width;
4928
+ const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(() => {
4929
+ const nextWidth = node.getBoundingClientRect().width;
4930
+ if (nextWidth !== width) {
4931
+ width = nextWidth;
4932
+ resize();
4933
+ }
4934
+ });
4935
+ observer?.observe(node);
4936
+ window.addEventListener("resize", resize);
4937
+ document.fonts?.addEventListener("loadingdone", resize);
4938
+ let active = true;
4939
+ document.fonts?.ready.then(() => {
4940
+ if (active) resize();
4941
+ });
4942
+ return () => {
4943
+ active = false;
4944
+ observer?.disconnect();
4945
+ window.removeEventListener("resize", resize);
4946
+ document.fonts?.removeEventListener("loadingdone", resize);
4947
+ node.style.removeProperty("height");
4948
+ node.style.removeProperty("overflow-y");
4949
+ };
4950
+ }, [
4951
+ enabled,
4952
+ ref,
4953
+ resize
4954
+ ]);
4955
+ return resize;
4956
+ }
4957
+ //#endregion
4958
+ //#region src/components/textarea/textarea.tsx
4959
+ const sizes = {
4960
+ sm: "px-3 py-2 text-sm/5",
4961
+ md: "px-3 py-2.5 text-sm/5",
4962
+ lg: "px-4 py-3 text-base/6"
4963
+ };
4964
+ function positiveRows(value, name) {
4965
+ if (!Number.isInteger(value) || value < 1) throw new RangeError(`Textarea ${name} must be a positive integer.`);
4966
+ return value;
4967
+ }
4968
+ const textLength = (value) => String(value ?? "").replace(/\r\n?/g, "\n").length;
4969
+ function Textarea({ label, floating = false, helperText, error, size = "md", autoResize = false, rows = 3, minRows = 3, maxRows, resize = "vertical", showCount = false, mono = false, className, textareaClassName, ref, id, required, disabled, readOnly, placeholder, value, defaultValue, maxLength, onChange, "aria-describedby": describedBy, "aria-invalid": ariaInvalid, ...props }) {
4970
+ const nativeRows = positiveRows(autoResize ? minRows : rows, autoResize ? "minRows" : "rows");
4971
+ if (autoResize && maxRows != null && positiveRows(maxRows, "maxRows") < nativeRows) throw new RangeError("Textarea maxRows must be greater than or equal to minRows.");
4972
+ const controlRef = useRef(null);
4973
+ const mergedRef = useMergedRef(controlRef, ref);
4974
+ const [uncontrolledLength, setUncontrolledLength] = useState(() => textLength(defaultValue));
4975
+ const length = value == null ? uncontrolledLength : textLength(value);
4976
+ const overLimit = maxLength != null && maxLength >= 0 && length > maxLength;
4977
+ const countId = `${useId()}-count`;
4978
+ const resizeToContent = useAutoResize(controlRef, autoResize, nativeRows, maxRows);
4979
+ useLayoutEffect(() => {
4980
+ const node = controlRef.current;
4981
+ const form = node?.form;
4982
+ if (!form || !node) return;
4983
+ let resetTimer;
4984
+ const reset = (event) => {
4985
+ clearTimeout(resetTimer);
4986
+ resetTimer = setTimeout(() => {
4987
+ if (event.defaultPrevented) return;
4988
+ setUncontrolledLength(node.value.length);
4989
+ resizeToContent();
4990
+ }, 0);
4991
+ };
4992
+ form.addEventListener("reset", reset);
4993
+ return () => {
4994
+ clearTimeout(resetTimer);
4995
+ form.removeEventListener("reset", reset);
4996
+ };
4997
+ }, [props.form, resizeToContent]);
4998
+ return /* @__PURE__ */ jsx(FieldShell, {
4999
+ multiline: true,
5000
+ label,
5001
+ floating,
5002
+ helperText,
5003
+ error: error || overLimit,
5004
+ size,
5005
+ id,
5006
+ required,
5007
+ disabled,
5008
+ readOnly,
5009
+ "aria-describedby": [describedBy, showCount ? countId : null].filter(Boolean).join(" ") || void 0,
5010
+ className,
5011
+ controlRef,
5012
+ footer: showCount ? /* @__PURE__ */ jsxs("span", {
5013
+ id: countId,
5014
+ className: cn("ms-auto shrink-0 text-xs/snug tabular-nums", overLimit ? "text-danger" : "text-fg-muted"),
5015
+ children: [length, maxLength != null && maxLength >= 0 ? ` / ${maxLength}` : ""]
5016
+ }) : void 0,
5017
+ children: (field) => /* @__PURE__ */ jsxs(Fragment$1, { children: [field.floatingLabel("max-w-[calc(100%-1rem)]"), /* @__PURE__ */ jsx("textarea", {
5018
+ ...props,
5019
+ ref: mergedRef,
5020
+ id: field.id,
5021
+ rows: nativeRows,
5022
+ value,
5023
+ defaultValue,
5024
+ maxLength,
5025
+ required,
5026
+ disabled,
5027
+ readOnly,
5028
+ placeholder: field.floating ? placeholder || " " : placeholder,
5029
+ "aria-invalid": field.invalid ? true : ariaInvalid,
5030
+ "aria-describedby": field.describedBy,
5031
+ spellCheck: mono ? false : props.spellCheck,
5032
+ autoCapitalize: mono ? "off" : props.autoCapitalize,
5033
+ autoCorrect: mono ? "off" : props.autoCorrect,
5034
+ onChange: (event) => {
5035
+ setUncontrolledLength(event.currentTarget.value.length);
5036
+ resizeToContent();
5037
+ onChange?.(event);
5038
+ },
5039
+ className: cn("block w-full min-w-0 flex-1 rounded-[inherit] border-0 bg-transparent text-fg outline-none", "placeholder:text-input-placeholder disabled:cursor-not-allowed pointer-coarse:text-base", sizes[size], autoResize || resize === "none" ? "resize-none" : "resize-y", mono && "font-mono", field.floating && "placeholder:opacity-0 focus:placeholder:opacity-100", textareaClassName)
5040
+ })] })
5041
+ });
5042
+ }
5043
+ //#endregion
2648
5044
  //#region src/components/theme-switcher/theme-switcher.tsx
2649
5045
  const systemThemeQuery = "(prefers-color-scheme: dark)";
2650
5046
  function subscribeSystemTheme(onChange) {
@@ -3113,7 +5509,7 @@ function Viewport({ id, position = "bottom-right", visibleToasts = 3, host }) {
3113
5509
  };
3114
5510
  const [y, x] = position.split("-");
3115
5511
  const directions = x === "center" ? [y] : [y, x];
3116
- return /* @__PURE__ */ jsxs(Fragment, { children: [
5512
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
3117
5513
  /* @__PURE__ */ jsx("section", {
3118
5514
  ref: viewportRef,
3119
5515
  popover: "manual",
@@ -3450,4 +5846,4 @@ function ToastCard({ record, layout, total, expanded, paused, frontHeight, ownHe
3450
5846
  });
3451
5847
  }
3452
5848
  //#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 };
5849
+ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, Badge, Button, Card, CardContent, CardHeader, Checkbox, CheckboxGroup, Combobox, CopyInput, Dialog, DialogBody, DialogFooter, DialogHeader, Drawer, DrawerBody, DrawerClose, DrawerFooter, DrawerHeader, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, Input, Meter, PasswordInput, Radio, RadioGroup, RangeSlider, SearchInput, Select, Sidebar, SidebarBadge, SidebarBody, SidebarFooter, SidebarGroup, SidebarHeader, SidebarIcon, SidebarItem, SidebarProvider, SidebarTrigger, Slider, Switch, Tab, TabList, TabNav, TabNavLink, TabPanel, Table, Tabs, Textarea, ThemeSwitcher, Toaster, Tooltip, cn, toast, useTheme };