gridsmith-ui 0.20.0 → 0.21.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,11 +1,9 @@
1
- import { Button, useAppShell, REACT_VERSION_PROBLEM, ControlSizeContext, useOverlayMark, useDirtyGuard, OverlayMarkCtx, OverlayCloseCtx, DiscardConfirm, Spinner, DrawerFooterCtx, useCompact, useElementWidth, Sheet, useCloseSettle, Drawer, useRememberedWidth, pxOf, ResizeEdge, defaultMaxWidth } from './chunk-6MXHGH3D.js';
2
- export { AppShell, Button, ButtonSizeContext, ControlSizeContext, Drawer, Sheet, SkipLink, Spinner, useAppShell, useUnsavedChanges } from './chunk-6MXHGH3D.js';
1
+ import { Button, useAppShell, REACT_VERSION_PROBLEM, ControlSizeContext, triggerType, useOverlayMark, useDirtyGuard, OverlayMarkScope, OverlayCloseCtx, DiscardConfirm, OVERLAY_ROOT_ATTR, Spinner, OverlayMarkCtx, DrawerFooterCtx, useCompact, useElementWidth, Sheet, useCloseSettle, Drawer, useRememberedWidth, pxOf, ResizeEdge, defaultMaxWidth } from './chunk-GGVASDXT.js';
2
+ export { AppShell, Button, ButtonSizeContext, ControlSizeContext, Drawer, Sheet, SkipLink, Spinner, useAppShell, useUnsavedChanges } from './chunk-GGVASDXT.js';
3
3
  import { Skeleton, TITLE_TEXT } from './chunk-NCIZQYXQ.js';
4
4
  export { Skeleton } from './chunk-NCIZQYXQ.js';
5
- import { useReportedChange, useOptionalFormContext, FormChangeCtx, FormCtx } from './chunk-XVB55M3N.js';
6
- export { useFormContext } from './chunk-XVB55M3N.js';
7
- import { FOCUSABLE, usePopupFocus, FIELD, ZOOM_KEYS, useOverlayLayer, useFocusTrap, useFocusBeforeOpen, rememberFocus, focusInitial, OVERLAY_CLOSE_ATTR, focusAfterRemoval, rovingTabStop, useGridKeyboard, useNonModalDialog, usePressMarks, useReturnFocus, useCloseOnFocusOut } from './chunk-JXANCXK6.js';
8
- export { useGridKeyboard } from './chunk-JXANCXK6.js';
5
+ import { FOCUSABLE, usePopupFocus, FIELD, ZOOM_KEYS, FormChangeCtx, useOverlayLayer, useFocusTrap, useFocusBeforeOpen, rememberFocus, focusInitial, OVERLAY_CLOSE_ATTR, useReportedChange, focusAfterRemoval, useOptionalFormContext, rovingTabStop, useGridKeyboard, FormCtx, useNonModalDialog, usePressMarks, useReturnFocus, useCloseOnFocusOut } from './chunk-RT7RYGC3.js';
6
+ export { useFormContext, useGridKeyboard } from './chunk-RT7RYGC3.js';
9
7
  import { Link } from './chunk-EQDR57AM.js';
10
8
  export { CodeBlock, Divider, Link } from './chunk-EQDR57AM.js';
11
9
  import { Badge } from './chunk-XYDIBW3J.js';
@@ -84,6 +82,7 @@ function DropdownMenu({ trigger, items, align = "end", className, container }) {
84
82
  /* @__PURE__ */ jsx(
85
83
  Slot,
86
84
  {
85
+ ...triggerType(trigger),
87
86
  ref: triggerRef,
88
87
  onClick: () => {
89
88
  setOpen(!open);
@@ -180,13 +179,14 @@ function BulkActionBar({ selectedCount, actions, onClearSelection, className })
180
179
  isMobile ? /* @__PURE__ */ jsx(
181
180
  DropdownMenu,
182
181
  {
183
- trigger: /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", iconLeft: /* @__PURE__ */ jsx(Icon, { name: "more-vertical", size: 14 }), children: "Actions" }),
182
+ trigger: /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", iconLeft: /* @__PURE__ */ jsx(Icon, { name: "more-vertical", size: 14 }), children: "Actions" }),
184
183
  items: dropdownItems,
185
184
  align: "start"
186
185
  }
187
186
  ) : /* @__PURE__ */ jsx("div", { className: "flex items-center gap-ds-2", children: actions.map((action, i) => /* @__PURE__ */ jsx(
188
187
  Button,
189
188
  {
189
+ type: "button",
190
190
  variant: "secondary",
191
191
  size: "sm",
192
192
  onClick: action.onClick,
@@ -208,6 +208,7 @@ function BulkActionBar({ selectedCount, actions, onClearSelection, className })
208
208
  }
209
209
  );
210
210
  }
211
+ var UNCONTROLLED = /* @__PURE__ */ Symbol("uncontrolled");
211
212
  var valueOf = (e) => {
212
213
  const t = e.target;
213
214
  if (!t) return void 0;
@@ -226,7 +227,7 @@ var INPUT_TEXT = {
226
227
  function Input({ className, size: sizeProp, error = false, prefix, suffix, ...props }) {
227
228
  const contextSize = useContext(ControlSizeContext);
228
229
  const size = sizeProp ?? contextSize ?? "md";
229
- const onChange = useReportedChange(props.onChange, props.value, valueOf);
230
+ const onChange = useReportedChange(props.onChange, props.value === void 0 ? UNCONTROLLED : props.value, valueOf);
230
231
  const borderColor = error ? "border-[var(--ds-status-error)]" : "border-[var(--ds-input-border)]";
231
232
  const hoverBorder = !error && !props.disabled && !props.readOnly ? "hover:border-[var(--ds-input-border-hover)]" : "";
232
233
  if (prefix || suffix) {
@@ -298,7 +299,7 @@ function Input({ className, size: sizeProp, error = false, prefix, suffix, ...pr
298
299
  );
299
300
  }
300
301
  function Textarea({ className, autoResize, maxRows, error = false, ref, ...props }) {
301
- const onChange = useReportedChange(props.onChange, props.value, valueOf);
302
+ const onChange = useReportedChange(props.onChange, props.value === void 0 ? UNCONTROLLED : props.value, valueOf);
302
303
  const internalRef = useRef(null);
303
304
  const textareaRef = ref ?? internalRef;
304
305
  const resize = useCallback(() => {
@@ -1175,10 +1176,11 @@ var CHECKBOX_SIZES = {
1175
1176
  sm: { box: "h-[16px] w-[16px]", glyph: 10, label: "text-xs" },
1176
1177
  md: { box: "h-[18px] w-[18px]", glyph: 12, label: "text-sm" }
1177
1178
  };
1178
- function Checkbox({ checked, onChange: onChangeProp, label, size = "md", disabled = false, indeterminate = false, className, ref }) {
1179
+ function Checkbox({ checked, onChange: onChangeProp, label, size = "md", disabled = false, indeterminate = false, className, ref, ...wiring }) {
1179
1180
  const onChange = useReportedChange(onChangeProp, checked);
1180
1181
  const internalRef = useRef(null);
1181
1182
  const dims = CHECKBOX_SIZES[size];
1183
+ const control = useFieldControl(wiring);
1182
1184
  const mergedRef = useCallback(
1183
1185
  (node) => {
1184
1186
  internalRef.current = node;
@@ -1206,6 +1208,11 @@ function Checkbox({ checked, onChange: onChangeProp, label, size = "md", disable
1206
1208
  {
1207
1209
  ref: mergedRef,
1208
1210
  type: "checkbox",
1211
+ id: control.id,
1212
+ "aria-labelledby": wiring["aria-labelledby"],
1213
+ "aria-describedby": control.describedBy,
1214
+ "aria-invalid": control.invalid || void 0,
1215
+ required: control.required || void 0,
1209
1216
  checked,
1210
1217
  disabled,
1211
1218
  onChange: (e) => onChange(e.target.checked),
@@ -1268,10 +1275,11 @@ function CheckboxGroupItem({ value, label, disabled, groupValue, onGroupChange }
1268
1275
  }
1269
1276
  );
1270
1277
  }
1271
- function RadioGroup({ value, onChange: onChangeProp, options, orientation = "vertical", className, label }) {
1278
+ function RadioGroup({ value, onChange: onChangeProp, options, orientation = "vertical", className, label, ...wiring }) {
1272
1279
  const onChange = useReportedChange(onChangeProp, value);
1273
1280
  const groupRef = useRef(null);
1274
1281
  const groupId = useId();
1282
+ const control = useFieldControl(wiring);
1275
1283
  const enabledOptions = options.filter((o) => !o.disabled);
1276
1284
  const tabStop = rovingTabStop(options, (o) => o.value === value, (o) => !!o.disabled);
1277
1285
  function handleKeyDown(e) {
@@ -1301,7 +1309,11 @@ function RadioGroup({ value, onChange: onChangeProp, options, orientation = "ver
1301
1309
  {
1302
1310
  ref: groupRef,
1303
1311
  role: "radiogroup",
1304
- "aria-labelledby": label ? groupId : void 0,
1312
+ id: control.id,
1313
+ "aria-labelledby": label ? groupId : control.labelId,
1314
+ "aria-describedby": control.describedBy,
1315
+ "aria-invalid": control.invalid || void 0,
1316
+ "aria-required": control.required || void 0,
1305
1317
  className: cn(
1306
1318
  "flex",
1307
1319
  orientation === "vertical" ? "flex-col gap-ds-2" : "flex-row flex-wrap gap-ds-3"
@@ -1796,13 +1808,13 @@ function DatePicker({ value, onChange: onChangeProp, placeholder = "Pick a date"
1796
1808
  className: "flex items-center justify-between bg-[var(--ds-bg-muted)] rounded-t-[var(--ds-radius-md)]",
1797
1809
  style: { padding: "var(--ds-space-2) var(--ds-space-3)" },
1798
1810
  children: [
1799
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: prevMonth, "aria-label": "Previous month", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14, className: "rotate-180" }) }),
1811
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", onClick: prevMonth, "aria-label": "Previous month", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14, className: "rotate-180" }) }),
1800
1812
  /* @__PURE__ */ jsxs("span", { className: "text-sm font-medium text-[var(--ds-text-primary)]", children: [
1801
1813
  MONTHS[month],
1802
1814
  " ",
1803
1815
  year
1804
1816
  ] }),
1805
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: nextMonth, "aria-label": "Next month", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14 }) })
1817
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", onClick: nextMonth, "aria-label": "Next month", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14 }) })
1806
1818
  ]
1807
1819
  }
1808
1820
  ),
@@ -1865,6 +1877,7 @@ function DatePicker({ value, onChange: onChangeProp, placeholder = "Pick a date"
1865
1877
  /* @__PURE__ */ jsx(
1866
1878
  Button,
1867
1879
  {
1880
+ type: "button",
1868
1881
  variant: "ghost",
1869
1882
  size: "sm",
1870
1883
  onClick: () => {
@@ -1878,6 +1891,7 @@ function DatePicker({ value, onChange: onChangeProp, placeholder = "Pick a date"
1878
1891
  value && /* @__PURE__ */ jsx(
1879
1892
  Button,
1880
1893
  {
1894
+ type: "button",
1881
1895
  variant: "ghost",
1882
1896
  size: "sm",
1883
1897
  onClick: () => choose(null),
@@ -2264,7 +2278,7 @@ function DateRangePicker({
2264
2278
  ),
2265
2279
  style: { top: pos.top, left: pos.left },
2266
2280
  children: [
2267
- presets.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-ds-0.5 p-ds-2 border-r-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)] min-w-[140px]", children: presets.map((p) => /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", className: "justify-start", onClick: () => {
2281
+ presets.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-ds-0.5 p-ds-2 border-r-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)] min-w-[140px]", children: presets.map((p) => /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "justify-start", onClick: () => {
2268
2282
  const r = p.range();
2269
2283
  returnFocus();
2270
2284
  onChange(r);
@@ -2273,7 +2287,7 @@ function DateRangePicker({
2273
2287
  }, children: p.label }, p.label)) }),
2274
2288
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
2275
2289
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between bg-[var(--ds-bg-muted)] rounded-tr-[var(--ds-radius-md)] px-ds-3 py-ds-2", children: [
2276
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => pageMonths(-1), "aria-label": "Previous month", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14, className: "rotate-180" }) }),
2290
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", onClick: () => pageMonths(-1), "aria-label": "Previous month", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14, className: "rotate-180" }) }),
2277
2291
  /* @__PURE__ */ jsxs("span", { className: "text-sm font-medium text-[var(--ds-text-primary)]", children: [
2278
2292
  MONTHS2[months[0].getMonth()],
2279
2293
  " ",
@@ -2283,7 +2297,7 @@ function DateRangePicker({
2283
2297
  " ",
2284
2298
  months[1].getFullYear()
2285
2299
  ] }),
2286
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => pageMonths(1), "aria-label": "Next month", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14 }) })
2300
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", onClick: () => pageMonths(1), "aria-label": "Next month", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14 }) })
2287
2301
  ] }),
2288
2302
  /* @__PURE__ */ jsx("div", { ref: gridRef, onKeyDown: grid.onKeyDown, className: "flex gap-ds-4 px-ds-3 py-ds-2", onMouseLeave: () => setHover(null), children: months.map((m) => /* @__PURE__ */ jsxs("div", { role: "grid", "aria-label": `${MONTHS2[m.getMonth()]} ${m.getFullYear()}`, className: "w-[224px] flex flex-col gap-ds-0.5", children: [
2289
2303
  /* @__PURE__ */ jsx("div", { role: "row", className: "grid grid-cols-7 gap-ds-0.5 mb-ds-0.5", children: dayNames.map((d) => /* @__PURE__ */ jsx("div", { role: "columnheader", className: "text-center text-2xs font-medium text-[var(--ds-text-muted)] py-ds-1", children: d }, d)) }),
@@ -2324,7 +2338,7 @@ function DateRangePicker({
2324
2338
  ] }, `${m.getFullYear()}-${m.getMonth()}`)) }),
2325
2339
  /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center bg-[var(--ds-bg-muted)] border-t-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)] rounded-br-[var(--ds-radius-md)] px-ds-3 py-ds-2", children: [
2326
2340
  /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--ds-text-muted)]", children: value.start && !value.end ? "Now pick an end date" : summary ?? "Pick a start date" }),
2327
- (value.start || value.end) && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
2341
+ (value.start || value.end) && /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
2328
2342
  onChange({ start: null, end: null });
2329
2343
  setHover(null);
2330
2344
  }, children: "Clear" })
@@ -2474,6 +2488,21 @@ function Progress({ value, max = 100, variant = "default", size = "md", indeterm
2474
2488
  ] })
2475
2489
  ] });
2476
2490
  }
2491
+
2492
+ // ../../showcase/src/lib/dev-warn.ts
2493
+ var warned = /* @__PURE__ */ new Set();
2494
+ function warnOnce(component, message) {
2495
+ let dev = false;
2496
+ try {
2497
+ dev = process.env.NODE_ENV !== "production";
2498
+ } catch {
2499
+ dev = false;
2500
+ }
2501
+ const text = `${component}: ${message}`;
2502
+ if (!dev || warned.has(text)) return;
2503
+ warned.add(text);
2504
+ console.warn(text);
2505
+ }
2477
2506
  function TruncFileName({ children, className }) {
2478
2507
  const { ref, isTruncated } = useTextOverflow({ lines: 1 });
2479
2508
  const el = /* @__PURE__ */ jsx("p", { ref, className: cn("truncate", className), children });
@@ -2488,6 +2517,8 @@ function formatSize(bytes) {
2488
2517
  }
2489
2518
  var running = (item) => item.status === "queued" || item.status === "uploading";
2490
2519
  var failureOf = (err) => err instanceof Error && err.message ? err.message : "The upload didn't work.";
2520
+ var percentOf = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0;
2521
+ var STILL_UPLOADING = "Wait for your files to finish uploading";
2491
2522
  function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accept, multiple = true, maxSize, className, error = false, label, id }) {
2492
2523
  const controlled = files !== void 0;
2493
2524
  const [own, setOwn] = useState([]);
@@ -2508,8 +2539,6 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2508
2539
  const rejectedId = `${ownId}-rejected`;
2509
2540
  const showLabel = label != null && !field;
2510
2541
  const constraints = fileConstraints(accept, maxSize, multiple);
2511
- const invalid = error || rejected != null;
2512
- const describedBy = [constraints ? constraintsId : null, field?.describedBy, rejected ? rejectedId : null].filter(Boolean).join(" ") || void 0;
2513
2542
  const patch = (itemId, change) => setOwn((prev) => prev.map((i) => i.id === itemId ? { ...i, ...change } : i));
2514
2543
  const stop = (itemId) => {
2515
2544
  controllers.current.get(itemId)?.abort();
@@ -2531,7 +2560,7 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2531
2560
  attempt = Promise.resolve(run(item.file, {
2532
2561
  signal: controller.signal,
2533
2562
  onProgress: (percent) => {
2534
- if (!controller.signal.aborted) patch(item.id, { progress: Math.max(0, Math.min(100, percent)) });
2563
+ if (!controller.signal.aborted) patch(item.id, { progress: percentOf(percent) });
2535
2564
  }
2536
2565
  }));
2537
2566
  } catch (err) {
@@ -2539,6 +2568,11 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2539
2568
  }
2540
2569
  attempt.then(() => settle({ status: "done", progress: 100 }), (err) => settle({ status: "error", error: failureOf(err) }));
2541
2570
  };
2571
+ const [hadUpload, setHadUpload] = useState(!!upload);
2572
+ if (!controlled && !!upload !== hadUpload) {
2573
+ setHadUpload(!!upload);
2574
+ setOwn((prev) => prev.map((i) => upload ? i.status === "added" && i.file ? { ...i, status: "queued" } : i : i.status === "queued" ? { ...i, status: "error", error: "The upload couldn't start." } : i));
2575
+ }
2542
2576
  useEffect(() => {
2543
2577
  if (controlled || !upload) return;
2544
2578
  let active = own.filter((i) => i.status === "uploading" && controllers.current.has(i.id)).length;
@@ -2564,31 +2598,67 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2564
2598
  useEffect(() => {
2565
2599
  if (!overlayOpen && !controlled) stopAll();
2566
2600
  }, [overlayOpen, controlled, stopAll]);
2567
- const busy = shown.some(running);
2601
+ const busy = shown.some((i) => i.status === "uploading" || i.status === "queued" && (controlled || !!upload));
2568
2602
  useEffect(() => {
2569
2603
  if (!busy || !overlayMark) return;
2570
2604
  return overlayMark.hold();
2571
2605
  }, [busy, overlayMark]);
2572
- const [heard, setHeard] = useState("");
2573
- const [seen, setSeen] = useState(() => new Map(shown.map((i) => [i.id, i.status])));
2574
- if (shown.length !== seen.size || shown.some((i) => seen.get(i.id) !== i.status)) {
2606
+ const form = useOptionalFormContext();
2607
+ const waitingKey = `${inputId}-uploads`;
2608
+ const waitingId = `${ownId}-waiting`;
2609
+ const busyRef = useLatest(busy);
2610
+ const registerCheck = form?.register;
2611
+ useEffect(() => registerCheck?.(waitingKey, () => busyRef.current ? STILL_UPLOADING : void 0), [registerCheck, waitingKey, busyRef]);
2612
+ const waiting = form?.errors[waitingKey];
2613
+ const clearWaiting = form?.setError;
2614
+ useEffect(() => {
2615
+ if (!busy && waiting) clearWaiting?.(waitingKey, void 0);
2616
+ }, [busy, waiting, clearWaiting, waitingKey]);
2617
+ useEffect(() => {
2618
+ const owner = inputRef.current?.form;
2619
+ if (!owner) return;
2620
+ const clear = () => setRejected(null);
2621
+ owner.addEventListener("submit", clear);
2622
+ return () => owner.removeEventListener("submit", clear);
2623
+ }, []);
2624
+ const invalid = error || rejected != null || !!waiting;
2625
+ const describedBy = [constraints ? constraintsId : null, field?.describedBy, rejected ? rejectedId : null, waiting ? waitingId : null].filter(Boolean).join(" ") || void 0;
2626
+ useEffect(() => {
2627
+ if (controlled && !onFiles) warnOnce("FileUpload", "`files` without `onFiles`: the page's list never gets the files the user picks. Add them in onFiles.");
2628
+ if (controlled && upload) warnOnce("FileUpload", "pass `files` or `upload`, not both: with `files` the page runs the uploads and FileUpload shows them.");
2629
+ if (!onRemove) warnOnce("FileUpload", "no `onRemove`: Remove shows only on a failed file, since the page would still have a file the user removed.");
2630
+ const ids = shown.map((i) => i.id);
2631
+ if (new Set(ids).size !== ids.length) warnOnce("FileUpload", "two files in the list share an id: give each file its own (the same file picked twice is two files).");
2632
+ }, [controlled, onFiles, upload, onRemove, shown]);
2633
+ const [heard, setHeard] = useState({ text: "", n: 0 });
2634
+ const signature2 = shown.map((i) => `${i.id}\0${i.status}`).join("");
2635
+ const [seen, setSeen] = useState(() => ({ signature: signature2, statuses: new Map(shown.map((i) => [i.id, i.status])) }));
2636
+ if (signature2 !== seen.signature) {
2575
2637
  const said = [];
2576
2638
  for (const item of shown) {
2577
- const before = seen.get(item.id);
2639
+ const before = seen.statuses.get(item.id);
2578
2640
  if (!before || before === item.status) continue;
2579
2641
  if (item.status === "done") said.push(`${item.name} uploaded`);
2580
2642
  else if (item.status === "error") said.push(`${item.name} didn't upload: ${item.error ?? "the upload failed"}`);
2581
2643
  }
2582
- setSeen(new Map(shown.map((i) => [i.id, i.status])));
2583
- if (said.length) setHeard(said.join(". "));
2644
+ setSeen({ signature: signature2, statuses: new Map(shown.map((i) => [i.id, i.status])) });
2645
+ if (said.length) setHeard((h) => ({ text: said.join(". "), n: h.n + 1 }));
2584
2646
  }
2585
2647
  const pendingFocus = useRef(null);
2648
+ const lastAction = useRef(null);
2586
2649
  useEffect(() => {
2587
- const index = pendingFocus.current;
2588
- if (index === null) return;
2589
- pendingFocus.current = null;
2590
- const rows = Array.from(listRef.current?.querySelectorAll(":scope > li") ?? []).map((row) => row.querySelector("button")).filter((b) => !!b);
2591
- focusAfterRemoval(rows, index, inputRef.current);
2650
+ const gone = pendingFocus.current;
2651
+ if (gone && !shown.some((i) => i.id === gone.id)) {
2652
+ pendingFocus.current = null;
2653
+ const rows = Array.from(listRef.current?.querySelectorAll(":scope > li") ?? []).map((row2) => row2.querySelector("button")).filter((b) => !!b);
2654
+ focusAfterRemoval(rows, gone.index, inputRef.current);
2655
+ return;
2656
+ }
2657
+ const last = lastAction.current;
2658
+ const active = document.activeElement;
2659
+ if (!last || last.el.isConnected || active && active !== document.body) return;
2660
+ const row = Array.from(listRef.current?.querySelectorAll(":scope > li") ?? []).find((li) => li.dataset.uploadId === last.id);
2661
+ row?.querySelector("button")?.focus();
2592
2662
  }, [shown]);
2593
2663
  const addFiles = useCallback(
2594
2664
  (fileList) => {
@@ -2615,7 +2685,7 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2615
2685
  );
2616
2686
  const indexOf = (item) => shown.findIndex((i) => i.id === item.id);
2617
2687
  const cancel = (item) => {
2618
- pendingFocus.current = indexOf(item);
2688
+ pendingFocus.current = { id: item.id, index: indexOf(item) };
2619
2689
  if (!controlled) {
2620
2690
  stop(item.id);
2621
2691
  setOwn((prev) => prev.filter((i) => i.id !== item.id));
@@ -2627,7 +2697,7 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2627
2697
  onRetry?.(item);
2628
2698
  };
2629
2699
  const remove = (item) => {
2630
- pendingFocus.current = indexOf(item);
2700
+ pendingFocus.current = { id: item.id, index: indexOf(item) };
2631
2701
  reportChange?.();
2632
2702
  if (!controlled) {
2633
2703
  stop(item.id);
@@ -2637,7 +2707,7 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2637
2707
  };
2638
2708
  const canCancel = !controlled || !!onCancel;
2639
2709
  const canRetry = !controlled || !!onRetry;
2640
- const canRemove = !controlled || !!onRemove;
2710
+ const canRemove = (item) => !!onRemove || !controlled && item.status === "error";
2641
2711
  return /* @__PURE__ */ jsxs("div", { className: cn("space-y-ds-3", className), children: [
2642
2712
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-ds-1.5", children: [
2643
2713
  showLabel && /* @__PURE__ */ jsx(Label, { htmlFor: inputId, children: label }),
@@ -2691,14 +2761,20 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2691
2761
  ]
2692
2762
  }
2693
2763
  ),
2694
- rejected && /* @__PURE__ */ jsx(InlineFieldError, { id: rejectedId, message: rejected, className: "mt-0" })
2764
+ rejected && /* @__PURE__ */ jsx(InlineFieldError, { id: rejectedId, message: rejected, className: "mt-0" }),
2765
+ waiting && /* @__PURE__ */ jsx(InlineFieldError, { id: waitingId, message: waiting, className: "mt-0" })
2695
2766
  ] }),
2696
2767
  shown.length > 0 && /* @__PURE__ */ jsx("ul", { ref: listRef, className: "space-y-ds-2", children: shown.map((item) => {
2697
- const words = item.status === "queued" ? "Waiting" : item.status === "uploading" ? `${Math.round(item.progress ?? 0)}%` : item.status === "done" ? "Uploaded" : item.status === "error" ? "Failed" : null;
2768
+ const progress = percentOf(item.progress);
2769
+ const words = item.status === "queued" ? "Waiting" : item.status === "uploading" ? `${Math.round(progress)}%` : item.status === "done" ? "Uploaded" : item.status === "error" ? "Failed" : null;
2698
2770
  return /* @__PURE__ */ jsxs(
2699
2771
  "li",
2700
2772
  {
2773
+ "data-upload-id": item.id,
2701
2774
  "data-upload-status": item.status,
2775
+ onFocus: (e) => {
2776
+ if (e.target instanceof HTMLButtonElement) lastAction.current = { id: item.id, el: e.target };
2777
+ },
2702
2778
  className: cn(
2703
2779
  "flex items-center gap-ds-3 text-sm",
2704
2780
  "border-[length:var(--ds-border-width)]",
@@ -2711,20 +2787,20 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2711
2787
  /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
2712
2788
  /* @__PURE__ */ jsx(TruncFileName, { className: "text-[var(--ds-text-primary)]", children: item.name }),
2713
2789
  (item.size != null || words) && /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--ds-text-muted)]", children: [item.size != null ? formatSize(item.size) : null, words].filter(Boolean).join(" \xB7 ") }),
2714
- item.status === "uploading" && /* @__PURE__ */ jsx(Progress, { value: item.progress ?? 0, size: "sm", label: `Uploading ${item.name}`, className: "mt-ds-1" }),
2790
+ item.status === "uploading" && /* @__PURE__ */ jsx(Progress, { value: progress, size: "sm", label: `Uploading ${item.name}`, className: "mt-ds-1" }),
2715
2791
  item.status === "error" && /* @__PURE__ */ jsx("p", { className: "mt-ds-1 text-xs text-[var(--ds-status-error)]", children: item.error ?? "The upload didn't work." })
2716
2792
  ] }),
2717
2793
  item.status === "done" && /* @__PURE__ */ jsx(Icon, { name: "check", size: 16, className: "text-[var(--ds-status-success)] shrink-0", "aria-hidden": "true" }),
2718
2794
  running(item) && canCancel && // the visible word starts the name, which says which file (WCAG 2.5.3)
2719
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => cancel(item), "aria-label": `Cancel ${item.name}`, children: "Cancel" }),
2720
- item.status === "error" && canRetry && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => retry(item), "aria-label": `Retry ${item.name}`, children: "Retry" }),
2721
- !running(item) && canRemove && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => remove(item), "aria-label": `Remove ${item.name}`, children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 14 }) })
2795
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => cancel(item), "aria-label": `Cancel ${item.name}`, children: "Cancel" }),
2796
+ item.status === "error" && canRetry && /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => retry(item), "aria-label": `Retry ${item.name}`, children: "Retry" }),
2797
+ !running(item) && canRemove(item) && /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", onClick: () => remove(item), "aria-label": `Remove ${item.name}`, children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 14 }) })
2722
2798
  ]
2723
2799
  },
2724
2800
  item.id
2725
2801
  );
2726
2802
  }) }),
2727
- /* @__PURE__ */ jsx("p", { role: "status", className: "sr-only", children: heard })
2803
+ /* @__PURE__ */ jsx("div", { role: "status", className: "sr-only", children: /* @__PURE__ */ jsx("span", { children: heard.text }, heard.n) })
2728
2804
  ] });
2729
2805
  }
2730
2806
  function SliderRange({
@@ -3418,7 +3494,7 @@ function NumberInput({
3418
3494
  setFocused(false);
3419
3495
  const parsed = parseFloat(draft);
3420
3496
  if (!isNaN(parsed) && isFinite(parsed)) {
3421
- const committed = commitValue(parsed);
3497
+ const committed = commitValue(parsed, draft === String(value));
3422
3498
  setDraft(String(committed));
3423
3499
  } else {
3424
3500
  const fallback = isFinite(min) ? min : 0;
@@ -3949,11 +4025,13 @@ function Rating({
3949
4025
  readOnly = false,
3950
4026
  size = "md",
3951
4027
  className,
3952
- error = false
4028
+ error = false,
4029
+ ...wiring
3953
4030
  }) {
3954
4031
  const onChange = useReportedChange(onChangeProp, value);
3955
4032
  const [hoverValue, setHoverValue] = useState(null);
3956
4033
  const displayValue = hoverValue ?? value;
4034
+ const control = useFieldControl({ ...wiring, error: error || void 0 });
3957
4035
  const handleClick = useCallback(
3958
4036
  (starIdx, isLeftHalf) => {
3959
4037
  if (readOnly || !onChangeProp) return;
@@ -4020,8 +4098,12 @@ function Rating({
4020
4098
  "div",
4021
4099
  {
4022
4100
  role: readOnly ? "img" : "radiogroup",
4023
- "aria-label": `Rating: ${value} out of ${max}`,
4024
- "aria-invalid": error || void 0,
4101
+ id: control.id,
4102
+ "aria-label": readOnly || !control.labelId ? `Rating: ${value} out of ${max}` : void 0,
4103
+ "aria-labelledby": readOnly ? void 0 : control.labelId,
4104
+ "aria-describedby": control.describedBy,
4105
+ "aria-invalid": readOnly ? void 0 : control.invalid || void 0,
4106
+ "aria-required": readOnly ? void 0 : control.required || void 0,
4025
4107
  onKeyDown: readOnly ? void 0 : handleKeyDown,
4026
4108
  className: cn(
4027
4109
  "inline-flex items-center gap-ds-0.5",
@@ -4431,11 +4513,6 @@ var Alert = forwardRef(function Alert2({
4431
4513
  }
4432
4514
  );
4433
4515
  });
4434
- function controlsOf(form) {
4435
- if (!form) return "";
4436
- const controls = form.querySelectorAll("input, select, textarea, [role='combobox'], [role='radiogroup'], [role='checkbox'], [role='switch'], [role='slider'], [role='spinbutton'], [contenteditable='true']");
4437
- return Array.from(controls).map((el) => el.id || el.getAttribute("name") || el.getAttribute("role") || el.tagName).join("|");
4438
- }
4439
4516
  var isThenable = (value) => !!value && typeof value.then === "function";
4440
4517
  function stampFormId(node, formId) {
4441
4518
  return Children.map(node, (child) => {
@@ -4494,29 +4571,18 @@ function FormWrapper({ onSubmit, validationMode: validationModeProp, children, a
4494
4571
  const pendingFocus = useRef(false);
4495
4572
  const onSubmitRef = useLatest(onSubmit);
4496
4573
  const overlayMark = useContext(OverlayMarkCtx);
4497
- const reportChange = overlayMark ? overlayMark.report : null;
4498
- const [savedCheck, setSavedCheck] = useState(null);
4499
- useEffect(() => {
4500
- if (!savedCheck) return;
4501
- const form = formRef.current;
4502
- if (form && !form.querySelector("[aria-invalid='true']") && controlsOf(form) === savedCheck.controls) overlayMark?.clear();
4503
- }, [savedCheck, overlayMark]);
4574
+ const reportChange = useMemo(() => overlayMark ? () => overlayMark.report(formId) : null, [overlayMark, formId]);
4575
+ useEffect(() => overlayMark?.attach(formId), [overlayMark, formId]);
4504
4576
  useEffect(() => {
4505
4577
  if (pendingSubmit.current && submitted) {
4506
4578
  pendingSubmit.current = false;
4507
- const form = formRef.current;
4508
- const passed = Object.keys(errorsRef.current).length === 0 && !form?.querySelector("[aria-invalid='true']");
4509
- const controls = controlsOf(form);
4579
+ const answer = overlayMark?.submit(formId, { element: () => formRef.current, failing: () => Object.keys(errorsRef.current).length > 0 });
4510
4580
  const result = onSubmitRef.current(errorsRef.current);
4511
- if (!passed || !overlayMark) return;
4512
- if (isThenable(result)) {
4513
- result.then(() => overlayMark.clear(), () => {
4514
- });
4515
- } else {
4516
- setSavedCheck({ controls });
4517
- }
4581
+ if (!answer) return;
4582
+ if (isThenable(result)) void result.then(() => answer.settle("resolved"));
4583
+ else answer.settle("returned");
4518
4584
  }
4519
- }, [submitted, submitCount, errors, errorsRef, onSubmitRef, overlayMark]);
4585
+ }, [submitted, submitCount, errors, errorsRef, onSubmitRef, overlayMark, formId]);
4520
4586
  const submittedRef = useLatest(submitted);
4521
4587
  const errorSummaryRef = useLatest(errorSummary);
4522
4588
  const summaryRef = useRef(null);
@@ -4601,7 +4667,9 @@ function FormWrapper({ onSubmit, validationMode: validationModeProp, children, a
4601
4667
  }, []);
4602
4668
  const handleSubmit = useCallback(
4603
4669
  (e) => {
4670
+ if (e.target !== e.currentTarget) return;
4604
4671
  e.preventDefault();
4672
+ e.stopPropagation();
4605
4673
  if (checks.current.size > 0) {
4606
4674
  const found = /* @__PURE__ */ new Map();
4607
4675
  for (const [field, check] of checks.current) found.set(field, check());
@@ -4645,16 +4713,26 @@ var FOCUSABLE2 = "input:not([disabled]):not([type='hidden']), select:not([disabl
4645
4713
  function AddAnother({ items, getId, itemName, columns, renderRow, onAdd, onRemove, addLabel = "Add another", label, minItems = 1, maxItems, className }) {
4646
4714
  const listRef = useRef(null);
4647
4715
  const addRef = useRef(null);
4716
+ const reportChange = useContext(FormChangeCtx);
4717
+ const getIdRef = useLatest(getId);
4648
4718
  const pending = useRef(null);
4649
- const firstFields = () => Array.from(listRef.current?.querySelectorAll(":scope > li") ?? []).map((row) => row.querySelector(FOCUSABLE2)).filter((el) => !!el);
4650
4719
  useEffect(() => {
4651
4720
  const next = pending.current;
4652
4721
  if (!next) return;
4722
+ const rows = () => Array.from(listRef.current?.querySelectorAll(":scope > li") ?? []);
4723
+ const firstFields = () => rows().map((row) => row.querySelector(FOCUSABLE2)).filter((el) => !!el);
4724
+ const ids = items.map((item) => getIdRef.current(item));
4725
+ if (next.kind === "remove") {
4726
+ if (ids.includes(next.id)) return;
4727
+ pending.current = null;
4728
+ focusAfterRemoval(firstFields(), next.index, addRef.current);
4729
+ return;
4730
+ }
4731
+ const added = ids.findIndex((id) => !next.before.has(id));
4732
+ if (added === -1) return;
4653
4733
  pending.current = null;
4654
- const fields = firstFields();
4655
- if (next.kind === "add") fields[fields.length - 1]?.focus();
4656
- else focusAfterRemoval(fields, next.index, addRef.current);
4657
- }, [items]);
4734
+ rows()[added]?.querySelector(FOCUSABLE2)?.focus();
4735
+ }, [items, getIdRef]);
4658
4736
  const canRemove = items.length > minItems;
4659
4737
  const canAdd = maxItems === void 0 || items.length < maxItems;
4660
4738
  const grid = { "--gs-add-another-cols": `repeat(${columns.length}, minmax(0, 1fr)) max-content` };
@@ -4669,13 +4747,15 @@ function AddAnother({ items, getId, itemName, columns, renderRow, onAdd, onRemov
4669
4747
  /* @__PURE__ */ jsx(AddAnotherRowCtx.Provider, { value: true, children: renderRow(item, index, (column) => `${column}, ${itemName(index)}`) }),
4670
4748
  canRemove && // the visible "Remove" starts its name, which says whose row it removes (WCAG 2.5.3 label in name)
4671
4749
  /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": `Remove ${itemName(index)}`, className: "justify-self-start @min-[30rem]:mt-[calc((var(--ds-control-height-md)-var(--ds-control-height-sm))/2)]", onClick: () => {
4672
- pending.current = { kind: "remove", index };
4750
+ pending.current = { kind: "remove", id, index };
4751
+ reportChange?.();
4673
4752
  onRemove(id);
4674
4753
  }, children: "Remove" })
4675
4754
  ] }, id);
4676
4755
  }) }),
4677
4756
  canAdd && /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, { ref: addRef, type: "button", variant: "secondary", onClick: () => {
4678
- pending.current = { kind: "add" };
4757
+ pending.current = { kind: "add", before: new Set(items.map(getId)) };
4758
+ reportChange?.();
4679
4759
  onAdd();
4680
4760
  }, children: addLabel }) })
4681
4761
  ] });
@@ -4695,7 +4775,7 @@ function TransferList({
4695
4775
  className,
4696
4776
  ref
4697
4777
  }) {
4698
- const onChange = useReportedChange(onChangeProp, selected);
4778
+ const onChange = useReportedChange(onChangeProp, selected, (_available, chosen) => chosen);
4699
4779
  const [checkedAvailable, setCheckedAvailable] = useState(/* @__PURE__ */ new Set());
4700
4780
  const [checkedSelected, setCheckedSelected] = useState(/* @__PURE__ */ new Set());
4701
4781
  const [searchLeft, setSearchLeft] = useState("");
@@ -8433,66 +8513,70 @@ function TableToolbar({
8433
8513
  const paged = total !== void 0 && !!pagination && pagination.pageSize > 0 && total > pagination.pageSize;
8434
8514
  const countText = total === void 0 ? null : grows ? shown < total ? `Showing ${shown} of ${total} ${noun(total)}` : resultCount !== void 0 ? `${total} ${noun(total)}` : null : paged ? `Showing ${Math.min((pagination.page - 1) * pagination.pageSize + 1, total)}\u2013${Math.min(pagination.page * pagination.pageSize, total)} of ${total} ${noun(total)}` : resultCount !== void 0 ? `${total} ${noun(total)}` : null;
8435
8515
  if (!hasControls && countText === null && !hasChips) return null;
8436
- return /* @__PURE__ */ jsx(ControlSizeContext.Provider, { value: size, children: /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-ds-3 min-w-0 mb-ds-3 last:mb-0", className), "data-table-toolbar": "", children: [
8437
- hasControls && /* @__PURE__ */ jsxs("div", { ref: rowRef, className: "flex items-center gap-ds-2 flex-wrap", children: [
8438
- search && /* @__PURE__ */ jsx(
8439
- Input,
8440
- {
8441
- "aria-label": search.label,
8442
- placeholder: search.placeholder ?? search.label,
8443
- value: search.value,
8444
- size,
8445
- onChange: (e) => search.onChange(e.target.value),
8446
- className: cn("flex-1 min-w-[160px]", !searchFills && "max-w-[280px]")
8447
- }
8448
- ),
8449
- primaryFilter && (compact ? /* @__PURE__ */ jsx(
8450
- Dropdown,
8451
- {
8452
- "aria-label": primaryFilter.label,
8453
- options: primaryFilter.options.map((o) => ({ id: o.value, label: o.label })),
8454
- value: primaryFilter.value,
8455
- onChange: (v) => primaryFilter.onChange(v),
8456
- size,
8457
- className: "w-[150px]"
8458
- }
8459
- ) : /* @__PURE__ */ jsx(
8460
- SegmentedControl,
8461
- {
8462
- "aria-label": primaryFilter.label,
8463
- options: primaryFilter.options,
8464
- value: primaryFilter.value,
8465
- size,
8466
- onChange: primaryFilter.onChange
8467
- }
8468
- )),
8469
- (filters || actions) && /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-ds-2 justify-end", inPane ? "flex-none" : "flex-1"), children: [
8470
- filters && /* @__PURE__ */ jsxs(
8471
- Button,
8516
+ return (
8517
+ // a list's search and filters are how it's viewed, not a form's data: they never mark an overlay's form (0.21.0)
8518
+ /* @__PURE__ */ jsx(FormChangeCtx, { value: null, children: /* @__PURE__ */ jsx(ControlSizeContext.Provider, { value: size, children: /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-ds-3 min-w-0 mb-ds-3 last:mb-0", className), "data-table-toolbar": "", children: [
8519
+ hasControls && /* @__PURE__ */ jsxs("div", { ref: rowRef, className: "flex items-center gap-ds-2 flex-wrap", children: [
8520
+ search && /* @__PURE__ */ jsx(
8521
+ Input,
8472
8522
  {
8473
- variant: open ? "secondary" : "outline",
8523
+ "aria-label": search.label,
8524
+ placeholder: search.placeholder ?? search.label,
8525
+ value: search.value,
8474
8526
  size,
8475
- iconLeft: /* @__PURE__ */ jsx(Icon, { name: "filter", size: iconSize }),
8476
- onClick: () => setOpen(!open),
8477
- "aria-expanded": open,
8478
- "aria-controls": filterRowId,
8479
- className: "mr-auto",
8480
- children: [
8481
- "Filters",
8482
- count > 0 && !open && /* @__PURE__ */ jsx(Badge, { variant: "default", size: "xs", className: "ml-ds-1", children: count })
8483
- ]
8527
+ onChange: (e) => search.onChange(e.target.value),
8528
+ className: cn("flex-1 min-w-[160px]", !searchFills && "max-w-[280px]")
8484
8529
  }
8485
8530
  ),
8486
- actions
8531
+ primaryFilter && (compact ? /* @__PURE__ */ jsx(
8532
+ Dropdown,
8533
+ {
8534
+ "aria-label": primaryFilter.label,
8535
+ options: primaryFilter.options.map((o) => ({ id: o.value, label: o.label })),
8536
+ value: primaryFilter.value,
8537
+ onChange: (v) => primaryFilter.onChange(v),
8538
+ size,
8539
+ className: "w-[150px]"
8540
+ }
8541
+ ) : /* @__PURE__ */ jsx(
8542
+ SegmentedControl,
8543
+ {
8544
+ "aria-label": primaryFilter.label,
8545
+ options: primaryFilter.options,
8546
+ value: primaryFilter.value,
8547
+ size,
8548
+ onChange: primaryFilter.onChange
8549
+ }
8550
+ )),
8551
+ (filters || actions) && /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-ds-2 justify-end", inPane ? "flex-none" : "flex-1"), children: [
8552
+ filters && /* @__PURE__ */ jsxs(
8553
+ Button,
8554
+ {
8555
+ type: "button",
8556
+ variant: open ? "secondary" : "outline",
8557
+ size,
8558
+ iconLeft: /* @__PURE__ */ jsx(Icon, { name: "filter", size: iconSize }),
8559
+ onClick: () => setOpen(!open),
8560
+ "aria-expanded": open,
8561
+ "aria-controls": filterRowId,
8562
+ className: "mr-auto",
8563
+ children: [
8564
+ "Filters",
8565
+ count > 0 && !open && /* @__PURE__ */ jsx(Badge, { variant: "default", size: "xs", className: "ml-ds-1", children: count })
8566
+ ]
8567
+ }
8568
+ ),
8569
+ actions
8570
+ ] })
8571
+ ] }),
8572
+ filters && open && /* @__PURE__ */ jsx("div", { id: filterRowId, className: "flex items-center gap-ds-3 flex-wrap", children: filters }),
8573
+ (countText !== null || hasChips) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2 flex-wrap", children: [
8574
+ countText !== null && /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--ds-text-muted)]", "aria-live": "polite", "data-result-count": "", children: countText }),
8575
+ activeFilters?.map((f) => /* @__PURE__ */ jsx(Tag, { size: "sm", variant: "subtle", onRemove: f.onRemove, children: f.label }, f.id)),
8576
+ hasChips && onClearAll && /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: onClearAll, children: "Clear all" })
8487
8577
  ] })
8488
- ] }),
8489
- filters && open && /* @__PURE__ */ jsx("div", { id: filterRowId, className: "flex items-center gap-ds-3 flex-wrap", children: filters }),
8490
- (countText !== null || hasChips) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2 flex-wrap", children: [
8491
- countText !== null && /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--ds-text-muted)]", "aria-live": "polite", "data-result-count": "", children: countText }),
8492
- activeFilters?.map((f) => /* @__PURE__ */ jsx(Tag, { size: "sm", variant: "subtle", onRemove: f.onRemove, children: f.label }, f.id)),
8493
- hasChips && onClearAll && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: onClearAll, children: "Clear all" })
8494
- ] })
8495
- ] }) });
8578
+ ] }) }) })
8579
+ );
8496
8580
  }
8497
8581
  var ICON_TEXT_OFFSET = "calc((2rem - 1.25rem) / 2)";
8498
8582
  function Marker({ item, vertical }) {
@@ -8657,6 +8741,8 @@ function CollapsibleTrigger({ children, className }) {
8657
8741
  if (isValidElement(children)) {
8658
8742
  const child = children;
8659
8743
  return cloneElement(child, {
8744
+ // a Button trigger in a form toggles; it never submits the form (0.21.0)
8745
+ ...triggerType(child),
8660
8746
  "aria-expanded": isOpen,
8661
8747
  className: cn(child.props.className, className),
8662
8748
  onClick: (e) => {
@@ -9206,7 +9292,7 @@ function ActionsRow({ message, onAction }) {
9206
9292
  ),
9207
9293
  children: message.actions.map((a) => {
9208
9294
  const { id, label, icon } = resolveAction(a);
9209
- return /* @__PURE__ */ jsx(Tooltip, { content: label, children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-xs", "aria-label": label, onClick: () => run(id), children: /* @__PURE__ */ jsx(Icon, { name: icon, size: 14 }) }) }, id);
9295
+ return /* @__PURE__ */ jsx(Tooltip, { content: label, children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-xs", "aria-label": label, onClick: () => run(id), children: /* @__PURE__ */ jsx(Icon, { name: icon, size: 14 }) }) }, id);
9210
9296
  })
9211
9297
  }
9212
9298
  );
@@ -9343,12 +9429,13 @@ function ChatInputBar({ onSend, busy, onStop, onAttach, onVoice, placeholder })
9343
9429
  ),
9344
9430
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-ds-2 pb-ds-1.5", children: [
9345
9431
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-0.5", children: [
9346
- onAttach && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Add attachment", onClick: onAttach, className: compact, children: /* @__PURE__ */ jsx(Icon, { name: "paperclip", size: glyph }) }),
9347
- onVoice && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Voice message", onClick: onVoice, className: compact, children: /* @__PURE__ */ jsx(Icon, { name: "mic", size: glyph }) })
9432
+ onAttach && /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": "Add attachment", onClick: onAttach, className: compact, children: /* @__PURE__ */ jsx(Icon, { name: "paperclip", size: glyph }) }),
9433
+ onVoice && /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": "Voice message", onClick: onVoice, className: compact, children: /* @__PURE__ */ jsx(Icon, { name: "mic", size: glyph }) })
9348
9434
  ] }),
9349
- busy ? /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "icon-sm", "aria-label": "Stop generating", onClick: onStop, className: compact, children: /* @__PURE__ */ jsx(Icon, { name: "square", size: glyph }) }) : /* @__PURE__ */ jsx(
9435
+ busy ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "tertiary", size: "icon-sm", "aria-label": "Stop generating", onClick: onStop, className: compact, children: /* @__PURE__ */ jsx(Icon, { name: "square", size: glyph }) }) : /* @__PURE__ */ jsx(
9350
9436
  Button,
9351
9437
  {
9438
+ type: "button",
9352
9439
  variant: text.trim() ? "default" : "ghost",
9353
9440
  size: "icon-sm",
9354
9441
  "aria-label": "Send message",
@@ -9462,7 +9549,7 @@ function ChatMessageList({
9462
9549
  }),
9463
9550
  typingUser && /* @__PURE__ */ jsx(TypingIndicator, { name: typingUser })
9464
9551
  ] }) }),
9465
- showJump && !empty && /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-x-0 bottom-ds-2 flex justify-center", children: /* @__PURE__ */ jsxs(Button, { variant: "secondary", size: "xs", className: "pointer-events-auto rounded-[var(--ds-radius-full)]", onClick: scrollToBottom, children: [
9552
+ showJump && !empty && /* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-x-0 bottom-ds-2 flex justify-center", children: /* @__PURE__ */ jsxs(Button, { type: "button", variant: "secondary", size: "xs", className: "pointer-events-auto rounded-[var(--ds-radius-full)]", onClick: scrollToBottom, children: [
9466
9553
  /* @__PURE__ */ jsx(Icon, { name: "chevron-down", size: 14 }),
9467
9554
  "Jump to latest"
9468
9555
  ] }) })
@@ -9517,10 +9604,11 @@ function ChatPanel({
9517
9604
  ] }),
9518
9605
  /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-ds-1", children: [
9519
9606
  actions,
9520
- onNewConversation && /* @__PURE__ */ jsx(Tooltip, { content: "New conversation", children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "New conversation", onClick: onNewConversation, children: /* @__PURE__ */ jsx(Icon, { name: "plus", size: 16 }) }) }),
9607
+ onNewConversation && /* @__PURE__ */ jsx(Tooltip, { content: "New conversation", children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": "New conversation", onClick: onNewConversation, children: /* @__PURE__ */ jsx(Icon, { name: "plus", size: 16 }) }) }),
9521
9608
  onExpandedChange && /* @__PURE__ */ jsx(Tooltip, { content: expanded ? "Collapse" : "Expand", children: /* @__PURE__ */ jsx(
9522
9609
  Button,
9523
9610
  {
9611
+ type: "button",
9524
9612
  variant: "ghost",
9525
9613
  size: "icon-sm",
9526
9614
  "aria-label": expanded ? "Collapse" : "Expand",
@@ -9529,7 +9617,7 @@ function ChatPanel({
9529
9617
  children: /* @__PURE__ */ jsx(Icon, { name: expanded ? "minimize-2" : "maximize-2", size: 16 })
9530
9618
  }
9531
9619
  ) }),
9532
- /* @__PURE__ */ jsx(Tooltip, { content: "Close", children: /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Close", onClick: onClose, ...{ [OVERLAY_CLOSE_ATTR]: "" }, children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 16 }) }) })
9620
+ /* @__PURE__ */ jsx(Tooltip, { content: "Close", children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": "Close", onClick: onClose, ...{ [OVERLAY_CLOSE_ATTR]: "" }, children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 16 }) }) })
9533
9621
  ] })
9534
9622
  ]
9535
9623
  }
@@ -10766,15 +10854,15 @@ var TimeViewport = forwardRef(function TimeViewport2({
10766
10854
  const hasHeader = headerNode != null || labelHeader != null;
10767
10855
  const bar = toolbar === false ? null : /* @__PURE__ */ jsxs("div", { className: cn("flex shrink-0 flex-wrap items-center gap-ds-1 px-ds-2 py-ds-1.5 border-b-[length:var(--ds-border-width)]", border), children: [
10768
10856
  onZoomChange && levels.length > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
10769
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Zoom out", "aria-disabled": !canZoomOut || void 0, className: "aria-disabled:opacity-disabled aria-disabled:cursor-not-allowed", onClick: () => zoom(1), children: /* @__PURE__ */ jsx(Icon, { name: "minus", size: 14 }) }),
10857
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": "Zoom out", "aria-disabled": !canZoomOut || void 0, className: "aria-disabled:opacity-disabled aria-disabled:cursor-not-allowed", onClick: () => zoom(1), children: /* @__PURE__ */ jsx(Icon, { name: "minus", size: 14 }) }),
10770
10858
  /* @__PURE__ */ jsx("span", { "aria-live": "polite", className: "min-w-[4.5rem] text-center text-xs text-[var(--ds-text-secondary)]", children: ZOOM_NAMES[scale.zoom] }),
10771
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Zoom in", "aria-disabled": !canZoomIn || void 0, className: "aria-disabled:opacity-disabled aria-disabled:cursor-not-allowed", onClick: () => zoom(-1), children: /* @__PURE__ */ jsx(Icon, { name: "plus", size: 14 }) })
10859
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": "Zoom in", "aria-disabled": !canZoomIn || void 0, className: "aria-disabled:opacity-disabled aria-disabled:cursor-not-allowed", onClick: () => zoom(-1), children: /* @__PURE__ */ jsx(Icon, { name: "plus", size: 14 }) })
10772
10860
  ] }),
10773
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Previous period", onClick: () => api.page(-1), children: /* @__PURE__ */ jsx(Icon, { name: "chevron-left", size: 14 }) }),
10774
- /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", disabled: todayAtX === null, onClick: () => {
10861
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": "Previous period", onClick: () => api.page(-1), children: /* @__PURE__ */ jsx(Icon, { name: "chevron-left", size: 14 }) }),
10862
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", disabled: todayAtX === null, onClick: () => {
10775
10863
  if (todayAtX !== null) setLeft(todayAtX - visibleWidth / 2);
10776
10864
  }, children: "Today" }),
10777
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Next period", onClick: () => api.page(1), children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14 }) }),
10865
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", "aria-label": "Next period", onClick: () => api.page(1), children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14 }) }),
10778
10866
  toolbar
10779
10867
  ] });
10780
10868
  return (
@@ -11031,16 +11119,16 @@ var SELECTED = "outline-solid outline-2 outline-offset-2 outline-[var(--ds-text-
11031
11119
  var UNDATED_INSET = 16;
11032
11120
  var NO_GROUPS = [];
11033
11121
  var NO_STATUSES = {};
11034
- var warned = /* @__PURE__ */ new Set();
11035
- function warnOnce(message) {
11122
+ var warned2 = /* @__PURE__ */ new Set();
11123
+ function warnOnce2(message) {
11036
11124
  let dev = false;
11037
11125
  try {
11038
11126
  dev = process.env.NODE_ENV !== "production";
11039
11127
  } catch {
11040
11128
  dev = false;
11041
11129
  }
11042
- if (!dev || warned.has(message)) return;
11043
- warned.add(message);
11130
+ if (!dev || warned2.has(message)) return;
11131
+ warned2.add(message);
11044
11132
  console.warn(`Gantt: ${message}`);
11045
11133
  }
11046
11134
  function GanttArrows({ arrows, width, height }) {
@@ -11122,7 +11210,7 @@ function Gantt({
11122
11210
  const selected = isControlled ? selectedId ?? null : selectedState;
11123
11211
  useEffect(() => {
11124
11212
  if (overridden !== null) {
11125
- warnOnce("selectedId was given a value after the Gantt had kept a selection of its own. To control the selection, pass selectedId from the first render (null for none); to let the Gantt keep it, leave selectedId out.");
11213
+ warnOnce2("selectedId was given a value after the Gantt had kept a selection of its own. To control the selection, pass selectedId from the first render (null for none); to let the Gantt keep it, leave selectedId out.");
11126
11214
  }
11127
11215
  }, [overridden]);
11128
11216
  const select = (item) => {
@@ -11134,8 +11222,8 @@ function Gantt({
11134
11222
  const phases = useMemo(() => uniqueById(groups), [groups]);
11135
11223
  useEffect(() => {
11136
11224
  const quoted = (ids) => ids.map((id) => `"${id}"`).join(", ");
11137
- if (plan.repeated.length) warnOnce(`more than one item has the id ${quoted(plan.repeated)}; only the first of each is shown.`);
11138
- if (phases.repeated.length) warnOnce(`more than one group has the id ${quoted(phases.repeated)}; only the first of each is shown.`);
11225
+ if (plan.repeated.length) warnOnce2(`more than one item has the id ${quoted(plan.repeated)}; only the first of each is shown.`);
11226
+ if (phases.repeated.length) warnOnce2(`more than one group has the id ${quoted(phases.repeated)}; only the first of each is shown.`);
11139
11227
  }, [plan, phases]);
11140
11228
  const tasks = plan.unique;
11141
11229
  const extent = useMemo(() => itemsExtent(tasks), [tasks]);
@@ -12032,14 +12120,17 @@ function TabsRoot(props) {
12032
12120
  const { tabs, mobile = "scroll" } = props;
12033
12121
  const collapseToDropdown = isMobile && (orientation === "responsive" || mobile === "dropdown");
12034
12122
  if (collapseToDropdown) {
12035
- return /* @__PURE__ */ jsx(
12036
- Dropdown,
12037
- {
12038
- options: tabs.filter((t) => !t.disabled).map((t) => ({ id: t.id, label: t.label, group: t.group })),
12039
- value: activeTab,
12040
- onChange: (v) => onTabChange(v),
12041
- className
12042
- }
12123
+ return (
12124
+ // choosing a tab is navigation, never a change to a form around it (0.21.0)
12125
+ /* @__PURE__ */ jsx(FormChangeCtx, { value: null, children: /* @__PURE__ */ jsx(
12126
+ Dropdown,
12127
+ {
12128
+ options: tabs.filter((t) => !t.disabled).map((t) => ({ id: t.id, label: t.label, group: t.group })),
12129
+ value: activeTab,
12130
+ onChange: (v) => onTabChange(v),
12131
+ className
12132
+ }
12133
+ ) })
12043
12134
  );
12044
12135
  }
12045
12136
  if (effectiveOrientation === "vertical") {
@@ -12141,7 +12232,7 @@ function SettingsLayout({
12141
12232
  return (
12142
12233
  // gap-x-ds-8 is the rail's gap, unused in one column: the threshold reads the same gap as the row (useColumnGap)
12143
12234
  /* @__PURE__ */ jsxs("div", { ref: rootRef, "data-settings-layout": "sidebar", "data-collapsed": "true", className: cn("flex flex-col gap-y-ds-6 gap-x-ds-8", className), children: [
12144
- /* @__PURE__ */ jsx(
12235
+ /* @__PURE__ */ jsx(FormChangeCtx, { value: null, children: /* @__PURE__ */ jsx(
12145
12236
  Dropdown,
12146
12237
  {
12147
12238
  "aria-label": ariaLabel,
@@ -12149,7 +12240,7 @@ function SettingsLayout({
12149
12240
  value,
12150
12241
  onChange: (v) => onChange(v)
12151
12242
  }
12152
- ),
12243
+ ) }),
12153
12244
  /* @__PURE__ */ jsx("div", { "data-settings-panel": "", className: PANEL_CLASSES, style: PANEL_STYLE, children })
12154
12245
  ] })
12155
12246
  );
@@ -12435,6 +12526,7 @@ function Popover({ open, onOpenChange, trigger, children, side = "bottom", align
12435
12526
  /* @__PURE__ */ jsx(
12436
12527
  Slot,
12437
12528
  {
12529
+ ...triggerType(trigger),
12438
12530
  ref: setTriggerNode,
12439
12531
  onClick: () => {
12440
12532
  setTriggerWidth(triggerRef.current?.offsetWidth);
@@ -12507,7 +12599,7 @@ function LoadMore({ hasMore, onLoadMore, loadMoreLabel, currentPage, scroll, cla
12507
12599
  if (!hasMore) return null;
12508
12600
  return /* @__PURE__ */ jsxs("div", { "data-ds-pager": "", "data-pagination-mode": scroll ? "infinite-scroll" : "load-more", className: cn("flex flex-col items-center", className), children: [
12509
12601
  scroll && /* @__PURE__ */ jsx("div", { ref: marker, "data-load-more-trigger": "", "aria-hidden": "true", className: "h-px w-full" }),
12510
- /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: onLoadMore, children: loadMoreLabel ?? "Load more" })
12602
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", onClick: onLoadMore, children: loadMoreLabel ?? "Load more" })
12511
12603
  ] });
12512
12604
  }
12513
12605
  function PageNumbers({ currentPage, totalPages, onPageChange, pageSize: pageSizeProp, pageSizeOptions, onPageSizeChange, className }) {
@@ -12517,7 +12609,7 @@ function PageNumbers({ currentPage, totalPages, onPageChange, pageSize: pageSize
12517
12609
  return /* @__PURE__ */ jsxs("nav", { "aria-label": "Pagination", "data-ds-pager": "", className: cn("flex flex-wrap items-center gap-ds-3", className), children: [
12518
12610
  pageSizeOptions && pageSize != null && onPageSizeChange && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2", children: [
12519
12611
  /* @__PURE__ */ jsx("span", { className: "text-sm text-[var(--ds-text-muted)] whitespace-nowrap", children: "Rows per page" }),
12520
- /* @__PURE__ */ jsx(
12612
+ /* @__PURE__ */ jsx(FormChangeCtx, { value: null, children: /* @__PURE__ */ jsx(
12521
12613
  Dropdown,
12522
12614
  {
12523
12615
  options: pageSizeOptions.map((n) => ({ id: String(n), label: String(n) })),
@@ -12526,7 +12618,7 @@ function PageNumbers({ currentPage, totalPages, onPageChange, pageSize: pageSize
12526
12618
  size: "sm",
12527
12619
  className: "w-[4.5rem]"
12528
12620
  }
12529
- )
12621
+ ) })
12530
12622
  ] }),
12531
12623
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-1", children: [
12532
12624
  /* @__PURE__ */ jsx(
@@ -13898,6 +13990,16 @@ function useToastOptional() {
13898
13990
  }
13899
13991
  var toastSeq = 0;
13900
13992
  var heardSeq = 0;
13993
+ var HEARD_MS = 7e3;
13994
+ var RUN_END_MS = 20;
13995
+ var isThenable2 = (value) => !!value && typeof value.then === "function";
13996
+ function textOf(node) {
13997
+ if (node == null || typeof node === "boolean") return "";
13998
+ if (typeof node === "string" || typeof node === "number") return String(node);
13999
+ if (Array.isArray(node)) return node.map(textOf).join("");
14000
+ if (typeof node === "object" && "props" in node) return textOf(node.props.children);
14001
+ return "";
14002
+ }
13901
14003
  var mergeable = (e) => !e.action && e.variant !== "error";
13902
14004
  function fill(entries, slots) {
13903
14005
  let shown = entries.filter((e) => e.shown).length;
@@ -13917,14 +14019,18 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
13917
14019
  const [list, setList] = useState([]);
13918
14020
  const entries = useRef([]);
13919
14021
  const isMobile = useBreakpoint("sm");
13920
- const createdIds = useRef([]);
14022
+ const shownDuringRuns = useRef([]);
14023
+ const showSeq = useRef(0);
14024
+ const runs = useRef(/* @__PURE__ */ new Map());
14025
+ const runSeq = useRef(0);
13921
14026
  const [heard, setHeard] = useState([]);
13922
14027
  const [hovered, setHovered] = useState(false);
13923
14028
  const [focusInside, setFocusInside] = useState(false);
13924
14029
  const paused = pauseOnHover && hovered || focusInside;
13925
14030
  const stackRef = useRef(null);
13926
14031
  const returnTo = useRef(null);
13927
- const answering = useRef([]);
14032
+ const pressed = useRef(/* @__PURE__ */ new Set());
14033
+ const pointerAt = useRef(0);
13928
14034
  const viaKey = useRef(false);
13929
14035
  const refocus = useRef(false);
13930
14036
  const pendingHeard = useRef(/* @__PURE__ */ new Map());
@@ -13943,58 +14049,75 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
13943
14049
  entries.current = fill(next, slots);
13944
14050
  setList(entries.current);
13945
14051
  }, [slots]);
13946
- const [slotsSeen, setSlotsSeen] = useState(slots);
13947
- if (slots !== slotsSeen) {
13948
- setSlotsSeen(slots);
14052
+ useEffect(() => {
13949
14053
  let kept = 0;
13950
- setList(fill(list.map((e) => e.shown && ++kept > slots ? { ...e, shown: false } : e), slots));
14054
+ const reslotted = entries.current.map((e) => e.shown && ++kept > slots ? { ...e, shown: false } : e);
14055
+ const next = fill(reslotted, slots);
14056
+ if (next.length === entries.current.length && next.every((e, i) => e === entries.current[i])) return;
14057
+ commit(next);
14058
+ }, [slots, commit]);
14059
+ if (list.every((t) => !t.shown) && (hovered || focusInside)) {
14060
+ setHovered(false);
14061
+ setFocusInside(false);
13951
14062
  }
13952
- useEffect(() => {
13953
- entries.current = list;
13954
- }, [list]);
14063
+ const clearTimers = useRef(/* @__PURE__ */ new Set());
13955
14064
  const announce = useCallback((entry) => {
13956
14065
  pendingHeard.current.set(entry.id, entry);
13957
14066
  if (heardTimer.current) return;
13958
14067
  heardTimer.current = setTimeout(() => {
13959
14068
  heardTimer.current = void 0;
13960
- const batch = [...pendingHeard.current.values()];
14069
+ const batch = [...pendingHeard.current.values()].map((e) => {
14070
+ const words = textOf(e.message).trim();
14071
+ const sentence = !words || /[.!?…]$/.test(words) ? words : `${words}.`;
14072
+ return { id: `heard-${++heardSeq}`, assertive: e.variant === "error", text: e.action ? `${sentence} Press F8 to reach ${e.action.label}.`.trim() : sentence };
14073
+ }).filter((h) => h.text);
13961
14074
  pendingHeard.current.clear();
13962
- setHeard((list2) => [...list2, ...batch.map((e) => ({
13963
- id: `heard-${++heardSeq}`,
13964
- assertive: e.variant === "error",
13965
- node: /* @__PURE__ */ jsxs(Fragment, { children: [
13966
- e.message,
13967
- e.action ? ` Press F8 to reach ${e.action.label}.` : ""
13968
- ] })
13969
- }))].slice(-5));
14075
+ if (!batch.length) return;
14076
+ setHeard((list2) => [...list2, ...batch].slice(-5));
14077
+ const ids = new Set(batch.map((h) => h.id));
14078
+ const timer = setTimeout(() => {
14079
+ clearTimers.current.delete(timer);
14080
+ setHeard((list2) => list2.filter((h) => !ids.has(h.id)));
14081
+ }, HEARD_MS);
14082
+ clearTimers.current.add(timer);
13970
14083
  }, 0);
13971
14084
  }, []);
13972
14085
  useEffect(() => () => {
13973
14086
  if (heardTimer.current) clearTimeout(heardTimer.current);
14087
+ heardTimer.current = void 0;
14088
+ clearTimers.current.forEach(clearTimeout);
14089
+ clearTimers.current.clear();
13974
14090
  }, []);
13975
- const leaving = useCallback(() => {
13976
- if (!stackRef.current?.contains(document.activeElement)) return;
14091
+ const leaving = useCallback((id) => {
14092
+ const stack = stackRef.current;
14093
+ const holder = id === void 0 ? stack : [...stack?.querySelectorAll("[data-toast-id]") ?? []].find((n) => n.getAttribute("data-toast-id") === id);
14094
+ if (!holder?.contains(document.activeElement)) return;
13977
14095
  refocus.current = true;
13978
14096
  setFocusInside(false);
13979
14097
  }, []);
13980
14098
  const remove = useCallback((id) => {
13981
- leaving();
14099
+ leaving(id);
13982
14100
  commit(entries.current.filter((t) => t.id !== id));
13983
14101
  }, [commit, leaving]);
13984
14102
  const dismissAll = useCallback(() => {
13985
14103
  leaving();
13986
14104
  commit([]);
13987
14105
  }, [commit, leaving]);
14106
+ const noteShown = useCallback((id, variant, action, fresh) => {
14107
+ if (runs.current.size) shownDuringRuns.current.push({ seq: ++showSeq.current, at: Date.now(), id, variant, action, fresh });
14108
+ }, []);
13988
14109
  const update = useCallback((id, message, options = {}) => {
13989
14110
  const list2 = entries.current;
13990
14111
  const at = list2.findIndex((t2) => t2.id === id);
13991
14112
  if (at === -1) return;
14113
+ pressed.current.delete(id);
13992
14114
  const t = list2[at];
13993
14115
  const next = [...list2];
13994
14116
  next[at] = { ...t, message, variant: options.variant ?? t.variant, duration: options.duration ?? t.duration, action: options.action ?? t.action, actionWindow: options.actionWindow ?? t.actionWindow, key: options.key ?? t.key, version: t.version + 1 };
13995
14117
  commit(next);
13996
14118
  announce(next[at]);
13997
- }, [commit, announce]);
14119
+ noteShown(id, next[at].variant, !!next[at].action, false);
14120
+ }, [commit, announce, noteShown]);
13998
14121
  const toast = useCallback((message, options = {}) => {
13999
14122
  const list2 = entries.current;
14000
14123
  const base = { message, variant: options.variant ?? "default", duration: options.duration ?? duration, action: options.action, actionWindow: options.actionWindow, key: options.key };
@@ -14003,41 +14126,31 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
14003
14126
  if (at !== -1) {
14004
14127
  const next = [...list2];
14005
14128
  next[at] = { ...list2[at], ...base, version: list2[at].version + 1 };
14006
- createdIds.current.push(options.id);
14129
+ pressed.current.delete(options.id);
14007
14130
  commit(next);
14008
14131
  announce(next[at]);
14132
+ noteShown(options.id, base.variant, !!base.action, false);
14009
14133
  return options.id;
14010
14134
  }
14011
14135
  }
14012
- while (answering.current.length) {
14013
- const answered = answering.current.shift();
14014
- const at = list2.findIndex((t) => t.id === answered);
14015
- if (at === -1) continue;
14016
- const next = [...list2];
14017
- next[at] = { ...list2[at], ...base, version: list2[at].version + 1 };
14018
- createdIds.current.push(answered);
14019
- commit(next);
14020
- announce(next[at]);
14021
- return answered;
14022
- }
14023
14136
  if (coalesce && base.key && mergeable(base)) {
14024
14137
  const at = list2.findIndex((t) => t.key === base.key && mergeable(t));
14025
14138
  if (at !== -1) {
14026
14139
  const next = [...list2];
14027
14140
  next[at] = { ...list2[at], ...base, version: list2[at].version + 1 };
14028
- createdIds.current.push(list2[at].id);
14029
14141
  commit(next);
14030
14142
  announce(next[at]);
14143
+ noteShown(list2[at].id, base.variant, !!base.action, false);
14031
14144
  return list2[at].id;
14032
14145
  }
14033
14146
  }
14034
14147
  const id = options.id ?? `toast-${++toastSeq}`;
14035
14148
  const entry = { id, ...base, shown: false, version: 0 };
14036
- createdIds.current.push(id);
14037
14149
  commit([...list2, entry]);
14038
14150
  announce(entry);
14151
+ noteShown(id, base.variant, !!base.action, true);
14039
14152
  return id;
14040
- }, [duration, coalesce, commit, announce]);
14153
+ }, [duration, coalesce, commit, announce, noteShown]);
14041
14154
  const api = useMemo(() => ({
14042
14155
  toast,
14043
14156
  success: (m, o) => toast(m, { ...o, variant: "success" }),
@@ -14059,34 +14172,53 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
14059
14172
  }
14060
14173
  }), [toast, update, remove, dismissAll, duration]);
14061
14174
  const ledger = useMemo(() => ({
14062
- created: () => createdIds.current.length,
14063
- latestSince: (count) => {
14064
- if (createdIds.current.length <= count) return void 0;
14065
- const id = createdIds.current[createdIds.current.length - 1];
14066
- const entry = entries.current.find((t) => t.id === id);
14067
- return entry ? { id, message: entry.message } : void 0;
14068
- }
14175
+ begin: () => {
14176
+ const run = ++runSeq.current;
14177
+ const overlapped = runs.current.size > 0;
14178
+ if (overlapped) runs.current.forEach((r) => {
14179
+ r.overlapped = true;
14180
+ });
14181
+ runs.current.set(run, { from: showSeq.current, overlapped });
14182
+ return run;
14183
+ },
14184
+ end: (run) => {
14185
+ const watched = runs.current.get(run);
14186
+ runs.current.delete(run);
14187
+ if (!watched) return { problem: false };
14188
+ const during = shownDuringRuns.current.filter((s) => s.seq > watched.from);
14189
+ const oldest = Math.min(...[...runs.current.values()].map((r) => r.from));
14190
+ shownDuringRuns.current = runs.current.size ? shownDuringRuns.current.filter((s) => s.seq > oldest) : [];
14191
+ const problem = during.some((s) => s.variant === "error" || s.variant === "warning");
14192
+ const endedAt = Date.now();
14193
+ const confirmations = during.filter((s) => s.fresh && !s.action && (s.variant === "success" || s.variant === "default"));
14194
+ if (watched.overlapped || confirmations.length !== 1 || endedAt - confirmations[0].at > RUN_END_MS) return { problem };
14195
+ const entry = entries.current.find((t) => t.id === confirmations[0].id);
14196
+ return { problem, own: entry && !entry.action ? { id: entry.id, message: entry.message, variant: entry.variant } : void 0 };
14197
+ },
14198
+ has: (id) => entries.current.some((t) => t.id === id)
14069
14199
  }), []);
14070
14200
  const pressAction = useCallback((id) => {
14071
14201
  const entry = entries.current.find((t) => t.id === id);
14072
14202
  if (!entry?.action) return;
14073
14203
  const run = entry.action.onClick;
14074
- leaving();
14204
+ leaving(id);
14075
14205
  commit(entries.current.map((t) => t.id === id ? { ...t, action: void 0, actionWindow: void 0, version: t.version + 1 } : t));
14076
- answering.current.push(id);
14206
+ pressed.current.add(id);
14077
14207
  const done = () => {
14078
- const waiting2 = answering.current.indexOf(id);
14079
- if (waiting2 === -1) return;
14080
- answering.current.splice(waiting2, 1);
14081
- remove(id);
14208
+ if (pressed.current.delete(id)) remove(id);
14209
+ };
14210
+ const failed = (err) => {
14211
+ done();
14212
+ console.error(err);
14082
14213
  };
14083
14214
  let result;
14084
14215
  try {
14085
14216
  result = run();
14086
- } catch {
14087
- result = void 0;
14217
+ } catch (err) {
14218
+ failed(err);
14219
+ return;
14088
14220
  }
14089
- if (result && typeof result.then === "function") result.then(done, done);
14221
+ if (isThenable2(result)) result.then(done, failed);
14090
14222
  else done();
14091
14223
  }, [commit, leaving, remove]);
14092
14224
  const lapse = useCallback((id) => {
@@ -14120,8 +14252,11 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
14120
14252
  const back = returnTo.current;
14121
14253
  const next = stackRef.current?.querySelector("button");
14122
14254
  const main = document.querySelector("main");
14255
+ const byPointer = Date.now() - pointerAt.current < 1e3;
14123
14256
  if (back?.isConnected) back.focus();
14124
- else if (next) next.focus();
14257
+ else if (byPointer) {
14258
+ if (stackRef.current?.contains(document.activeElement)) document.activeElement.blur();
14259
+ } else if (next) next.focus();
14125
14260
  else if (main) {
14126
14261
  if (!main.hasAttribute("tabindex")) {
14127
14262
  main.setAttribute("tabindex", "-1");
@@ -14136,8 +14271,8 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
14136
14271
  const hasBottomNav = isMobile && bottomNavs > 0;
14137
14272
  return /* @__PURE__ */ jsx(ToastContext.Provider, { value: api, children: /* @__PURE__ */ jsxs(ToastLedgerCtx.Provider, { value: ledger, children: [
14138
14273
  /* @__PURE__ */ jsx(BottomNavPresenceCtx.Provider, { value: registerBottomNav, children }),
14139
- /* @__PURE__ */ jsx("div", { className: "sr-only", role: "status", "aria-live": "polite", "data-toast-announcer": "polite", children: heard.filter((h) => !h.assertive).map((h) => /* @__PURE__ */ jsx("p", { children: h.node }, h.id)) }),
14140
- /* @__PURE__ */ jsx("div", { className: "sr-only", role: "alert", "aria-live": "assertive", "data-toast-announcer": "assertive", children: heard.filter((h) => h.assertive).map((h) => /* @__PURE__ */ jsx("p", { children: h.node }, h.id)) }),
14274
+ /* @__PURE__ */ jsx("div", { className: "sr-only", "aria-live": "polite", "data-toast-announcer": "polite", children: heard.filter((h) => !h.assertive).map((h) => /* @__PURE__ */ jsx("p", { children: h.text }, h.id)) }),
14275
+ /* @__PURE__ */ jsx("div", { className: "sr-only", "aria-live": "assertive", "data-toast-announcer": "assertive", children: heard.filter((h) => h.assertive).map((h) => /* @__PURE__ */ jsx("p", { children: h.text }, h.id)) }),
14141
14276
  shown.length > 0 && createPortal(
14142
14277
  /* @__PURE__ */ jsxs(
14143
14278
  "div",
@@ -14150,6 +14285,9 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
14150
14285
  "data-toast-stack": "",
14151
14286
  onMouseEnter: () => setHovered(true),
14152
14287
  onMouseLeave: () => setHovered(false),
14288
+ onPointerDownCapture: () => {
14289
+ pointerAt.current = Date.now();
14290
+ },
14153
14291
  onFocusCapture: (e) => {
14154
14292
  setFocusInside(true);
14155
14293
  const from = e.relatedTarget;
@@ -14166,7 +14304,7 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
14166
14304
  ),
14167
14305
  style: hasBottomNav ? { bottom: "calc(var(--ds-space-12) + var(--ds-space-2) + var(--ds-space-4) + env(safe-area-inset-bottom))" } : void 0,
14168
14306
  children: [
14169
- shown.map((t) => /* @__PURE__ */ jsx("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx(
14307
+ shown.map((t) => /* @__PURE__ */ jsx("div", { "data-toast-id": t.id, className: "pointer-events-auto", children: /* @__PURE__ */ jsx(
14170
14308
  Toast,
14171
14309
  {
14172
14310
  inline: true,
@@ -15963,7 +16101,9 @@ function Dialog({ open, onClose: onCloseProp, title, description, children, foot
15963
16101
  "form",
15964
16102
  {
15965
16103
  onSubmit: (e) => {
16104
+ if (e.target !== e.currentTarget) return;
15966
16105
  e.preventDefault();
16106
+ e.stopPropagation();
15967
16107
  if (variant === "destructive") return;
15968
16108
  const submitter = e.nativeEvent.submitter;
15969
16109
  const implicit = !submitter || submitter === defaultButtonRef.current;
@@ -15992,6 +16132,7 @@ function Dialog({ open, onClose: onCloseProp, title, description, children, foot
15992
16132
  {
15993
16133
  ref: dialogRef,
15994
16134
  role: alert ? "alertdialog" : "dialog",
16135
+ ...{ [OVERLAY_ROOT_ATTR]: "" },
15995
16136
  "aria-modal": "true",
15996
16137
  "data-gs": "dialog",
15997
16138
  "aria-labelledby": titleId,
@@ -16032,7 +16173,7 @@ function Dialog({ open, onClose: onCloseProp, title, description, children, foot
16032
16173
  }
16033
16174
  ),
16034
16175
  description && /* @__PURE__ */ jsx("p", { id: descriptionId, className: "mt-ds-2 text-sm text-[var(--ds-text-muted)]", children: description }),
16035
- children && /* @__PURE__ */ jsx("div", { className: "mt-ds-4", children: /* @__PURE__ */ jsx(OverlayMarkCtx, { value: mark, children: /* @__PURE__ */ jsx(OverlayCloseCtx, { value: onClose, children }) }) })
16176
+ children && /* @__PURE__ */ jsx("div", { className: "mt-ds-4", children: /* @__PURE__ */ jsx(OverlayMarkScope, { mark, children: /* @__PURE__ */ jsx(OverlayCloseCtx, { value: onClose, children }) }) })
16036
16177
  ] })
16037
16178
  ] }) }),
16038
16179
  /* @__PURE__ */ jsx(DiscardConfirm, { open: guard.confirming, onKeep: guard.keep, onDiscard: guard.discard }),
@@ -16193,7 +16334,7 @@ var Navbar = forwardRef(function Navbar2({
16193
16334
  }
16194
16335
  focus.onExit?.();
16195
16336
  };
16196
- const exitControl = focus ? /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: requestExit, children: focus.exitLabel ?? (focus.kind === "before-app" ? "Save and log out" : "Exit") }) : null;
16337
+ const exitControl = focus ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: requestExit, children: focus.exitLabel ?? (focus.kind === "before-app" ? "Save and log out" : "Exit") }) : null;
16197
16338
  const shownUserMenu = focus?.kind === "before-app" ? null : userMenu;
16198
16339
  const asking = confirmExit && focus?.kind === "in-app" && !!appShell?.hasUnsavedChanges;
16199
16340
  if (confirmExit && !asking) setConfirmExit(false);
@@ -16209,8 +16350,8 @@ var Navbar = forwardRef(function Navbar2({
16209
16350
  alert: true,
16210
16351
  container: shellEl ?? void 0,
16211
16352
  footer: /* @__PURE__ */ jsxs(Fragment, { children: [
16212
- /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => setConfirmExit(false), children: "Keep editing" }),
16213
- /* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: () => {
16353
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", onClick: () => setConfirmExit(false), children: "Keep editing" }),
16354
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", onClick: () => {
16214
16355
  setConfirmExit(false);
16215
16356
  focus.onExit?.();
16216
16357
  }, children: "Discard changes" })
@@ -17410,42 +17551,65 @@ function useDestructiveAction(options) {
17410
17551
  };
17411
17552
  const failureMessage = (err) => err instanceof Error && err.message ? err.message : `${title.replace(/\?$/, "")} failed. Try again.`;
17412
17553
  const done = doneMessage ?? undoMessage ?? pastTenseOf(label);
17554
+ const watch = async (step) => {
17555
+ const run2 = ledger?.begin();
17556
+ try {
17557
+ await step();
17558
+ } catch (err) {
17559
+ if (run2 !== void 0) ledger.end(run2);
17560
+ throw err;
17561
+ }
17562
+ return run2 !== void 0 ? ledger.end(run2) : { problem: false };
17563
+ };
17413
17564
  const confirm = async () => {
17414
17565
  setBusy(true);
17415
- const before = ledger?.created() ?? 0;
17416
17566
  try {
17417
- await onConfirm();
17567
+ const seen = await watch(onConfirm);
17418
17568
  close();
17419
- if (toast && (ledger?.created() ?? 0) === before) toast.success(done);
17569
+ if (toast && !seen.problem && !seen.own) toast.success(done);
17420
17570
  } catch (err) {
17421
17571
  toast?.error(failureMessage(err));
17422
17572
  } finally {
17423
17573
  setBusy(false);
17424
17574
  }
17425
17575
  };
17426
- const undo = async () => {
17576
+ const undo = async (id) => {
17577
+ const here = () => !!toast && (ledger?.has(id) ?? false);
17427
17578
  try {
17428
- await onUndo();
17429
- toast?.success(restoredMessage ?? "Restored");
17579
+ const seen = await watch(onUndo);
17580
+ if (seen.problem) {
17581
+ if (here()) toast.dismiss(id);
17582
+ } else if (seen.own) {
17583
+ if (here()) {
17584
+ toast.update(id, seen.own.message, { variant: seen.own.variant });
17585
+ toast.dismiss(seen.own.id);
17586
+ }
17587
+ } else if (here()) toast.update(id, restoredMessage ?? "Restored", { variant: "success" });
17588
+ else toast?.success(restoredMessage ?? "Restored");
17430
17589
  } catch (err) {
17431
- toast?.error(err instanceof Error && err.message ? err.message : "Undo didn't work. Try again.");
17590
+ const message = err instanceof Error && err.message ? err.message : "Undo didn't work. Try again.";
17591
+ if (here()) toast.update(id, message, { variant: "error" });
17592
+ else toast?.error(message);
17432
17593
  }
17433
17594
  };
17434
17595
  const run = () => {
17435
17596
  if (undoable) {
17436
17597
  void (async () => {
17437
- const before = ledger?.created() ?? 0;
17598
+ let seen;
17438
17599
  try {
17439
- await onConfirm();
17600
+ seen = await watch(onConfirm);
17440
17601
  } catch (err) {
17441
17602
  toast.error(failureMessage(err));
17442
17603
  return;
17443
17604
  }
17605
+ if (seen.problem) return;
17444
17606
  const seconds = preferences.undoWindowSeconds ?? 5;
17445
- const undoAction = { label: "Undo", onClick: () => undo() };
17446
- const pages = (ledger?.created() ?? 0) > before ? ledger?.latestSince(before) : void 0;
17447
- if (pages) toast.update(pages.id, pages.message, { variant: "success", action: undoAction, actionWindow: seconds * 1e3 });
17448
- else toast.success(done, { action: undoAction, actionWindow: seconds * 1e3 });
17607
+ let id = "";
17608
+ const undoAction = { label: "Undo", onClick: () => undo(id) };
17609
+ if (seen.own) {
17610
+ id = seen.own.id;
17611
+ toast.update(id, seen.own.message, { action: undoAction, actionWindow: seconds * 1e3 });
17612
+ } else id = toast.success(done, { action: undoAction, actionWindow: seconds * 1e3 });
17449
17613
  })();
17450
17614
  return;
17451
17615
  }
@@ -17461,8 +17625,8 @@ function useDestructiveAction(options) {
17461
17625
  description,
17462
17626
  variant: "destructive",
17463
17627
  footer: /* @__PURE__ */ jsxs(Fragment, { children: [
17464
- /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: close, disabled: busy, children: "Cancel" }),
17465
- /* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: () => void confirm(), disabled: !matches2 || busy, loading: busy, children: label })
17628
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", onClick: close, disabled: busy, children: "Cancel" }),
17629
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", onClick: () => void confirm(), disabled: !matches2 || busy, loading: busy, children: label })
17466
17630
  ] }),
17467
17631
  children: typedRequired && /* @__PURE__ */ jsx(Field, { label: /* @__PURE__ */ jsxs(Fragment, { children: [
17468
17632
  "Type ",
@@ -17511,7 +17675,7 @@ var PAST_TENSE = {
17511
17675
  reject: "Rejected",
17512
17676
  decline: "Declined"
17513
17677
  };
17514
- var warned2 = /* @__PURE__ */ new Set();
17678
+ var warned3 = /* @__PURE__ */ new Set();
17515
17679
  function pastTenseOf(label) {
17516
17680
  const words = label.trim().toLowerCase().split(/\s+/);
17517
17681
  const found = PAST_TENSE[words.slice(0, 2).join(" ")] ?? PAST_TENSE[words[0] ?? ""];
@@ -17522,8 +17686,8 @@ function pastTenseOf(label) {
17522
17686
  } catch {
17523
17687
  dev = false;
17524
17688
  }
17525
- if (dev && !warned2.has(label)) {
17526
- warned2.add(label);
17689
+ if (dev && !warned3.has(label)) {
17690
+ warned3.add(label);
17527
17691
  console.warn(`useDestructiveAction: "${label}" isn't a verb the hook knows; pass doneMessage so the confirmation says what happened.`);
17528
17692
  }
17529
17693
  return "Done";