gridsmith-ui 0.20.0 → 0.22.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.
Files changed (48) hide show
  1. package/dist/chat.js +3 -3
  2. package/dist/chunk-2RV23OBK.js +1349 -0
  3. package/dist/chunk-2RV23OBK.js.map +1 -0
  4. package/dist/chunk-7TOJ4G7S.js +197 -0
  5. package/dist/chunk-7TOJ4G7S.js.map +1 -0
  6. package/dist/{chunk-6MXHGH3D.js → chunk-AAYCQXZQ.js} +90 -161
  7. package/dist/chunk-AAYCQXZQ.js.map +1 -0
  8. package/dist/{chunk-ILIODA7W.js → chunk-CXMV7SXJ.js} +4 -3
  9. package/dist/chunk-CXMV7SXJ.js.map +1 -0
  10. package/dist/{chunk-EQDR57AM.js → chunk-EYMGLK4M.js} +3 -3
  11. package/dist/{chunk-EQDR57AM.js.map → chunk-EYMGLK4M.js.map} +1 -1
  12. package/dist/chunk-IVCVNAXG.js +115 -0
  13. package/dist/chunk-IVCVNAXG.js.map +1 -0
  14. package/dist/chunk-RG4KUCRS.js +9 -0
  15. package/dist/chunk-RG4KUCRS.js.map +1 -0
  16. package/dist/{chunk-JXANCXK6.js → chunk-RT7RYGC3.js} +39 -4
  17. package/dist/chunk-RT7RYGC3.js.map +1 -0
  18. package/dist/{chunk-XYDIBW3J.js → chunk-SBTBMAIU.js} +4 -3
  19. package/dist/chunk-SBTBMAIU.js.map +1 -0
  20. package/dist/data-grid.d.ts +38 -4
  21. package/dist/data-grid.js +304 -264
  22. package/dist/data-grid.js.map +1 -1
  23. package/dist/editor.d.ts +6 -5
  24. package/dist/editor.js +47 -11
  25. package/dist/editor.js.map +1 -1
  26. package/dist/index.d.ts +120 -153
  27. package/dist/index.js +1156 -1814
  28. package/dist/index.js.map +1 -1
  29. package/dist/kanban.js +2 -2
  30. package/dist/resizable.js +9 -8
  31. package/dist/resizable.js.map +1 -1
  32. package/dist/roots.css +10 -0
  33. package/dist/{table-utils-GounRE6s.d.ts → table-utils-BkjNEWts.d.ts} +5 -1
  34. package/dist/use-pagination-DiJo0aW5.d.ts +153 -0
  35. package/dist/virtualized.d.ts +11 -1
  36. package/dist/virtualized.js +67 -18
  37. package/dist/virtualized.js.map +1 -1
  38. package/package.json +1 -1
  39. package/dist/chunk-6MXHGH3D.js.map +0 -1
  40. package/dist/chunk-ILIODA7W.js.map +0 -1
  41. package/dist/chunk-JN37OVTP.js +0 -67
  42. package/dist/chunk-JN37OVTP.js.map +0 -1
  43. package/dist/chunk-JXANCXK6.js.map +0 -1
  44. package/dist/chunk-TKVTZWBN.js +0 -50
  45. package/dist/chunk-TKVTZWBN.js.map +0 -1
  46. package/dist/chunk-XVB55M3N.js +0 -42
  47. package/dist/chunk-XVB55M3N.js.map +0 -1
  48. package/dist/chunk-XYDIBW3J.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,27 +1,28 @@
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 { useAppShell, REACT_VERSION_PROBLEM, useOverlayMark, useDirtyGuard, OverlayMarkScope, OverlayCloseCtx, DiscardConfirm, OVERLAY_ROOT_ATTR, OverlayMarkCtx, DrawerFooterCtx, useCompact, useElementWidth, Sheet, useCloseSettle, Drawer, useRememberedWidth, pxOf, ResizeEdge, defaultMaxWidth } from './chunk-AAYCQXZQ.js';
2
+ export { AppShell, Drawer, Sheet, SkipLink, useAppShell, useUnsavedChanges } from './chunk-AAYCQXZQ.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';
9
- import { Link } from './chunk-EQDR57AM.js';
10
- export { CodeBlock, Divider, Link } from './chunk-EQDR57AM.js';
11
- import { Badge } from './chunk-XYDIBW3J.js';
12
- export { Badge } from './chunk-XYDIBW3J.js';
13
- import { inferColumnKinds, CardValue } from './chunk-JN37OVTP.js';
14
- import { SplitPaneCtx, useShellPreferences, TableCardCtx, PREFERENCE_HOUSE_DEFAULTS, preferenceActionKey } from './chunk-TKVTZWBN.js';
15
- export { PreferencesContext, preferenceActionKey, usePreferences } from './chunk-TKVTZWBN.js';
5
+ import { Link } from './chunk-EYMGLK4M.js';
6
+ export { CodeBlock, Divider, Link } from './chunk-EYMGLK4M.js';
7
+ import { Badge } from './chunk-SBTBMAIU.js';
8
+ export { Badge } from './chunk-SBTBMAIU.js';
9
+ import { inferColumnKinds, useCardsSwitch, CARDS_BELOW, kindOfRenderedText, CardValue } from './chunk-IVCVNAXG.js';
10
+ import { useWholeListCheck, pagerPart, Pagination, Dropdown, useFieldControl, useField, warnOnce, Label, InlineFieldError, AddAnotherRowCtx, Input, Field } from './chunk-2RV23OBK.js';
11
+ export { Dropdown, Field, InlineFieldError, Input, Label, NumberInput, Pagination, Popover, Textarea, Toggle, usePagination } from './chunk-2RV23OBK.js';
12
+ import { SplitPaneCtx, TableCardCtx } from './chunk-RG4KUCRS.js';
13
+ import { Button, ControlSizeContext, triggerType, Spinner, useShellPreferences, preferenceActionKey } from './chunk-7TOJ4G7S.js';
14
+ export { Button, ButtonSizeContext, ControlSizeContext, PreferencesContext, Spinner, preferenceActionKey, usePreferences } from './chunk-7TOJ4G7S.js';
16
15
  import { useBreakpoint, readBreakpointPx } from './chunk-CRQ6KCRD.js';
17
16
  export { BreakpointWidthProvider, useBreakpoint } from './chunk-CRQ6KCRD.js';
18
- import { useTextOverflow, Tooltip, usePosition } from './chunk-ILIODA7W.js';
19
- export { Tooltip, useTextOverflow } from './chunk-ILIODA7W.js';
17
+ import { FOCUSABLE, usePopupFocus, FIELD, ZOOM_KEYS, FormChangeCtx, useOverlayLayer, useFocusTrap, useFocusBeforeOpen, rememberFocus, focusInitial, OVERLAY_CLOSE_ATTR, useReportedChange, rovingTabStop, useGridKeyboard, useOptionalFormContext, focusAfterRemoval, FormCtx, useNonModalDialog, useReturnFocus, useCloseOnFocusOut } from './chunk-RT7RYGC3.js';
18
+ export { useFormContext, useGridKeyboard } from './chunk-RT7RYGC3.js';
19
+ import { useTextOverflow, Tooltip, usePosition } from './chunk-CXMV7SXJ.js';
20
+ export { Tooltip, useTextOverflow } from './chunk-CXMV7SXJ.js';
20
21
  import { mergeRefs, Slot } from './chunk-7Q46JXSK.js';
21
22
  import { useLatest } from './chunk-WH2FUFAC.js';
22
23
  import { Icon } from './chunk-4LDYR7QP.js';
23
- import { cn, GLASS_SURFACE, GLASS_BACKDROP, GLASS_DROPDOWN, lockBodyScroll, unlockBodyScroll, OVERLAY_SURFACE, GLASS_OVERLAY, ERROR_ACTIVE_RING, ACTIVE_RING, ERROR_FOCUS_RING, FOCUS_RING, ERROR_HAS_FOCUS_RING, HAS_FOCUS_RING, ERROR_FOCUS_WITHIN_RING, FOCUS_WITHIN_RING } from './chunk-M2QRNBDQ.js';
24
- import { createContext, forwardRef, useState, useRef, useCallback, useLayoutEffect, useMemo, useContext, Children, isValidElement, useId, memo, useEffect, useImperativeHandle, Fragment as Fragment$1, cloneElement, useSyncExternalStore } from 'react';
24
+ import { cn, GLASS_SURFACE, GLASS_BACKDROP, GLASS_DROPDOWN, lockBodyScroll, unlockBodyScroll, OVERLAY_SURFACE, GLASS_OVERLAY, ERROR_HAS_FOCUS_RING, HAS_FOCUS_RING, ACTIVE_RING, ERROR_ACTIVE_RING, ERROR_FOCUS_RING, FOCUS_RING, FOCUS_WITHIN_RING } from './chunk-M2QRNBDQ.js';
25
+ import { createContext, forwardRef, useState, useRef, useCallback, useLayoutEffect, useMemo, useContext, isValidElement, useId, memo, useEffect, useImperativeHandle, Children, Fragment as Fragment$1, cloneElement, useSyncExternalStore } from 'react';
25
26
  import { createPortal, flushSync } from 'react-dom';
26
27
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
27
28
 
@@ -84,6 +85,9 @@ function DropdownMenu({ trigger, items, align = "end", className, container }) {
84
85
  /* @__PURE__ */ jsx(
85
86
  Slot,
86
87
  {
88
+ ...triggerType(trigger),
89
+ "aria-haspopup": "menu",
90
+ "aria-expanded": open,
87
91
  ref: triggerRef,
88
92
  onClick: () => {
89
93
  setOpen(!open);
@@ -180,13 +184,14 @@ function BulkActionBar({ selectedCount, actions, onClearSelection, className })
180
184
  isMobile ? /* @__PURE__ */ jsx(
181
185
  DropdownMenu,
182
186
  {
183
- trigger: /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", iconLeft: /* @__PURE__ */ jsx(Icon, { name: "more-vertical", size: 14 }), children: "Actions" }),
187
+ trigger: /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", iconLeft: /* @__PURE__ */ jsx(Icon, { name: "more-vertical", size: 14 }), children: "Actions" }),
184
188
  items: dropdownItems,
185
189
  align: "start"
186
190
  }
187
191
  ) : /* @__PURE__ */ jsx("div", { className: "flex items-center gap-ds-2", children: actions.map((action, i) => /* @__PURE__ */ jsx(
188
192
  Button,
189
193
  {
194
+ type: "button",
190
195
  variant: "secondary",
191
196
  size: "sm",
192
197
  onClick: action.onClick,
@@ -208,732 +213,6 @@ function BulkActionBar({ selectedCount, actions, onClearSelection, className })
208
213
  }
209
214
  );
210
215
  }
211
- var valueOf = (e) => {
212
- const t = e.target;
213
- if (!t) return void 0;
214
- return t instanceof HTMLInputElement && (t.type === "checkbox" || t.type === "radio") ? t.checked : t.value;
215
- };
216
- var INPUT_HEIGHT = {
217
- sm: "h-[var(--ds-control-height-sm)]",
218
- md: "h-[var(--ds-control-height-md)]",
219
- lg: "h-[var(--ds-control-height-lg)]"
220
- };
221
- var INPUT_TEXT = {
222
- sm: "px-ds-2 py-ds-1 text-xs",
223
- md: "px-ds-3 py-ds-2 text-sm",
224
- lg: "px-ds-4 py-ds-3 text-base"
225
- };
226
- function Input({ className, size: sizeProp, error = false, prefix, suffix, ...props }) {
227
- const contextSize = useContext(ControlSizeContext);
228
- const size = sizeProp ?? contextSize ?? "md";
229
- const onChange = useReportedChange(props.onChange, props.value, valueOf);
230
- const borderColor = error ? "border-[var(--ds-status-error)]" : "border-[var(--ds-input-border)]";
231
- const hoverBorder = !error && !props.disabled && !props.readOnly ? "hover:border-[var(--ds-input-border-hover)]" : "";
232
- if (prefix || suffix) {
233
- return /* @__PURE__ */ jsxs(
234
- "div",
235
- {
236
- className: cn(
237
- "flex items-center w-full",
238
- INPUT_HEIGHT[size],
239
- "bg-[var(--ds-input-bg)]",
240
- "border-[length:var(--ds-border-width)]",
241
- borderColor,
242
- "rounded-[var(--ds-radius-md)]",
243
- "shadow-[var(--ds-input-shadow)]",
244
- hoverBorder,
245
- error ? ERROR_HAS_FOCUS_RING : HAS_FOCUS_RING,
246
- "transition-colors duration-[var(--ds-transition-speed)]",
247
- props.disabled && "cursor-not-allowed opacity-disabled",
248
- props.readOnly && "bg-[var(--ds-bg-muted)] cursor-default",
249
- className
250
- ),
251
- children: [
252
- prefix && /* @__PURE__ */ jsx("span", { className: "shrink-0 pl-ds-3 text-[var(--ds-text-muted)] flex items-center", children: prefix }),
253
- /* @__PURE__ */ jsx(
254
- "input",
255
- {
256
- "aria-invalid": error || void 0,
257
- className: cn(
258
- "flex-1 min-w-0 bg-transparent outline-none",
259
- INPUT_TEXT[size],
260
- "text-[var(--ds-text-primary)]",
261
- "placeholder:text-[var(--ds-text-muted)]",
262
- "disabled:cursor-not-allowed",
263
- "read-only:text-[var(--ds-text-secondary)] read-only:cursor-default"
264
- ),
265
- ...props,
266
- onChange: props.onChange || props.value === void 0 ? onChange : void 0
267
- }
268
- ),
269
- suffix && /* @__PURE__ */ jsx("span", { className: "shrink-0 pr-ds-3 text-[var(--ds-text-muted)] flex items-center", children: suffix })
270
- ]
271
- }
272
- );
273
- }
274
- return /* @__PURE__ */ jsx(
275
- "input",
276
- {
277
- "aria-invalid": error || void 0,
278
- className: cn(
279
- "flex w-full",
280
- INPUT_TEXT[size],
281
- INPUT_HEIGHT[size],
282
- "bg-[var(--ds-input-bg)] text-[var(--ds-text-primary)]",
283
- "border-[length:var(--ds-border-width)]",
284
- borderColor,
285
- "rounded-[var(--ds-radius-md)]",
286
- "shadow-[var(--ds-input-shadow)]",
287
- "placeholder:text-[var(--ds-text-muted)]",
288
- hoverBorder,
289
- error ? ERROR_FOCUS_RING : FOCUS_RING,
290
- "disabled:cursor-not-allowed disabled:opacity-disabled",
291
- "read-only:bg-[var(--ds-bg-muted)] read-only:text-[var(--ds-text-secondary)] read-only:cursor-default",
292
- "transition-colors duration-[var(--ds-transition-speed)]",
293
- className
294
- ),
295
- ...props,
296
- onChange: props.onChange || props.value === void 0 ? onChange : void 0
297
- }
298
- );
299
- }
300
- function Textarea({ className, autoResize, maxRows, error = false, ref, ...props }) {
301
- const onChange = useReportedChange(props.onChange, props.value, valueOf);
302
- const internalRef = useRef(null);
303
- const textareaRef = ref ?? internalRef;
304
- const resize = useCallback(() => {
305
- const el = textareaRef.current;
306
- if (!el || !autoResize) return;
307
- el.style.height = "auto";
308
- let target = el.scrollHeight;
309
- if (maxRows) {
310
- const lineHeight = parseFloat(getComputedStyle(el).lineHeight) || 20;
311
- const maxHeight = lineHeight * maxRows + parseFloat(getComputedStyle(el).paddingTop) + parseFloat(getComputedStyle(el).paddingBottom);
312
- target = Math.min(target, maxHeight);
313
- }
314
- el.style.height = `${target}px`;
315
- }, [autoResize, maxRows, textareaRef]);
316
- useEffect(() => {
317
- resize();
318
- }, [resize, props.value, props.defaultValue]);
319
- return /* @__PURE__ */ jsx(
320
- "textarea",
321
- {
322
- ref: textareaRef,
323
- "aria-invalid": error || void 0,
324
- className: cn(
325
- "flex min-h-[var(--ds-control-height-md)] w-full px-ds-3 py-ds-2 text-sm",
326
- "bg-[var(--ds-input-bg)] text-[var(--ds-text-primary)]",
327
- "border-[length:var(--ds-border-width)]",
328
- error ? "border-[var(--ds-status-error)]" : "border-[var(--ds-input-border)] hover:border-[var(--ds-input-border-hover)]",
329
- "rounded-[var(--ds-radius-md)]",
330
- "shadow-[var(--ds-input-shadow)]",
331
- "placeholder:text-[var(--ds-text-muted)]",
332
- error ? ERROR_FOCUS_RING : FOCUS_RING,
333
- "disabled:cursor-not-allowed disabled:opacity-disabled disabled:hover:border-[var(--ds-input-border)]",
334
- "read-only:bg-[var(--ds-bg-muted)] read-only:text-[var(--ds-text-secondary)] read-only:cursor-default read-only:hover:border-[var(--ds-input-border)]",
335
- "transition-colors duration-[var(--ds-transition-speed)]",
336
- autoResize && "resize-none overflow-hidden",
337
- className
338
- ),
339
- onInput: autoResize ? resize : void 0,
340
- ...props,
341
- onChange: props.onChange || props.value === void 0 ? onChange : void 0
342
- }
343
- );
344
- }
345
- function Label({ className, children, ...props }) {
346
- return /* @__PURE__ */ jsx(
347
- "label",
348
- {
349
- className: cn(
350
- "text-sm font-medium leading-none text-[var(--ds-text-secondary)]",
351
- "peer-disabled:cursor-not-allowed peer-disabled:opacity-disabled",
352
- className
353
- ),
354
- ...props,
355
- children
356
- }
357
- );
358
- }
359
- var TOGGLE_SIZES = {
360
- sm: { w: 32, h: 18, thumb: 12 },
361
- md: { w: 44, h: 24, thumb: 16 }
362
- };
363
- function Toggle({ checked, onChange, label, disabled, error = false, layout = "full", size = "md", "aria-labelledby": ariaLabelledBy, ...rest }) {
364
- const change = useReportedChange(onChange, checked);
365
- const dims = TOGGLE_SIZES[size];
366
- const travel = dims.w - dims.thumb - 3;
367
- const switchButton = /* @__PURE__ */ jsx(
368
- "button",
369
- {
370
- type: "button",
371
- role: "switch",
372
- "aria-checked": checked,
373
- "aria-label": !label && !ariaLabelledBy ? "Toggle" : void 0,
374
- "aria-labelledby": ariaLabelledBy,
375
- "aria-invalid": error || void 0,
376
- disabled,
377
- onClick: () => !disabled && change(!checked),
378
- ...rest,
379
- className: cn(
380
- "relative inline-flex shrink-0 items-center overflow-hidden",
381
- "border-[length:var(--ds-border-width)]",
382
- "rounded-[var(--ds-radius-full)]",
383
- "transition-colors duration-[var(--ds-transition-speed)]",
384
- "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
385
- disabled ? "cursor-not-allowed" : "cursor-pointer",
386
- checked ? "bg-[var(--ds-accent-primary)]" : "bg-[var(--ds-bg-muted)]",
387
- error ? "border-[var(--ds-status-error)]" : checked ? "border-transparent" : "border-[var(--ds-border-default)]"
388
- ),
389
- style: { width: dims.w, height: dims.h },
390
- children: /* @__PURE__ */ jsx(
391
- "span",
392
- {
393
- className: cn(
394
- "pointer-events-none inline-block shrink-0",
395
- "bg-[var(--ds-bg-surface)]",
396
- "border-[length:var(--ds-border-width)] border-transparent",
397
- "rounded-[var(--ds-radius-full)]",
398
- "transition-transform"
399
- ),
400
- style: {
401
- width: dims.thumb,
402
- height: dims.thumb,
403
- transform: checked ? `translateX(calc(${travel}px - var(--ds-border-width)))` : "translateX(calc(1px + var(--ds-border-width)))"
404
- }
405
- }
406
- )
407
- }
408
- );
409
- if (!label) {
410
- return /* @__PURE__ */ jsx("div", { className: cn(disabled && "opacity-disabled"), children: switchButton });
411
- }
412
- return /* @__PURE__ */ jsxs(
413
- "label",
414
- {
415
- className: cn(
416
- layout === "full" ? "flex items-center justify-between gap-ds-4" : "inline-flex items-center gap-ds-3",
417
- disabled ? "cursor-not-allowed opacity-disabled" : "cursor-pointer"
418
- ),
419
- children: [
420
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-[var(--ds-text-primary)]", children: label }),
421
- switchButton
422
- ]
423
- }
424
- );
425
- }
426
- function InlineFieldError({ message, id, className }) {
427
- return /* @__PURE__ */ jsxs(
428
- "p",
429
- {
430
- id,
431
- role: "alert",
432
- className: cn(
433
- "flex items-center gap-ds-1.5 text-xs text-[var(--ds-status-error)]",
434
- "mt-ds-1.5",
435
- className
436
- ),
437
- children: [
438
- /* @__PURE__ */ jsx(Icon, { name: "error", size: 14, className: "shrink-0" }),
439
- /* @__PURE__ */ jsx("span", { children: message })
440
- ]
441
- }
442
- );
443
- }
444
- var FieldContext = createContext(null);
445
- function useField() {
446
- return useContext(FieldContext);
447
- }
448
- function useFieldControl(own, options = {}) {
449
- const field = useField();
450
- const id = own.id ?? field?.controlId;
451
- const invalid = !!own.error || own["aria-invalid"] === true || own["aria-invalid"] === "true" || !!field?.invalid;
452
- const required = !!own.required || own["aria-required"] === true || own["aria-required"] === "true" || !!field?.required;
453
- const noteId = options.requiredInDescription && required && id ? `${id}-required` : void 0;
454
- const ids = [own["aria-describedby"], field?.describedBy, noteId].filter(Boolean).join(" ").split(/\s+/).filter(Boolean);
455
- return {
456
- id,
457
- labelId: own["aria-labelledby"] ?? field?.labelId,
458
- describedBy: [...new Set(ids)].join(" ") || void 0,
459
- invalid,
460
- required,
461
- requiredNote: noteId ? /* @__PURE__ */ jsx("span", { id: noteId, hidden: true, children: "Required" }) : null
462
- };
463
- }
464
- var AddAnotherRowCtx = createContext(false);
465
- function Field({ label, children, hint, error, required, labelAction, hideLabel = false, id: idProp, validate, className }) {
466
- const inGroupRow = useContext(AddAnotherRowCtx);
467
- const generated = useId();
468
- const id = idProp ?? `field-${generated}`;
469
- const hintId = `${id}-hint`;
470
- const errorId = `${id}-error`;
471
- const form = useOptionalFormContext();
472
- const mode = form?.validationMode ?? "onBlur";
473
- const [localError, setLocalError] = useState(void 0);
474
- const [left, setLeft] = useState(false);
475
- const checkError = validate ? form ? form.errors[id] : localError : void 0;
476
- const child = typeof children === "function" ? null : Children.only(children);
477
- const childProps = child && isValidElement(child) ? child.props : null;
478
- const latest = useLatest({ validate, childProps, id, mode, left, showing: !!checkError, form });
479
- const check = useCallback(() => {
480
- const { validate: fn, childProps: props, id: controlId } = latest.current;
481
- if (!fn) return void 0;
482
- const value = props && "value" in props ? props.value : props && "checked" in props ? props.checked : document.getElementById(controlId)?.value;
483
- const message = fn(value);
484
- return typeof message === "string" && message.length > 0 ? message : void 0;
485
- }, [latest]);
486
- const show = useCallback((message) => {
487
- const { form: f, id: controlId } = latest.current;
488
- if (f) f.setError(controlId, message);
489
- else setLocalError(message);
490
- }, [latest]);
491
- const register = form?.register;
492
- const checks = !!validate;
493
- useEffect(() => register && checks ? register(id, check) : void 0, [register, checks, id, check]);
494
- const leaving = useRef(null);
495
- useEffect(() => () => {
496
- if (leaving.current) clearTimeout(leaving.current);
497
- }, []);
498
- const onFocusCapture = () => {
499
- if (leaving.current) {
500
- clearTimeout(leaving.current);
501
- leaving.current = null;
502
- }
503
- };
504
- const onBlurCapture = () => {
505
- if (!validate) return;
506
- if (leaving.current) clearTimeout(leaving.current);
507
- leaving.current = setTimeout(() => {
508
- leaving.current = null;
509
- setLeft(true);
510
- if (latest.current.mode !== "onSubmit" || latest.current.showing) show(check());
511
- }, 0);
512
- };
513
- const recheck = () => {
514
- if (!validate) return;
515
- setTimeout(() => {
516
- const { mode: m, left: wasLeft, showing } = latest.current;
517
- if (m === "onChange" && wasLeft || showing) show(check());
518
- }, 0);
519
- };
520
- const shownError = typeof error === "string" && error.length > 0 ? error : checkError;
521
- const hasError = typeof shownError === "string" && shownError.length > 0;
522
- const describedBy = [hint ? hintId : null, hasError ? errorId : null].filter(Boolean).join(" ") || void 0;
523
- const labelId = `${id}-label`;
524
- const fieldContext = useMemo(() => ({ controlId: id, labelId, describedBy, invalid: hasError, required: !!required }), [id, labelId, describedBy, hasError, required]);
525
- const controlProps = {
526
- id,
527
- "aria-describedby": describedBy,
528
- ...hasError ? { "aria-invalid": true, error: true } : {},
529
- ...required ? { "aria-required": true, required: true } : {}
530
- };
531
- let control;
532
- if (typeof children === "function") {
533
- control = children(controlProps);
534
- } else {
535
- if (!child || !isValidElement(child)) throw new Error("Field expects a single form control as its child");
536
- const isNative = typeof child.type === "string";
537
- const { error: errorFlag, ...ariaOnly } = controlProps;
538
- const ownOnChange = childProps?.onChange;
539
- const onChange = validate && !isNative ? { onChange: (...args) => {
540
- ownOnChange?.(...args);
541
- recheck();
542
- } } : {};
543
- control = cloneElement(child, isNative ? ariaOnly : { ...ariaOnly, ...errorFlag ? { error: true } : {}, ...onChange });
544
- }
545
- const labelNode = /* @__PURE__ */ jsxs(Label, { id: labelId, htmlFor: id, className: cn(hideLabel && "sr-only", hideLabel && inGroupRow && "@max-[30rem]:not-sr-only"), children: [
546
- label,
547
- required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "ml-ds-0.5 text-[var(--ds-status-error)]", children: "*" })
548
- ] });
549
- return (
550
- // In a settings panel a field keeps the narrow reading width however wide the page is (--ds-form-max, Sprint 25.9 A4)
551
- /* @__PURE__ */ jsxs(
552
- "div",
553
- {
554
- className: cn("flex flex-col gap-ds-1.5 max-w-[var(--ds-form-max,none)]", className),
555
- ...validate ? { onFocusCapture, onBlurCapture, onInputCapture: recheck } : {},
556
- children: [
557
- hideLabel && !labelAction ? labelNode : /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-ds-2", children: [
558
- labelNode,
559
- labelAction
560
- ] }),
561
- /* @__PURE__ */ jsx(FieldContext.Provider, { value: fieldContext, children: control }),
562
- hint && /* @__PURE__ */ jsx("p", { id: hintId, className: "text-xs text-[var(--ds-text-muted)]", children: hint }),
563
- hasError && /* @__PURE__ */ jsx(InlineFieldError, { id: errorId, message: shownError, className: "mt-0" })
564
- ]
565
- }
566
- )
567
- );
568
- }
569
- var DROPDOWN_HEIGHT = {
570
- sm: "px-ds-2 py-ds-1 text-xs h-[var(--ds-control-height-sm)]",
571
- md: "px-ds-3 py-ds-2 text-sm h-[var(--ds-control-height-md)]",
572
- lg: "px-ds-4 py-ds-3 text-base h-[var(--ds-control-height-lg)]"
573
- };
574
- function Dropdown({
575
- options,
576
- value,
577
- onChange: onChangeProp,
578
- label,
579
- disabled,
580
- placeholder = "Select...",
581
- multiple,
582
- searchable,
583
- size: sizeProp,
584
- className,
585
- ref,
586
- container,
587
- error = false,
588
- showChips = true,
589
- "aria-label": ariaLabel,
590
- ...wiring
591
- }) {
592
- const onChange = useReportedChange(onChangeProp, value);
593
- const contextSize = useContext(ControlSizeContext);
594
- const generatedId = useId();
595
- const control = useFieldControl({ ...wiring, error: error || void 0 }, { requiredInDescription: true });
596
- const triggerId = control.id ?? generatedId;
597
- const labelSpanId = `${triggerId}-label`;
598
- const labelledBy = control.labelId ? `${control.labelId} ${triggerId}` : label && !ariaLabel ? `${labelSpanId} ${triggerId}` : void 0;
599
- const invalid = control.invalid;
600
- const size = sizeProp ?? contextSize ?? "md";
601
- const [open, setOpen] = useState(false);
602
- const [search, setSearch] = useState("");
603
- const [positioned, setPositioned] = useState(false);
604
- const triggerRef = useRef(null);
605
- const menuRef = useRef(null);
606
- const searchRef = useRef(null);
607
- const [pos, setPos] = useState({ top: 0, left: 0, width: 0 });
608
- const selectedValues = Array.isArray(value) ? value : [value].filter(Boolean);
609
- const updatePos = useCallback(() => {
610
- if (!triggerRef.current) return;
611
- const rect = triggerRef.current.getBoundingClientRect();
612
- let top = rect.bottom + 4;
613
- let left = rect.left;
614
- const width = rect.width;
615
- if (menuRef.current) {
616
- const menuRect = menuRef.current.getBoundingClientRect();
617
- const vw = window.innerWidth;
618
- const vh = window.innerHeight;
619
- if (left + menuRect.width > vw) left = Math.max(0, vw - menuRect.width - 8);
620
- if (top + menuRect.height > vh) {
621
- const above = rect.top - 4 - menuRect.height;
622
- if (above >= 0) top = above;
623
- else top = Math.max(0, vh - menuRect.height - 8);
624
- }
625
- }
626
- setPos({ top, left, width });
627
- }, []);
628
- const [prevOpen, setPrevOpen] = useState(open);
629
- if (open !== prevOpen) {
630
- setPrevOpen(open);
631
- if (open) setSearch("");
632
- else setPositioned(false);
633
- }
634
- useEffect(() => {
635
- if (!open) return;
636
- updatePos();
637
- const raf = requestAnimationFrame(() => {
638
- updatePos();
639
- setPositioned(true);
640
- });
641
- const onScroll = () => updatePos();
642
- window.addEventListener("scroll", onScroll, true);
643
- window.addEventListener("resize", updatePos);
644
- return () => {
645
- cancelAnimationFrame(raf);
646
- window.removeEventListener("scroll", onScroll, true);
647
- window.removeEventListener("resize", updatePos);
648
- };
649
- }, [open, updatePos]);
650
- const moved = useRef(false);
651
- useLayoutEffect(() => {
652
- if (!open || !positioned) return;
653
- moved.current = false;
654
- if (searchRef.current) {
655
- searchRef.current.focus();
656
- return;
657
- }
658
- const els = Array.from(menuRef.current?.querySelectorAll('[role="option"]') ?? []);
659
- (els.find((el) => el.getAttribute("aria-selected") === "true") ?? els[0])?.focus();
660
- }, [open, positioned]);
661
- useEffect(() => {
662
- if (!open) return;
663
- const handler = (e) => {
664
- const target = e.target;
665
- if (triggerRef.current?.contains(target) || menuRef.current?.contains(target)) return;
666
- setOpen(false);
667
- };
668
- document.addEventListener("mousedown", handler);
669
- return () => document.removeEventListener("mousedown", handler);
670
- }, [open]);
671
- const commitMoved = () => {
672
- if (multiple || !moved.current) return;
673
- const el = document.activeElement;
674
- const id = el instanceof HTMLElement && menuRef.current?.contains(el) ? el.getAttribute("data-option-id") : null;
675
- if (id) onChange(id);
676
- };
677
- const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: menuRef, close: () => setOpen(false), tabExits: "any", onTabOut: commitMoved });
678
- const typeahead = useRef({ buffer: "", at: 0 });
679
- useEffect(() => {
680
- if (!open) return;
681
- const optionButtons = () => Array.from(menuRef.current?.querySelectorAll('[role="option"]') ?? []);
682
- const focusOption = (idx) => {
683
- const els = optionButtons();
684
- if (els.length === 0) return;
685
- els[(idx % els.length + els.length) % els.length].focus();
686
- moved.current = true;
687
- };
688
- const onKey = (e) => {
689
- const inMenu = menuRef.current?.contains(document.activeElement) || triggerRef.current === document.activeElement;
690
- if (!inMenu) return;
691
- const els = optionButtons();
692
- const current = els.findIndex((el) => el === document.activeElement);
693
- if (e.key === "Escape") {
694
- if (e.defaultPrevented || !isTopLayer()) return;
695
- e.preventDefault();
696
- returnFocus();
697
- setOpen(false);
698
- return;
699
- }
700
- if (e.key === "ArrowDown") {
701
- e.preventDefault();
702
- focusOption(current < 0 ? 0 : current + 1);
703
- return;
704
- }
705
- if (e.key === "ArrowUp") {
706
- e.preventDefault();
707
- focusOption(current < 0 ? els.length - 1 : current - 1);
708
- return;
709
- }
710
- const inSearch = document.activeElement === searchRef.current;
711
- if (e.key === "Home" && !inSearch) {
712
- e.preventDefault();
713
- focusOption(0);
714
- return;
715
- }
716
- if (e.key === "End" && !inSearch) {
717
- e.preventDefault();
718
- focusOption(els.length - 1);
719
- return;
720
- }
721
- if (!searchable && e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
722
- const now = Date.now();
723
- typeahead.current.buffer = now - typeahead.current.at > 700 ? e.key : typeahead.current.buffer + e.key;
724
- typeahead.current.at = now;
725
- const q = typeahead.current.buffer.toLowerCase();
726
- const hit = els.findIndex((el) => (el.textContent ?? "").trim().toLowerCase().startsWith(q));
727
- if (hit >= 0) {
728
- e.preventDefault();
729
- focusOption(hit);
730
- }
731
- }
732
- };
733
- document.addEventListener("keydown", onKey);
734
- return () => document.removeEventListener("keydown", onKey);
735
- }, [open, searchable, isTopLayer, returnFocus]);
736
- const filtered = searchable && search ? options.filter((o) => o.label.toLowerCase().includes(search.toLowerCase())) : options;
737
- const groups = /* @__PURE__ */ new Map();
738
- for (const opt of filtered) {
739
- const g = opt.group ?? "";
740
- if (!groups.has(g)) groups.set(g, []);
741
- groups.get(g).push(opt);
742
- }
743
- const hasGroups = Array.from(groups.keys()).some((k) => k !== "");
744
- const handleSelect = (id) => {
745
- if (multiple) {
746
- const arr = selectedValues.includes(id) ? selectedValues.filter((v) => v !== id) : [...selectedValues, id];
747
- onChange(arr);
748
- } else {
749
- returnFocus();
750
- onChange(id);
751
- setOpen(false);
752
- }
753
- };
754
- const chipsRef = useRef(null);
755
- const pendingChipFocus = useRef(null);
756
- useLayoutEffect(() => {
757
- const pending = pendingChipFocus.current;
758
- if (pending === null) return;
759
- pendingChipFocus.current = null;
760
- if (selectedValues.includes(pending.value)) return;
761
- const chips = Array.from(chipsRef.current?.querySelectorAll("[data-chip-remove]") ?? []);
762
- focusAfterRemoval(chips, pending.index, triggerRef.current);
763
- });
764
- const displayLabel = multiple ? selectedValues.length > 0 ? `${selectedValues.length} selected` : placeholder : options.find((o) => o.id === selectedValues[0])?.label ?? placeholder;
765
- const { ref: overflowRef, isTruncated } = useTextOverflow({ lines: 1, content: displayLabel });
766
- return /* @__PURE__ */ jsxs("div", { ref, className: cn("relative", className), children: [
767
- label && /* @__PURE__ */ jsx("span", { id: labelSpanId, className: "block mb-ds-1.5 text-sm font-medium text-[var(--ds-text-secondary)]", children: label }),
768
- control.requiredNote,
769
- /* @__PURE__ */ jsxs(
770
- "button",
771
- {
772
- ref: triggerRef,
773
- id: triggerId,
774
- type: "button",
775
- onClick: () => !disabled && setOpen(!open),
776
- onKeyDown: (e) => {
777
- if (disabled || open) return;
778
- if (e.key === "ArrowDown" || e.key === "ArrowUp") {
779
- e.preventDefault();
780
- setOpen(true);
781
- }
782
- },
783
- disabled,
784
- "aria-invalid": invalid || void 0,
785
- "aria-haspopup": "listbox",
786
- "aria-expanded": open,
787
- "aria-label": labelledBy ? void 0 : ariaLabel,
788
- "aria-labelledby": labelledBy,
789
- "aria-describedby": control.describedBy,
790
- className: cn(
791
- "w-full flex items-center justify-between gap-ds-2",
792
- DROPDOWN_HEIGHT[size],
793
- "bg-[var(--ds-input-bg)] text-[var(--ds-text-primary)]",
794
- "border-[length:var(--ds-border-width)]",
795
- invalid ? "border-[var(--ds-status-error)]" : "border-[var(--ds-input-border)]",
796
- "rounded-[var(--ds-radius-md)]",
797
- "shadow-[var(--ds-input-shadow)]",
798
- "transition-colors",
799
- !invalid && !disabled && !open && "hover:border-[var(--ds-input-border-hover)]",
800
- open && (invalid ? ERROR_ACTIVE_RING : ACTIVE_RING),
801
- invalid ? ERROR_FOCUS_RING : FOCUS_RING,
802
- "cursor-pointer disabled:cursor-not-allowed disabled:opacity-disabled",
803
- !selectedValues.length && "text-[var(--ds-text-muted)]"
804
- ),
805
- children: [
806
- isTruncated ? /* @__PURE__ */ jsx(Tooltip, { content: displayLabel, children: /* @__PURE__ */ jsx("span", { ref: overflowRef, className: "truncate", children: displayLabel }) }) : /* @__PURE__ */ jsx("span", { ref: overflowRef, className: "truncate", children: displayLabel }),
807
- /* @__PURE__ */ jsx("span", { className: cn("transition-transform", open && "rotate-180"), children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14, className: "rotate-90" }) })
808
- ]
809
- }
810
- ),
811
- open && createPortal(
812
- /* @__PURE__ */ jsxs(
813
- "div",
814
- {
815
- ref: menuRef,
816
- tabIndex: -1,
817
- onClick: (e) => e.stopPropagation(),
818
- className: cn(
819
- "fixed z-[var(--ds-z-popover)] outline-none",
820
- "bg-[var(--ds-bg-surface-elevated)]",
821
- "[backdrop-filter:var(--ds-glass-filter)] [-webkit-backdrop-filter:var(--ds-glass-filter)]",
822
- "border-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
823
- "rounded-[var(--ds-radius-md)] overflow-hidden",
824
- "shadow-[var(--ds-shadow-dropdown),var(--ds-glass-inset)]"
825
- ),
826
- style: { top: pos.top, left: pos.left, minWidth: pos.width, width: "max-content", maxWidth: "calc(100vw - 16px)", visibility: positioned ? "visible" : "hidden" },
827
- children: [
828
- searchable && /* @__PURE__ */ jsx("div", { className: "border-b-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]", style: { padding: "var(--ds-space-2)" }, children: /* @__PURE__ */ jsx(
829
- "input",
830
- {
831
- ref: searchRef,
832
- type: "text",
833
- value: search,
834
- onChange: (e) => setSearch(e.target.value),
835
- placeholder: "Search\u2026",
836
- className: cn("w-full bg-transparent text-[var(--ds-text-primary)] placeholder:text-[var(--ds-text-muted)] outline-none", size === "sm" ? "text-xs" : "text-sm")
837
- }
838
- ) }),
839
- /* @__PURE__ */ jsx("div", { role: "listbox", className: "overflow-y-auto", style: { padding: "var(--ds-space-1)", maxHeight: "var(--ds-dropdown-max-height)" }, children: filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: cn("text-[var(--ds-text-muted)] text-center py-ds-4", size === "sm" ? "text-xs" : "text-sm"), children: "No options found" }) : hasGroups ? Array.from(groups.entries()).map(([group, opts]) => /* @__PURE__ */ jsxs("div", { children: [
840
- group && /* @__PURE__ */ jsx("p", { className: "text-2xs font-medium uppercase tracking-wider text-[var(--ds-text-muted)] px-ds-2 pt-ds-2 pb-ds-1", children: group }),
841
- opts.map((opt) => /* @__PURE__ */ jsx(
842
- OptionButton,
843
- {
844
- opt,
845
- isSelected: selectedValues.includes(opt.id),
846
- multiple,
847
- onSelect: handleSelect,
848
- size
849
- },
850
- opt.id
851
- ))
852
- ] }, group)) : filtered.map((opt) => /* @__PURE__ */ jsx(
853
- OptionButton,
854
- {
855
- opt,
856
- isSelected: selectedValues.includes(opt.id),
857
- multiple,
858
- onSelect: handleSelect,
859
- size
860
- },
861
- opt.id
862
- )) })
863
- ]
864
- }
865
- ),
866
- container ?? document.body
867
- ),
868
- multiple && showChips && selectedValues.length > 0 && /* @__PURE__ */ jsx("div", { ref: chipsRef, className: "flex flex-wrap gap-ds-1 mt-ds-1.5", children: selectedValues.map((v, i) => {
869
- const opt = options.find((o) => o.id === v);
870
- return opt ? /* @__PURE__ */ jsxs(
871
- "span",
872
- {
873
- className: "inline-flex items-center gap-ds-1 text-xs rounded-[var(--ds-radius-sm)] bg-[var(--ds-bg-muted)] text-[var(--ds-text-secondary)] px-ds-1.5 py-ds-0.5",
874
- children: [
875
- opt.label,
876
- /* @__PURE__ */ jsx(
877
- "button",
878
- {
879
- type: "button",
880
- "data-chip-remove": "",
881
- onClick: () => {
882
- pendingChipFocus.current = { index: i, value: v };
883
- handleSelect(v);
884
- },
885
- className: "hover:text-[var(--ds-text-primary)] cursor-pointer",
886
- "aria-label": `Remove ${opt.label}`,
887
- children: /* @__PURE__ */ jsx(Icon, { name: "close", size: 10 })
888
- }
889
- )
890
- ]
891
- },
892
- v
893
- ) : null;
894
- }) })
895
- ] });
896
- }
897
- function OptionButton({
898
- opt,
899
- isSelected,
900
- multiple,
901
- onSelect,
902
- size = "md"
903
- }) {
904
- return /* @__PURE__ */ jsxs(
905
- "button",
906
- {
907
- type: "button",
908
- role: "option",
909
- "data-option-id": opt.id,
910
- "aria-selected": isSelected,
911
- tabIndex: -1,
912
- onClick: () => onSelect(opt.id),
913
- className: cn(
914
- "w-full text-left cursor-pointer rounded-[var(--ds-radius-sm)]",
915
- "transition-colors flex items-center gap-ds-2",
916
- "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
917
- size === "sm" ? "px-ds-2 py-ds-1 text-xs" : "px-ds-2 py-ds-1.5 text-sm",
918
- isSelected && !multiple ? "bg-[var(--ds-accent-primary)] text-[var(--ds-text-on-primary)]" : "text-[var(--ds-text-primary)] hover:bg-[var(--ds-bg-emphasis)]"
919
- ),
920
- children: [
921
- multiple && /* @__PURE__ */ jsx(
922
- "span",
923
- {
924
- className: cn(
925
- "w-[var(--ds-icon-size-sm)] h-[var(--ds-icon-size-sm)] rounded-[var(--ds-radius-sm)] border-[length:var(--ds-border-width)] shrink-0 flex items-center justify-center",
926
- isSelected ? "bg-[var(--ds-accent-primary)] border-[var(--ds-accent-primary)]" : "border-[var(--ds-border-default)]"
927
- ),
928
- children: isSelected && /* @__PURE__ */ jsx(Icon, { name: "check", size: 10, className: "text-[var(--ds-text-on-primary)]" })
929
- }
930
- ),
931
- /* @__PURE__ */ jsx("span", { className: "flex-1", children: opt.label }),
932
- isSelected && !multiple && /* @__PURE__ */ jsx(Icon, { name: "check", size: size === "sm" ? 12 : 14 })
933
- ]
934
- }
935
- );
936
- }
937
216
  var CONTROL_HEIGHT = {
938
217
  sm: "h-[var(--ds-control-height-sm)]",
939
218
  md: "h-[var(--ds-control-height-md)]",
@@ -1175,10 +454,11 @@ var CHECKBOX_SIZES = {
1175
454
  sm: { box: "h-[16px] w-[16px]", glyph: 10, label: "text-xs" },
1176
455
  md: { box: "h-[18px] w-[18px]", glyph: 12, label: "text-sm" }
1177
456
  };
1178
- function Checkbox({ checked, onChange: onChangeProp, label, size = "md", disabled = false, indeterminate = false, className, ref }) {
457
+ function Checkbox({ checked, onChange: onChangeProp, label, size = "md", disabled = false, indeterminate = false, className, ref, ...wiring }) {
1179
458
  const onChange = useReportedChange(onChangeProp, checked);
1180
459
  const internalRef = useRef(null);
1181
460
  const dims = CHECKBOX_SIZES[size];
461
+ const control = useFieldControl(wiring);
1182
462
  const mergedRef = useCallback(
1183
463
  (node) => {
1184
464
  internalRef.current = node;
@@ -1206,6 +486,11 @@ function Checkbox({ checked, onChange: onChangeProp, label, size = "md", disable
1206
486
  {
1207
487
  ref: mergedRef,
1208
488
  type: "checkbox",
489
+ id: control.id,
490
+ "aria-labelledby": wiring["aria-labelledby"],
491
+ "aria-describedby": control.describedBy,
492
+ "aria-invalid": control.invalid || void 0,
493
+ required: control.required || void 0,
1209
494
  checked,
1210
495
  disabled,
1211
496
  onChange: (e) => onChange(e.target.checked),
@@ -1268,10 +553,11 @@ function CheckboxGroupItem({ value, label, disabled, groupValue, onGroupChange }
1268
553
  }
1269
554
  );
1270
555
  }
1271
- function RadioGroup({ value, onChange: onChangeProp, options, orientation = "vertical", className, label }) {
556
+ function RadioGroup({ value, onChange: onChangeProp, options, orientation = "vertical", className, label, ...wiring }) {
1272
557
  const onChange = useReportedChange(onChangeProp, value);
1273
558
  const groupRef = useRef(null);
1274
559
  const groupId = useId();
560
+ const control = useFieldControl(wiring);
1275
561
  const enabledOptions = options.filter((o) => !o.disabled);
1276
562
  const tabStop = rovingTabStop(options, (o) => o.value === value, (o) => !!o.disabled);
1277
563
  function handleKeyDown(e) {
@@ -1301,7 +587,11 @@ function RadioGroup({ value, onChange: onChangeProp, options, orientation = "ver
1301
587
  {
1302
588
  ref: groupRef,
1303
589
  role: "radiogroup",
1304
- "aria-labelledby": label ? groupId : void 0,
590
+ id: control.id,
591
+ "aria-labelledby": label ? groupId : control.labelId,
592
+ "aria-describedby": control.describedBy,
593
+ "aria-invalid": control.invalid || void 0,
594
+ "aria-required": control.required || void 0,
1305
595
  className: cn(
1306
596
  "flex",
1307
597
  orientation === "vertical" ? "flex-col gap-ds-2" : "flex-row flex-wrap gap-ds-3"
@@ -1796,13 +1086,13 @@ function DatePicker({ value, onChange: onChangeProp, placeholder = "Pick a date"
1796
1086
  className: "flex items-center justify-between bg-[var(--ds-bg-muted)] rounded-t-[var(--ds-radius-md)]",
1797
1087
  style: { padding: "var(--ds-space-2) var(--ds-space-3)" },
1798
1088
  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" }) }),
1089
+ /* @__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
1090
  /* @__PURE__ */ jsxs("span", { className: "text-sm font-medium text-[var(--ds-text-primary)]", children: [
1801
1091
  MONTHS[month],
1802
1092
  " ",
1803
1093
  year
1804
1094
  ] }),
1805
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: nextMonth, "aria-label": "Next month", children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 14 }) })
1095
+ /* @__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
1096
  ]
1807
1097
  }
1808
1098
  ),
@@ -1865,6 +1155,7 @@ function DatePicker({ value, onChange: onChangeProp, placeholder = "Pick a date"
1865
1155
  /* @__PURE__ */ jsx(
1866
1156
  Button,
1867
1157
  {
1158
+ type: "button",
1868
1159
  variant: "ghost",
1869
1160
  size: "sm",
1870
1161
  onClick: () => {
@@ -1878,6 +1169,7 @@ function DatePicker({ value, onChange: onChangeProp, placeholder = "Pick a date"
1878
1169
  value && /* @__PURE__ */ jsx(
1879
1170
  Button,
1880
1171
  {
1172
+ type: "button",
1881
1173
  variant: "ghost",
1882
1174
  size: "sm",
1883
1175
  onClick: () => choose(null),
@@ -2264,7 +1556,7 @@ function DateRangePicker({
2264
1556
  ),
2265
1557
  style: { top: pos.top, left: pos.left },
2266
1558
  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: () => {
1559
+ 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
1560
  const r = p.range();
2269
1561
  returnFocus();
2270
1562
  onChange(r);
@@ -2273,7 +1565,7 @@ function DateRangePicker({
2273
1565
  }, children: p.label }, p.label)) }),
2274
1566
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
2275
1567
  /* @__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" }) }),
1568
+ /* @__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
1569
  /* @__PURE__ */ jsxs("span", { className: "text-sm font-medium text-[var(--ds-text-primary)]", children: [
2278
1570
  MONTHS2[months[0].getMonth()],
2279
1571
  " ",
@@ -2283,7 +1575,7 @@ function DateRangePicker({
2283
1575
  " ",
2284
1576
  months[1].getFullYear()
2285
1577
  ] }),
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 }) })
1578
+ /* @__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
1579
  ] }),
2288
1580
  /* @__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
1581
  /* @__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 +1616,7 @@ function DateRangePicker({
2324
1616
  ] }, `${m.getFullYear()}-${m.getMonth()}`)) }),
2325
1617
  /* @__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
1618
  /* @__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: () => {
1619
+ (value.start || value.end) && /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => {
2328
1620
  onChange({ start: null, end: null });
2329
1621
  setHover(null);
2330
1622
  }, children: "Clear" })
@@ -2488,6 +1780,8 @@ function formatSize(bytes) {
2488
1780
  }
2489
1781
  var running = (item) => item.status === "queued" || item.status === "uploading";
2490
1782
  var failureOf = (err) => err instanceof Error && err.message ? err.message : "The upload didn't work.";
1783
+ var percentOf = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 0;
1784
+ var STILL_UPLOADING = "Wait for your files to finish uploading";
2491
1785
  function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accept, multiple = true, maxSize, className, error = false, label, id }) {
2492
1786
  const controlled = files !== void 0;
2493
1787
  const [own, setOwn] = useState([]);
@@ -2508,8 +1802,6 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2508
1802
  const rejectedId = `${ownId}-rejected`;
2509
1803
  const showLabel = label != null && !field;
2510
1804
  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
1805
  const patch = (itemId, change) => setOwn((prev) => prev.map((i) => i.id === itemId ? { ...i, ...change } : i));
2514
1806
  const stop = (itemId) => {
2515
1807
  controllers.current.get(itemId)?.abort();
@@ -2531,7 +1823,7 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2531
1823
  attempt = Promise.resolve(run(item.file, {
2532
1824
  signal: controller.signal,
2533
1825
  onProgress: (percent) => {
2534
- if (!controller.signal.aborted) patch(item.id, { progress: Math.max(0, Math.min(100, percent)) });
1826
+ if (!controller.signal.aborted) patch(item.id, { progress: percentOf(percent) });
2535
1827
  }
2536
1828
  }));
2537
1829
  } catch (err) {
@@ -2539,6 +1831,11 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2539
1831
  }
2540
1832
  attempt.then(() => settle({ status: "done", progress: 100 }), (err) => settle({ status: "error", error: failureOf(err) }));
2541
1833
  };
1834
+ const [hadUpload, setHadUpload] = useState(!!upload);
1835
+ if (!controlled && !!upload !== hadUpload) {
1836
+ setHadUpload(!!upload);
1837
+ 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));
1838
+ }
2542
1839
  useEffect(() => {
2543
1840
  if (controlled || !upload) return;
2544
1841
  let active = own.filter((i) => i.status === "uploading" && controllers.current.has(i.id)).length;
@@ -2564,31 +1861,67 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2564
1861
  useEffect(() => {
2565
1862
  if (!overlayOpen && !controlled) stopAll();
2566
1863
  }, [overlayOpen, controlled, stopAll]);
2567
- const busy = shown.some(running);
1864
+ const busy = shown.some((i) => i.status === "uploading" || i.status === "queued" && (controlled || !!upload));
2568
1865
  useEffect(() => {
2569
1866
  if (!busy || !overlayMark) return;
2570
1867
  return overlayMark.hold();
2571
1868
  }, [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)) {
1869
+ const form = useOptionalFormContext();
1870
+ const waitingKey = `${inputId}-uploads`;
1871
+ const waitingId = `${ownId}-waiting`;
1872
+ const busyRef = useLatest(busy);
1873
+ const registerCheck = form?.register;
1874
+ useEffect(() => registerCheck?.(waitingKey, () => busyRef.current ? STILL_UPLOADING : void 0), [registerCheck, waitingKey, busyRef]);
1875
+ const waiting = form?.errors[waitingKey];
1876
+ const clearWaiting = form?.setError;
1877
+ useEffect(() => {
1878
+ if (!busy && waiting) clearWaiting?.(waitingKey, void 0);
1879
+ }, [busy, waiting, clearWaiting, waitingKey]);
1880
+ useEffect(() => {
1881
+ const owner = inputRef.current?.form;
1882
+ if (!owner) return;
1883
+ const clear = () => setRejected(null);
1884
+ owner.addEventListener("submit", clear);
1885
+ return () => owner.removeEventListener("submit", clear);
1886
+ }, []);
1887
+ const invalid = error || rejected != null || !!waiting;
1888
+ const describedBy = [constraints ? constraintsId : null, field?.describedBy, rejected ? rejectedId : null, waiting ? waitingId : null].filter(Boolean).join(" ") || void 0;
1889
+ useEffect(() => {
1890
+ if (controlled && !onFiles) warnOnce("FileUpload", "`files` without `onFiles`: the page's list never gets the files the user picks. Add them in onFiles.");
1891
+ if (controlled && upload) warnOnce("FileUpload", "pass `files` or `upload`, not both: with `files` the page runs the uploads and FileUpload shows them.");
1892
+ if (!onRemove) warnOnce("FileUpload", "no `onRemove`: Remove shows only on a failed file, since the page would still have a file the user removed.");
1893
+ const ids = shown.map((i) => i.id);
1894
+ 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).");
1895
+ }, [controlled, onFiles, upload, onRemove, shown]);
1896
+ const [heard, setHeard] = useState({ text: "", n: 0 });
1897
+ const signature = shown.map((i) => `${i.id}\0${i.status}`).join("");
1898
+ const [seen, setSeen] = useState(() => ({ signature, statuses: new Map(shown.map((i) => [i.id, i.status])) }));
1899
+ if (signature !== seen.signature) {
2575
1900
  const said = [];
2576
1901
  for (const item of shown) {
2577
- const before = seen.get(item.id);
1902
+ const before = seen.statuses.get(item.id);
2578
1903
  if (!before || before === item.status) continue;
2579
1904
  if (item.status === "done") said.push(`${item.name} uploaded`);
2580
1905
  else if (item.status === "error") said.push(`${item.name} didn't upload: ${item.error ?? "the upload failed"}`);
2581
1906
  }
2582
- setSeen(new Map(shown.map((i) => [i.id, i.status])));
2583
- if (said.length) setHeard(said.join(". "));
1907
+ setSeen({ signature, statuses: new Map(shown.map((i) => [i.id, i.status])) });
1908
+ if (said.length) setHeard((h) => ({ text: said.join(". "), n: h.n + 1 }));
2584
1909
  }
2585
1910
  const pendingFocus = useRef(null);
1911
+ const lastAction = useRef(null);
2586
1912
  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);
1913
+ const gone = pendingFocus.current;
1914
+ if (gone && !shown.some((i) => i.id === gone.id)) {
1915
+ pendingFocus.current = null;
1916
+ const rows = Array.from(listRef.current?.querySelectorAll(":scope > li") ?? []).map((row2) => row2.querySelector("button")).filter((b) => !!b);
1917
+ focusAfterRemoval(rows, gone.index, inputRef.current);
1918
+ return;
1919
+ }
1920
+ const last = lastAction.current;
1921
+ const active = document.activeElement;
1922
+ if (!last || last.el.isConnected || active && active !== document.body) return;
1923
+ const row = Array.from(listRef.current?.querySelectorAll(":scope > li") ?? []).find((li) => li.dataset.uploadId === last.id);
1924
+ row?.querySelector("button")?.focus();
2592
1925
  }, [shown]);
2593
1926
  const addFiles = useCallback(
2594
1927
  (fileList) => {
@@ -2615,7 +1948,7 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2615
1948
  );
2616
1949
  const indexOf = (item) => shown.findIndex((i) => i.id === item.id);
2617
1950
  const cancel = (item) => {
2618
- pendingFocus.current = indexOf(item);
1951
+ pendingFocus.current = { id: item.id, index: indexOf(item) };
2619
1952
  if (!controlled) {
2620
1953
  stop(item.id);
2621
1954
  setOwn((prev) => prev.filter((i) => i.id !== item.id));
@@ -2627,7 +1960,7 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2627
1960
  onRetry?.(item);
2628
1961
  };
2629
1962
  const remove = (item) => {
2630
- pendingFocus.current = indexOf(item);
1963
+ pendingFocus.current = { id: item.id, index: indexOf(item) };
2631
1964
  reportChange?.();
2632
1965
  if (!controlled) {
2633
1966
  stop(item.id);
@@ -2637,7 +1970,7 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2637
1970
  };
2638
1971
  const canCancel = !controlled || !!onCancel;
2639
1972
  const canRetry = !controlled || !!onRetry;
2640
- const canRemove = !controlled || !!onRemove;
1973
+ const canRemove = (item) => !!onRemove || !controlled && item.status === "error";
2641
1974
  return /* @__PURE__ */ jsxs("div", { className: cn("space-y-ds-3", className), children: [
2642
1975
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-ds-1.5", children: [
2643
1976
  showLabel && /* @__PURE__ */ jsx(Label, { htmlFor: inputId, children: label }),
@@ -2691,14 +2024,20 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2691
2024
  ]
2692
2025
  }
2693
2026
  ),
2694
- rejected && /* @__PURE__ */ jsx(InlineFieldError, { id: rejectedId, message: rejected, className: "mt-0" })
2027
+ rejected && /* @__PURE__ */ jsx(InlineFieldError, { id: rejectedId, message: rejected, className: "mt-0" }),
2028
+ waiting && /* @__PURE__ */ jsx(InlineFieldError, { id: waitingId, message: waiting, className: "mt-0" })
2695
2029
  ] }),
2696
2030
  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;
2031
+ const progress = percentOf(item.progress);
2032
+ const words = item.status === "queued" ? "Waiting" : item.status === "uploading" ? `${Math.round(progress)}%` : item.status === "done" ? "Uploaded" : item.status === "error" ? "Failed" : null;
2698
2033
  return /* @__PURE__ */ jsxs(
2699
2034
  "li",
2700
2035
  {
2036
+ "data-upload-id": item.id,
2701
2037
  "data-upload-status": item.status,
2038
+ onFocus: (e) => {
2039
+ if (e.target instanceof HTMLButtonElement) lastAction.current = { id: item.id, el: e.target };
2040
+ },
2702
2041
  className: cn(
2703
2042
  "flex items-center gap-ds-3 text-sm",
2704
2043
  "border-[length:var(--ds-border-width)]",
@@ -2711,20 +2050,20 @@ function FileUpload({ onFiles, files, upload, onCancel, onRetry, onRemove, accep
2711
2050
  /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
2712
2051
  /* @__PURE__ */ jsx(TruncFileName, { className: "text-[var(--ds-text-primary)]", children: item.name }),
2713
2052
  (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" }),
2053
+ item.status === "uploading" && /* @__PURE__ */ jsx(Progress, { value: progress, size: "sm", label: `Uploading ${item.name}`, className: "mt-ds-1" }),
2715
2054
  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
2055
  ] }),
2717
2056
  item.status === "done" && /* @__PURE__ */ jsx(Icon, { name: "check", size: 16, className: "text-[var(--ds-status-success)] shrink-0", "aria-hidden": "true" }),
2718
2057
  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 }) })
2058
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => cancel(item), "aria-label": `Cancel ${item.name}`, children: "Cancel" }),
2059
+ item.status === "error" && canRetry && /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => retry(item), "aria-label": `Retry ${item.name}`, children: "Retry" }),
2060
+ !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
2061
  ]
2723
2062
  },
2724
2063
  item.id
2725
2064
  );
2726
2065
  }) }),
2727
- /* @__PURE__ */ jsx("p", { role: "status", className: "sr-only", children: heard })
2066
+ /* @__PURE__ */ jsx("div", { role: "status", className: "sr-only", children: /* @__PURE__ */ jsx("span", { children: heard.text }, heard.n) })
2728
2067
  ] });
2729
2068
  }
2730
2069
  function SliderRange({
@@ -3334,222 +2673,6 @@ function TimePicker({
3334
2673
  )
3335
2674
  ] });
3336
2675
  }
3337
- var sizes = {
3338
- sm: { box: "h-[var(--ds-control-height-sm)]", input: "text-xs self-stretch", btn: "w-7 text-xs" },
3339
- md: { box: "h-[var(--ds-control-height-md)]", input: "text-sm self-stretch", btn: "w-9 text-sm" },
3340
- lg: { box: "h-[var(--ds-control-height-lg)]", input: "text-base self-stretch", btn: "w-10 text-base" }
3341
- };
3342
- function NumberInput({
3343
- value,
3344
- onChange: onChangeProp,
3345
- min = -Infinity,
3346
- max = Infinity,
3347
- step = 1,
3348
- disabled = false,
3349
- placeholder = "0",
3350
- prefix,
3351
- suffix,
3352
- size: sizeProp,
3353
- className,
3354
- error = false,
3355
- allowDecimals,
3356
- id
3357
- }) {
3358
- const onChange = useReportedChange(onChangeProp, value);
3359
- const contextSize = useContext(ControlSizeContext);
3360
- const size = sizeProp ?? contextSize ?? "md";
3361
- const intervalRef = useRef(void 0);
3362
- const timeoutRef = useRef(void 0);
3363
- const inputRef = useRef(null);
3364
- const [focused, setFocused] = useState(false);
3365
- const [draft, setDraft] = useState(String(value));
3366
- const decimals = allowDecimals ?? step % 1 !== 0;
3367
- const [prevSync, setPrevSync] = useState({ value, focused });
3368
- if (value !== prevSync.value || focused !== prevSync.focused) {
3369
- setPrevSync({ value, focused });
3370
- if (!focused) setDraft(String(value));
3371
- }
3372
- const clamp = useCallback(
3373
- (v) => Math.min(max, Math.max(min, v)),
3374
- [min, max]
3375
- );
3376
- const valueRef = useLatest(value);
3377
- const commitValue = useCallback((v, own = false) => {
3378
- const clamped = clamp(v);
3379
- valueRef.current = clamped;
3380
- setDraft(String(clamped));
3381
- (own ? onChangeProp : onChange)(clamped);
3382
- return clamped;
3383
- }, [onChange, onChangeProp, clamp, valueRef]);
3384
- const increment = useCallback(() => commitValue(valueRef.current + step), [commitValue, step, valueRef]);
3385
- const decrement = useCallback(() => commitValue(valueRef.current - step), [commitValue, step, valueRef]);
3386
- const startRepeat = useCallback(
3387
- (fn) => {
3388
- fn();
3389
- timeoutRef.current = setTimeout(() => {
3390
- intervalRef.current = setInterval(fn, 75);
3391
- }, 400);
3392
- },
3393
- []
3394
- );
3395
- const stopRepeat = useCallback(() => {
3396
- clearTimeout(timeoutRef.current);
3397
- clearInterval(intervalRef.current);
3398
- }, []);
3399
- const handleInput = useCallback(
3400
- (e) => {
3401
- const raw = e.target.value;
3402
- if (raw === "" || raw === "-") {
3403
- setDraft(raw);
3404
- return;
3405
- }
3406
- if (!decimals && raw.includes(".")) return;
3407
- const validPattern = decimals ? /^-?\d*\.?\d*$/ : /^-?\d*$/;
3408
- if (!validPattern.test(raw)) return;
3409
- setDraft(raw);
3410
- const parsed = parseFloat(raw);
3411
- if (!isNaN(parsed) && isFinite(parsed)) {
3412
- onChange(clamp(parsed));
3413
- }
3414
- },
3415
- [onChange, clamp, decimals]
3416
- );
3417
- const handleBlur = useCallback(() => {
3418
- setFocused(false);
3419
- const parsed = parseFloat(draft);
3420
- if (!isNaN(parsed) && isFinite(parsed)) {
3421
- const committed = commitValue(parsed);
3422
- setDraft(String(committed));
3423
- } else {
3424
- const fallback = isFinite(min) ? min : 0;
3425
- const committed = commitValue(value !== void 0 ? value : fallback, true);
3426
- setDraft(String(committed));
3427
- }
3428
- }, [draft, commitValue, min, value]);
3429
- const handleFocus = useCallback(() => {
3430
- setFocused(true);
3431
- }, []);
3432
- const handleKeyDown = useCallback(
3433
- (e) => {
3434
- if (e.key === "ArrowUp") {
3435
- e.preventDefault();
3436
- increment();
3437
- }
3438
- if (e.key === "ArrowDown") {
3439
- e.preventDefault();
3440
- decrement();
3441
- }
3442
- },
3443
- [increment, decrement]
3444
- );
3445
- const s = sizes[size];
3446
- const atMin = value <= min;
3447
- const atMax = value >= max;
3448
- return /* @__PURE__ */ jsxs(
3449
- "div",
3450
- {
3451
- className: cn(
3452
- "flex items-center w-full",
3453
- s.box,
3454
- "border-[length:var(--ds-border-width)]",
3455
- error ? "border-[var(--ds-status-error)]" : "border-[var(--ds-input-border)]",
3456
- "rounded-[var(--ds-radius-md)]",
3457
- "shadow-[var(--ds-input-shadow)]",
3458
- "bg-[var(--ds-input-bg)]",
3459
- "transition-colors duration-[var(--ds-transition-speed)]",
3460
- !error && !disabled && "hover:border-[var(--ds-input-border-hover)]",
3461
- error ? ERROR_FOCUS_WITHIN_RING : cn(FOCUS_WITHIN_RING, "focus-within:hover:border-[var(--ds-border-focus)]"),
3462
- disabled && "opacity-disabled cursor-not-allowed",
3463
- className
3464
- ),
3465
- children: [
3466
- /* @__PURE__ */ jsx(
3467
- "button",
3468
- {
3469
- type: "button",
3470
- tabIndex: -1,
3471
- disabled: disabled || atMin,
3472
- onMouseDown: (e) => {
3473
- e.preventDefault();
3474
- inputRef.current?.focus();
3475
- if (!disabled && !atMin) startRepeat(decrement);
3476
- },
3477
- onMouseUp: stopRepeat,
3478
- onMouseLeave: stopRepeat,
3479
- className: cn(
3480
- "inline-flex items-center justify-center shrink-0",
3481
- "border-r-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
3482
- "text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] hover:bg-[var(--ds-bg-muted)]",
3483
- "transition-colors cursor-pointer",
3484
- "rounded-l-[var(--ds-radius-md)]",
3485
- s.btn,
3486
- s.input,
3487
- (disabled || atMin) && "opacity-disabled cursor-not-allowed hover:bg-transparent hover:text-[var(--ds-text-muted)]"
3488
- ),
3489
- "aria-label": "Decrease",
3490
- children: "\u2212"
3491
- }
3492
- ),
3493
- /* @__PURE__ */ jsxs("div", { className: cn("flex items-center flex-1 min-w-0 px-ds-2 gap-ds-1", s.input), children: [
3494
- prefix && /* @__PURE__ */ jsx("span", { className: "text-[var(--ds-text-muted)] shrink-0 text-xs", children: prefix }),
3495
- /* @__PURE__ */ jsx(
3496
- "input",
3497
- {
3498
- ref: inputRef,
3499
- id,
3500
- type: "text",
3501
- inputMode: decimals ? "decimal" : "numeric",
3502
- "aria-invalid": error || void 0,
3503
- value: focused ? draft : String(value),
3504
- onChange: handleInput,
3505
- onFocus: handleFocus,
3506
- onBlur: handleBlur,
3507
- onKeyDown: handleKeyDown,
3508
- disabled,
3509
- placeholder,
3510
- className: cn(
3511
- "w-full min-w-0 bg-transparent text-center outline-none",
3512
- "text-[var(--ds-text-primary)]",
3513
- "placeholder:text-[var(--ds-text-muted)]",
3514
- "font-[family-name:var(--ds-font-mono)]",
3515
- s.input,
3516
- disabled && "cursor-not-allowed"
3517
- )
3518
- }
3519
- ),
3520
- suffix && /* @__PURE__ */ jsx("span", { className: "text-[var(--ds-text-muted)] shrink-0 text-xs", children: suffix })
3521
- ] }),
3522
- /* @__PURE__ */ jsx(
3523
- "button",
3524
- {
3525
- type: "button",
3526
- tabIndex: -1,
3527
- disabled: disabled || atMax,
3528
- onMouseDown: (e) => {
3529
- e.preventDefault();
3530
- inputRef.current?.focus();
3531
- if (!disabled && !atMax) startRepeat(increment);
3532
- },
3533
- onMouseUp: stopRepeat,
3534
- onMouseLeave: stopRepeat,
3535
- className: cn(
3536
- "inline-flex items-center justify-center shrink-0",
3537
- "border-l-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
3538
- "text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] hover:bg-[var(--ds-bg-muted)]",
3539
- "transition-colors cursor-pointer",
3540
- "rounded-r-[var(--ds-radius-md)]",
3541
- s.btn,
3542
- s.input,
3543
- (disabled || atMax) && "opacity-disabled cursor-not-allowed hover:bg-transparent hover:text-[var(--ds-text-muted)]"
3544
- ),
3545
- "aria-label": "Increase",
3546
- children: "+"
3547
- }
3548
- )
3549
- ]
3550
- }
3551
- );
3552
- }
3553
2676
  var variantStyles = {
3554
2677
  default: "bg-[var(--ds-bg-emphasis)] text-[var(--ds-text-primary)]",
3555
2678
  subtle: "bg-[var(--ds-bg-muted)] text-[var(--ds-text-secondary)]",
@@ -3791,7 +2914,7 @@ function TagInput({
3791
2914
  }
3792
2915
  );
3793
2916
  }
3794
- var sizes2 = {
2917
+ var sizes = {
3795
2918
  sm: "w-[var(--ds-control-height-sm)] h-[var(--ds-control-height-sm)] text-sm",
3796
2919
  md: "w-[var(--ds-control-height-md)] h-[var(--ds-control-height-md)] text-base",
3797
2920
  lg: "w-[var(--ds-control-height-lg)] h-[var(--ds-control-height-lg)] text-lg"
@@ -3895,7 +3018,7 @@ function PinInput({
3895
3018
  !error && !disabled && "hover:border-[var(--ds-input-border-hover)]",
3896
3019
  error ? ERROR_FOCUS_RING : FOCUS_RING,
3897
3020
  "placeholder:text-[var(--ds-text-muted)]",
3898
- sizes2[size],
3021
+ sizes[size],
3899
3022
  disabled && "opacity-disabled cursor-not-allowed"
3900
3023
  )
3901
3024
  },
@@ -3949,11 +3072,13 @@ function Rating({
3949
3072
  readOnly = false,
3950
3073
  size = "md",
3951
3074
  className,
3952
- error = false
3075
+ error = false,
3076
+ ...wiring
3953
3077
  }) {
3954
3078
  const onChange = useReportedChange(onChangeProp, value);
3955
3079
  const [hoverValue, setHoverValue] = useState(null);
3956
3080
  const displayValue = hoverValue ?? value;
3081
+ const control = useFieldControl({ ...wiring, error: error || void 0 });
3957
3082
  const handleClick = useCallback(
3958
3083
  (starIdx, isLeftHalf) => {
3959
3084
  if (readOnly || !onChangeProp) return;
@@ -4020,8 +3145,12 @@ function Rating({
4020
3145
  "div",
4021
3146
  {
4022
3147
  role: readOnly ? "img" : "radiogroup",
4023
- "aria-label": `Rating: ${value} out of ${max}`,
4024
- "aria-invalid": error || void 0,
3148
+ id: control.id,
3149
+ "aria-label": readOnly || !control.labelId ? `Rating: ${value} out of ${max}` : void 0,
3150
+ "aria-labelledby": readOnly ? void 0 : control.labelId,
3151
+ "aria-describedby": control.describedBy,
3152
+ "aria-invalid": readOnly ? void 0 : control.invalid || void 0,
3153
+ "aria-required": readOnly ? void 0 : control.required || void 0,
4025
3154
  onKeyDown: readOnly ? void 0 : handleKeyDown,
4026
3155
  className: cn(
4027
3156
  "inline-flex items-center gap-ds-0.5",
@@ -4431,11 +3560,6 @@ var Alert = forwardRef(function Alert2({
4431
3560
  }
4432
3561
  );
4433
3562
  });
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
3563
  var isThenable = (value) => !!value && typeof value.then === "function";
4440
3564
  function stampFormId(node, formId) {
4441
3565
  return Children.map(node, (child) => {
@@ -4494,29 +3618,18 @@ function FormWrapper({ onSubmit, validationMode: validationModeProp, children, a
4494
3618
  const pendingFocus = useRef(false);
4495
3619
  const onSubmitRef = useLatest(onSubmit);
4496
3620
  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]);
3621
+ const reportChange = useMemo(() => overlayMark ? () => overlayMark.report(formId) : null, [overlayMark, formId]);
3622
+ useEffect(() => overlayMark?.attach(formId), [overlayMark, formId]);
4504
3623
  useEffect(() => {
4505
3624
  if (pendingSubmit.current && submitted) {
4506
3625
  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);
3626
+ const answer = overlayMark?.submit(formId, { element: () => formRef.current, failing: () => Object.keys(errorsRef.current).length > 0 });
4510
3627
  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
- }
3628
+ if (!answer) return;
3629
+ if (isThenable(result)) void result.then(() => answer.settle("resolved"));
3630
+ else answer.settle("returned");
4518
3631
  }
4519
- }, [submitted, submitCount, errors, errorsRef, onSubmitRef, overlayMark]);
3632
+ }, [submitted, submitCount, errors, errorsRef, onSubmitRef, overlayMark, formId]);
4520
3633
  const submittedRef = useLatest(submitted);
4521
3634
  const errorSummaryRef = useLatest(errorSummary);
4522
3635
  const summaryRef = useRef(null);
@@ -4601,7 +3714,9 @@ function FormWrapper({ onSubmit, validationMode: validationModeProp, children, a
4601
3714
  }, []);
4602
3715
  const handleSubmit = useCallback(
4603
3716
  (e) => {
3717
+ if (e.target !== e.currentTarget) return;
4604
3718
  e.preventDefault();
3719
+ e.stopPropagation();
4605
3720
  if (checks.current.size > 0) {
4606
3721
  const found = /* @__PURE__ */ new Map();
4607
3722
  for (const [field, check] of checks.current) found.set(field, check());
@@ -4645,16 +3760,26 @@ var FOCUSABLE2 = "input:not([disabled]):not([type='hidden']), select:not([disabl
4645
3760
  function AddAnother({ items, getId, itemName, columns, renderRow, onAdd, onRemove, addLabel = "Add another", label, minItems = 1, maxItems, className }) {
4646
3761
  const listRef = useRef(null);
4647
3762
  const addRef = useRef(null);
3763
+ const reportChange = useContext(FormChangeCtx);
3764
+ const getIdRef = useLatest(getId);
4648
3765
  const pending = useRef(null);
4649
- const firstFields = () => Array.from(listRef.current?.querySelectorAll(":scope > li") ?? []).map((row) => row.querySelector(FOCUSABLE2)).filter((el) => !!el);
4650
3766
  useEffect(() => {
4651
3767
  const next = pending.current;
4652
3768
  if (!next) return;
3769
+ const rows = () => Array.from(listRef.current?.querySelectorAll(":scope > li") ?? []);
3770
+ const firstFields = () => rows().map((row) => row.querySelector(FOCUSABLE2)).filter((el) => !!el);
3771
+ const ids = items.map((item) => getIdRef.current(item));
3772
+ if (next.kind === "remove") {
3773
+ if (ids.includes(next.id)) return;
3774
+ pending.current = null;
3775
+ focusAfterRemoval(firstFields(), next.index, addRef.current);
3776
+ return;
3777
+ }
3778
+ const added = ids.findIndex((id) => !next.before.has(id));
3779
+ if (added === -1) return;
4653
3780
  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]);
3781
+ rows()[added]?.querySelector(FOCUSABLE2)?.focus();
3782
+ }, [items, getIdRef]);
4658
3783
  const canRemove = items.length > minItems;
4659
3784
  const canAdd = maxItems === void 0 || items.length < maxItems;
4660
3785
  const grid = { "--gs-add-another-cols": `repeat(${columns.length}, minmax(0, 1fr)) max-content` };
@@ -4669,13 +3794,15 @@ function AddAnother({ items, getId, itemName, columns, renderRow, onAdd, onRemov
4669
3794
  /* @__PURE__ */ jsx(AddAnotherRowCtx.Provider, { value: true, children: renderRow(item, index, (column) => `${column}, ${itemName(index)}`) }),
4670
3795
  canRemove && // the visible "Remove" starts its name, which says whose row it removes (WCAG 2.5.3 label in name)
4671
3796
  /* @__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 };
3797
+ pending.current = { kind: "remove", id, index };
3798
+ reportChange?.();
4673
3799
  onRemove(id);
4674
3800
  }, children: "Remove" })
4675
3801
  ] }, id);
4676
3802
  }) }),
4677
3803
  canAdd && /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, { ref: addRef, type: "button", variant: "secondary", onClick: () => {
4678
- pending.current = { kind: "add" };
3804
+ pending.current = { kind: "add", before: new Set(items.map(getId)) };
3805
+ reportChange?.();
4679
3806
  onAdd();
4680
3807
  }, children: addLabel }) })
4681
3808
  ] });
@@ -4695,7 +3822,7 @@ function TransferList({
4695
3822
  className,
4696
3823
  ref
4697
3824
  }) {
4698
- const onChange = useReportedChange(onChangeProp, selected);
3825
+ const onChange = useReportedChange(onChangeProp, selected, (_available, chosen) => chosen);
4699
3826
  const [checkedAvailable, setCheckedAvailable] = useState(/* @__PURE__ */ new Set());
4700
3827
  const [checkedSelected, setCheckedSelected] = useState(/* @__PURE__ */ new Set());
4701
3828
  const [searchLeft, setSearchLeft] = useState("");
@@ -5225,50 +4352,25 @@ function MentionInput({
5225
4352
  ]
5226
4353
  },
5227
4354
  s.id
5228
- ))
5229
- }
5230
- ),
5231
- document.body
5232
- )
5233
- ] });
5234
- }
5235
- var HEADER_LAYOUT = {
5236
- row: "flex flex-wrap items-start justify-between gap-ds-4",
5237
- block: "flex flex-col space-y-ds-1.5 grow basis-1/2 break-words max-w-full has-[[data-title-one-line]]:min-w-0",
5238
- titleLeads: "grow basis-1/2 max-w-max break-words has-[[data-title-one-line]]:min-w-0",
5239
- floorOff: "min-w-0",
5240
- actions: "flex items-center gap-ds-2 shrink-0 ml-auto",
5241
- below: "mt-ds-1.5"
5242
- };
5243
- function useTitleFloorFits(rowRef, active) {
5244
- const [fits, setFits] = useState(true);
5245
- useLayoutEffect(() => {
5246
- const row = rowRef.current;
5247
- if (!active || !row) return;
5248
- const measure = () => {
5249
- const title = row.querySelector("h1, h2, h3, h4, h5, h6");
5250
- if (!title) return;
5251
- const width = title.style.width;
5252
- title.style.width = "min-content";
5253
- const longest = title.getBoundingClientRect().width;
5254
- title.style.width = width;
5255
- if (longest === 0) return;
5256
- const next = longest <= row.clientWidth + 0.5;
5257
- setFits((was) => was === next ? was : next);
5258
- };
5259
- measure();
5260
- if (typeof ResizeObserver === "undefined") return;
5261
- const observer = new ResizeObserver(measure);
5262
- observer.observe(row);
5263
- return () => observer.disconnect();
5264
- }, [rowRef, active]);
5265
- return fits;
4355
+ ))
4356
+ }
4357
+ ),
4358
+ document.body
4359
+ )
4360
+ ] });
5266
4361
  }
5267
- function actionsAreBadges(actions) {
5268
- const items = Children.toArray(actions);
5269
- return items.length > 0 && items.every((item) => isValidElement(item) && (item.type === Fragment$1 ? actionsAreBadges(item.props.children) : item.type === Badge));
4362
+ var CardRowCtx = createContext(null);
4363
+ var TITLE_LINE = "h-[calc(var(--ds-font-size-lg)*var(--ds-line-height-lg))]";
4364
+ var HeaderRowCtx = createContext(false);
4365
+ function holdsHeader(children) {
4366
+ let found = false;
4367
+ Children.forEach(children, (child) => {
4368
+ if (found || !isValidElement(child)) return;
4369
+ if (child.type === Fragment$1) found = holdsHeader(child.props.children);
4370
+ else if (child.type === CardHeader) found = true;
4371
+ });
4372
+ return found;
5270
4373
  }
5271
- var CardCtx = createContext(false);
5272
4374
  var CardActionCtx = createContext(null);
5273
4375
  var CARD_ACTION_ATTR = "data-card-action";
5274
4376
  var CardSurfaceCtx = createContext(false);
@@ -5419,7 +4521,7 @@ var Card = forwardRef(function Card2({
5419
4521
  const handleKeyDown = isSelectable && !disabled ? (e) => {
5420
4522
  if (e.key === "Enter" || e.key === " ") {
5421
4523
  e.preventDefault();
5422
- onSelectedChange?.(!selected);
4524
+ if (!e.repeat) onSelectedChange?.(!selected);
5423
4525
  }
5424
4526
  } : isAction && !disabled ? (e) => {
5425
4527
  if (e.target !== e.currentTarget) return;
@@ -5436,13 +4538,84 @@ var Card = forwardRef(function Card2({
5436
4538
  } : void 0;
5437
4539
  const handleCheckableClick = (e) => {
5438
4540
  e.stopPropagation();
5439
- onSelectedChange?.(!selected);
4541
+ if (!disabled) onSelectedChange?.(!selected);
5440
4542
  };
5441
4543
  const isButton = isSelectable || isAction && !holdsControls;
5442
4544
  const role = selectType === "radio" ? "radio" : selectType === "checkbox" ? "checkbox" : selectType === "toggle" ? "switch" : isButton ? "button" : void 0;
5443
4545
  const ariaAttr = selectType === "radio" || selectType === "checkbox" || selectType === "toggle" ? { "aria-checked": !!selected } : isSelectable ? { "aria-pressed": !!selected } : {};
5444
4546
  const indicator = selectType === "radio" ? /* @__PURE__ */ jsx(RadioIndicator, { checked: !!selected, disabled }) : selectType === "checkbox" ? /* @__PURE__ */ jsx(CheckboxIndicator, { checked: !!selected, indeterminate, disabled }) : selectType === "toggle" ? /* @__PURE__ */ jsx(ToggleIndicator, { checked: !!selected, disabled }) : null;
5445
- const defaultCheckBadge = !hasSelectType && !checkable && selected && !loading ? /* @__PURE__ */ jsx("div", { className: "absolute top-ds-3 right-ds-3 w-5 h-5 rounded-[var(--ds-radius-full)] bg-[var(--ds-accent-primary)] flex items-center justify-center", children: /* @__PURE__ */ jsx(Icon, { name: "check", size: 12, className: "text-[var(--ds-text-on-primary)]" }) }) : null;
4547
+ const defaultCheckBadge = !hasSelectType && !checkable && selected && !loading ? /* @__PURE__ */ jsx("span", { "data-card-check": "", className: "w-5 h-5 rounded-[var(--ds-radius-full)] bg-[var(--ds-accent-primary)] flex items-center justify-center", children: /* @__PURE__ */ jsx(Icon, { name: "check", size: 12, className: "text-[var(--ds-text-on-primary)]" }) }) : null;
4548
+ const checkboxes = useRef(/* @__PURE__ */ new Set());
4549
+ const checkboxFocused = useRef(false);
4550
+ const checkboxRef = useCallback((node) => {
4551
+ if (!node) return;
4552
+ checkboxes.current.add(node);
4553
+ return () => {
4554
+ checkboxes.current.delete(node);
4555
+ };
4556
+ }, []);
4557
+ useLayoutEffect(() => {
4558
+ if (!checkboxFocused.current) return;
4559
+ const active = document.activeElement;
4560
+ if (active && active !== document.body) return;
4561
+ for (const node of checkboxes.current) {
4562
+ if (node.isConnected) {
4563
+ node.focus({ preventScroll: true });
4564
+ return;
4565
+ }
4566
+ }
4567
+ });
4568
+ const checkbox = checkable ? /* @__PURE__ */ jsx(
4569
+ "div",
4570
+ {
4571
+ ref: checkboxRef,
4572
+ "data-card-checkbox": "",
4573
+ className: cn(
4574
+ "rounded-[calc(var(--ds-radius-sm)*0.6)] focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
4575
+ disabled ? "cursor-not-allowed" : "cursor-pointer"
4576
+ ),
4577
+ onClick: handleCheckableClick,
4578
+ role: "checkbox",
4579
+ "aria-checked": !!selected,
4580
+ "aria-disabled": disabled || void 0,
4581
+ ...inner.titleId ? { "aria-labelledby": inner.titleId } : { "aria-label": "Select" },
4582
+ tabIndex: 0,
4583
+ onFocus: () => {
4584
+ checkboxFocused.current = true;
4585
+ },
4586
+ onBlur: (e) => {
4587
+ const node = e.currentTarget;
4588
+ queueMicrotask(() => {
4589
+ if (node.isConnected) checkboxFocused.current = false;
4590
+ });
4591
+ },
4592
+ onKeyDown: (e) => {
4593
+ if (e.key !== " ") return;
4594
+ e.preventDefault();
4595
+ e.stopPropagation();
4596
+ if (!e.repeat && !disabled) onSelectedChange?.(!selected);
4597
+ },
4598
+ children: /* @__PURE__ */ jsx(CheckboxIndicator, { checked: !!selected, disabled })
4599
+ }
4600
+ ) : null;
4601
+ const check = indicator ?? defaultCheckBadge;
4602
+ const headers = useRef([]);
4603
+ const [owner, setOwner] = useState(null);
4604
+ const [guess, setGuess] = useState(() => holdsHeader(children));
4605
+ const register = useCallback((header) => {
4606
+ headers.current = [...headers.current, header];
4607
+ setOwner(headers.current[0]);
4608
+ return () => {
4609
+ headers.current = headers.current.filter((h) => h !== header);
4610
+ setOwner(headers.current[0] ?? null);
4611
+ setGuess(false);
4612
+ };
4613
+ }, []);
4614
+ useLayoutEffect(() => {
4615
+ if (headers.current.length === 0) setGuess(false);
4616
+ }, []);
4617
+ const hasHeader = owner !== null || guess;
4618
+ const rowParts = { checkbox, check, owner, register };
5446
4619
  return /* @__PURE__ */ jsxs(
5447
4620
  "div",
5448
4621
  {
@@ -5473,32 +4646,22 @@ var Card = forwardRef(function Card2({
5473
4646
  ...ariaAttr,
5474
4647
  ...props,
5475
4648
  children: [
5476
- defaultCheckBadge,
5477
- indicator && /* @__PURE__ */ jsx("div", { className: "absolute top-ds-3 right-ds-3 z-[1]", children: indicator }),
5478
- checkable && /* @__PURE__ */ jsx(
5479
- "div",
5480
- {
5481
- className: "absolute top-ds-3 left-ds-3 z-[1] cursor-pointer",
5482
- onClick: handleCheckableClick,
5483
- role: "checkbox",
5484
- "aria-checked": !!selected,
5485
- ...inner.titleId ? { "aria-labelledby": inner.titleId } : { "aria-label": "Select" },
5486
- tabIndex: 0,
5487
- onKeyDown: (e) => {
5488
- if (e.key === "Enter" || e.key === " ") {
5489
- e.preventDefault();
5490
- e.stopPropagation();
5491
- onSelectedChange?.(!selected);
5492
- }
5493
- },
5494
- children: /* @__PURE__ */ jsx(CheckboxIndicator, { checked: !!selected })
5495
- }
5496
- ),
5497
- /* @__PURE__ */ jsx(CardSurfaceCtx, { value: true, children: /* @__PURE__ */ jsx(CardActionCtx, { value: action, children: /* @__PURE__ */ jsx(CardCtx, { value: checkable, children: loading ? (
4649
+ !loading && !hasHeader && checkbox && /* @__PURE__ */ jsxs("div", { "data-card-header-row": "", className: "flex items-center gap-ds-2 px-ds-5 pt-ds-4", children: [
4650
+ checkbox,
4651
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "grow" }),
4652
+ check && /* @__PURE__ */ jsx("span", { "data-card-selection": "", className: "shrink-0 flex", children: check })
4653
+ ] }),
4654
+ !loading && !hasHeader && !checkbox && check && /* @__PURE__ */ jsx("div", { "data-card-selection": "", className: "absolute top-ds-3 right-ds-3 z-[1] flex", children: check }),
4655
+ /* @__PURE__ */ jsx(CardSurfaceCtx, { value: true, children: /* @__PURE__ */ jsx(CardActionCtx, { value: action, children: /* @__PURE__ */ jsx(CardRowCtx, { value: rowParts, children: loading ? (
5498
4656
  // Round 15 (the owner's 52.1): a placeholder stands where the content will stand — inside the card's
5499
4657
  // content inset (CardContent's padding), a title-height line then two body lines — never flush against the border.
5500
- /* @__PURE__ */ jsx("div", { "data-card-loading": "", "aria-busy": "true", className: cn("pr-ds-5 py-ds-4", checkable ? "pl-ds-10" : "pl-ds-5"), children: /* @__PURE__ */ jsxs("div", { className: "space-y-ds-3", children: [
5501
- /* @__PURE__ */ jsx(Skeleton, { variant: "custom", className: "h-5 w-1/3" }),
4658
+ // Its header holds a badge-sized block (A2), so a grid of item cards keeps its columns when they load.
4659
+ /* @__PURE__ */ jsx("div", { "data-card-loading": "", "aria-busy": "true", className: "px-ds-5 py-ds-4", children: /* @__PURE__ */ jsxs("div", { className: "space-y-ds-3", children: [
4660
+ /* @__PURE__ */ jsxs("div", { "data-card-header-row": "", className: "flex items-center gap-ds-2", children: [
4661
+ checkbox,
4662
+ /* @__PURE__ */ jsx(Skeleton, { variant: "custom", className: "h-5 w-1/3" }),
4663
+ /* @__PURE__ */ jsx("div", { "data-header-actions": "", className: "contents", children: /* @__PURE__ */ jsx("div", { "data-gs-badge": "", className: "shrink-0", children: /* @__PURE__ */ jsx(Skeleton, { variant: "custom", className: "h-5 w-14 rounded-[var(--ds-radius-full)]" }) }) })
4664
+ ] }),
5502
4665
  /* @__PURE__ */ jsx(Skeleton, { variant: "custom", className: "h-3 w-full" }),
5503
4666
  /* @__PURE__ */ jsx(Skeleton, { variant: "custom", className: "h-3 w-2/3" })
5504
4667
  ] }) })
@@ -5507,60 +4670,95 @@ var Card = forwardRef(function Card2({
5507
4670
  }
5508
4671
  );
5509
4672
  });
5510
- var CardHeader = forwardRef(function CardHeader2({ className, children, actions, icon, menuItems, ...props }, ref) {
5511
- const isCheckable = useContext(CardCtx);
5512
- const hasActions = actions || menuItems;
5513
- const pl = isCheckable ? "pl-ds-10" : "pl-ds-5";
5514
- const actionsBlock = hasActions ? /* @__PURE__ */ jsxs("div", { className: HEADER_LAYOUT.actions, children: [
5515
- actions,
5516
- menuItems && /* @__PURE__ */ jsx(
5517
- DropdownMenu,
5518
- {
5519
- trigger: /* @__PURE__ */ jsx(
5520
- "button",
4673
+ function partsOf(children) {
4674
+ const parts = [];
4675
+ const open = (node) => Children.toArray(node).forEach((child) => {
4676
+ if (isValidElement(child) && child.type === Fragment$1) open(child.props.children);
4677
+ else if (child !== "") parts.push(child);
4678
+ });
4679
+ open(children);
4680
+ return parts;
4681
+ }
4682
+ function splitTitle(children) {
4683
+ const parts = partsOf(children);
4684
+ const at = parts.findIndex((p) => isValidElement(p) && p.type === CardTitle);
4685
+ if (at < 0) return { lead: [], title: parts[0] ?? null, rest: parts.slice(1) };
4686
+ return { lead: parts.slice(0, at), title: parts[at], rest: parts.slice(at + 1) };
4687
+ }
4688
+ var TITLE_FLOOR = "basis-[calc(var(--ds-font-size-lg)*4.5)]";
4689
+ var TITLE_ROW = cn(
4690
+ "flex flex-wrap items-start justify-end gap-x-ds-2 gap-y-ds-2 min-w-0 grow basis-0",
4691
+ "[&>[data-header-actions]>*]:order-5",
4692
+ "[&>[data-header-actions]>:has([data-gs-badge]):not(:has(a,button,input,select,textarea,[role=button],[tabindex]:not([tabindex='-1'])))]:order-2",
4693
+ "[&>[data-header-actions]>:has([aria-haspopup])]:order-6"
4694
+ );
4695
+ var CardHeader = forwardRef(function CardHeader2(allProps, ref) {
4696
+ const { className, children, actions, icon, menuItems, ...props } = allProps;
4697
+ const row = useContext(CardRowCtx);
4698
+ const [self] = useState(() => ({}));
4699
+ const register = row?.register;
4700
+ useLayoutEffect(() => register?.(self), [register, self]);
4701
+ const owns = !!row && row.owner === self;
4702
+ const iconBlock = icon ? (
4703
+ // the icon stays where it was, a tile above the title (28 of 66 headers with actions have one)
4704
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 h-10 w-10 rounded-[var(--ds-radius-md)] bg-[var(--ds-bg-muted)] flex items-center justify-center", children: /* @__PURE__ */ jsx(Icon, { name: icon, size: 20, className: "text-[var(--ds-text-secondary)]" }) })
4705
+ ) : null;
4706
+ const menu = menuItems ? /* @__PURE__ */ jsx(
4707
+ DropdownMenu,
4708
+ {
4709
+ trigger: /* @__PURE__ */ jsx(
4710
+ "button",
4711
+ {
4712
+ type: "button",
4713
+ "aria-label": "More actions",
4714
+ className: cn(
4715
+ "inline-flex items-center justify-center h-8 w-8 rounded-[var(--ds-radius-md)]",
4716
+ "text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] hover:bg-[var(--ds-bg-muted)]",
4717
+ "transition-colors duration-[var(--ds-transition-speed)]"
4718
+ ),
4719
+ onClick: (e) => e.stopPropagation(),
4720
+ children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: 16 })
4721
+ }
4722
+ ),
4723
+ items: menuItems
4724
+ }
4725
+ ) : null;
4726
+ const band = (order) => cn("shrink-0 self-start flex items-center", TITLE_LINE, order);
4727
+ const checkbox = owns && row.checkbox ? /* @__PURE__ */ jsx("span", { className: band("order-0"), children: row.checkbox }) : null;
4728
+ const check = owns && row.check ? /* @__PURE__ */ jsx("span", { "data-card-selection": "", className: band("order-4"), children: row.check }) : null;
4729
+ const menuPart = menu ? /* @__PURE__ */ jsx("span", { className: band("order-6"), children: menu }) : null;
4730
+ const { lead, title, rest } = splitTitle(children);
4731
+ const leadBlock = lead.length > 0 ? /* @__PURE__ */ jsx("div", { "data-card-lead": "", className: "flex flex-col space-y-ds-1.5 min-w-0 grow", children: lead }) : null;
4732
+ const topLine = iconBlock ?? leadBlock;
4733
+ const ownBlock = title !== null && !(isValidElement(title) && title.type === CardTitle);
4734
+ const actionsSlot = "actions" in allProps ? /* @__PURE__ */ jsx("div", { "data-header-actions": "", className: "contents", children: partsOf(actions).map((item, i) => /* @__PURE__ */ jsx("span", { className: cn("flex items-center shrink-0", TITLE_LINE), children: item }, isValidElement(item) && item.key !== null ? item.key : i)) }) : null;
4735
+ return /* @__PURE__ */ jsxs("div", { ref, "data-card-header": "", className: cn("flex flex-col px-ds-5 pt-ds-4 last:pb-ds-4 min-w-0", className), ...props, children: [
4736
+ topLine && /* @__PURE__ */ jsxs("div", { "data-card-icon-row": iconBlock ? "" : void 0, "data-card-lead-row": iconBlock ? void 0 : "", className: cn("flex items-start gap-ds-2", iconBlock ? "mb-ds-2.5" : "mb-ds-1.5"), children: [
4737
+ checkbox,
4738
+ topLine,
4739
+ iconBlock && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "grow -ml-ds-2" }),
4740
+ check,
4741
+ menuPart
4742
+ ] }),
4743
+ iconBlock && leadBlock && /* @__PURE__ */ jsx("div", { className: "flex mb-ds-1.5", children: leadBlock }),
4744
+ /* @__PURE__ */ jsxs("div", { "data-card-header-row": "", className: "flex items-start gap-ds-2 min-w-0", children: [
4745
+ !topLine && checkbox,
4746
+ /* @__PURE__ */ jsxs("div", { "data-card-title-line": "", className: TITLE_ROW, children: [
4747
+ title && /* @__PURE__ */ jsx(
4748
+ "div",
5521
4749
  {
5522
- type: "button",
5523
- "aria-label": "More actions",
5524
- className: cn(
5525
- "inline-flex items-center justify-center h-8 w-8 rounded-[var(--ds-radius-md)]",
5526
- "text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] hover:bg-[var(--ds-bg-muted)]",
5527
- "transition-colors duration-[var(--ds-transition-speed)]"
5528
- ),
5529
- onClick: (e) => e.stopPropagation(),
5530
- children: /* @__PURE__ */ jsx(Icon, { name: "more-horizontal", size: 16 })
4750
+ "data-card-title-row": "",
4751
+ className: cn("order-1 min-w-0 me-auto", TITLE_FLOOR, ownBlock ? "grow" : "grow-[999] max-w-max"),
4752
+ children: /* @__PURE__ */ jsx(HeaderRowCtx, { value: true, children: title })
5531
4753
  }
5532
4754
  ),
5533
- items: menuItems
5534
- }
5535
- )
5536
- ] }) : null;
5537
- const iconBlock = icon ? /* @__PURE__ */ jsx("span", { className: "shrink-0 h-10 w-10 rounded-[var(--ds-radius-md)] bg-[var(--ds-bg-muted)] flex items-center justify-center mb-ds-1", children: /* @__PURE__ */ jsx(Icon, { name: icon, size: 20, className: "text-[var(--ds-text-secondary)]" }) }) : null;
5538
- const parts = Children.toArray(children);
5539
- const titleLeads = !!hasActions && !iconBlock && !menuItems && actionsAreBadges(actions) && parts.length > 0 && isValidElement(parts[0]) && parts[0].type === CardTitle;
5540
- const leadRowRef = useRef(null);
5541
- const leadFloorFits = useTitleFloorFits(leadRowRef, titleLeads);
5542
- if (hasActions) {
5543
- if (titleLeads) {
5544
- const below = parts.slice(1);
5545
- return /* @__PURE__ */ jsxs("div", { ref, className: cn(pl, "pr-ds-5 pt-ds-4 last:pb-ds-4", className), ...props, children: [
5546
- /* @__PURE__ */ jsxs("div", { ref: leadRowRef, className: HEADER_LAYOUT.row, children: [
5547
- /* @__PURE__ */ jsx("div", { "data-card-title-row": "", className: cn(HEADER_LAYOUT.titleLeads, !leadFloorFits && HEADER_LAYOUT.floorOff), children: parts[0] }),
5548
- actionsBlock
5549
- ] }),
5550
- below.length > 0 && /* @__PURE__ */ jsx("div", { className: cn("flex flex-col space-y-ds-1.5 break-words", HEADER_LAYOUT.below), children: below })
5551
- ] });
5552
- }
5553
- return /* @__PURE__ */ jsx("div", { ref, className: cn(pl, "pr-ds-5 pt-ds-4 last:pb-ds-4", className), ...props, children: /* @__PURE__ */ jsxs("div", { className: HEADER_LAYOUT.row, children: [
5554
- /* @__PURE__ */ jsxs("div", { className: HEADER_LAYOUT.block, children: [
5555
- iconBlock,
5556
- children
4755
+ actionsSlot,
4756
+ !ownBlock && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "order-3 grow basis-0 -ml-ds-2" })
5557
4757
  ] }),
5558
- actionsBlock
5559
- ] }) });
5560
- }
5561
- return /* @__PURE__ */ jsxs("div", { ref, className: cn("flex flex-col space-y-ds-1.5 pr-ds-5 pt-ds-4 last:pb-ds-4", pl, className), ...props, children: [
5562
- iconBlock,
5563
- children
4758
+ !topLine && check,
4759
+ !topLine && menuPart
4760
+ ] }),
4761
+ rest.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-col space-y-ds-1.5 break-words mt-ds-1.5 min-w-0", children: rest })
5564
4762
  ] });
5565
4763
  });
5566
4764
  var cardLineClampClasses = {
@@ -5571,7 +4769,10 @@ var cardLineClampClasses = {
5571
4769
  5: "line-clamp-5",
5572
4770
  6: "line-clamp-6"
5573
4771
  };
5574
- var CardTitle = forwardRef(function CardTitle2({ className, children, lines = 2, id, ...props }, forwardedRef) {
4772
+ var WHOLE_ON_KEYBOARD = "[[data-card]:has(:focus-visible)_&]:whitespace-normal [[data-card]:has(:focus-visible)_&]:line-clamp-none";
4773
+ var CardTitle = forwardRef(function CardTitle2({ className, children, lines: linesProp, id, ...props }, forwardedRef) {
4774
+ const inHeaderRow = useContext(HeaderRowCtx);
4775
+ const lines = linesProp ?? (inHeaderRow ? 1 : 2);
5575
4776
  const action = useContext(CardActionCtx);
5576
4777
  const generatedId = useId();
5577
4778
  const { ref: overflowRef, isTruncated } = useTextOverflow({ lines, disabled: lines === 0 });
@@ -5587,7 +4788,7 @@ var CardTitle = forwardRef(function CardTitle2({ className, children, lines = 2,
5587
4788
  id: id ?? generatedId,
5588
4789
  "data-card-title": "",
5589
4790
  ...lines === 1 ? { "data-title-one-line": "" } : {},
5590
- className: cn(TITLE_TEXT, !action && lines > 0 && lineClampClass, action && lines === 1 && "text-ellipsis", className),
4791
+ className: cn(TITLE_TEXT, !action && lines > 0 && [lineClampClass, WHOLE_ON_KEYBOARD], action && lines === 1 && "text-ellipsis", className),
5591
4792
  ...props,
5592
4793
  children: action ? (
5593
4794
  // The card's action when the card holds controls of its own (Sprint 25.7): a real button, which a click
@@ -5604,14 +4805,13 @@ var CardTitle = forwardRef(function CardTitle2({ className, children, lines = 2,
5604
4805
  "block w-full text-left cursor-pointer [text-overflow:inherit] focus-visible:outline-none disabled:cursor-not-allowed",
5605
4806
  "after:absolute after:inset-0 after:rounded-[var(--ds-radius-md)] after:pointer-events-none focus-visible:after:shadow-[var(--ds-shadow-focus)]"
5606
4807
  ),
5607
- children: /* @__PURE__ */ jsx("span", { ref: (node) => overflowRef(lines > 0 ? node : null), className: cn("block [text-overflow:inherit]", lines > 0 && lineClampClass), children })
4808
+ children: /* @__PURE__ */ jsx("span", { ref: (node) => overflowRef(lines > 0 ? node : null), className: cn("block [text-overflow:inherit]", lines > 0 && [lineClampClass, WHOLE_ON_KEYBOARD]), children })
5608
4809
  }
5609
4810
  )
5610
4811
  ) : children
5611
4812
  }
5612
4813
  );
5613
- if (!isTruncated || lines === 0) return heading;
5614
- return /* @__PURE__ */ jsx(Tooltip, { content: children, children: heading });
4814
+ return /* @__PURE__ */ jsx(Tooltip, { content: children, disabled: !isTruncated, children: heading });
5615
4815
  });
5616
4816
  var CardDescription = forwardRef(function CardDescription2({ className, children, lines = 2, ...props }, forwardedRef) {
5617
4817
  const { ref: overflowRef, isTruncated } = useTextOverflow({ lines, disabled: lines === 0 });
@@ -5629,18 +4829,15 @@ var CardDescription = forwardRef(function CardDescription2({ className, children
5629
4829
  children
5630
4830
  }
5631
4831
  );
5632
- if (!isTruncated || lines === 0) return el;
5633
- return /* @__PURE__ */ jsx(Tooltip, { content: children, children: el });
4832
+ return /* @__PURE__ */ jsx(Tooltip, { content: children, disabled: !isTruncated, children: el });
5634
4833
  });
5635
4834
  var CardContent = forwardRef(function CardContent2({ className, children, ...props }, ref) {
5636
- const isCheckable = useContext(CardCtx);
5637
- return /* @__PURE__ */ jsx("div", { ref, className: cn("flex-1 pr-ds-5 py-ds-4", isCheckable ? "pl-ds-10" : "pl-ds-5", className), ...props, children });
4835
+ return /* @__PURE__ */ jsx("div", { ref, className: cn("flex-1 px-ds-5 py-ds-4", className), ...props, children });
5638
4836
  });
5639
4837
  var CardFooter = forwardRef(function CardFooter2({ className, children, ...props }, ref) {
5640
- const isCheckable = useContext(CardCtx);
5641
4838
  return (
5642
4839
  // a footer that holds only buttons puts them at the end (round 11: a lone Save sat on the left); text keeps the start
5643
- /* @__PURE__ */ jsx("div", { ref, className: cn("flex items-center gap-ds-2 pr-ds-5 pb-ds-4 pt-ds-2 mt-auto min-w-0 [&:not(:has(>:not(button)))]:justify-end", isCheckable ? "pl-ds-10" : "pl-ds-5", className), ...props, children })
4840
+ /* @__PURE__ */ jsx("div", { ref, className: cn("flex items-center gap-ds-2 px-ds-5 pb-ds-4 pt-ds-2 mt-auto min-w-0 [&:not(:has(>:not(button)))]:justify-end", className), ...props, children })
5644
4841
  );
5645
4842
  });
5646
4843
  function TruncLabel({ children }) {
@@ -5756,6 +4953,9 @@ function DataList({ items, columns = 1, className }) {
5756
4953
  );
5757
4954
  }
5758
4955
  var COUNT_FROM_PAGE = [
4956
+ // one card counts too: a lone card fell back to React's count, which counts components that draw nothing, and a grid
4957
+ // with one card inside another took the outer grid's count (the review of 30 September)
4958
+ "[&:has(>:only-child)]:[--gs-cards:1]",
5759
4959
  "[&:has(>:nth-child(2):last-child)]:[--gs-cards:2]",
5760
4960
  "[&:has(>:nth-child(3):last-child)]:[--gs-cards:3]",
5761
4961
  "[&:has(>:nth-child(4):last-child)]:[--gs-cards:4]",
@@ -5768,6 +4968,7 @@ var COUNT_FROM_PAGE = [
5768
4968
  "[&:has(>:nth-child(11):last-child)]:[--gs-cards:11]",
5769
4969
  "[&:has(>:nth-child(12))]:[--gs-cards:12]"
5770
4970
  ].join(" ");
4971
+ var MIN_FROM_HEADERS = "[--gs-card-min:14rem] has-[[data-header-actions]]:[--gs-card-min:20rem]";
5771
4972
  function countCards(children) {
5772
4973
  let n = 0;
5773
4974
  Children.forEach(children, (child) => {
@@ -5776,14 +4977,18 @@ function countCards(children) {
5776
4977
  });
5777
4978
  return n;
5778
4979
  }
5779
- function CardGrid({ minWidth = "14rem", maxColumns = 4, compare = false, gap = "var(--ds-space-4)", className, style, children, ref, ...props }) {
4980
+ function CardGrid({ minWidth, maxColumns = 4, compare = false, gap = "var(--ds-space-4)", fill: fill2 = true, className, style, children, ref, ...props }) {
5780
4981
  const most = Math.max(1, Math.floor(maxColumns));
5781
- const n = compare ? "var(--gs-cards, var(--gs-cards-counted))" : String(most);
5782
- const share = `calc((100% - (${n} - 1) * var(--gs-card-gap)) / ${n})`;
5783
- const column = compare ? `max(${share}, min(100%, calc((${n} * ${minWidth} + (${n} - 1) * var(--gs-card-gap) - 100%) * 999)))` : `max(min(${minWidth}, 100%), ${share})`;
4982
+ const drawn = "var(--gs-cards, var(--gs-cards-counted))";
4983
+ const min = "var(--gs-card-min)";
4984
+ const shareOf = (n) => `calc((100% - (${n} - 1) * var(--gs-card-gap)) / ${n})`;
4985
+ const collection = `max(min(${min}, 100%), ${shareOf(String(most))})`;
4986
+ const column = compare ? `max(${shareOf(drawn)}, min(100%, calc((${drawn} * ${min} + (${drawn} - 1) * var(--gs-card-gap) - 100%) * 999)))` : fill2 ? `max(${shareOf(`max(${drawn}, 2)`)}, ${collection})` : collection;
5784
4987
  const template = {
5785
4988
  "--gs-card-gap": gap,
5786
- ...compare ? { "--gs-cards-counted": Math.max(1, countCards(children)) } : {},
4989
+ // A2: a width passed wins; a comparison keeps 14rem; a collection's comes from its headers (MIN_FROM_HEADERS)
4990
+ ...minWidth ? { "--gs-card-min": minWidth } : compare ? { "--gs-card-min": "14rem" } : {},
4991
+ ...compare || fill2 ? { "--gs-cards-counted": Math.max(1, countCards(children)) } : {},
5787
4992
  gap: "var(--gs-card-gap)",
5788
4993
  gridTemplateColumns: `repeat(auto-fill, minmax(${column}, 1fr))`,
5789
4994
  // a comparison's cards share their rows: the headers, the contents and the footers line up across the row
@@ -5796,7 +5001,8 @@ function CardGrid({ minWidth = "14rem", maxColumns = 4, compare = false, gap = "
5796
5001
  "data-card-grid": compare ? "compare" : "",
5797
5002
  className: cn(
5798
5003
  "grid",
5799
- compare && COUNT_FROM_PAGE,
5004
+ !minWidth && !compare && MIN_FROM_HEADERS,
5005
+ (compare || fill2) && COUNT_FROM_PAGE,
5800
5006
  // each card spans its three rows and lays its parts in them; anything else just fills them
5801
5007
  compare && "[&>*]:row-span-3 [&>[data-card]]:grid [&>[data-card]]:grid-rows-subgrid [&>[data-card]]:gap-y-0",
5802
5008
  className
@@ -6084,7 +5290,7 @@ var EDGE_FADE = 24;
6084
5290
  var NONE = { left: false, right: false, vertical: false, gutterRight: 0, gutterBottom: 0 };
6085
5291
  var same = (a, b) => a.left === b.left && a.right === b.right && a.vertical === b.vertical && a.gutterRight === b.gutterRight && a.gutterBottom === b.gutterBottom;
6086
5292
  function useScrollEdges(options = {}) {
6087
- const { enabled = true, fade = EDGE_FADE } = options;
5293
+ const { enabled = true, fade = EDGE_FADE, noFade } = options;
6088
5294
  const elRef = useRef(null);
6089
5295
  const observer = useRef(null);
6090
5296
  const borders = useRef({ x: 0, y: 0 });
@@ -6116,9 +5322,11 @@ function useScrollEdges(options = {}) {
6116
5322
  for (const child of Array.from(el.children)) o.observe(child);
6117
5323
  observer.current = o;
6118
5324
  }, [enabled, measure]);
6119
- const shown = enabled && (state.left || state.right);
5325
+ const fadeLeft = state.left && !noFade?.left;
5326
+ const fadeRight = state.right && !noFade?.right;
5327
+ const shown = enabled && (fadeLeft || fadeRight);
6120
5328
  const { gutterRight: gr, gutterBottom: gb } = state;
6121
- const fadeLayer = `linear-gradient(to right, ${state.left ? `transparent 0, black ${fade}px` : "black 0"}, ${state.right ? `black calc(100% - ${gr + fade}px), transparent calc(100% - ${gr}px)` : "black 100%"})`;
5329
+ const fadeLayer = `linear-gradient(to right, ${fadeLeft ? `transparent 0, black ${fade}px` : "black 0"}, ${fadeRight ? `black calc(100% - ${gr + fade}px), transparent calc(100% - ${gr}px)` : "black 100%"})`;
6122
5330
  const gutters = [gb > 0 && { size: `100% ${gb}px`, position: "0 100%" }, gr > 0 && { size: `${gr}px 100%`, position: "100% 0" }].filter(Boolean);
6123
5331
  const mask = shown ? {
6124
5332
  image: [fadeLayer, ...gutters.map(() => "linear-gradient(black, black)")].join(", "),
@@ -6131,18 +5339,54 @@ function useScrollEdges(options = {}) {
6131
5339
  /** The scroller itself, for code that moves it. */
6132
5340
  elRef,
6133
5341
  measure,
6134
- /** Content hides past a side edge: the scroller moves sideways, and its edges fade. */
6135
- overflowing: shown,
5342
+ /** Content hides past a side edge: the scroller moves sideways, and its edges fade (but those in `noFade`). */
5343
+ overflowing: enabled && (state.left || state.right),
6136
5344
  /** It scrolls either way, sideways or up and down: a keyboard needs to reach it. */
6137
5345
  scrollable: enabled && (state.left || state.right || state.vertical),
6138
5346
  edges: { left: state.left, right: state.right },
6139
5347
  /** For the scroller: `data-fade` names the faded edges ("left", "right" or "left right"), the style masks them. */
6140
5348
  fadeProps: {
6141
- "data-fade": shown ? [state.left && "left", state.right && "right"].filter(Boolean).join(" ") : void 0,
5349
+ "data-fade": shown ? [fadeLeft && "left", fadeRight && "right"].filter(Boolean).join(" ") : void 0,
6142
5350
  style: mask ? { maskImage: mask.image, WebkitMaskImage: mask.image, maskSize: mask.size, WebkitMaskSize: mask.size, maskPosition: mask.position, WebkitMaskPosition: mask.position, maskRepeat: "no-repeat", WebkitMaskRepeat: "no-repeat" } : void 0
6143
5351
  }
6144
5352
  };
6145
5353
  }
5354
+ var HEADER_LAYOUT = {
5355
+ row: "flex flex-wrap items-start justify-between gap-ds-4",
5356
+ block: "flex flex-col space-y-ds-1.5 grow basis-1/2 break-words max-w-full has-[[data-title-one-line]]:min-w-0",
5357
+ titleLeads: "grow basis-1/2 max-w-max break-words has-[[data-title-one-line]]:min-w-0",
5358
+ floorOff: "min-w-0",
5359
+ actions: "flex items-center gap-ds-2 shrink-0 ml-auto",
5360
+ below: "mt-ds-1.5"
5361
+ };
5362
+ function useTitleFloorFits(rowRef, active) {
5363
+ const [fits, setFits] = useState(true);
5364
+ useLayoutEffect(() => {
5365
+ const row = rowRef.current;
5366
+ if (!active || !row) return;
5367
+ const measure = () => {
5368
+ const title = row.querySelector("h1, h2, h3, h4, h5, h6");
5369
+ if (!title) return;
5370
+ const width = title.style.width;
5371
+ title.style.width = "min-content";
5372
+ const longest = title.getBoundingClientRect().width;
5373
+ title.style.width = width;
5374
+ if (longest === 0) return;
5375
+ const next = longest <= row.clientWidth + 0.5;
5376
+ setFits((was) => was === next ? was : next);
5377
+ };
5378
+ measure();
5379
+ if (typeof ResizeObserver === "undefined") return;
5380
+ const observer = new ResizeObserver(measure);
5381
+ observer.observe(row);
5382
+ return () => observer.disconnect();
5383
+ }, [rowRef, active]);
5384
+ return fits;
5385
+ }
5386
+ function actionsAreBadges(actions) {
5387
+ const items = Children.toArray(actions);
5388
+ return items.length > 0 && items.every((item) => isValidElement(item) && (item.type === Fragment$1 ? actionsAreBadges(item.props.children) : item.type === Badge));
5389
+ }
6146
5390
  function TruncCell({ children, lines }) {
6147
5391
  const { ref, isTruncated } = useTextOverflow({ lines });
6148
5392
  const clampClass = lines === 1 ? "truncate" : `line-clamp-${lines}`;
@@ -6157,6 +5401,95 @@ var FRAME_CLASSES = cn(
6157
5401
  GLASS_SURFACE,
6158
5402
  "bg-[var(--ds-bg-surface)]"
6159
5403
  );
5404
+ var PINNED_BACKGROUND = "linear-gradient(var(--gs-row-tint, transparent), var(--gs-row-tint, transparent)), linear-gradient(var(--gs-pinned-surface), var(--gs-pinned-surface)), var(--gs-pinned-canvas)";
5405
+ var PINNED_LAYERS = cn(
5406
+ "[--gs-pinned-surface:var(--ds-bg-surface)] [--gs-pinned-canvas:var(--ds-bg-canvas)]",
5407
+ "supports-[color:oklch(from_red_l_c_h)]:[--gs-pinned-surface:oklch(from_var(--ds-bg-surface)_l_c_h_/_max(alpha,min(1,alpha*1000)))]",
5408
+ "supports-[color:oklch(from_red_l_c_h)]:[--gs-pinned-canvas:oklch(from_var(--ds-bg-canvas)_l_c_h_/_1)]"
5409
+ );
5410
+ var PIN_SHADOW = "oklch(0 0 0 / 0.18)";
5411
+ var ROW_CONTROL = `${FOCUSABLE}, label, [role="button"], [role="checkbox"], [role="switch"], [role="menuitem"], [role="link"]`;
5412
+ function clickIsRows(e) {
5413
+ const target = e.target;
5414
+ if (!(target instanceof Element) || !e.currentTarget.contains(target)) return false;
5415
+ const selection = typeof window !== "undefined" ? window.getSelection() : null;
5416
+ if (selection && !selection.isCollapsed && selection.anchorNode && e.currentTarget.contains(selection.anchorNode)) return false;
5417
+ const control = target.closest(ROW_CONTROL);
5418
+ return !control || control === e.currentTarget || !e.currentTarget.contains(control);
5419
+ }
5420
+ function sortableValue(v) {
5421
+ if (v === null || v === void 0 || v === "") return void 0;
5422
+ if (v instanceof Date) return v.getTime();
5423
+ if (typeof v === "string" || typeof v === "number") return v;
5424
+ if (typeof v === "boolean") return v ? 1 : 0;
5425
+ return isValidElement(v) || typeof v === "object" ? null : String(v);
5426
+ }
5427
+ var COLLATOR = new Intl.Collator(void 0, { numeric: true });
5428
+ function compareValues(a, b) {
5429
+ if (typeof a === "number" && typeof b === "number") return a - b;
5430
+ return COLLATOR.compare(String(a), String(b));
5431
+ }
5432
+ var UNIT_SCALE = {
5433
+ "": 1,
5434
+ "%": 1,
5435
+ b: 1,
5436
+ byte: 1,
5437
+ bytes: 1,
5438
+ kb: 1e3,
5439
+ kib: 1024,
5440
+ mb: 1e6,
5441
+ mib: 1024 ** 2,
5442
+ gb: 1e9,
5443
+ gib: 1024 ** 3,
5444
+ tb: 1e12,
5445
+ tib: 1024 ** 4,
5446
+ pb: 1e15,
5447
+ pib: 1024 ** 5,
5448
+ ms: 1,
5449
+ s: 1e3,
5450
+ sec: 1e3,
5451
+ min: 6e4,
5452
+ h: 36e5,
5453
+ hr: 36e5,
5454
+ hrs: 36e5,
5455
+ g: 1,
5456
+ kg: 1e3,
5457
+ m: 1,
5458
+ km: 1e3,
5459
+ "\xB0c": 1,
5460
+ "\xB0f": 1
5461
+ };
5462
+ function numberOf(text) {
5463
+ const m = /^\s*([-\u2212])?\s*[$€£¥]?\s*([-\u2212])?\s*(\d[\d,]*\.?\d*|\.\d+)\s*(%|°[CF]|[a-z]*)\s*$/i.exec(text);
5464
+ const scale = m ? UNIT_SCALE[m[4].toLowerCase()] : void 0;
5465
+ if (!m || scale === void 0) return void 0;
5466
+ const n = parseFloat(m[3].replace(/,/g, ""));
5467
+ return Number.isFinite(n) ? (m[1] || m[2] ? -1 : 1) * n * scale : void 0;
5468
+ }
5469
+ function timeOf(text) {
5470
+ const t = Date.parse(text);
5471
+ return Number.isNaN(t) ? void 0 : t;
5472
+ }
5473
+ function rowIdsOf(data, field) {
5474
+ const seen = /* @__PURE__ */ new Set();
5475
+ let repeated = null;
5476
+ const ids = data.map((row, index) => {
5477
+ const v = row[field];
5478
+ if (typeof v !== "string" && typeof v !== "number") return index;
5479
+ if (!seen.has(String(v))) {
5480
+ seen.add(String(v));
5481
+ return v;
5482
+ }
5483
+ repeated ??= String(v);
5484
+ return `${v}~${index}`;
5485
+ });
5486
+ return { ids, repeated };
5487
+ }
5488
+ function rowMarks(row, index, field, id) {
5489
+ const v = row[field];
5490
+ return typeof v === "string" || typeof v === "number" ? { "data-row-key": String(id) } : { "data-row-index": String(index) };
5491
+ }
5492
+ var idPart = (id) => String(id).replace(/\s+/g, "_");
6160
5493
  function TableHeading({ title, description, actions, titleId, descriptionId, inset }) {
6161
5494
  const heading = /* @__PURE__ */ jsx("h3", { id: titleId, className: TITLE_CLASSES, children: title });
6162
5495
  const actionsBlock = actions ? /* @__PURE__ */ jsx("div", { className: HEADER_LAYOUT.actions, children: actions }) : null;
@@ -6178,8 +5511,10 @@ function TableHeading({ title, description, actions, titleId, descriptionId, ins
6178
5511
  descriptionText(HEADER_LAYOUT.below)
6179
5512
  ] }) });
6180
5513
  }
5514
+ var NO_PINS = { start: [], end: [], nameFits: false, frame: 0 };
5515
+ var samePins = (a, b) => a.nameFits === b.nameFits && a.frame === b.frame && a.start.join() === b.start.join() && a.end.join() === b.end.join();
6181
5516
  function Table({
6182
- columns,
5517
+ columns: columnsProp,
6183
5518
  data,
6184
5519
  className,
6185
5520
  sortable,
@@ -6191,6 +5526,10 @@ function Table({
6191
5526
  onSelectionChange,
6192
5527
  expandable,
6193
5528
  renderExpanded,
5529
+ rowKey = "id",
5530
+ expandedRows: expandedRowsProp,
5531
+ defaultExpandedRows,
5532
+ onExpandedChange,
6194
5533
  density = "default",
6195
5534
  stickyHeader = false,
6196
5535
  striped = false,
@@ -6198,6 +5537,8 @@ function Table({
6198
5537
  emptyState,
6199
5538
  onRowClick,
6200
5539
  mobileBreakpoint: mobileBreakpointProp,
5540
+ cardsBelow = CARDS_BELOW,
5541
+ pager,
6201
5542
  title,
6202
5543
  description,
6203
5544
  actions,
@@ -6209,7 +5550,10 @@ function Table({
6209
5550
  const descriptionId = `${headingId}-description`;
6210
5551
  const [internalSortKey, setInternalSortKey] = useState(null);
6211
5552
  const [internalSortDir, setInternalSortDir] = useState(null);
6212
- const [expandedRows, setExpandedRows] = useState(/* @__PURE__ */ new Set());
5553
+ const columns = useMemo(
5554
+ () => [...columnsProp.filter((c) => c.pin === "start"), ...columnsProp.filter((c) => !c.pin), ...columnsProp.filter((c) => c.pin === "end")],
5555
+ [columnsProp]
5556
+ );
6213
5557
  const isControlled = controlledSortKey !== void 0;
6214
5558
  const sortKey = isControlled ? controlledSortKey : internalSortKey;
6215
5559
  const sortDir = isControlled ? controlledSortDir ?? null : internalSortDir;
@@ -6229,6 +5573,7 @@ function Table({
6229
5573
  nextKey = key;
6230
5574
  nextDir = "asc";
6231
5575
  }
5576
+ pager?.setPage(1);
6232
5577
  if (onSortChange) {
6233
5578
  onSortChange(nextKey, nextDir);
6234
5579
  } else {
@@ -6236,139 +5581,348 @@ function Table({
6236
5581
  setInternalSortDir(nextDir);
6237
5582
  }
6238
5583
  },
6239
- [sortKey, sortDir, onSortChange]
5584
+ [sortKey, sortDir, onSortChange, pager]
5585
+ );
5586
+ const columnKeys = useMemo(() => columns.map((c) => c.key), [columns]);
5587
+ const inferredKinds = useMemo(() => inferColumnKinds(data, columnKeys), [data, columnKeys]);
5588
+ const kindOf = useCallback(
5589
+ (col) => col.kind ?? inferredKinds.get(col.key),
5590
+ [inferredKinds]
5591
+ );
5592
+ const sortColumn = sortKey ? columns.find((c) => c.key === sortKey) : void 0;
5593
+ const sortKind = sortColumn ? kindOf(sortColumn) : void 0;
5594
+ const { sortedAll, unsortable } = useMemo(() => {
5595
+ const indexed = data.map((row, i) => ({ row, originalIdx: i }));
5596
+ if (!sortKey || !sortDir) return { sortedAll: indexed, unsortable: null };
5597
+ const read = (row) => {
5598
+ if (sortColumn?.sortValue) return sortableValue(sortColumn.sortValue(row));
5599
+ const v = sortableValue(row[sortKey]);
5600
+ if (typeof v !== "string") return v;
5601
+ return (sortKind === "number" ? numberOf(v) : sortKind === "date" ? timeOf(v) : void 0) ?? v;
5602
+ };
5603
+ const valued = indexed.map((entry) => ({ ...entry, value: read(entry.row) }));
5604
+ const elements = valued.some((v) => v.value === null);
5605
+ const sorted = elements ? indexed : valued.sort((a, b) => {
5606
+ if (a.value === void 0 || b.value === void 0) return a.value === b.value ? 0 : a.value === void 0 ? 1 : -1;
5607
+ const cmp = compareValues(a.value, b.value);
5608
+ return sortDir === "asc" ? cmp : -cmp;
5609
+ });
5610
+ return { sortedAll: sorted, unsortable: elements ? sortKey : null };
5611
+ }, [data, sortKey, sortDir, sortColumn, sortKind]);
5612
+ const canSort = useMemo(() => {
5613
+ const can = /* @__PURE__ */ new Set();
5614
+ for (const col of columns) if (col.sortValue || data.every((row) => sortableValue(row[col.key]) !== null)) can.add(col.key);
5615
+ return can;
5616
+ }, [columns, data]);
5617
+ useEffect(() => {
5618
+ if (unsortable) warnOnce("Table", `the column "${unsortable}" is sortable, but its cells are elements, which can't be compared: give it a sortValue, (row) => the name, number or date it shows (data_display#1).`);
5619
+ }, [unsortable]);
5620
+ useEffect(() => {
5621
+ if (pager && data.length !== pager.totalItems) warnOnce("Table", `the pager counts ${pager.totalItems} rows but data holds ${data.length}: give the table and usePagination the same rows, every row of the list, and the table draws the pager's part (Sprint 25.13 A3).`);
5622
+ }, [pager, data.length]);
5623
+ const rangeStart = pager ? pager.range[0] : null;
5624
+ const rangeEnd2 = pager ? pager.range[1] : null;
5625
+ const drawn = useMemo(
5626
+ () => rangeStart === null ? sortedAll : rangeStart === 0 ? [] : sortedAll.slice(rangeStart - 1, rangeEnd2 ?? void 0),
5627
+ [sortedAll, rangeStart, rangeEnd2]
6240
5628
  );
6241
- const sortedWithIndices = sortKey && sortDir ? data.map((row, i) => ({ row, originalIdx: i })).sort((a, b) => {
6242
- const aVal = a.row[sortKey];
6243
- const bVal = b.row[sortKey];
6244
- if (aVal == null || bVal == null) return 0;
6245
- const cmp = String(aVal).localeCompare(String(bVal), void 0, { numeric: true });
6246
- return sortDir === "asc" ? cmp : -cmp;
6247
- }) : data.map((row, i) => ({ row, originalIdx: i }));
5629
+ const drawnStart = rangeStart ? rangeStart - 1 : 0;
6248
5630
  const selectedSet = new Set(selectedRows);
6249
- const allSelected = data.length > 0 && selectedRows.length === data.length;
6250
- const someSelected = selectedRows.length > 0 && !allSelected;
5631
+ const drawnIdx = drawn.map((d) => d.originalIdx);
5632
+ const allSelected = drawnIdx.length > 0 && drawnIdx.every((i) => selectedSet.has(i));
5633
+ const someSelected = !allSelected && drawnIdx.some((i) => selectedSet.has(i));
6251
5634
  const toggleAll = () => {
6252
- if (allSelected) onSelectionChange?.([]);
6253
- else onSelectionChange?.(data.map((_, i) => i));
5635
+ const shown = new Set(drawnIdx);
5636
+ if (allSelected) onSelectionChange?.(selectedRows.filter((r) => !shown.has(r)));
5637
+ else onSelectionChange?.([...selectedRows.filter((r) => !shown.has(r)), ...drawnIdx]);
6254
5638
  };
6255
5639
  const toggleRow = (idx) => {
6256
5640
  if (selectedSet.has(idx)) onSelectionChange?.(selectedRows.filter((r) => r !== idx));
6257
5641
  else onSelectionChange?.([...selectedRows, idx]);
6258
5642
  };
6259
- const toggleExpand = (idx) => {
6260
- setExpandedRows((prev) => {
6261
- const next = new Set(prev);
6262
- if (next.has(idx)) next.delete(idx);
6263
- else next.add(idx);
6264
- return next;
6265
- });
5643
+ const expandControlled = expandedRowsProp !== void 0;
5644
+ const [ownExpanded, setOwnExpanded] = useState(defaultExpandedRows ?? []);
5645
+ const expanded = expandControlled ? expandedRowsProp : ownExpanded;
5646
+ const expandedSet = new Set(expanded.map(String));
5647
+ const { ids: rowIds, repeated } = useMemo(() => rowIdsOf(data, rowKey), [data, rowKey]);
5648
+ useEffect(() => {
5649
+ if (repeated !== null) warnOnce("Table", `two rows share the ${rowKey} "${repeated}": give each row its own, or name a field that is one (rowKey), so an opened row and focus stay on their row.`);
5650
+ }, [repeated, rowKey]);
5651
+ const idOf = (_row, index) => rowIds[index] ?? index;
5652
+ const marksOf = (row, index) => rowMarks(row, index, rowKey, idOf(row, index));
5653
+ const openedHere = useRef(null);
5654
+ const toggleExpand = (id) => {
5655
+ const opening = !expandedSet.has(String(id));
5656
+ const next = opening ? [...expanded, id] : expanded.filter((x) => String(x) !== String(id));
5657
+ openedHere.current = opening ? String(id) : null;
5658
+ if (!expandControlled) setOwnExpanded(next);
5659
+ onExpandedChange?.(next);
6266
5660
  };
6267
- const columnKeys = useMemo(() => columns.map((c) => c.key), [columns]);
6268
- const inferredKinds = useMemo(() => inferColumnKinds(data, columnKeys), [data, columnKeys]);
6269
- const kindOf = useCallback(
6270
- (col) => col.kind ?? inferredKinds.get(col.key),
6271
- [inferredKinds]
6272
- );
5661
+ const canOpenByClick = !!expandable && !!renderExpanded && !onRowClick;
6273
5662
  const preferences = useShellPreferences();
6274
5663
  const mobileBreakpoint = mobileBreakpointProp ?? preferences.tableToCardsBreakpoint ?? "md";
6275
- const isMobile = useBreakpoint(mobileBreakpoint);
6276
- const { ref: scrollRef, measure: measureScroll, scrollable, fadeProps } = useScrollEdges({ enabled: !isMobile });
5664
+ const narrowScreen = useBreakpoint(mobileBreakpoint);
5665
+ const { cards: isMobile, rootRef } = useCardsSwitch(narrowScreen, cardsBelow, title);
5666
+ const setRoot = useCallback((node) => {
5667
+ rootRef.current = node;
5668
+ if (typeof ref === "function") ref(node);
5669
+ else if (ref) ref.current = node;
5670
+ }, [ref, rootRef]);
5671
+ const tableRef = useRef(null);
5672
+ const [pins, setPins] = useState(NO_PINS);
5673
+ const lead = (selectable ? 1 : 0) + (expandable ? 1 : 0);
5674
+ const endCount = columns.filter((c) => c.pin === "end").length;
5675
+ const nameIndex = columns.length > endCount ? lead : -1;
5676
+ const { ref: scrollRef, elRef: scrollEl, measure: measureScroll, scrollable, edges, fadeProps } = useScrollEdges({
5677
+ enabled: !isMobile,
5678
+ noFade: { left: lead + (pins.nameFits ? 1 : 0) > 0, right: endCount > 0 }
5679
+ });
6277
5680
  const [regionFocused, setRegionFocused] = useState(false);
5681
+ useLayoutEffect(() => {
5682
+ const table2 = tableRef.current;
5683
+ const scroller = scrollEl.current;
5684
+ if (!table2 || !scroller || isMobile) return;
5685
+ const measure = () => {
5686
+ const cells = Array.from(table2.querySelectorAll(":scope > thead > tr > th"));
5687
+ if (!cells.length) return;
5688
+ const widths = cells.map((c) => c.getBoundingClientRect().width);
5689
+ const start = [];
5690
+ let at = 0;
5691
+ for (let i = 0; i < lead; i++) {
5692
+ start.push(at);
5693
+ at += widths[i] ?? 0;
5694
+ }
5695
+ const nameWidth = nameIndex >= 0 ? widths[nameIndex] ?? 0 : 0;
5696
+ const end = [];
5697
+ let fromRight = 0;
5698
+ for (let i = cells.length - 1; i >= cells.length - endCount; i--) {
5699
+ end.unshift(fromRight);
5700
+ fromRight += widths[i] ?? 0;
5701
+ }
5702
+ const frame = scroller.clientWidth;
5703
+ const nameFits = nameIndex >= 0 && frame > 0 && at + nameWidth + fromRight <= frame / 2;
5704
+ if (nameFits) start.push(at);
5705
+ const next = { start, end, nameFits, frame };
5706
+ setPins((prev) => samePins(prev, next) ? prev : next);
5707
+ };
5708
+ measure();
5709
+ if (typeof ResizeObserver === "undefined") return;
5710
+ const observer = new ResizeObserver(measure);
5711
+ observer.observe(table2);
5712
+ observer.observe(scroller);
5713
+ return () => observer.disconnect();
5714
+ }, [isMobile, lead, endCount, nameIndex, scrollEl, columns, drawn.length]);
5715
+ const pinnedStart = lead + (pins.nameFits ? 1 : 0);
5716
+ const pinStyle = (position, total) => {
5717
+ if (position < pinnedStart && pins.start[position] !== void 0) {
5718
+ const inner = position === pinnedStart - 1;
5719
+ return { position: "sticky", left: pins.start[position], zIndex: 1, background: PINNED_BACKGROUND, boxShadow: inner && edges.left ? `6px 0 6px -6px ${PIN_SHADOW}` : void 0 };
5720
+ }
5721
+ const fromEnd = total - 1 - position;
5722
+ if (fromEnd < endCount && pins.end[endCount - 1 - fromEnd] !== void 0) {
5723
+ const inner = fromEnd === endCount - 1;
5724
+ return { position: "sticky", right: pins.end[endCount - 1 - fromEnd], zIndex: 1, background: PINNED_BACKGROUND, boxShadow: inner && edges.right ? `-6px 0 6px -6px ${PIN_SHADOW}` : void 0 };
5725
+ }
5726
+ return void 0;
5727
+ };
5728
+ const fade = fadeProps;
5729
+ const [renderedKinds, setRenderedKinds] = useState(() => /* @__PURE__ */ new Map());
5730
+ useLayoutEffect(() => {
5731
+ const table2 = tableRef.current;
5732
+ if (!table2 || isMobile) return;
5733
+ const next = /* @__PURE__ */ new Map();
5734
+ for (const col of columns) {
5735
+ if (kindOf(col)) continue;
5736
+ const texts = Array.from(table2.querySelectorAll(`:scope > tbody > tr > td[data-col="${CSS.escape(col.key)}"]`)).slice(0, 50).map((td) => td.textContent ?? "");
5737
+ const kind = texts.length ? kindOfRenderedText(texts) : void 0;
5738
+ if (kind) next.set(col.key, kind);
5739
+ }
5740
+ setRenderedKinds((prev) => prev.size === next.size && [...next].every(([k, v]) => prev.get(k) === v) ? prev : next);
5741
+ }, [columns, kindOf, drawn, isMobile]);
5742
+ const expandedKey = expanded.map(String).join("");
5743
+ const shownBefore = useRef(/* @__PURE__ */ new Set());
5744
+ const pendingScroll = useRef(null);
5745
+ const scrollToRow = useCallback((key) => {
5746
+ const row = rootRef.current?.querySelector(`[data-row-key="${CSS.escape(key)}"], [data-row-index="${CSS.escape(key)}"]`);
5747
+ if (row && typeof row.scrollIntoView === "function") row.scrollIntoView({ block: "nearest" });
5748
+ }, [rootRef]);
5749
+ useEffect(() => {
5750
+ const now = new Set(expanded.map(String));
5751
+ const added = [...now].filter((k) => !shownBefore.current.has(k) && k !== openedHere.current);
5752
+ shownBefore.current = now;
5753
+ openedHere.current = null;
5754
+ if (!expandControlled) return;
5755
+ const key = added[added.length - 1];
5756
+ if (key === void 0) return;
5757
+ if (pager) {
5758
+ const position = sortedAll.findIndex((r) => String(idOf(r.row, r.originalIdx)) === key);
5759
+ if (position >= 0 && (position < drawnStart || position >= drawnStart + drawn.length)) {
5760
+ pendingScroll.current = key;
5761
+ pager.setPage(Math.floor(position / pager.pageSize) + 1);
5762
+ return;
5763
+ }
5764
+ }
5765
+ scrollToRow(key);
5766
+ }, [expandedKey]);
5767
+ useEffect(() => {
5768
+ const key = pendingScroll.current;
5769
+ if (!key || !drawn.some((r) => String(idOf(r.row, r.originalIdx)) === key)) return;
5770
+ pendingScroll.current = null;
5771
+ scrollToRow(key);
5772
+ }, [drawnStart, drawn.length, scrollToRow]);
6278
5773
  const cardHeading = title ? /* @__PURE__ */ jsx(TableHeading, { title, description, actions, titleId, descriptionId, inset: false }) : null;
6279
- const titledCards = (body) => /* @__PURE__ */ jsxs("div", { ref, "data-ds-list": "", "data-gs": "table", className: cn("flex flex-col", className), children: [
5774
+ const titledCards = (body) => /* @__PURE__ */ jsxs("div", { ref: listRoot, "data-ds-list": "", "data-gs": "table", className: cn("flex flex-col", listClass), onFocus: noteFocus, onBlur: forgetFocus, children: [
6280
5775
  cardHeading,
6281
5776
  body
6282
5777
  ] });
5778
+ const control = pager && !loading && data.length > 0 ? /* @__PURE__ */ jsx(Pagination, { ...pager.props }) : null;
5779
+ const listRoot = control ? void 0 : setRoot;
5780
+ const listClass = control ? void 0 : className;
5781
+ const withControl = (list) => control ? /* @__PURE__ */ jsxs("div", { ref: setRoot, "data-gs-paged": "", className: cn("flex flex-col min-w-0", className), children: [
5782
+ list,
5783
+ control
5784
+ ] }) : list;
5785
+ const focusWas = useRef(null);
5786
+ const noteFocus = (e) => {
5787
+ const target = e.target;
5788
+ const row = target.closest("[data-row-key], [data-row-index]");
5789
+ if (!row) {
5790
+ focusWas.current = null;
5791
+ return;
5792
+ }
5793
+ const control2 = target === row ? null : target.getAttribute("aria-labelledby") ?? target.getAttribute("aria-label") ?? target.textContent;
5794
+ focusWas.current = { row: row.hasAttribute("data-row-key") ? `[data-row-key="${CSS.escape(row.getAttribute("data-row-key"))}"]` : `[data-row-index="${CSS.escape(row.getAttribute("data-row-index"))}"]`, control: control2 };
5795
+ };
5796
+ const forgetFocus = (e) => {
5797
+ const node = e.target;
5798
+ queueMicrotask(() => {
5799
+ if (node.isConnected && !rootRef.current?.contains(document.activeElement)) focusWas.current = null;
5800
+ });
5801
+ };
5802
+ useLayoutEffect(() => {
5803
+ const was = focusWas.current;
5804
+ const active = document.activeElement;
5805
+ if (!was || active && active !== document.body) return;
5806
+ const row = rootRef.current?.querySelector(was.row);
5807
+ if (!row) return;
5808
+ const controls = Array.from(row.querySelectorAll(FOCUSABLE));
5809
+ const same2 = was.control === null ? null : controls.find((el) => (el.getAttribute("aria-labelledby") ?? el.getAttribute("aria-label") ?? el.textContent) === was.control);
5810
+ (same2 ?? (row.tabIndex >= 0 ? row : controls[0]))?.focus({ preventScroll: true });
5811
+ }, [isMobile, rootRef]);
6283
5812
  if (isMobile) {
6284
5813
  if (loading) {
6285
5814
  if (title) return titledCards(/* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-ds-10", children: /* @__PURE__ */ jsx(Spinner, { label: "Loading table data" }) }));
6286
- return /* @__PURE__ */ jsx("div", { ref, "data-ds-list": "", "data-gs": "table", className: cn("flex items-center justify-center py-ds-10", className), children: /* @__PURE__ */ jsx(Spinner, { label: "Loading table data" }) });
5815
+ return /* @__PURE__ */ jsx("div", { ref: setRoot, "data-ds-list": "", "data-gs": "table", className: cn("flex items-center justify-center py-ds-10", className), children: /* @__PURE__ */ jsx(Spinner, { label: "Loading table data" }) });
6287
5816
  }
6288
5817
  if (data.length === 0 && emptyState) {
6289
5818
  if (title) return titledCards(/* @__PURE__ */ jsx("div", { className: "py-ds-10 text-center text-sm text-[var(--ds-text-muted)]", children: emptyState }));
6290
- return /* @__PURE__ */ jsx("div", { ref, "data-ds-list": "", "data-gs": "table", className: cn("py-ds-10 text-center text-sm text-[var(--ds-text-muted)]", className), children: emptyState });
5819
+ return /* @__PURE__ */ jsx("div", { ref: setRoot, "data-ds-list": "", "data-gs": "table", className: cn("py-ds-10 text-center text-sm text-[var(--ds-text-muted)]", className), children: emptyState });
6291
5820
  }
6292
- const cards = /* @__PURE__ */ jsx(TableCardCtx, { value: true, children: /* @__PURE__ */ jsx("div", { ref: title ? void 0 : ref, "data-ds-list": title ? void 0 : "", "data-gs": title ? void 0 : "table", className: cn("flex flex-col gap-ds-3 overflow-y-auto", title ? "min-h-0" : className), children: sortedWithIndices.map(({ row, originalIdx }, i) => {
6293
- const isSelected = selectedSet.has(originalIdx);
6294
- return /* @__PURE__ */ jsxs(
6295
- "div",
6296
- {
6297
- "data-row": "",
6298
- className: cn(
6299
- "border-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
6300
- "rounded-[var(--ds-radius-md)] bg-[var(--ds-bg-surface)]",
6301
- "p-ds-4 space-y-ds-3",
6302
- isSelected && ACTIVE_RING,
6303
- onRowClick && "cursor-pointer hover:bg-[var(--ds-bg-muted)] focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]"
6304
- ),
6305
- onClick: onRowClick ? () => onRowClick(row, originalIdx) : void 0,
6306
- tabIndex: onRowClick ? 0 : void 0,
6307
- onKeyDown: onRowClick ? (e) => {
6308
- if (e.target !== e.currentTarget) return;
6309
- if (e.key === "Enter" || e.key === " ") {
6310
- e.preventDefault();
6311
- onRowClick(row, originalIdx);
6312
- }
6313
- } : void 0,
6314
- children: [
6315
- selectable && /* @__PURE__ */ jsx(Checkbox, { checked: isSelected, onChange: () => toggleRow(originalIdx), label: "Select" }),
6316
- columns.map((col) => /* @__PURE__ */ jsxs("div", { className: "flex justify-between gap-ds-2 text-sm", children: [
6317
- /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[var(--ds-text-secondary)] font-medium text-xs uppercase tracking-wider", children: col.header }),
6318
- /* @__PURE__ */ jsx(CardValue, { value: row[col.key] })
6319
- ] }, col.key)),
6320
- expandable && renderExpanded && /* @__PURE__ */ jsx("div", { className: "pt-ds-2 border-t-[length:var(--ds-divider-width)] border-[var(--ds-border-subtle)] text-sm text-[var(--ds-text-secondary)]", children: renderExpanded(row, i) })
6321
- ]
6322
- },
6323
- i
6324
- );
6325
- }) }) });
6326
- return title ? titledCards(cards) : cards;
5821
+ const cards = /* @__PURE__ */ jsx(TableCardCtx, { value: true, children: /* @__PURE__ */ jsxs("div", { ref: title ? void 0 : listRoot, "data-ds-list": title ? void 0 : "", "data-gs": title ? void 0 : "table", className: cn("flex flex-col gap-ds-3 overflow-y-auto", title ? "min-h-0" : listClass), onFocus: title ? void 0 : noteFocus, onBlur: title ? void 0 : forgetFocus, children: [
5822
+ selectable && /* @__PURE__ */ jsx("span", { hidden: true, id: `${headingId}-select`, children: "Select" }),
5823
+ drawn.map(({ row, originalIdx }) => {
5824
+ const isSelected = selectedSet.has(originalIdx);
5825
+ const id = idOf(row, originalIdx);
5826
+ return /* @__PURE__ */ jsxs(
5827
+ "div",
5828
+ {
5829
+ "data-row": "",
5830
+ ...marksOf(row, originalIdx),
5831
+ className: cn(
5832
+ "border-[length:var(--ds-border-width)] border-[var(--ds-border-subtle)]",
5833
+ "rounded-[var(--ds-radius-md)] bg-[var(--ds-bg-surface)]",
5834
+ "p-ds-4 space-y-ds-3",
5835
+ isSelected && ACTIVE_RING,
5836
+ onRowClick && "cursor-pointer hover:bg-[var(--ds-bg-muted)] focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]"
5837
+ ),
5838
+ onClick: onRowClick ? (e) => {
5839
+ if (clickIsRows(e)) onRowClick(row, originalIdx);
5840
+ } : void 0,
5841
+ tabIndex: onRowClick ? 0 : void 0,
5842
+ onKeyDown: onRowClick ? (e) => {
5843
+ if (e.target !== e.currentTarget) return;
5844
+ if (e.key === "Enter" || e.key === " ") {
5845
+ e.preventDefault();
5846
+ onRowClick(row, originalIdx);
5847
+ }
5848
+ } : void 0,
5849
+ children: [
5850
+ selectable && // "Select" and the row's name, as in the table (T15)
5851
+ /* @__PURE__ */ jsx(Checkbox, { checked: isSelected, onChange: () => toggleRow(originalIdx), "aria-labelledby": nameIndex >= 0 ? `${headingId}-select ${headingId}-card-name-${idPart(id)}` : `${headingId}-select` }),
5852
+ columns.map((col, c) => /* @__PURE__ */ jsxs("div", { className: "flex justify-between gap-ds-2 text-sm", children: [
5853
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[var(--ds-text-secondary)] font-medium text-xs uppercase tracking-wider", children: col.header }),
5854
+ /* @__PURE__ */ jsx(CardValue, { value: row[col.key], id: lead + c === nameIndex ? `${headingId}-card-name-${idPart(id)}` : void 0 })
5855
+ ] }, col.key)),
5856
+ expandable && renderExpanded && /* @__PURE__ */ jsx("div", { className: "pt-ds-2 border-t-[length:var(--ds-divider-width)] border-[var(--ds-border-subtle)] text-sm text-[var(--ds-text-secondary)]", children: renderExpanded(row, originalIdx) })
5857
+ ]
5858
+ },
5859
+ String(id)
5860
+ );
5861
+ })
5862
+ ] }) });
5863
+ return withControl(title ? titledCards(cards) : cards);
6327
5864
  }
6328
- const table = /* @__PURE__ */ jsxs("table", { className: "w-full text-sm", ...title ? { "aria-labelledby": titleId, "aria-describedby": description ? descriptionId : void 0 } : ariaLabel ? { "aria-label": ariaLabel } : {}, children: [
6329
- /* @__PURE__ */ jsx("thead", { className: cn("bg-[var(--ds-bg-muted)]", stickyHeader && "sticky top-0 z-10"), children: /* @__PURE__ */ jsxs("tr", { className: "border-b-[length:var(--ds-divider-width)] border-[var(--ds-border-subtle)]", children: [
6330
- selectable && /* @__PURE__ */ jsx("th", { className: cn("w-10", density === "compact" ? "px-ds-2 py-ds-2" : "px-ds-3 py-ds-3"), children: /* @__PURE__ */ jsx(
5865
+ const cellCount = columns.length + lead;
5866
+ const table = /* @__PURE__ */ jsxs("table", { ref: tableRef, className: cn("w-full text-sm", PINNED_LAYERS), ...title ? { "aria-labelledby": titleId, "aria-describedby": description ? descriptionId : void 0 } : ariaLabel ? { "aria-label": ariaLabel } : {}, children: [
5867
+ /* @__PURE__ */ jsx("thead", { className: cn("bg-[var(--ds-bg-muted)]", stickyHeader && "sticky top-0 z-10"), children: /* @__PURE__ */ jsxs("tr", { className: "border-b-[length:var(--ds-divider-width)] border-[var(--ds-border-subtle)] [--gs-row-tint:var(--ds-bg-muted)]", children: [
5868
+ selectable && /* @__PURE__ */ jsx("th", { className: cn("w-10", density === "compact" ? "px-ds-2 py-ds-2" : "px-ds-3 py-ds-3"), style: pinStyle(0, cellCount), children: /* @__PURE__ */ jsx(
6331
5869
  Checkbox,
6332
5870
  {
6333
5871
  checked: allSelected,
6334
5872
  indeterminate: someSelected,
6335
- onChange: toggleAll
5873
+ onChange: toggleAll,
5874
+ "aria-labelledby": `${headingId}-select-all`
6336
5875
  }
6337
5876
  ) }),
6338
- expandable && /* @__PURE__ */ jsx("th", { className: cn("w-8", density === "compact" ? "px-ds-1 py-ds-2" : "px-ds-2 py-ds-3") }),
6339
- columns.map((col) => {
6340
- const isSortable = sortable || col.sortable;
6341
- const isActiveSort = sortKey === col.key;
5877
+ expandable && /* @__PURE__ */ jsx("th", { className: cn("w-8", density === "compact" ? "px-ds-1 py-ds-2" : "px-ds-2 py-ds-3"), style: pinStyle(selectable ? 1 : 0, cellCount) }),
5878
+ columns.map((col, c) => {
5879
+ const isSortable = col.header.trim() !== "" && (col.sortable ?? (!!sortable && canSort.has(col.key)));
5880
+ const isActiveSort = sortKey === col.key && !!sortDir && unsortable !== col.key;
6342
5881
  const headerKind = kindOf(col);
6343
5882
  const headerAlign = col.align ?? (headerKind === "number" ? "right" : "left");
6344
5883
  const shrink = (headerKind === "number" || headerKind === "date") && !col.maxWidth;
5884
+ const style = { maxWidth: col.maxWidth, width: col.width, minWidth: col.minWidth, ...pinStyle(lead + c, cellCount) };
6345
5885
  return /* @__PURE__ */ jsx(
6346
5886
  "th",
6347
5887
  {
5888
+ "aria-sort": isSortable && isActiveSort ? sortDir === "asc" ? "ascending" : "descending" : void 0,
6348
5889
  className: cn(
6349
5890
  density === "compact" ? "px-ds-3 py-ds-2" : "px-ds-4 py-ds-3",
6350
5891
  shrink && "w-px whitespace-nowrap",
6351
5892
  "font-[var(--ds-weight-heading)] text-[var(--ds-text-secondary)]",
6352
5893
  "text-xs uppercase tracking-wider",
6353
- headerAlign === "right" ? "text-right" : headerAlign === "center" ? "text-center" : "text-left",
6354
- isSortable && "cursor-pointer select-none hover:text-[var(--ds-text-primary)]"
5894
+ headerAlign === "right" ? "text-right" : headerAlign === "center" ? "text-center" : "text-left"
6355
5895
  ),
6356
- style: col.maxWidth || col.width || col.minWidth ? { maxWidth: col.maxWidth, width: col.width, minWidth: col.minWidth } : void 0,
6357
- onClick: isSortable ? () => handleSort(col.key) : void 0,
6358
- children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-ds-1", children: [
6359
- col.header,
6360
- isSortable && isActiveSort && /* @__PURE__ */ jsx(
6361
- Icon,
5896
+ style,
5897
+ children: isSortable ? (
5898
+ // A47: a button with the column's name, reached with Tab and pressed with Enter or Space
5899
+ /* @__PURE__ */ jsxs(
5900
+ "button",
6362
5901
  {
6363
- name: "chevron-right",
6364
- size: 12,
5902
+ type: "button",
5903
+ onClick: () => handleSort(col.key),
6365
5904
  className: cn(
6366
- "transition-transform",
6367
- sortDir === "asc" ? "-rotate-90" : "rotate-90"
6368
- )
5905
+ "inline-flex items-center gap-ds-1 uppercase tracking-wider cursor-pointer select-none",
5906
+ "[font:inherit] text-inherit hover:text-[var(--ds-text-primary)]",
5907
+ "rounded-[var(--ds-radius-sm)] focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]"
5908
+ ),
5909
+ children: [
5910
+ col.header,
5911
+ /* @__PURE__ */ jsx(
5912
+ Icon,
5913
+ {
5914
+ name: "chevron-right",
5915
+ size: 12,
5916
+ className: cn(
5917
+ "transition-transform",
5918
+ !isActiveSort ? "invisible" : sortDir === "asc" ? "-rotate-90" : "rotate-90"
5919
+ )
5920
+ }
5921
+ )
5922
+ ]
6369
5923
  }
6370
5924
  )
6371
- ] })
5925
+ ) : /* @__PURE__ */ jsx("span", { className: "inline-flex items-center gap-ds-1", children: col.header })
6372
5926
  },
6373
5927
  col.key
6374
5928
  );
@@ -6377,31 +5931,44 @@ function Table({
6377
5931
  /* @__PURE__ */ jsx("tbody", { children: loading ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
6378
5932
  "td",
6379
5933
  {
6380
- colSpan: columns.length + (selectable ? 1 : 0) + (expandable ? 1 : 0),
5934
+ colSpan: cellCount,
6381
5935
  className: "px-ds-4 py-ds-10 text-center",
6382
5936
  children: /* @__PURE__ */ jsx(Spinner, { label: "Loading table data" })
6383
5937
  }
6384
5938
  ) }) : data.length === 0 && emptyState ? /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
6385
5939
  "td",
6386
5940
  {
6387
- colSpan: columns.length + (selectable ? 1 : 0) + (expandable ? 1 : 0),
5941
+ colSpan: cellCount,
6388
5942
  className: "px-ds-4 py-ds-10 text-center text-sm text-[var(--ds-text-muted)]",
6389
5943
  children: emptyState
6390
5944
  }
6391
- ) }) : sortedWithIndices.map(({ row, originalIdx }, i) => {
5945
+ ) }) : drawn.map(({ row, originalIdx }, i) => {
6392
5946
  const isSelected = selectedSet.has(originalIdx);
6393
- const isExpanded = expandedRows.has(i);
5947
+ const id = idOf(row, originalIdx);
5948
+ const isExpanded = expandedSet.has(String(id));
5949
+ const clickable = !!onRowClick || canOpenByClick;
6394
5950
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [
6395
5951
  /* @__PURE__ */ jsxs(
6396
5952
  "tr",
6397
5953
  {
5954
+ ...marksOf(row, originalIdx),
6398
5955
  className: cn(
6399
5956
  "border-b-[length:var(--ds-divider-width)] border-[var(--ds-border-subtle)] last:border-b-0",
6400
- "transition-colors",
6401
- isSelected ? "bg-[var(--ds-accent-subtle)]" : striped && i % 2 === 1 ? "bg-[var(--ds-bg-muted)]/50" : "hover:bg-[var(--ds-bg-muted)]",
6402
- onRowClick && "cursor-pointer focus-visible:outline-none focus-visible:shadow-[inset_var(--ds-shadow-focus)]"
5957
+ // the row's background and its tint change together, so its pinned cells and the rest fade as one
5958
+ // (roots.css registers --gs-row-tint as a colour so it can)
5959
+ "transition-[background-color,--gs-row-tint]",
5960
+ // the row's tint, as a variable too, so its pinned cells draw it over their solid ground (A35)
5961
+ isSelected ? "bg-[var(--ds-accent-subtle)] [--gs-row-tint:var(--ds-accent-subtle)]" : striped && i % 2 === 1 ? "bg-[var(--ds-bg-muted)]/50 [--gs-row-tint:color-mix(in_oklab,var(--ds-bg-muted)_50%,transparent)]" : "hover:bg-[var(--ds-bg-muted)] hover:[--gs-row-tint:var(--ds-bg-muted)]",
5962
+ clickable && "cursor-pointer",
5963
+ // an outline inside the row: it shows over the pinned cells, whose solid ground hid the inset
5964
+ // shadow the row drew before (the review of 30 September)
5965
+ onRowClick && "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--ds-border-focus)]"
6403
5966
  ),
6404
- onClick: onRowClick ? () => onRowClick(row, originalIdx) : void 0,
5967
+ onClick: clickable ? (e) => {
5968
+ if (!clickIsRows(e)) return;
5969
+ if (onRowClick) onRowClick(row, originalIdx);
5970
+ else toggleExpand(id);
5971
+ } : void 0,
6405
5972
  tabIndex: onRowClick ? 0 : void 0,
6406
5973
  onKeyDown: onRowClick ? (e) => {
6407
5974
  if (e.target !== e.currentTarget) return;
@@ -6411,15 +5978,15 @@ function Table({
6411
5978
  }
6412
5979
  } : void 0,
6413
5980
  children: [
6414
- selectable && /* @__PURE__ */ jsx("td", { className: cn("w-10", density === "compact" ? "px-ds-2 py-ds-2" : "px-ds-3 py-ds-3"), children: /* @__PURE__ */ jsx(Checkbox, { checked: isSelected, onChange: () => toggleRow(originalIdx) }) }),
6415
- expandable && /* @__PURE__ */ jsx("td", { className: cn("w-8", density === "compact" ? "px-ds-1 py-ds-2" : "px-ds-2 py-ds-3"), children: /* @__PURE__ */ jsx(
5981
+ selectable && /* @__PURE__ */ jsx("td", { className: cn("w-10", density === "compact" ? "px-ds-2 py-ds-2" : "px-ds-3 py-ds-3"), style: pinStyle(0, cellCount), children: /* @__PURE__ */ jsx(Checkbox, { checked: isSelected, onChange: () => toggleRow(originalIdx), "aria-labelledby": nameIndex >= 0 ? `${headingId}-select ${headingId}-name-${idPart(id)}` : `${headingId}-select` }) }),
5982
+ expandable && /* @__PURE__ */ jsx("td", { className: cn("w-8", density === "compact" ? "px-ds-1 py-ds-2" : "px-ds-2 py-ds-3"), style: pinStyle(selectable ? 1 : 0, cellCount), children: /* @__PURE__ */ jsx(
6416
5983
  "button",
6417
5984
  {
6418
5985
  type: "button",
6419
- onClick: () => toggleExpand(i),
5986
+ onClick: () => toggleExpand(id),
6420
5987
  className: "text-[var(--ds-text-muted)] hover:text-[var(--ds-text-primary)] cursor-pointer",
6421
5988
  "aria-expanded": isExpanded,
6422
- "aria-label": "Expand row",
5989
+ ...nameIndex >= 0 ? { "aria-labelledby": `${headingId}-details ${headingId}-name-${idPart(id)}` } : { "aria-label": "Details" },
6423
5990
  children: /* @__PURE__ */ jsx(
6424
5991
  Icon,
6425
5992
  {
@@ -6430,12 +5997,13 @@ function Table({
6430
5997
  )
6431
5998
  }
6432
5999
  ) }),
6433
- columns.map((col) => {
6000
+ columns.map((col, c) => {
6434
6001
  const truncLines = col.truncate === true ? 1 : col.truncate ? col.truncate.lines : 0;
6435
6002
  const kind = kindOf(col);
6436
6003
  const alignClass = col.align ? col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left" : kind === "number" ? "text-right" : "text-left";
6437
6004
  const cellContent = row[col.key];
6438
6005
  const shrink = (kind === "number" || kind === "date") && !col.maxWidth;
6006
+ const whole = !kind && renderedKinds.has(col.key);
6439
6007
  let inner;
6440
6008
  if (truncLines > 0) {
6441
6009
  inner = /* @__PURE__ */ jsx(TruncCell, { lines: truncLines, children: cellContent });
@@ -6451,13 +6019,17 @@ function Table({
6451
6019
  return /* @__PURE__ */ jsx(
6452
6020
  "td",
6453
6021
  {
6022
+ id: lead > 0 && lead + c === nameIndex ? `${headingId}-name-${idPart(id)}` : void 0,
6023
+ "data-col": col.key,
6454
6024
  className: cn(
6455
6025
  density === "compact" ? "px-ds-3 py-ds-2" : "px-ds-4 py-ds-3",
6456
6026
  "text-[var(--ds-text-primary)]",
6457
6027
  alignClass,
6458
- shrink && "w-px whitespace-nowrap"
6028
+ shrink && "w-px whitespace-nowrap",
6029
+ whole && "whitespace-nowrap",
6030
+ kind === "identifier" && "[overflow-wrap:anywhere]"
6459
6031
  ),
6460
- style: col.maxWidth || col.width || col.minWidth ? { maxWidth: col.maxWidth, width: col.width, minWidth: col.minWidth } : void 0,
6032
+ style: col.maxWidth || col.width || col.minWidth || c + lead < pinnedStart || c >= columns.length - endCount ? { maxWidth: col.maxWidth, width: col.width, minWidth: col.minWidth, ...pinStyle(lead + c, cellCount) } : void 0,
6461
6033
  children: inner
6462
6034
  },
6463
6035
  col.key
@@ -6469,50 +6041,57 @@ function Table({
6469
6041
  expandable && isExpanded && renderExpanded && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsx(
6470
6042
  "td",
6471
6043
  {
6472
- colSpan: columns.length + (selectable ? 1 : 0) + 1,
6044
+ colSpan: cellCount,
6473
6045
  className: "bg-[var(--ds-bg-muted)] px-ds-6 py-ds-4 text-sm text-[var(--ds-text-primary)]",
6474
- children: renderExpanded(row, i)
6475
- }
6476
- ) })
6477
- ] }, i);
6478
- }) })
6479
- ] });
6480
- const region = scrollable || regionFocused ? { tabIndex: 0, role: "region", ...title ? { "aria-labelledby": titleId } : { "aria-label": ariaLabel ?? "Table" } } : {};
6481
- return /* @__PURE__ */ jsxs(
6482
- "div",
6483
- {
6484
- ref,
6485
- "data-ds-list": "",
6486
- "data-gs": "table",
6487
- className: cn("w-full min-w-0 flex flex-col", FRAME_CLASSES, "has-[>[data-table-scroll]:focus-visible]:shadow-[var(--ds-shadow-focus)]", className),
6488
- children: [
6489
- title && /* @__PURE__ */ jsx(TableHeading, { title, description, actions, titleId, descriptionId, inset: true }),
6490
- /* @__PURE__ */ jsx(
6491
- "div",
6492
- {
6493
- ref: scrollRef,
6494
- "data-table-scroll": "",
6495
- onScroll: measureScroll,
6496
- onFocus: (e) => {
6497
- if (e.target === e.currentTarget) setRegionFocused(true);
6498
- },
6499
- onBlur: (e) => {
6500
- if (e.target === e.currentTarget) setRegionFocused(false);
6501
- },
6502
- ...fadeProps,
6503
- ...region,
6504
- className: cn("min-h-0 overflow-x-auto focus-visible:outline-none", title ? "rounded-b-[inherit]" : "rounded-[inherit]"),
6505
- children: table
6506
- }
6507
- )
6508
- ]
6509
- }
6046
+ children: /* @__PURE__ */ jsx("div", { "data-row-panel": "", className: "sticky left-ds-6", style: pins.frame ? { maxWidth: `calc(${pins.frame}px - 2 * var(--ds-space-6))` } : void 0, children: renderExpanded(row, originalIdx) })
6047
+ }
6048
+ ) })
6049
+ ] }, String(id));
6050
+ }) })
6051
+ ] });
6052
+ const region = scrollable || regionFocused ? { tabIndex: 0, role: "region", ...title ? { "aria-labelledby": titleId } : { "aria-label": ariaLabel ?? "Table" } } : {};
6053
+ return withControl(
6054
+ /* @__PURE__ */ jsxs(
6055
+ "div",
6056
+ {
6057
+ ref: listRoot,
6058
+ "data-ds-list": "",
6059
+ "data-gs": "table",
6060
+ className: cn("w-full min-w-0 flex flex-col", FRAME_CLASSES, "has-[>[data-table-scroll]:focus-visible]:shadow-[var(--ds-shadow-focus)]", control && "min-h-0", listClass),
6061
+ onFocus: noteFocus,
6062
+ onBlur: forgetFocus,
6063
+ children: [
6064
+ title && /* @__PURE__ */ jsx(TableHeading, { title, description, actions, titleId, descriptionId, inset: true }),
6065
+ selectable && /* @__PURE__ */ jsx("span", { hidden: true, id: `${headingId}-select-all`, children: "Select all rows shown" }),
6066
+ selectable && /* @__PURE__ */ jsx("span", { hidden: true, id: `${headingId}-select`, children: "Select" }),
6067
+ expandable && /* @__PURE__ */ jsx("span", { hidden: true, id: `${headingId}-details`, children: "Details" }),
6068
+ /* @__PURE__ */ jsx(
6069
+ "div",
6070
+ {
6071
+ ref: scrollRef,
6072
+ "data-table-scroll": "",
6073
+ onScroll: measureScroll,
6074
+ onFocus: (e) => {
6075
+ if (e.target === e.currentTarget) setRegionFocused(true);
6076
+ },
6077
+ onBlur: (e) => {
6078
+ if (e.target === e.currentTarget) setRegionFocused(false);
6079
+ },
6080
+ ...fade,
6081
+ ...region,
6082
+ className: cn("min-h-0 overflow-x-auto focus-visible:outline-none", title ? "rounded-b-[inherit]" : "rounded-[inherit]"),
6083
+ children: table
6084
+ }
6085
+ )
6086
+ ]
6087
+ }
6088
+ )
6510
6089
  );
6511
6090
  }
6512
6091
  function optionsKeyForFit(options) {
6513
6092
  return options.map((o) => `${o.value}:${o.label}`).join("|");
6514
6093
  }
6515
- var sizes3 = {
6094
+ var sizes2 = {
6516
6095
  xs: "text-xs px-ds-2",
6517
6096
  sm: "text-xs px-ds-3",
6518
6097
  md: "text-sm px-ds-3",
@@ -6714,7 +6293,7 @@ function SegmentedControl({
6714
6293
  "rounded-[var(--ds-radius-md)]",
6715
6294
  "transition-colors cursor-pointer",
6716
6295
  "focus-visible:outline-none focus-visible:shadow-[var(--ds-shadow-focus)]",
6717
- sizes3[size],
6296
+ sizes2[size],
6718
6297
  opt.value === value ? "text-[var(--ds-text-primary)]" : "text-[var(--ds-text-muted)] hover:text-[var(--ds-text-secondary)]",
6719
6298
  opt.disabled && "opacity-disabled cursor-not-allowed"
6720
6299
  ),
@@ -8433,66 +8012,70 @@ function TableToolbar({
8433
8012
  const paged = total !== void 0 && !!pagination && pagination.pageSize > 0 && total > pagination.pageSize;
8434
8013
  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
8014
  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,
8015
+ return (
8016
+ // 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)
8017
+ /* @__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: [
8018
+ hasControls && /* @__PURE__ */ jsxs("div", { ref: rowRef, className: "flex items-center gap-ds-2 flex-wrap", children: [
8019
+ search && /* @__PURE__ */ jsx(
8020
+ Input,
8472
8021
  {
8473
- variant: open ? "secondary" : "outline",
8022
+ "aria-label": search.label,
8023
+ placeholder: search.placeholder ?? search.label,
8024
+ value: search.value,
8474
8025
  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
- ]
8026
+ onChange: (e) => search.onChange(e.target.value),
8027
+ className: cn("flex-1 min-w-[160px]", !searchFills && "max-w-[280px]")
8484
8028
  }
8485
8029
  ),
8486
- actions
8030
+ primaryFilter && (compact ? /* @__PURE__ */ jsx(
8031
+ Dropdown,
8032
+ {
8033
+ "aria-label": primaryFilter.label,
8034
+ options: primaryFilter.options.map((o) => ({ id: o.value, label: o.label })),
8035
+ value: primaryFilter.value,
8036
+ onChange: (v) => primaryFilter.onChange(v),
8037
+ size,
8038
+ className: "w-[150px]"
8039
+ }
8040
+ ) : /* @__PURE__ */ jsx(
8041
+ SegmentedControl,
8042
+ {
8043
+ "aria-label": primaryFilter.label,
8044
+ options: primaryFilter.options,
8045
+ value: primaryFilter.value,
8046
+ size,
8047
+ onChange: primaryFilter.onChange
8048
+ }
8049
+ )),
8050
+ (filters || actions) && /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-ds-2 justify-end", inPane ? "flex-none" : "flex-1"), children: [
8051
+ filters && /* @__PURE__ */ jsxs(
8052
+ Button,
8053
+ {
8054
+ type: "button",
8055
+ variant: open ? "secondary" : "outline",
8056
+ size,
8057
+ iconLeft: /* @__PURE__ */ jsx(Icon, { name: "filter", size: iconSize }),
8058
+ onClick: () => setOpen(!open),
8059
+ "aria-expanded": open,
8060
+ "aria-controls": filterRowId,
8061
+ className: "mr-auto",
8062
+ children: [
8063
+ "Filters",
8064
+ count > 0 && !open && /* @__PURE__ */ jsx(Badge, { variant: "default", size: "xs", className: "ml-ds-1", children: count })
8065
+ ]
8066
+ }
8067
+ ),
8068
+ actions
8069
+ ] })
8070
+ ] }),
8071
+ filters && open && /* @__PURE__ */ jsx("div", { id: filterRowId, className: "flex items-center gap-ds-3 flex-wrap", children: filters }),
8072
+ (countText !== null || hasChips) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2 flex-wrap", children: [
8073
+ countText !== null && /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--ds-text-muted)]", "aria-live": "polite", "data-result-count": "", children: countText }),
8074
+ activeFilters?.map((f) => /* @__PURE__ */ jsx(Tag, { size: "sm", variant: "subtle", onRemove: f.onRemove, children: f.label }, f.id)),
8075
+ hasChips && onClearAll && /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: onClearAll, children: "Clear all" })
8487
8076
  ] })
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
- ] }) });
8077
+ ] }) }) })
8078
+ );
8496
8079
  }
8497
8080
  var ICON_TEXT_OFFSET = "calc((2rem - 1.25rem) / 2)";
8498
8081
  function Marker({ item, vertical }) {
@@ -8617,7 +8200,9 @@ function VerticalLayout({ items, className }) {
8617
8200
  ] })
8618
8201
  ] }, i)) });
8619
8202
  }
8620
- function Timeline({ items, orientation = "vertical", minItemWidth = 200, className, ref }) {
8203
+ function Timeline({ items: allItems, orientation = "vertical", minItemWidth = 200, className, ref, pager }) {
8204
+ useWholeListCheck("Timeline", allItems.length, pager);
8205
+ const items = pagerPart(allItems, pager);
8621
8206
  const measureRef = useRef(null);
8622
8207
  const width = useElementWidth(measureRef);
8623
8208
  const setRefs = useCallback((node) => {
@@ -8627,7 +8212,12 @@ function Timeline({ items, orientation = "vertical", minItemWidth = 200, classNa
8627
8212
  }, [ref]);
8628
8213
  const fits = width === 0 || width >= items.length * minItemWidth;
8629
8214
  const horizontal = orientation === "horizontal" || orientation === "responsive" && fits;
8630
- return /* @__PURE__ */ jsx("div", { ref: setRefs, "data-ds-list": "", "data-timeline-orientation": horizontal ? "horizontal" : "vertical", className, children: horizontal ? /* @__PURE__ */ jsx(HorizontalLayout, { items, minItemWidth }) : /* @__PURE__ */ jsx(VerticalLayout, { items }) });
8215
+ const list = /* @__PURE__ */ jsx("div", { ref: setRefs, "data-ds-list": "", "data-timeline-orientation": horizontal ? "horizontal" : "vertical", className: pager ? void 0 : className, children: horizontal ? /* @__PURE__ */ jsx(HorizontalLayout, { items, minItemWidth }) : /* @__PURE__ */ jsx(VerticalLayout, { items }) });
8216
+ if (!pager) return list;
8217
+ return /* @__PURE__ */ jsxs("div", { "data-gs-paged": "", className: cn("flex flex-col min-w-0", className), children: [
8218
+ list,
8219
+ allItems.length > 0 && /* @__PURE__ */ jsx(Pagination, { ...pager.props })
8220
+ ] });
8631
8221
  }
8632
8222
  var CollapsibleContext = createContext(null);
8633
8223
  function useCollapsible() {
@@ -8657,6 +8247,8 @@ function CollapsibleTrigger({ children, className }) {
8657
8247
  if (isValidElement(children)) {
8658
8248
  const child = children;
8659
8249
  return cloneElement(child, {
8250
+ // a Button trigger in a form toggles; it never submits the form (0.21.0)
8251
+ ...triggerType(child),
8660
8252
  "aria-expanded": isOpen,
8661
8253
  className: cn(child.props.className, className),
8662
8254
  onClick: (e) => {
@@ -9206,7 +8798,7 @@ function ActionsRow({ message, onAction }) {
9206
8798
  ),
9207
8799
  children: message.actions.map((a) => {
9208
8800
  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);
8801
+ 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
8802
  })
9211
8803
  }
9212
8804
  );
@@ -9343,12 +8935,13 @@ function ChatInputBar({ onSend, busy, onStop, onAttach, onVoice, placeholder })
9343
8935
  ),
9344
8936
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-ds-2 pb-ds-1.5", children: [
9345
8937
  /* @__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 }) })
8938
+ 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 }) }),
8939
+ 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
8940
  ] }),
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(
8941
+ 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
8942
  Button,
9351
8943
  {
8944
+ type: "button",
9352
8945
  variant: text.trim() ? "default" : "ghost",
9353
8946
  size: "icon-sm",
9354
8947
  "aria-label": "Send message",
@@ -9462,7 +9055,7 @@ function ChatMessageList({
9462
9055
  }),
9463
9056
  typingUser && /* @__PURE__ */ jsx(TypingIndicator, { name: typingUser })
9464
9057
  ] }) }),
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: [
9058
+ 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
9059
  /* @__PURE__ */ jsx(Icon, { name: "chevron-down", size: 14 }),
9467
9060
  "Jump to latest"
9468
9061
  ] }) })
@@ -9517,10 +9110,11 @@ function ChatPanel({
9517
9110
  ] }),
9518
9111
  /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-ds-1", children: [
9519
9112
  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 }) }) }),
9113
+ 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
9114
  onExpandedChange && /* @__PURE__ */ jsx(Tooltip, { content: expanded ? "Collapse" : "Expand", children: /* @__PURE__ */ jsx(
9522
9115
  Button,
9523
9116
  {
9117
+ type: "button",
9524
9118
  variant: "ghost",
9525
9119
  size: "icon-sm",
9526
9120
  "aria-label": expanded ? "Collapse" : "Expand",
@@ -9529,7 +9123,7 @@ function ChatPanel({
9529
9123
  children: /* @__PURE__ */ jsx(Icon, { name: expanded ? "minimize-2" : "maximize-2", size: 16 })
9530
9124
  }
9531
9125
  ) }),
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 }) }) })
9126
+ /* @__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
9127
  ] })
9534
9128
  ]
9535
9129
  }
@@ -9878,7 +9472,8 @@ var StackedList = forwardRef(function StackedList2({
9878
9472
  selectedId,
9879
9473
  className,
9880
9474
  flush,
9881
- hover
9475
+ hover,
9476
+ pager
9882
9477
  }, ref) {
9883
9478
  const isPhone = useBreakpoint("sm");
9884
9479
  const inCard = useContext(CardSurfaceCtx);
@@ -9891,20 +9486,22 @@ var StackedList = forwardRef(function StackedList2({
9891
9486
  console.warn('StackedList: `flush` and `hover` are deprecated \u2014 pass variant="articles" for an article list; the default variant takes its inset from the pane or card it sits in.');
9892
9487
  }, [legacy]);
9893
9488
  const articles = variant === "articles";
9489
+ useWholeListCheck("StackedList", items.length, pager);
9490
+ const shown = pagerPart(items, pager);
9894
9491
  const stackMeta = isPhone && articles;
9895
9492
  const selectable = selectedId !== void 0 || items.some((i) => i.selected !== void 0);
9896
9493
  const inset = articles ? inCard ? "px-ds-5" : "px-0" : inCard ? "px-ds-5" : inPane ? "pl-ds-3 pr-ds-4" : selectable ? "pl-ds-3 pr-0" : "px-0";
9897
- return /* @__PURE__ */ jsx(
9494
+ const list = /* @__PURE__ */ jsx(
9898
9495
  "ul",
9899
9496
  {
9900
9497
  ref,
9901
9498
  "data-ds-list": "",
9902
9499
  className: cn(
9903
9500
  divided && "divide-y-[length:var(--ds-border-width)] divide-[var(--ds-border-subtle)]",
9904
- className
9501
+ !pager && className
9905
9502
  ),
9906
9503
  role: "list",
9907
- children: items.map((item) => {
9504
+ children: shown.map((item) => {
9908
9505
  const isSelected = item.selected ?? (selectedId !== void 0 && item.id === selectedId);
9909
9506
  const interactive = !!item.onClick;
9910
9507
  const hasEmphasis = item.unread !== void 0;
@@ -9996,6 +9593,11 @@ var StackedList = forwardRef(function StackedList2({
9996
9593
  })
9997
9594
  }
9998
9595
  );
9596
+ if (!pager) return list;
9597
+ return /* @__PURE__ */ jsxs("div", { "data-gs-paged": "", className: cn("flex flex-col min-w-0", className), children: [
9598
+ list,
9599
+ items.length > 0 && /* @__PURE__ */ jsx(Pagination, { ...pager.props })
9600
+ ] });
9999
9601
  });
10000
9602
  var TIME_ZOOMS = ["day", "week", "month", "quarter"];
10001
9603
  var DAY_MS = 864e5;
@@ -10766,15 +10368,15 @@ var TimeViewport = forwardRef(function TimeViewport2({
10766
10368
  const hasHeader = headerNode != null || labelHeader != null;
10767
10369
  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
10370
  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 }) }),
10371
+ /* @__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
10372
  /* @__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 }) })
10373
+ /* @__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
10374
  ] }),
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: () => {
10375
+ /* @__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 }) }),
10376
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", disabled: todayAtX === null, onClick: () => {
10775
10377
  if (todayAtX !== null) setLeft(todayAtX - visibleWidth / 2);
10776
10378
  }, 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 }) }),
10379
+ /* @__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
10380
  toolbar
10779
10381
  ] });
10780
10382
  return (
@@ -11032,7 +10634,7 @@ var UNDATED_INSET = 16;
11032
10634
  var NO_GROUPS = [];
11033
10635
  var NO_STATUSES = {};
11034
10636
  var warned = /* @__PURE__ */ new Set();
11035
- function warnOnce(message) {
10637
+ function warnOnce2(message) {
11036
10638
  let dev = false;
11037
10639
  try {
11038
10640
  dev = process.env.NODE_ENV !== "production";
@@ -11122,7 +10724,7 @@ function Gantt({
11122
10724
  const selected = isControlled ? selectedId ?? null : selectedState;
11123
10725
  useEffect(() => {
11124
10726
  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.");
10727
+ 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
10728
  }
11127
10729
  }, [overridden]);
11128
10730
  const select = (item) => {
@@ -11134,8 +10736,8 @@ function Gantt({
11134
10736
  const phases = useMemo(() => uniqueById(groups), [groups]);
11135
10737
  useEffect(() => {
11136
10738
  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.`);
10739
+ if (plan.repeated.length) warnOnce2(`more than one item has the id ${quoted(plan.repeated)}; only the first of each is shown.`);
10740
+ if (phases.repeated.length) warnOnce2(`more than one group has the id ${quoted(phases.repeated)}; only the first of each is shown.`);
11139
10741
  }, [plan, phases]);
11140
10742
  const tasks = plan.unique;
11141
10743
  const extent = useMemo(() => itemsExtent(tasks), [tasks]);
@@ -12032,14 +11634,17 @@ function TabsRoot(props) {
12032
11634
  const { tabs, mobile = "scroll" } = props;
12033
11635
  const collapseToDropdown = isMobile && (orientation === "responsive" || mobile === "dropdown");
12034
11636
  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
- }
11637
+ return (
11638
+ // choosing a tab is navigation, never a change to a form around it (0.21.0)
11639
+ /* @__PURE__ */ jsx(FormChangeCtx, { value: null, children: /* @__PURE__ */ jsx(
11640
+ Dropdown,
11641
+ {
11642
+ options: tabs.filter((t) => !t.disabled).map((t) => ({ id: t.id, label: t.label, group: t.group })),
11643
+ value: activeTab,
11644
+ onChange: (v) => onTabChange(v),
11645
+ className
11646
+ }
11647
+ ) })
12043
11648
  );
12044
11649
  }
12045
11650
  if (effectiveOrientation === "vertical") {
@@ -12141,7 +11746,7 @@ function SettingsLayout({
12141
11746
  return (
12142
11747
  // gap-x-ds-8 is the rail's gap, unused in one column: the threshold reads the same gap as the row (useColumnGap)
12143
11748
  /* @__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(
11749
+ /* @__PURE__ */ jsx(FormChangeCtx, { value: null, children: /* @__PURE__ */ jsx(
12145
11750
  Dropdown,
12146
11751
  {
12147
11752
  "aria-label": ariaLabel,
@@ -12149,7 +11754,7 @@ function SettingsLayout({
12149
11754
  value,
12150
11755
  onChange: (v) => onChange(v)
12151
11756
  }
12152
- ),
11757
+ ) }),
12153
11758
  /* @__PURE__ */ jsx("div", { "data-settings-panel": "", className: PANEL_CLASSES, style: PANEL_STYLE, children })
12154
11759
  ] })
12155
11760
  );
@@ -12394,280 +11999,6 @@ var Breadcrumbs = forwardRef(function Breadcrumbs2({
12394
11999
  dropdown
12395
12000
  ] });
12396
12001
  });
12397
- function Popover({ open, onOpenChange, trigger, children, side = "bottom", align = "center", width, className, container }) {
12398
- const triggerRef = useRef(null);
12399
- const floatingRef = useRef(null);
12400
- const [triggerWidth, setTriggerWidth] = useState(void 0);
12401
- const setTriggerNode = useCallback((node) => {
12402
- triggerRef.current = node;
12403
- setTriggerWidth(node?.offsetWidth);
12404
- }, []);
12405
- const pos = usePosition(triggerRef, floatingRef, open, { side, align, offset: 8, flip: true });
12406
- const onOpenChangeRef = useLatest(onOpenChange);
12407
- const { props: pressMarks, marked } = usePressMarks();
12408
- const { isTop: isTopLayer, returnFocus } = usePopupFocus(open, { trigger: triggerRef, panel: floatingRef, close: () => onOpenChangeRef.current(false), tabExits: "edges", marked });
12409
- useEffect(() => {
12410
- if (!open) return;
12411
- const onKeyDown = (e) => {
12412
- if (e.key !== "Escape" || e.defaultPrevented || !isTopLayer()) return;
12413
- const active = document.activeElement;
12414
- if (active && active !== document.body && !triggerRef.current?.contains(active) && !floatingRef.current?.contains(active)) return;
12415
- e.preventDefault();
12416
- returnFocus();
12417
- onOpenChangeRef.current(false);
12418
- };
12419
- const onPress = (e) => {
12420
- const target = e.target;
12421
- if (marked(e) || triggerRef.current?.contains(target) || floatingRef.current?.contains(target)) return;
12422
- onOpenChangeRef.current(false);
12423
- };
12424
- document.addEventListener("keydown", onKeyDown);
12425
- document.addEventListener("mousedown", onPress);
12426
- return () => {
12427
- document.removeEventListener("keydown", onKeyDown);
12428
- document.removeEventListener("mousedown", onPress);
12429
- };
12430
- }, [open, onOpenChangeRef, isTopLayer, returnFocus, marked]);
12431
- useEffect(() => {
12432
- if (open) focusInitial(floatingRef.current);
12433
- }, [open]);
12434
- return /* @__PURE__ */ jsxs(Fragment, { children: [
12435
- /* @__PURE__ */ jsx(
12436
- Slot,
12437
- {
12438
- ref: setTriggerNode,
12439
- onClick: () => {
12440
- setTriggerWidth(triggerRef.current?.offsetWidth);
12441
- onOpenChange(!open);
12442
- },
12443
- children: trigger
12444
- }
12445
- ),
12446
- open && createPortal(
12447
- /* @__PURE__ */ jsx(
12448
- "div",
12449
- {
12450
- ref: floatingRef,
12451
- "data-gs": "popover",
12452
- ...pressMarks,
12453
- onClick: (e) => e.stopPropagation(),
12454
- className: cn(
12455
- "fixed z-[var(--ds-z-popover)]",
12456
- "bg-[var(--ds-bg-surface-elevated)]",
12457
- GLASS_DROPDOWN,
12458
- "border-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
12459
- "rounded-[var(--ds-radius-md)]",
12460
- className
12461
- ),
12462
- style: {
12463
- top: pos.top,
12464
- left: pos.left,
12465
- width: width === "trigger" ? triggerWidth : width === "auto" ? void 0 : width
12466
- },
12467
- children
12468
- }
12469
- ),
12470
- container ?? document.body
12471
- )
12472
- ] });
12473
- }
12474
- function Pagination(props) {
12475
- if ((props.mode === "load-more" || props.mode === "infinite-scroll") && props.onLoadMore) return /* @__PURE__ */ jsx(LoadMore, { ...props, onLoadMore: props.onLoadMore, scroll: props.mode === "infinite-scroll" });
12476
- return /* @__PURE__ */ jsx(PageNumbers, { ...props });
12477
- }
12478
- function contentTop(el) {
12479
- let top = el.getBoundingClientRect().top + window.scrollY;
12480
- for (let n = el.parentElement; n && n !== document.documentElement; n = n.parentElement) top += n.scrollTop;
12481
- return top;
12482
- }
12483
- function LoadMore({ hasMore, onLoadMore, loadMoreLabel, currentPage, scroll, className }) {
12484
- const marker = useRef(null);
12485
- const load = useLatest(onLoadMore);
12486
- const loadedAt = useRef(null);
12487
- useEffect(() => {
12488
- const el = marker.current;
12489
- if (!scroll || !hasMore || !el || typeof IntersectionObserver === "undefined") return;
12490
- const observer = new IntersectionObserver((entries) => {
12491
- if (!entries.some((e) => e.isIntersecting)) return;
12492
- const at = contentTop(el);
12493
- if (loadedAt.current !== null && Math.abs(at - loadedAt.current) < 1) return;
12494
- loadedAt.current = at;
12495
- load.current();
12496
- }, { rootMargin: "200px 0px" });
12497
- observer.observe(el);
12498
- const rearm = () => {
12499
- loadedAt.current = null;
12500
- };
12501
- window.addEventListener("scroll", rearm, { capture: true, passive: true });
12502
- return () => {
12503
- observer.disconnect();
12504
- window.removeEventListener("scroll", rearm, { capture: true });
12505
- };
12506
- }, [scroll, hasMore, currentPage, load]);
12507
- if (!hasMore) return null;
12508
- 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
- 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" })
12511
- ] });
12512
- }
12513
- function PageNumbers({ currentPage, totalPages, onPageChange, pageSize: pageSizeProp, pageSizeOptions, onPageSizeChange, className }) {
12514
- const pages = buildPageRange(currentPage, totalPages);
12515
- const preferences = useShellPreferences();
12516
- const pageSize = pageSizeProp ?? preferences.itemsPerPage;
12517
- return /* @__PURE__ */ jsxs("nav", { "aria-label": "Pagination", "data-ds-pager": "", className: cn("flex flex-wrap items-center gap-ds-3", className), children: [
12518
- pageSizeOptions && pageSize != null && onPageSizeChange && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2", children: [
12519
- /* @__PURE__ */ jsx("span", { className: "text-sm text-[var(--ds-text-muted)] whitespace-nowrap", children: "Rows per page" }),
12520
- /* @__PURE__ */ jsx(
12521
- Dropdown,
12522
- {
12523
- options: pageSizeOptions.map((n) => ({ id: String(n), label: String(n) })),
12524
- value: String(pageSize),
12525
- onChange: (v) => onPageSizeChange(Number(v)),
12526
- size: "sm",
12527
- className: "w-[4.5rem]"
12528
- }
12529
- )
12530
- ] }),
12531
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-1", children: [
12532
- /* @__PURE__ */ jsx(
12533
- "button",
12534
- {
12535
- type: "button",
12536
- onClick: () => onPageChange(currentPage - 1),
12537
- disabled: currentPage <= 1,
12538
- "aria-label": "Previous page",
12539
- className: cn(
12540
- "inline-flex items-center justify-center h-[var(--ds-control-height-sm)] w-[var(--ds-control-height-sm)] text-sm cursor-pointer",
12541
- "rounded-[var(--ds-radius-md)]",
12542
- "border-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
12543
- "bg-[var(--ds-bg-surface)] text-[var(--ds-text-secondary)]",
12544
- "transition-colors duration-[var(--ds-transition-speed)]",
12545
- "hover:bg-[var(--ds-bg-muted)] hover:text-[var(--ds-text-primary)]",
12546
- "disabled:opacity-disabled disabled:cursor-not-allowed"
12547
- ),
12548
- children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 16, className: "rotate-180" })
12549
- }
12550
- ),
12551
- pages.map(
12552
- (p, i) => p === "..." ? /* @__PURE__ */ jsx(EllipsisJumper, { totalPages, onPageChange }, `ellipsis-${i}`) : /* @__PURE__ */ jsx(
12553
- "button",
12554
- {
12555
- type: "button",
12556
- onClick: () => onPageChange(p),
12557
- "aria-current": p === currentPage ? "page" : void 0,
12558
- className: cn(
12559
- "inline-flex items-center justify-center h-[var(--ds-control-height-sm)] w-[var(--ds-control-height-sm)] text-sm font-medium cursor-pointer",
12560
- "rounded-[var(--ds-radius-md)]",
12561
- "border-[length:var(--ds-border-width)]",
12562
- "transition-colors duration-[var(--ds-transition-speed)]",
12563
- p === currentPage ? "bg-[var(--ds-accent-primary)] [background-image:var(--ds-gradient)] text-[var(--ds-text-on-primary)] border-[var(--ds-accent-primary)]" : "bg-[var(--ds-bg-surface)] text-[var(--ds-text-secondary)] border-[var(--ds-border-default)] hover:bg-[var(--ds-bg-muted)] hover:text-[var(--ds-text-primary)]"
12564
- ),
12565
- children: p
12566
- },
12567
- p
12568
- )
12569
- ),
12570
- /* @__PURE__ */ jsx(
12571
- "button",
12572
- {
12573
- type: "button",
12574
- onClick: () => onPageChange(currentPage + 1),
12575
- disabled: currentPage >= totalPages,
12576
- "aria-label": "Next page",
12577
- className: cn(
12578
- "inline-flex items-center justify-center h-[var(--ds-control-height-sm)] w-[var(--ds-control-height-sm)] text-sm cursor-pointer",
12579
- "rounded-[var(--ds-radius-md)]",
12580
- "border-[length:var(--ds-border-width)] border-[var(--ds-border-default)]",
12581
- "bg-[var(--ds-bg-surface)] text-[var(--ds-text-secondary)]",
12582
- "transition-colors duration-[var(--ds-transition-speed)]",
12583
- "hover:bg-[var(--ds-bg-muted)] hover:text-[var(--ds-text-primary)]",
12584
- "disabled:opacity-disabled disabled:cursor-not-allowed"
12585
- ),
12586
- children: /* @__PURE__ */ jsx(Icon, { name: "chevron-right", size: 16 })
12587
- }
12588
- )
12589
- ] })
12590
- ] });
12591
- }
12592
- function buildPageRange(current, total) {
12593
- if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
12594
- const pages = [1];
12595
- if (current > 3) pages.push("...");
12596
- const start = Math.max(2, current - 1);
12597
- const end = Math.min(total - 1, current + 1);
12598
- for (let i = start; i <= end; i++) pages.push(i);
12599
- if (current < total - 2) pages.push("...");
12600
- pages.push(total);
12601
- return pages;
12602
- }
12603
- function EllipsisJumper({ totalPages, onPageChange }) {
12604
- const [open, setOpen] = useState(false);
12605
- const [jumpPage, setJumpPage] = useState(1);
12606
- const handleOpenChange = (next) => {
12607
- if (next) setJumpPage(1);
12608
- setOpen(next);
12609
- };
12610
- const handleGo = () => {
12611
- const clamped = Math.min(totalPages, Math.max(1, jumpPage));
12612
- onPageChange(clamped);
12613
- setOpen(false);
12614
- };
12615
- return /* @__PURE__ */ jsx(
12616
- Popover,
12617
- {
12618
- open,
12619
- onOpenChange: handleOpenChange,
12620
- trigger: /* @__PURE__ */ jsx(
12621
- "button",
12622
- {
12623
- type: "button",
12624
- "aria-label": "Go to page",
12625
- onClick: () => handleOpenChange(true),
12626
- className: cn(
12627
- "inline-flex items-center justify-center h-[var(--ds-control-height-sm)] w-[var(--ds-control-height-sm)] text-sm cursor-pointer",
12628
- "rounded-[var(--ds-radius-md)]",
12629
- "text-[var(--ds-text-muted)]",
12630
- "transition-colors duration-[var(--ds-transition-speed)]",
12631
- "hover:bg-[var(--ds-bg-muted)] hover:text-[var(--ds-text-primary)]"
12632
- ),
12633
- children: "\u2026"
12634
- }
12635
- ),
12636
- children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-ds-2 p-ds-3", children: [
12637
- /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-[var(--ds-text-secondary)]", children: "Go to page" }),
12638
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-ds-2", children: [
12639
- /* @__PURE__ */ jsx(
12640
- NumberInput,
12641
- {
12642
- value: jumpPage,
12643
- onChange: setJumpPage,
12644
- min: 1,
12645
- max: totalPages,
12646
- step: 1,
12647
- size: "sm",
12648
- className: "w-[5rem]"
12649
- }
12650
- ),
12651
- /* @__PURE__ */ jsx(
12652
- "button",
12653
- {
12654
- type: "button",
12655
- onClick: handleGo,
12656
- className: cn(
12657
- "px-ds-2.5 h-[var(--ds-control-height-sm)] text-xs font-medium cursor-pointer",
12658
- "rounded-[var(--ds-radius-md)]",
12659
- "bg-[var(--ds-accent-primary)] text-[var(--ds-text-on-primary)]",
12660
- "transition-colors duration-[var(--ds-transition-speed)]",
12661
- "hover:opacity-90"
12662
- ),
12663
- children: "Go"
12664
- }
12665
- )
12666
- ] })
12667
- ] })
12668
- }
12669
- );
12670
- }
12671
12002
  var HeaderTabRowContext = createContext(null);
12672
12003
  function useHeaderTabRow() {
12673
12004
  return useContext(HeaderTabRowContext);
@@ -13898,6 +13229,16 @@ function useToastOptional() {
13898
13229
  }
13899
13230
  var toastSeq = 0;
13900
13231
  var heardSeq = 0;
13232
+ var HEARD_MS = 7e3;
13233
+ var RUN_END_MS = 20;
13234
+ var isThenable2 = (value) => !!value && typeof value.then === "function";
13235
+ function textOf(node) {
13236
+ if (node == null || typeof node === "boolean") return "";
13237
+ if (typeof node === "string" || typeof node === "number") return String(node);
13238
+ if (Array.isArray(node)) return node.map(textOf).join("");
13239
+ if (typeof node === "object" && "props" in node) return textOf(node.props.children);
13240
+ return "";
13241
+ }
13901
13242
  var mergeable = (e) => !e.action && e.variant !== "error";
13902
13243
  function fill(entries, slots) {
13903
13244
  let shown = entries.filter((e) => e.shown).length;
@@ -13917,14 +13258,18 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
13917
13258
  const [list, setList] = useState([]);
13918
13259
  const entries = useRef([]);
13919
13260
  const isMobile = useBreakpoint("sm");
13920
- const createdIds = useRef([]);
13261
+ const shownDuringRuns = useRef([]);
13262
+ const showSeq = useRef(0);
13263
+ const runs = useRef(/* @__PURE__ */ new Map());
13264
+ const runSeq = useRef(0);
13921
13265
  const [heard, setHeard] = useState([]);
13922
13266
  const [hovered, setHovered] = useState(false);
13923
13267
  const [focusInside, setFocusInside] = useState(false);
13924
13268
  const paused = pauseOnHover && hovered || focusInside;
13925
13269
  const stackRef = useRef(null);
13926
13270
  const returnTo = useRef(null);
13927
- const answering = useRef([]);
13271
+ const pressed = useRef(/* @__PURE__ */ new Set());
13272
+ const pointerAt = useRef(0);
13928
13273
  const viaKey = useRef(false);
13929
13274
  const refocus = useRef(false);
13930
13275
  const pendingHeard = useRef(/* @__PURE__ */ new Map());
@@ -13943,58 +13288,75 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
13943
13288
  entries.current = fill(next, slots);
13944
13289
  setList(entries.current);
13945
13290
  }, [slots]);
13946
- const [slotsSeen, setSlotsSeen] = useState(slots);
13947
- if (slots !== slotsSeen) {
13948
- setSlotsSeen(slots);
13291
+ useEffect(() => {
13949
13292
  let kept = 0;
13950
- setList(fill(list.map((e) => e.shown && ++kept > slots ? { ...e, shown: false } : e), slots));
13293
+ const reslotted = entries.current.map((e) => e.shown && ++kept > slots ? { ...e, shown: false } : e);
13294
+ const next = fill(reslotted, slots);
13295
+ if (next.length === entries.current.length && next.every((e, i) => e === entries.current[i])) return;
13296
+ commit(next);
13297
+ }, [slots, commit]);
13298
+ if (list.every((t) => !t.shown) && (hovered || focusInside)) {
13299
+ setHovered(false);
13300
+ setFocusInside(false);
13951
13301
  }
13952
- useEffect(() => {
13953
- entries.current = list;
13954
- }, [list]);
13302
+ const clearTimers = useRef(/* @__PURE__ */ new Set());
13955
13303
  const announce = useCallback((entry) => {
13956
13304
  pendingHeard.current.set(entry.id, entry);
13957
13305
  if (heardTimer.current) return;
13958
13306
  heardTimer.current = setTimeout(() => {
13959
13307
  heardTimer.current = void 0;
13960
- const batch = [...pendingHeard.current.values()];
13308
+ const batch = [...pendingHeard.current.values()].map((e) => {
13309
+ const words = textOf(e.message).trim();
13310
+ const sentence = !words || /[.!?…]$/.test(words) ? words : `${words}.`;
13311
+ return { id: `heard-${++heardSeq}`, assertive: e.variant === "error", text: e.action ? `${sentence} Press F8 to reach ${e.action.label}.`.trim() : sentence };
13312
+ }).filter((h) => h.text);
13961
13313
  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));
13314
+ if (!batch.length) return;
13315
+ setHeard((list2) => [...list2, ...batch].slice(-5));
13316
+ const ids = new Set(batch.map((h) => h.id));
13317
+ const timer = setTimeout(() => {
13318
+ clearTimers.current.delete(timer);
13319
+ setHeard((list2) => list2.filter((h) => !ids.has(h.id)));
13320
+ }, HEARD_MS);
13321
+ clearTimers.current.add(timer);
13970
13322
  }, 0);
13971
13323
  }, []);
13972
13324
  useEffect(() => () => {
13973
13325
  if (heardTimer.current) clearTimeout(heardTimer.current);
13326
+ heardTimer.current = void 0;
13327
+ clearTimers.current.forEach(clearTimeout);
13328
+ clearTimers.current.clear();
13974
13329
  }, []);
13975
- const leaving = useCallback(() => {
13976
- if (!stackRef.current?.contains(document.activeElement)) return;
13330
+ const leaving = useCallback((id) => {
13331
+ const stack = stackRef.current;
13332
+ const holder = id === void 0 ? stack : [...stack?.querySelectorAll("[data-toast-id]") ?? []].find((n) => n.getAttribute("data-toast-id") === id);
13333
+ if (!holder?.contains(document.activeElement)) return;
13977
13334
  refocus.current = true;
13978
13335
  setFocusInside(false);
13979
13336
  }, []);
13980
13337
  const remove = useCallback((id) => {
13981
- leaving();
13338
+ leaving(id);
13982
13339
  commit(entries.current.filter((t) => t.id !== id));
13983
13340
  }, [commit, leaving]);
13984
13341
  const dismissAll = useCallback(() => {
13985
13342
  leaving();
13986
13343
  commit([]);
13987
13344
  }, [commit, leaving]);
13345
+ const noteShown = useCallback((id, variant, action, fresh) => {
13346
+ if (runs.current.size) shownDuringRuns.current.push({ seq: ++showSeq.current, at: Date.now(), id, variant, action, fresh });
13347
+ }, []);
13988
13348
  const update = useCallback((id, message, options = {}) => {
13989
13349
  const list2 = entries.current;
13990
13350
  const at = list2.findIndex((t2) => t2.id === id);
13991
13351
  if (at === -1) return;
13352
+ pressed.current.delete(id);
13992
13353
  const t = list2[at];
13993
13354
  const next = [...list2];
13994
13355
  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
13356
  commit(next);
13996
13357
  announce(next[at]);
13997
- }, [commit, announce]);
13358
+ noteShown(id, next[at].variant, !!next[at].action, false);
13359
+ }, [commit, announce, noteShown]);
13998
13360
  const toast = useCallback((message, options = {}) => {
13999
13361
  const list2 = entries.current;
14000
13362
  const base = { message, variant: options.variant ?? "default", duration: options.duration ?? duration, action: options.action, actionWindow: options.actionWindow, key: options.key };
@@ -14003,41 +13365,31 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
14003
13365
  if (at !== -1) {
14004
13366
  const next = [...list2];
14005
13367
  next[at] = { ...list2[at], ...base, version: list2[at].version + 1 };
14006
- createdIds.current.push(options.id);
13368
+ pressed.current.delete(options.id);
14007
13369
  commit(next);
14008
13370
  announce(next[at]);
13371
+ noteShown(options.id, base.variant, !!base.action, false);
14009
13372
  return options.id;
14010
13373
  }
14011
13374
  }
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
13375
  if (coalesce && base.key && mergeable(base)) {
14024
13376
  const at = list2.findIndex((t) => t.key === base.key && mergeable(t));
14025
13377
  if (at !== -1) {
14026
13378
  const next = [...list2];
14027
13379
  next[at] = { ...list2[at], ...base, version: list2[at].version + 1 };
14028
- createdIds.current.push(list2[at].id);
14029
13380
  commit(next);
14030
13381
  announce(next[at]);
13382
+ noteShown(list2[at].id, base.variant, !!base.action, false);
14031
13383
  return list2[at].id;
14032
13384
  }
14033
13385
  }
14034
13386
  const id = options.id ?? `toast-${++toastSeq}`;
14035
13387
  const entry = { id, ...base, shown: false, version: 0 };
14036
- createdIds.current.push(id);
14037
13388
  commit([...list2, entry]);
14038
13389
  announce(entry);
13390
+ noteShown(id, base.variant, !!base.action, true);
14039
13391
  return id;
14040
- }, [duration, coalesce, commit, announce]);
13392
+ }, [duration, coalesce, commit, announce, noteShown]);
14041
13393
  const api = useMemo(() => ({
14042
13394
  toast,
14043
13395
  success: (m, o) => toast(m, { ...o, variant: "success" }),
@@ -14059,34 +13411,53 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
14059
13411
  }
14060
13412
  }), [toast, update, remove, dismissAll, duration]);
14061
13413
  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
- }
13414
+ begin: () => {
13415
+ const run = ++runSeq.current;
13416
+ const overlapped = runs.current.size > 0;
13417
+ if (overlapped) runs.current.forEach((r) => {
13418
+ r.overlapped = true;
13419
+ });
13420
+ runs.current.set(run, { from: showSeq.current, overlapped });
13421
+ return run;
13422
+ },
13423
+ end: (run) => {
13424
+ const watched = runs.current.get(run);
13425
+ runs.current.delete(run);
13426
+ if (!watched) return { problem: false };
13427
+ const during = shownDuringRuns.current.filter((s) => s.seq > watched.from);
13428
+ const oldest = Math.min(...[...runs.current.values()].map((r) => r.from));
13429
+ shownDuringRuns.current = runs.current.size ? shownDuringRuns.current.filter((s) => s.seq > oldest) : [];
13430
+ const problem = during.some((s) => s.variant === "error" || s.variant === "warning");
13431
+ const endedAt = Date.now();
13432
+ const confirmations = during.filter((s) => s.fresh && !s.action && (s.variant === "success" || s.variant === "default"));
13433
+ if (watched.overlapped || confirmations.length !== 1 || endedAt - confirmations[0].at > RUN_END_MS) return { problem };
13434
+ const entry = entries.current.find((t) => t.id === confirmations[0].id);
13435
+ return { problem, own: entry && !entry.action ? { id: entry.id, message: entry.message, variant: entry.variant } : void 0 };
13436
+ },
13437
+ has: (id) => entries.current.some((t) => t.id === id)
14069
13438
  }), []);
14070
13439
  const pressAction = useCallback((id) => {
14071
13440
  const entry = entries.current.find((t) => t.id === id);
14072
13441
  if (!entry?.action) return;
14073
13442
  const run = entry.action.onClick;
14074
- leaving();
13443
+ leaving(id);
14075
13444
  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);
13445
+ pressed.current.add(id);
14077
13446
  const done = () => {
14078
- const waiting2 = answering.current.indexOf(id);
14079
- if (waiting2 === -1) return;
14080
- answering.current.splice(waiting2, 1);
14081
- remove(id);
13447
+ if (pressed.current.delete(id)) remove(id);
13448
+ };
13449
+ const failed = (err) => {
13450
+ done();
13451
+ console.error(err);
14082
13452
  };
14083
13453
  let result;
14084
13454
  try {
14085
13455
  result = run();
14086
- } catch {
14087
- result = void 0;
13456
+ } catch (err) {
13457
+ failed(err);
13458
+ return;
14088
13459
  }
14089
- if (result && typeof result.then === "function") result.then(done, done);
13460
+ if (isThenable2(result)) result.then(done, failed);
14090
13461
  else done();
14091
13462
  }, [commit, leaving, remove]);
14092
13463
  const lapse = useCallback((id) => {
@@ -14120,8 +13491,11 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
14120
13491
  const back = returnTo.current;
14121
13492
  const next = stackRef.current?.querySelector("button");
14122
13493
  const main = document.querySelector("main");
13494
+ const byPointer = Date.now() - pointerAt.current < 1e3;
14123
13495
  if (back?.isConnected) back.focus();
14124
- else if (next) next.focus();
13496
+ else if (byPointer) {
13497
+ if (stackRef.current?.contains(document.activeElement)) document.activeElement.blur();
13498
+ } else if (next) next.focus();
14125
13499
  else if (main) {
14126
13500
  if (!main.hasAttribute("tabindex")) {
14127
13501
  main.setAttribute("tabindex", "-1");
@@ -14136,8 +13510,8 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
14136
13510
  const hasBottomNav = isMobile && bottomNavs > 0;
14137
13511
  return /* @__PURE__ */ jsx(ToastContext.Provider, { value: api, children: /* @__PURE__ */ jsxs(ToastLedgerCtx.Provider, { value: ledger, children: [
14138
13512
  /* @__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)) }),
13513
+ /* @__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)) }),
13514
+ /* @__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
13515
  shown.length > 0 && createPortal(
14142
13516
  /* @__PURE__ */ jsxs(
14143
13517
  "div",
@@ -14150,6 +13524,9 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
14150
13524
  "data-toast-stack": "",
14151
13525
  onMouseEnter: () => setHovered(true),
14152
13526
  onMouseLeave: () => setHovered(false),
13527
+ onPointerDownCapture: () => {
13528
+ pointerAt.current = Date.now();
13529
+ },
14153
13530
  onFocusCapture: (e) => {
14154
13531
  setFocusInside(true);
14155
13532
  const from = e.relatedTarget;
@@ -14166,7 +13543,7 @@ function ToastProvider({ children, maxVisible = 3, position = "bottom-right", du
14166
13543
  ),
14167
13544
  style: hasBottomNav ? { bottom: "calc(var(--ds-space-12) + var(--ds-space-2) + var(--ds-space-4) + env(safe-area-inset-bottom))" } : void 0,
14168
13545
  children: [
14169
- shown.map((t) => /* @__PURE__ */ jsx("div", { className: "pointer-events-auto", children: /* @__PURE__ */ jsx(
13546
+ shown.map((t) => /* @__PURE__ */ jsx("div", { "data-toast-id": t.id, className: "pointer-events-auto", children: /* @__PURE__ */ jsx(
14170
13547
  Toast,
14171
13548
  {
14172
13549
  inline: true,
@@ -15963,7 +15340,9 @@ function Dialog({ open, onClose: onCloseProp, title, description, children, foot
15963
15340
  "form",
15964
15341
  {
15965
15342
  onSubmit: (e) => {
15343
+ if (e.target !== e.currentTarget) return;
15966
15344
  e.preventDefault();
15345
+ e.stopPropagation();
15967
15346
  if (variant === "destructive") return;
15968
15347
  const submitter = e.nativeEvent.submitter;
15969
15348
  const implicit = !submitter || submitter === defaultButtonRef.current;
@@ -15992,6 +15371,7 @@ function Dialog({ open, onClose: onCloseProp, title, description, children, foot
15992
15371
  {
15993
15372
  ref: dialogRef,
15994
15373
  role: alert ? "alertdialog" : "dialog",
15374
+ ...{ [OVERLAY_ROOT_ATTR]: "" },
15995
15375
  "aria-modal": "true",
15996
15376
  "data-gs": "dialog",
15997
15377
  "aria-labelledby": titleId,
@@ -16032,7 +15412,7 @@ function Dialog({ open, onClose: onCloseProp, title, description, children, foot
16032
15412
  }
16033
15413
  ),
16034
15414
  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 }) }) })
15415
+ children && /* @__PURE__ */ jsx("div", { className: "mt-ds-4", children: /* @__PURE__ */ jsx(OverlayMarkScope, { mark, children: /* @__PURE__ */ jsx(OverlayCloseCtx, { value: onClose, children }) }) })
16036
15416
  ] })
16037
15417
  ] }) }),
16038
15418
  /* @__PURE__ */ jsx(DiscardConfirm, { open: guard.confirming, onKeep: guard.keep, onDiscard: guard.discard }),
@@ -16193,7 +15573,7 @@ var Navbar = forwardRef(function Navbar2({
16193
15573
  }
16194
15574
  focus.onExit?.();
16195
15575
  };
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;
15576
+ 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
15577
  const shownUserMenu = focus?.kind === "before-app" ? null : userMenu;
16198
15578
  const asking = confirmExit && focus?.kind === "in-app" && !!appShell?.hasUnsavedChanges;
16199
15579
  if (confirmExit && !asking) setConfirmExit(false);
@@ -16209,8 +15589,8 @@ var Navbar = forwardRef(function Navbar2({
16209
15589
  alert: true,
16210
15590
  container: shellEl ?? void 0,
16211
15591
  footer: /* @__PURE__ */ jsxs(Fragment, { children: [
16212
- /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: () => setConfirmExit(false), children: "Keep editing" }),
16213
- /* @__PURE__ */ jsx(Button, { variant: "destructive", onClick: () => {
15592
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", onClick: () => setConfirmExit(false), children: "Keep editing" }),
15593
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", onClick: () => {
16214
15594
  setConfirmExit(false);
16215
15595
  focus.onExit?.();
16216
15596
  }, children: "Discard changes" })
@@ -17327,67 +16707,6 @@ function getPopoverPosition(rect, placement, padding) {
17327
16707
  top = Math.max(8, Math.min(top, window.innerHeight - popH - 8));
17328
16708
  return { top, left };
17329
16709
  }
17330
- function firstKey(item) {
17331
- if (item !== null && typeof item === "object") return "id" in item ? item.id : void 0;
17332
- return item;
17333
- }
17334
- function signature(list, resetOn) {
17335
- let reset = "";
17336
- try {
17337
- reset = JSON.stringify(resetOn ?? null);
17338
- } catch {
17339
- reset = String(resetOn);
17340
- }
17341
- let first = "";
17342
- try {
17343
- first = JSON.stringify(firstKey(list[0]) ?? null);
17344
- } catch {
17345
- first = "";
17346
- }
17347
- return `${list.length}|${first}|${reset}`;
17348
- }
17349
- function usePagination(list, options = {}) {
17350
- const shell = useShellPreferences();
17351
- const style = options.style ?? shell.paginationStyle ?? PREFERENCE_HOUSE_DEFAULTS.paginationStyle;
17352
- const [chosenSize, setChosenSize] = useState(null);
17353
- const pageSize = Math.max(1, Math.floor(chosenSize ?? options.pageSize ?? shell.itemsPerPage ?? PREFERENCE_HOUSE_DEFAULTS.itemsPerPage));
17354
- const [page, setPageState] = useState(1);
17355
- const sig = signature(list, options.resetOn);
17356
- const [seen, setSeen] = useState(sig);
17357
- if (seen !== sig) {
17358
- setSeen(sig);
17359
- if (page !== 1) setPageState(1);
17360
- }
17361
- const totalItems = list.length;
17362
- const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
17363
- const current = Math.min(Math.max(1, seen !== sig ? 1 : page), totalPages);
17364
- const grows = style !== "pagination";
17365
- const start = grows ? 0 : (current - 1) * pageSize;
17366
- const items = list.slice(start, current * pageSize);
17367
- const range2 = items.length === 0 ? [0, 0] : [start + 1, start + items.length];
17368
- const hasMore = current < totalPages;
17369
- const setPage = (next) => setPageState(Math.min(Math.max(1, Math.floor(next)), totalPages));
17370
- const loadMore = () => setPageState((p) => Math.min(p + 1, totalPages));
17371
- const setPageSize = (size) => {
17372
- setChosenSize(Math.max(1, Math.floor(size)));
17373
- setPageState(1);
17374
- };
17375
- return {
17376
- style,
17377
- items,
17378
- page: current,
17379
- pageSize,
17380
- totalItems,
17381
- totalPages,
17382
- range: range2,
17383
- shown: items.length,
17384
- hasMore,
17385
- loadMore,
17386
- setPage,
17387
- setPageSize,
17388
- props: { currentPage: current, totalPages, onPageChange: setPage, pageSize, onPageSizeChange: setPageSize, mode: style, hasMore, onLoadMore: loadMore }
17389
- };
17390
- }
17391
16710
  function useDestructiveAction(options) {
17392
16711
  const { action, title, description, confirmLabel, typeToConfirm, onConfirm, onUndo, doneMessage, undoMessage, restoredMessage, severity } = options;
17393
16712
  const preferences = useShellPreferences();
@@ -17410,42 +16729,65 @@ function useDestructiveAction(options) {
17410
16729
  };
17411
16730
  const failureMessage = (err) => err instanceof Error && err.message ? err.message : `${title.replace(/\?$/, "")} failed. Try again.`;
17412
16731
  const done = doneMessage ?? undoMessage ?? pastTenseOf(label);
16732
+ const watch = async (step) => {
16733
+ const run2 = ledger?.begin();
16734
+ try {
16735
+ await step();
16736
+ } catch (err) {
16737
+ if (run2 !== void 0) ledger.end(run2);
16738
+ throw err;
16739
+ }
16740
+ return run2 !== void 0 ? ledger.end(run2) : { problem: false };
16741
+ };
17413
16742
  const confirm = async () => {
17414
16743
  setBusy(true);
17415
- const before = ledger?.created() ?? 0;
17416
16744
  try {
17417
- await onConfirm();
16745
+ const seen = await watch(onConfirm);
17418
16746
  close();
17419
- if (toast && (ledger?.created() ?? 0) === before) toast.success(done);
16747
+ if (toast && !seen.problem && !seen.own) toast.success(done);
17420
16748
  } catch (err) {
17421
16749
  toast?.error(failureMessage(err));
17422
16750
  } finally {
17423
16751
  setBusy(false);
17424
16752
  }
17425
16753
  };
17426
- const undo = async () => {
16754
+ const undo = async (id) => {
16755
+ const here = () => !!toast && (ledger?.has(id) ?? false);
17427
16756
  try {
17428
- await onUndo();
17429
- toast?.success(restoredMessage ?? "Restored");
16757
+ const seen = await watch(onUndo);
16758
+ if (seen.problem) {
16759
+ if (here()) toast.dismiss(id);
16760
+ } else if (seen.own) {
16761
+ if (here()) {
16762
+ toast.update(id, seen.own.message, { variant: seen.own.variant });
16763
+ toast.dismiss(seen.own.id);
16764
+ }
16765
+ } else if (here()) toast.update(id, restoredMessage ?? "Restored", { variant: "success" });
16766
+ else toast?.success(restoredMessage ?? "Restored");
17430
16767
  } catch (err) {
17431
- toast?.error(err instanceof Error && err.message ? err.message : "Undo didn't work. Try again.");
16768
+ const message = err instanceof Error && err.message ? err.message : "Undo didn't work. Try again.";
16769
+ if (here()) toast.update(id, message, { variant: "error" });
16770
+ else toast?.error(message);
17432
16771
  }
17433
16772
  };
17434
16773
  const run = () => {
17435
16774
  if (undoable) {
17436
16775
  void (async () => {
17437
- const before = ledger?.created() ?? 0;
16776
+ let seen;
17438
16777
  try {
17439
- await onConfirm();
16778
+ seen = await watch(onConfirm);
17440
16779
  } catch (err) {
17441
16780
  toast.error(failureMessage(err));
17442
16781
  return;
17443
16782
  }
16783
+ if (seen.problem) return;
17444
16784
  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 });
16785
+ let id = "";
16786
+ const undoAction = { label: "Undo", onClick: () => undo(id) };
16787
+ if (seen.own) {
16788
+ id = seen.own.id;
16789
+ toast.update(id, seen.own.message, { action: undoAction, actionWindow: seconds * 1e3 });
16790
+ } else id = toast.success(done, { action: undoAction, actionWindow: seconds * 1e3 });
17449
16791
  })();
17450
16792
  return;
17451
16793
  }
@@ -17461,8 +16803,8 @@ function useDestructiveAction(options) {
17461
16803
  description,
17462
16804
  variant: "destructive",
17463
16805
  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 })
16806
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", onClick: close, disabled: busy, children: "Cancel" }),
16807
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "destructive", onClick: () => void confirm(), disabled: !matches2 || busy, loading: busy, children: label })
17466
16808
  ] }),
17467
16809
  children: typedRequired && /* @__PURE__ */ jsx(Field, { label: /* @__PURE__ */ jsxs(Fragment, { children: [
17468
16810
  "Type ",
@@ -17668,6 +17010,6 @@ var TextOverflow = forwardRef(function TextOverflow2({
17668
17010
  return /* @__PURE__ */ jsx(Tooltip, { content: resolvedTooltipContent, children: content });
17669
17011
  });
17670
17012
 
17671
- export { Accordion, ActionPanel, ActionPanelGroup, AddAnother, Alert, AsidePanel, Avatar, AvatarGroup, BottomNav, Breadcrumbs, BulkActionBar, Calendar, Card, CardContent, CardDescription, CardFooter, CardGrid, CardHeader, CardTitle, Carousel, ChatMessageList, ChatPanel, Checkbox, CheckboxGroup, CheckboxGroupItem, Collapsible, ColorPicker, Combobox, CommandPalette, ContextMenu, CornerPanel, DEFAULT_DATE_RANGE_PRESETS, DataList, DatePicker, DateRangePicker, DescriptionList, Dialog, Dock, Dropdown, DropdownMenu, EmptyState, EntityCell, Field, FileUpload, FormWrapper, Gantt, HoverCard, InlineFieldError, Input, Kbd, Label, Launcher, MentionInput, MobileHeaderMenu, NavItem, NavMenu, Navbar, NotificationCenter, NumberInput, PageContainer, PageHeader, Pagination, PinInput, Popover, Progress, RadioGroup, Rating, ScrollArea, SectionHeading, SegmentedControl, SettingsLayout, Sidebar, SidebarNav, SliderRange, StackedList, StatsCard, StatusDot, Stepper, TIME_ZOOMS, Table, TableToolbar, Tabs, Tag, TagInput, TextOverflow, Textarea, TimeBar, TimePicker, TimeRow, TimeScaleHeader, TimeViewport, Timeline, Toast, ToastProvider, Toggle, Tour, TransferList, TreeView, dateOfDay, dayOf, endDayOf, fitLabel, fitLabels, formatDateRange, timeExtent, useContentWidth, useDestructiveAction, useFittedLabels, usePagination, useSidebar, useTextMeasure, useTimeScale, useTimeViewport, useTimeViewportLayout, useToast, useToastOptional };
17013
+ export { Accordion, ActionPanel, ActionPanelGroup, AddAnother, Alert, AsidePanel, Avatar, AvatarGroup, BottomNav, Breadcrumbs, BulkActionBar, Calendar, Card, CardContent, CardDescription, CardFooter, CardGrid, CardHeader, CardTitle, Carousel, ChatMessageList, ChatPanel, Checkbox, CheckboxGroup, CheckboxGroupItem, Collapsible, ColorPicker, Combobox, CommandPalette, ContextMenu, CornerPanel, DEFAULT_DATE_RANGE_PRESETS, DataList, DatePicker, DateRangePicker, DescriptionList, Dialog, Dock, DropdownMenu, EmptyState, EntityCell, FileUpload, FormWrapper, Gantt, HoverCard, Kbd, Launcher, MentionInput, MobileHeaderMenu, NavItem, NavMenu, Navbar, NotificationCenter, PageContainer, PageHeader, PinInput, Progress, RadioGroup, Rating, ScrollArea, SectionHeading, SegmentedControl, SettingsLayout, Sidebar, SidebarNav, SliderRange, StackedList, StatsCard, StatusDot, Stepper, TIME_ZOOMS, Table, TableToolbar, Tabs, Tag, TagInput, TextOverflow, TimeBar, TimePicker, TimeRow, TimeScaleHeader, TimeViewport, Timeline, Toast, ToastProvider, Tour, TransferList, TreeView, dateOfDay, dayOf, endDayOf, fitLabel, fitLabels, formatDateRange, timeExtent, useContentWidth, useDestructiveAction, useFittedLabels, useSidebar, useTextMeasure, useTimeScale, useTimeViewport, useTimeViewportLayout, useToast, useToastOptional };
17672
17014
  //# sourceMappingURL=index.js.map
17673
17015
  //# sourceMappingURL=index.js.map