@flood-md/react 0.2.10 → 0.2.12

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 (57) hide show
  1. package/README.md +275 -5
  2. package/dist/app-shell.d.ts +3 -0
  3. package/dist/app-shell.js +66 -0
  4. package/dist/bulk-action-bar.d.ts +3 -0
  5. package/dist/bulk-action-bar.js +17 -0
  6. package/dist/collection-toolbar.d.ts +3 -0
  7. package/dist/collection-toolbar.js +26 -0
  8. package/dist/collection-view-switcher.d.ts +3 -0
  9. package/dist/collection-view-switcher.js +19 -0
  10. package/dist/combobox.d.ts +6 -1
  11. package/dist/combobox.js +77 -29
  12. package/dist/command-list.d.ts +5 -1
  13. package/dist/command-list.js +2 -2
  14. package/dist/data-display.js +28 -4
  15. package/dist/data-table-columns.d.ts +25 -0
  16. package/dist/data-table-columns.js +271 -0
  17. package/dist/data-table-expansion.d.ts +21 -0
  18. package/dist/data-table-expansion.js +79 -0
  19. package/dist/data-table.js +140 -12
  20. package/dist/date-picker.js +2 -2
  21. package/dist/date-range-picker.js +2 -2
  22. package/dist/date-time-picker.js +2 -2
  23. package/dist/date-time-range-picker.js +2 -2
  24. package/dist/disclosure.d.ts +2 -1
  25. package/dist/disclosure.js +4 -4
  26. package/dist/editable.js +57 -18
  27. package/dist/filter-condition.d.ts +3 -0
  28. package/dist/filter-condition.js +21 -0
  29. package/dist/filter-group.d.ts +2 -0
  30. package/dist/filter-group.js +62 -0
  31. package/dist/foundations.css +1131 -93
  32. package/dist/index.d.ts +469 -5
  33. package/dist/index.js +72 -9
  34. package/dist/menu.js +5 -4
  35. package/dist/menubar.d.ts +2 -0
  36. package/dist/menubar.js +85 -0
  37. package/dist/navigation-group.d.ts +3 -0
  38. package/dist/navigation-group.js +13 -0
  39. package/dist/navigation-item.d.ts +3 -0
  40. package/dist/navigation-item.js +68 -0
  41. package/dist/navigation-layout.d.ts +2 -1
  42. package/dist/navigation-layout.js +12 -4
  43. package/dist/navigation-tree.js +51 -7
  44. package/dist/navigation.js +2 -2
  45. package/dist/overflow-actions.d.ts +2 -0
  46. package/dist/overflow-actions.js +75 -0
  47. package/dist/overlay-runtime.d.ts +6 -2
  48. package/dist/overlay-runtime.js +41 -5
  49. package/dist/overlays.d.ts +1 -0
  50. package/dist/overlays.js +34 -18
  51. package/dist/select.js +4 -1
  52. package/dist/sidebar.d.ts +2 -0
  53. package/dist/sidebar.js +58 -0
  54. package/dist/sparkline.d.ts +2 -0
  55. package/dist/sparkline.js +65 -0
  56. package/dist/toolbar.js +2 -2
  57. package/package.json +2 -2
package/dist/combobox.js CHANGED
@@ -1,39 +1,68 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
3
- import { Input, InputGroup, IconButton, ScrollArea, Tag } from "./index.js";
3
+ import { Input, InputGroup, IconButton, ScrollArea, Tag, Button } from "./index.js";
4
4
  import { mergeDescriptions, resolveFieldStatus } from "./field-state.js";
5
5
  import { positionLayer, registerLayer } from "./overlay-runtime.js";
6
6
  const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
7
7
  const searchable = (text) => text.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase();
8
8
  /** Shared search/layer model. Public single and multiple value contracts stay distinct. */
9
- export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelection({ options, selectedValues: value, initialValues: defaultValue, onSelectionChange: onValueChange, multiple = false, removeItemLabel = "Remove", clearable = false, emptyMessage = "No results found.", listLabel = "Options", toggleLabel = "Show options", clearLabel = "Clear selection", invalidSelectionMessage = "Select an option.", controlSize = "md", readOnly, disabled, required, name, id: explicitId, className, onKeyDown, onBlur, onClick, onInvalid, autoComplete = "off", ...props }, forwardedRef) {
9
+ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelection({ options, selectedOptions = [], query: controlledQuery, onQueryChange, filtering = "local", optionsQuery, loadState = "ready", loadingMessage = "Loading options…", errorMessage = "Could not load options.", onRetry, retryLabel = "Try again", freeText = false, textValue, initialText = "", onTextChange, onOptionSelect, selectedValues: value, initialValues: defaultValue, onSelectionChange: onValueChange, multiple = false, removeItemLabel = "Remove", clearable = false, emptyMessage = "No results found.", listLabel = "Options", toggleLabel = "Show options", clearLabel = "Clear selection", invalidSelectionMessage = "Select an option.", controlSize = "md", readOnly, disabled, required, name, id: explicitId, className, onKeyDown, onBlur, onClick, onInvalid, autoComplete = "off", ...props }, forwardedRef) {
10
10
  if (new Set(options.map(option => option.value)).size !== options.length || options.some(option => !option.value || !option.label.trim()))
11
11
  throw new Error("Flood Combobox options need unique nonempty values and labels");
12
- if (![emptyMessage, listLabel, toggleLabel, clearLabel, invalidSelectionMessage].every(text => text.trim()))
12
+ if (![emptyMessage, listLabel, toggleLabel, clearLabel, invalidSelectionMessage, loadingMessage, errorMessage, retryLabel].every(text => text.trim()))
13
13
  throw new Error("Flood Combobox messages and action labels must be nonempty");
14
+ if (new Set(selectedOptions.map(option => option.value)).size !== selectedOptions.length || selectedOptions.some(option => !option.value || !option.label.trim()))
15
+ throw new Error("Flood selectedOptions requires unique named records");
14
16
  for (const values of [defaultValue, value])
15
17
  if (values !== undefined && (!Array.isArray(values) || new Set(values).size !== values.length || values.some(item => typeof item !== "string" || !item)))
16
18
  throw new Error("Flood selection values must be unique nonempty strings");
17
19
  if (multiple && !removeItemLabel.trim())
18
20
  throw new Error("Flood MultiSelect requires an accessible removal label");
21
+ if (filtering === "manual" && ((freeText ? textValue : controlledQuery) === undefined || optionsQuery === undefined))
22
+ throw new Error("Flood manual search requires controlled text/query and optionsQuery");
19
23
  const generatedId = useId(), id = explicitId ?? `flood-combobox-${generatedId}`;
20
24
  const menuId = `${id}-options`, validationId = `${id}-validation`;
21
25
  const input = useRef(null), root = useRef(null), panel = useRef(null), viewport = useRef(null);
22
26
  useImperativeHandle(forwardedRef, () => input.current, []);
23
- const initial = useRef([...defaultValue]);
24
- const [local, setLocal] = useState(() => [...defaultValue]), [query, setQuery] = useState(null);
27
+ const initial = useRef([...defaultValue]), initialFreeText = useRef(initialText);
28
+ const [local, setLocal] = useState(() => [...defaultValue]), [draftQuery, setDraftQuery] = useState(null);
29
+ const [localText, setLocalText] = useState(initialText);
30
+ const text = textValue ?? localText;
31
+ const query = freeText ? text : draftQuery === null ? null : controlledQuery ?? draftQuery;
32
+ const setQuery = (next) => { setDraftQuery(next); if (!freeText && next !== null)
33
+ onQueryChange?.(next); };
34
+ const publishText = (next) => { if (textValue === undefined)
35
+ setLocalText(next); if (next !== text)
36
+ onTextChange?.(next); };
25
37
  const [open, setOpen] = useState(false), [activeValue, setActiveValue] = useState();
26
38
  const [validation, setValidation] = useState("");
27
- const selections = (value ?? local).map(id => options.find(option => option.value === id)).filter((option) => Boolean(option));
39
+ const selections = (value ?? local).map(id => selectedOptions.find(option => option.value === id) ?? options.find(option => option.value === id)).filter((option) => Boolean(option));
28
40
  const selected = selections[0], selectedIds = selections.map(option => option.value);
29
- const displayed = query ?? (multiple ? "" : selected?.label ?? "");
30
- const filtered = query === null ? options : options.filter(option => searchable([option.label, ...(option.keywords ?? [])].join(" ")).includes(searchable(query)));
41
+ const displayed = freeText ? text : query ?? (multiple ? "" : selected?.label ?? "");
42
+ const resultQuery = freeText ? text : controlledQuery ?? draftQuery ?? "";
43
+ const pending = loadState === "loading" || (filtering === "manual" && optionsQuery !== resultQuery);
44
+ const failed = loadState === "error" && !pending;
45
+ const resultMessage = pending ? loadingMessage : failed ? errorMessage : emptyMessage;
46
+ const filtered = pending || failed ? [] : filtering === "manual" || query === null ? options : options.filter(option => searchable([option.label, ...(option.keywords ?? [])].join(" ")).includes(searchable(query)));
31
47
  const enabled = filtered.filter(option => !option.disabled);
48
+ const groups = [];
49
+ for (const option of filtered) {
50
+ const previous = groups.at(-1);
51
+ if (previous && previous.label === option.group)
52
+ previous.options.push(option);
53
+ else
54
+ groups.push({ label: option.group, options: [option] });
55
+ }
32
56
  const active = enabled.find(option => option.value === activeValue) ?? enabled[0];
33
57
  const optionId = (optionValue) => `${menuId}-${options.findIndex(option => option.value === optionValue)}`;
34
- const current = useRef({ value, selected, readOnly, disabled });
35
- current.current = { value, selected, readOnly, disabled };
36
- const close = () => { setOpen(false); setQuery(null); };
58
+ const current = useRef({ value, textValue, selected, readOnly, disabled });
59
+ current.current = { value, textValue, selected, readOnly, disabled };
60
+ const close = () => {
61
+ if (panel.current?.contains(panel.current.ownerDocument.activeElement))
62
+ input.current?.focus({ preventScroll: true });
63
+ setOpen(false);
64
+ setQuery(null);
65
+ };
37
66
  const inert = () => Boolean(readOnly || disabled || input.current?.matches(":disabled"));
38
67
  const publish = (next) => {
39
68
  if (value === undefined)
@@ -43,14 +72,23 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
43
72
  setValidation("");
44
73
  };
45
74
  const remove = (id) => {
46
- if (inert() || options.find(option => option.value === id)?.disabled)
75
+ if (inert() || selections.find(option => option.value === id)?.disabled)
47
76
  return;
48
77
  publish(selectedIds.filter(value => value !== id));
49
78
  input.current?.focus({ preventScroll: true });
50
79
  };
51
80
  const select = (next) => {
52
- if (inert() || (next !== "" && !options.some(option => option.value === next && !option.disabled)))
81
+ if (inert() || (next !== "" && !filtered.some(option => option.value === next && !option.disabled)))
53
82
  return;
83
+ if (freeText) {
84
+ const option = filtered.find(option => option.value === next);
85
+ publishText(option?.label ?? "");
86
+ if (option)
87
+ onOptionSelect?.(option);
88
+ close();
89
+ input.current?.focus({ preventScroll: true });
90
+ return;
91
+ }
54
92
  if (multiple) {
55
93
  publish(next === "" ? selections.filter(option => option.disabled).map(option => option.value) : selectedIds.includes(next) ? selectedIds.filter(id => id !== next) : [...selectedIds, next]);
56
94
  if (next) {
@@ -75,7 +113,7 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
75
113
  };
76
114
  useBrowserLayoutEffect(() => {
77
115
  const control = input.current;
78
- control?.setCustomValidity(required && !selections.length ? invalidSelectionMessage : "");
116
+ control?.setCustomValidity(!freeText && required && !selections.length ? invalidSelectionMessage : "");
79
117
  if (validation && control?.validity.valid)
80
118
  setValidation("");
81
119
  });
@@ -92,6 +130,8 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
92
130
  return;
93
131
  if (current.current.value === undefined)
94
132
  setLocal(initial.current);
133
+ if (current.current.textValue === undefined)
134
+ setLocalText(initialFreeText.current);
95
135
  setQuery(null);
96
136
  setOpen(false);
97
137
  setValidation("");
@@ -167,7 +207,7 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
167
207
  popup.style.visibility = "hidden";
168
208
  popup.showPopover();
169
209
  update();
170
- unregister = registerLayer(document, { panel: popup, anchor: () => anchor, close: () => { setOpen(false); setQuery(null); } });
210
+ unregister = registerLayer(document, { panel: popup, anchor: () => anchor, close });
171
211
  };
172
212
  const toggle = (event) => { if (alive && event.newState === "closed" && !popup.matches(":popover-open")) {
173
213
  setOpen(false);
@@ -192,7 +232,8 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
192
232
  }, [open, active?.value, query, options]);
193
233
  const glyph = (clear) => _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: clear ? _jsx("path", { d: "m6 6 12 12M18 6 6 18" }) : _jsx("path", { d: "m6 9 6 6 6-6" }) });
194
234
  const inputProps = {
195
- ...props, id, type: "text", value: displayed, disabled, readOnly, required: required && (!multiple || !selections.length), controlSize, autoComplete,
235
+ ...props, id, type: "text", value: displayed, placeholder: multiple && selections.length ? undefined : props.placeholder,
236
+ name: freeText ? name : undefined, disabled, readOnly, required: required && (!multiple || !selections.length), controlSize, autoComplete,
196
237
  role: "combobox", "aria-autocomplete": "list", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? menuId : undefined,
197
238
  "aria-required": required || undefined, "aria-activedescendant": open && active ? optionId(active.value) : undefined,
198
239
  "aria-describedby": mergeDescriptions(props["aria-describedby"], validation && validationId, multiple && Boolean(selections.length) && `${id}-selection`),
@@ -201,11 +242,14 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
201
242
  if (inert())
202
243
  return;
203
244
  const text = event.currentTarget.value;
204
- setQuery(text);
245
+ if (freeText)
246
+ publishText(text);
247
+ else
248
+ setQuery(text);
205
249
  setActiveValue(undefined);
206
250
  setOpen(true);
207
251
  setValidation("");
208
- if (!multiple && text === "" && selected)
252
+ if (!freeText && !multiple && text === "" && selected)
209
253
  publish([]);
210
254
  },
211
255
  onClick: event => { onClick?.(event); if (!event.defaultPrevented)
@@ -255,25 +299,28 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
255
299
  }
256
300
  }
257
301
  else if (event.key === "Enter" && open) {
258
- event.preventDefault();
302
+ if (!freeText || active)
303
+ event.preventDefault();
259
304
  if (active)
260
305
  select(active.value);
306
+ else
307
+ close();
261
308
  }
262
309
  else if (event.key === "Escape" && open) {
263
310
  event.preventDefault();
264
311
  event.stopPropagation();
265
312
  close();
266
313
  }
267
- else if (event.key === "Tab")
314
+ else if (event.key === "Tab" && !(failed && onRetry && !event.shiftKey))
268
315
  close();
269
316
  },
270
317
  };
271
318
  const removable = selections.some(option => !option.disabled);
272
- const actions = _jsxs("span", { className: "flood-combobox__actions", children: [clearable && _jsx(IconButton, { label: clearLabel, variant: "ghost", size: controlSize, className: !selected ? "flood-combobox__clear-empty" : undefined, disabled: disabled || readOnly || !selected || (multiple && !removable), "aria-hidden": !selected || undefined, tabIndex: -1, onPointerDown: event => event.preventDefault(), onClick: () => select(""), children: glyph(true) }), _jsx(IconButton, { label: toggleLabel, variant: "ghost", size: controlSize, disabled: disabled || readOnly, tabIndex: -1, "aria-expanded": open, "aria-controls": open ? menuId : undefined, onPointerDown: event => event.preventDefault(), onClick: () => { if (open)
319
+ const actions = _jsxs("span", { className: "flood-combobox__actions", children: [clearable && _jsx(IconButton, { label: clearLabel, variant: "ghost", size: controlSize, className: !(freeText ? text : selected) ? "flood-combobox__clear-empty" : undefined, disabled: disabled || readOnly || !(freeText ? text : selected) || (multiple && !removable), "aria-hidden": !(freeText ? text : selected) || undefined, tabIndex: -1, onPointerDown: event => event.preventDefault(), onClick: () => select(""), children: glyph(true) }), _jsx(IconButton, { label: toggleLabel, variant: "ghost", size: controlSize, disabled: disabled || readOnly, tabIndex: -1, "aria-expanded": open, "aria-controls": open ? menuId : undefined, onPointerDown: event => event.preventDefault(), onClick: () => { if (open)
273
320
  close();
274
321
  else
275
322
  show(); input.current?.focus({ preventScroll: true }); }, children: glyph(false) })] });
276
- return _jsxs("div", { className: multiple ? "flood-combobox flood-multi-select" : "flood-combobox", ref: root, hidden: props.hidden, dir: props.dir, lang: props.lang, children: [multiple ? _jsxs("div", { className: ["flood-input-group", "flood-multi-select__control", `flood-control--${controlSize}`, className].filter(Boolean).join(" "), style: props.style, "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, "data-status": resolveFieldStatus(props.status, props.invalid || Boolean(validation), props["aria-invalid"]), onClick: event => { if (event.target === event.currentTarget && !inert()) {
323
+ return _jsxs("div", { className: multiple ? "flood-combobox flood-multi-select" : "flood-combobox", ref: root, hidden: props.hidden, dir: props.dir, lang: props.lang, children: [multiple ? _jsxs("div", { className: ["flood-input-group", "flood-multi-select__control", `flood-control--${controlSize}`, className].filter(Boolean).join(" "), style: props.style, "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, "data-status": resolveFieldStatus(props.status, props.invalid || Boolean(validation), props["aria-invalid"]), "data-has-selection": Boolean(selections.length) || undefined, "data-query": Boolean(displayed) || undefined, "data-clearable": clearable || undefined, onClick: event => { if (event.target === event.currentTarget && !inert()) {
277
324
  input.current?.focus();
278
325
  show();
279
326
  } }, children: [selections.map(option => _jsx(Tag, { size: controlSize === "sm" ? "sm" : controlSize === "md" ? "md" : "lg", disabled: disabled || option.disabled, onRemove: readOnly ? undefined : () => remove(option.value), removeLabel: `${removeItemLabel} ${option.label}`, removeButtonProps: { tabIndex: -1, onKeyDown: event => {
@@ -300,11 +347,12 @@ export const SearchSelection = /* @__PURE__ */ forwardRef(function SearchSelecti
300
347
  event.preventDefault();
301
348
  (buttons[Math.max(0, index - 1)])?.focus();
302
349
  }
303
- } }, children: option.label }, option.value)), _jsxs("div", { className: "flood-multi-select__editor", children: [_jsx(Input, { ...inputProps, ref: input, style: undefined }), actions] })] }) : _jsx(InputGroup, { ...inputProps, ref: input, className: ["flood-combobox__control", className].filter(Boolean).join(" "), end: actions }), (multiple ? selections : [selected]).map((option, index) => _jsx("input", { type: "hidden", name: name, value: option?.value ?? "", disabled: disabled, form: props.form }, option?.value ?? index)), multiple && Boolean(selections.length) && _jsx("span", { id: `${id}-selection`, className: "flood-visually-hidden", children: selections.map(option => option.label).join(", ") }), open && _jsx("div", { ref: panel, popover: "manual", className: "flood-select__menu flood-combobox__menu", onPointerDown: event => { if (event.pointerType === "mouse")
304
- event.preventDefault(); }, children: _jsxs(ScrollArea, { ref: viewport, id: menuId, role: "listbox", "aria-label": listLabel, "aria-multiselectable": multiple || undefined, orientation: "vertical", tabIndex: -1, className: "flood-select__viewport", children: [!filtered.length && _jsx("div", { className: "flood-select__empty", role: "option", "aria-disabled": "true", "aria-selected": "false", children: emptyMessage }), filtered.map(option => _jsxs("div", { id: optionId(option.value), role: "option", "aria-selected": selectedIds.includes(option.value), "aria-disabled": option.disabled || undefined, className: "flood-select__option", "data-active": option.value === active?.value || undefined, onPointerMove: event => { if (event.pointerType === "mouse" && !option.disabled)
305
- setActiveValue(option.value); }, onPointerDown: event => {
306
- // Touch must retain its compatibility click and native scrolling.
307
- if (event.pointerType === "mouse")
308
- event.preventDefault();
309
- }, onMouseDown: event => event.preventDefault(), onClick: () => select(option.value), children: [_jsxs("span", { className: "flood-combobox__option-text", children: [_jsx("span", { children: option.label }), option.description && _jsx("span", { className: "flood-combobox__description", children: option.description })] }), _jsx("span", { className: "flood-select__selected", "aria-hidden": "true", children: selectedIds.includes(option.value) ? "✓" : "" })] }, option.value))] }) }), open && !filtered.length && _jsx("span", { className: "flood-visually-hidden", role: "status", children: emptyMessage }), validation && _jsx("span", { id: validationId, className: "flood-select__validation", role: "alert", children: validation })] });
350
+ } }, children: option.label }, option.value)), _jsxs("div", { className: "flood-multi-select__editor", children: [_jsx(Input, { ...inputProps, ref: input, style: undefined }), actions] })] }) : _jsx(InputGroup, { ...inputProps, ref: input, className: ["flood-combobox__control", className].filter(Boolean).join(" "), end: actions }), !freeText && (multiple ? selections : [selected]).map((option, index) => _jsx("input", { type: "hidden", name: name, value: option?.value ?? "", disabled: disabled, form: props.form }, option?.value ?? index)), multiple && Boolean(selections.length) && _jsx("span", { id: `${id}-selection`, className: "flood-visually-hidden", children: selections.map(option => option.label).join(", ") }), open && _jsxs("div", { ref: panel, popover: "manual", className: "flood-select__menu flood-combobox__menu", onPointerDown: event => { if (event.pointerType === "mouse")
351
+ event.preventDefault(); }, children: [_jsxs(ScrollArea, { ref: viewport, id: menuId, role: "listbox", "aria-label": listLabel, "aria-busy": pending || undefined, "aria-multiselectable": multiple || undefined, orientation: "vertical", tabIndex: -1, className: "flood-select__viewport", children: [!filtered.length && _jsx("div", { className: "flood-select__empty", role: "option", "aria-disabled": "true", "aria-selected": "false", children: resultMessage }), groups.map((group, index) => _jsxs("div", { role: group.label ? "group" : "presentation", "aria-label": group.label, children: [group.label && _jsx("div", { className: "flood-select__group", "aria-hidden": "true", children: group.label }), group.options.map(option => _jsxs("div", { id: optionId(option.value), role: "option", "aria-selected": selectedIds.includes(option.value), "aria-disabled": option.disabled || undefined, className: "flood-select__option", "data-active": option.value === active?.value || undefined, onPointerMove: event => { if (event.pointerType === "mouse" && !option.disabled)
352
+ setActiveValue(option.value); }, onPointerDown: event => {
353
+ // Touch must retain its compatibility click and native scrolling.
354
+ if (event.pointerType === "mouse")
355
+ event.preventDefault();
356
+ }, onMouseDown: event => event.preventDefault(), onClick: () => select(option.value), children: [_jsxs("span", { className: "flood-combobox__option-text", children: [_jsx("span", { children: option.label }), option.description && _jsx("span", { className: "flood-combobox__description", children: option.description })] }), _jsx("span", { className: "flood-select__selected", "aria-hidden": "true", children: selectedIds.includes(option.value) ? "✓" : "" })] }, option.value))] }, index))] }), failed && onRetry && _jsx(Button, { variant: "subtle", size: "sm", onClick: () => { input.current?.focus({ preventScroll: true }); onRetry(); }, onBlur: event => { if (!root.current?.contains(event.relatedTarget))
357
+ close(); }, children: retryLabel })] }), open && !filtered.length && _jsx("span", { className: "flood-visually-hidden", role: "status", children: resultMessage }), validation && _jsx("span", { id: validationId, className: "flood-select__validation", role: "alert", children: validation })] });
310
358
  });
@@ -1,3 +1,7 @@
1
1
  import { type CommandListProps } from "./index.js";
2
+ interface SearchCommandListProps extends CommandListProps {
3
+ integrated?: boolean;
4
+ }
2
5
  /** Searchable action suggestions. No selected form value or modal lifecycle. */
3
- export declare const SearchCommandList: import("react").ForwardRefExoticComponent<CommandListProps & import("react").RefAttributes<HTMLInputElement>>;
6
+ export declare const SearchCommandList: import("react").ForwardRefExoticComponent<SearchCommandListProps & import("react").RefAttributes<HTMLInputElement>>;
7
+ export {};
@@ -3,7 +3,7 @@ import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } f
3
3
  import { Input, Label, ScrollArea, Surface, Text, VisuallyHidden } from "./index.js";
4
4
  const searchable = (value) => value.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase();
5
5
  /** Searchable action suggestions. No selected form value or modal lifecycle. */
6
- export const SearchCommandList = /* @__PURE__ */ forwardRef(function SearchCommandList({ label, hideLabel = false, items, query, defaultQuery = "", onQueryChange, onAction, disabled = false, loading = false, placeholder = "Search commands…", emptyMessage = "No commands found.", loadingMessage = "Loading commands…", controlSize = "md", id: explicitId, className, onKeyDown, ...props }, forwardedRef) {
6
+ export const SearchCommandList = /* @__PURE__ */ forwardRef(function SearchCommandList({ label, hideLabel = false, items, query, defaultQuery = "", onQueryChange, onAction, disabled = false, loading = false, placeholder = "Search commands…", emptyMessage = "No commands found.", loadingMessage = "Loading commands…", controlSize = "md", integrated = false, id: explicitId, className, onKeyDown, ...props }, forwardedRef) {
7
7
  if (![label, emptyMessage, loadingMessage].every(value => value.trim()))
8
8
  throw new Error("Flood CommandList requires nonempty accessible labels and state messages");
9
9
  if (new Set(items.map(item => item.id)).size !== items.length || items.some(item => !item.id.trim() || !item.label.trim()))
@@ -84,7 +84,7 @@ export const SearchCommandList = /* @__PURE__ */ forwardRef(function SearchComma
84
84
  setActiveId(enabled[next].id);
85
85
  }, children: [_jsx(Label, { htmlFor: inputId, className: hideLabel ? "flood-visually-hidden" : undefined, children: label }), _jsx(Input, { ref: input, id: inputId, value: text, disabled: disabled, type: "text", controlSize: controlSize, placeholder: placeholder, autoComplete: "off", spellCheck: false, role: "combobox", "aria-autocomplete": "list", "aria-haspopup": "listbox", "aria-expanded": "true", "aria-controls": listId, "aria-activedescendant": active ? optionId(active) : undefined, onCompositionStart: () => { composing.current = true; }, onCompositionEnd: () => { composing.current = false; }, onChange: event => { const next = event.currentTarget.value; if (disabled || event.currentTarget.matches(":disabled"))
86
86
  return; if (query === undefined)
87
- setLocalQuery(next); onQueryChange?.(next); } }), _jsx(Surface, { padding: "1", className: "flood-command-list__surface", children: _jsx(ScrollArea, { ref: viewport, id: listId, role: "listbox", "aria-label": label, "aria-busy": loading || undefined, "aria-disabled": effectiveDisabled || undefined, tabIndex: -1, className: "flood-command-list__viewport", style: { maxHeight: "18rem" }, children: status ? _jsx("div", { role: "option", "aria-disabled": "true", "aria-selected": "false", className: "flood-command-list__empty", children: _jsx(Text, { size: "compact", tone: "subtle", children: status }) }) : [...groups].map(([group, entries], groupIndex) => {
87
+ setLocalQuery(next); onQueryChange?.(next); } }), _jsx(Surface, { elevation: integrated ? "overlay" : "default", padding: integrated ? "2" : "1", style: integrated ? { background: "transparent" } : undefined, className: "flood-command-list__surface", children: _jsx(ScrollArea, { ref: viewport, id: listId, role: "listbox", "aria-label": label, "aria-busy": loading || undefined, "aria-disabled": effectiveDisabled || undefined, tabIndex: -1, className: "flood-command-list__viewport", children: status ? _jsx("div", { role: "option", "aria-disabled": "true", "aria-selected": "false", className: "flood-command-list__empty", children: _jsx(Text, { size: "compact", tone: "subtle", children: status }) }) : [...groups].map(([group, entries], groupIndex) => {
88
88
  const content = entries.map(item => _jsxs("div", { id: optionId(item), role: "option", "aria-disabled": item.disabled || effectiveDisabled || undefined, "aria-selected": item.id === active?.id, "data-active": item.id === active?.id || undefined, className: "flood-command-list__option", onPointerMove: event => { if (event.pointerType === "mouse" && !blocked() && !item.disabled)
89
89
  setActiveId(item.id); }, onPointerDown: event => { if (event.pointerType === "mouse")
90
90
  event.preventDefault(); }, onMouseDown: event => event.preventDefault(), onClick: () => invoke(item), children: [_jsx("span", { className: "flood-command-list__label", children: item.label }), item.description && _jsx("span", { className: "flood-command-list__description", children: item.description })] }, item.id));
@@ -1,11 +1,35 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef } from "react";
2
+ import { forwardRef, useEffect, useRef } from "react";
3
3
  import { ScrollArea } from "./index.js";
4
4
  export const DescribedData = /* @__PURE__ */ forwardRef(function DescribedData({ items, layout, density, dividers, className, ...props }, ref) {
5
- if (items.some(item => !item.id.trim()) || new Set(items.map(item => item.id)).size !== items.length)
5
+ if (items.some(item => typeof item.id !== "string" || !item.id.trim()) || new Set(items.map(item => item.id)).size !== items.length)
6
6
  throw new Error("Flood description items require unique nonempty IDs");
7
7
  return _jsx("dl", { ...props, ref: ref, className: ["flood-description-list", className].filter(Boolean).join(" "), "data-layout": layout, "data-density": density, "data-dividers": dividers, children: items.map(item => _jsxs("div", { className: "flood-description-list__item", children: [_jsx("dt", { children: item.term }), _jsx("dd", { children: item.description })] }, item.id)) });
8
8
  });
9
- export const TabularData = /* @__PURE__ */ forwardRef(function TabularData({ caption, children, hideCaption, density, striped, scrollLabel, cellVerticalAlign, className, hidden, dir, lang, ...props }, ref) {
10
- return _jsx("div", { className: "flood-table-container", hidden: hidden, dir: dir, lang: lang, children: _jsx(ScrollArea, { orientation: "horizontal", "aria-label": scrollLabel ?? caption, children: _jsxs("table", { ...props, ref: ref, hidden: hidden, dir: dir, lang: lang, className: ["flood-table", className].filter(Boolean).join(" "), "data-density": density, "data-striped": striped, "data-cell-vertical-align": cellVerticalAlign, children: [_jsx("caption", { className: hideCaption ? "flood-visually-hidden" : undefined, children: caption }), children] }) }) });
9
+ export const TabularData = /* @__PURE__ */ forwardRef(function TabularData({ caption, children, hideCaption, density, striped, scrollLabel, cellVerticalAlign, maxViewportHeight, stickyHeader, className, hidden, dir, lang, ...props }, ref) {
10
+ const viewport = useRef(null);
11
+ const scrollVertically = maxViewportHeight !== undefined;
12
+ const pinHeader = scrollVertically && stickyHeader;
13
+ useEffect(() => {
14
+ const node = viewport.current;
15
+ const header = node?.querySelector("table")?.tHead;
16
+ if (!pinHeader || !node || !header)
17
+ return;
18
+ const update = () => { node.style.scrollPaddingBlockStart = `${header.offsetHeight}px`; };
19
+ update();
20
+ const observer = new ResizeObserver(update);
21
+ observer.observe(header);
22
+ return () => {
23
+ observer.disconnect();
24
+ node.style.removeProperty("scroll-padding-block-start");
25
+ };
26
+ }, [pinHeader, children]);
27
+ return _jsx("div", { className: "flood-table-container", hidden: hidden, dir: dir, lang: lang, children: _jsx(ScrollArea, { ref: viewport, orientation: scrollVertically ? "both" : "horizontal", contentInset: scrollVertically ? "scrollbar" : undefined, tabIndex: scrollVertically ? 0 : undefined, style: scrollVertically ? { maxBlockSize: maxViewportHeight } : undefined, "aria-label": scrollLabel ?? caption, onFocus: pinHeader ? event => {
28
+ const target = event.target;
29
+ const header = event.currentTarget.querySelector("table")?.tHead;
30
+ if (header && target !== event.currentTarget && !header.contains(target) &&
31
+ !target.closest("[popover], dialog[open]") && target.getBoundingClientRect().top < header.getBoundingClientRect().bottom) {
32
+ target.scrollIntoView({ block: "nearest", inline: "nearest" });
33
+ }
34
+ } : undefined, children: _jsxs("table", { ...props, ref: ref, hidden: hidden, dir: dir, lang: lang, className: ["flood-table", className].filter(Boolean).join(" "), "data-density": density, "data-striped": striped, "data-cell-vertical-align": cellVerticalAlign, "data-sticky-header": pinHeader || undefined, children: [_jsx("caption", { className: hideCaption ? "flood-visually-hidden" : undefined, children: caption }), children] }) }) });
11
35
  });
@@ -0,0 +1,25 @@
1
+ import { type CSSProperties } from "react";
2
+ import { type DataTableColumn, type DataTableProps } from "./index.js";
3
+ export declare function useDataTableColumnLayout(columns: readonly DataTableColumn[], visibleColumns: readonly DataTableColumn[], { columnWidths, defaultColumnWidths, onColumnWidthsChange, measureViewport, }: Pick<DataTableProps, "columnWidths" | "defaultColumnWidths" | "onColumnWidthsChange"> & {
4
+ measureViewport?: boolean;
5
+ }): {
6
+ table: import("react").RefObject<HTMLTableElement | null>;
7
+ fixedWidth: number | undefined;
8
+ viewportWidth: number | undefined;
9
+ disclosureWidth: number | undefined;
10
+ atDefaults: boolean;
11
+ reset: () => void;
12
+ utilityCell: (id: string, header?: boolean) => {
13
+ "data-pin-edge": "end" | "start" | undefined;
14
+ "data-pin-boundary": true | undefined;
15
+ style: CSSProperties | undefined;
16
+ "data-data-table-utility": string;
17
+ };
18
+ columnCell: (column: DataTableColumn, header?: boolean) => {
19
+ "data-pin-edge": "end" | "start" | undefined;
20
+ "data-pin-boundary": true | undefined;
21
+ style: CSSProperties | undefined;
22
+ "data-data-table-column": string;
23
+ };
24
+ renderHandle: (column: DataTableColumn, label: string, disabled: boolean) => false | import("react").JSX.Element | undefined;
25
+ };
@@ -0,0 +1,271 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ import { IconSlot } from "./index.js";
4
+ const defaultMinWidth = 64;
5
+ const defaultMaxWidth = 960;
6
+ const columnKey = (id) => `column:${id}`;
7
+ const utilityKey = (id) => `utility:${id}`;
8
+ const validWidth = (value) => typeof value === "number" && Number.isFinite(value) && value > 0;
9
+ function bounds(column) {
10
+ const min = validWidth(column.minWidth) ? Math.ceil(column.minWidth) : defaultMinWidth;
11
+ const max = validWidth(column.maxWidth) ? Math.floor(column.maxWidth) : defaultMaxWidth;
12
+ return { min, max: Math.max(min, max) };
13
+ }
14
+ function clampWidth(value, { min, max }) {
15
+ return Math.max(min, Math.min(max, Math.round(value)));
16
+ }
17
+ function normalizeWidths(columns, widths) {
18
+ return new Map(columns.flatMap(column => {
19
+ const value = widths && Object.prototype.hasOwnProperty.call(widths, column.id) ? widths[column.id] : undefined;
20
+ return column.resizable && validWidth(value) ? [[column.id, clampWidth(value, bounds(column))]] : [];
21
+ }));
22
+ }
23
+ function equalWidths(a, b) {
24
+ return a.size === b.size && [...a].every(([key, width]) => b.get(key) === width);
25
+ }
26
+ function measuredColumnWidth(header) {
27
+ const style = header.ownerDocument.defaultView?.getComputedStyle(header);
28
+ if (!style)
29
+ return header.offsetWidth;
30
+ const inlineSize = Number.parseFloat(style.inlineSize);
31
+ if (!Number.isFinite(inlineSize))
32
+ return header.offsetWidth;
33
+ const borderBox = style.boxSizing === "border-box" ? inlineSize : inlineSize +
34
+ Number.parseFloat(style.paddingInlineStart) + Number.parseFloat(style.paddingInlineEnd) +
35
+ Number.parseFloat(style.borderInlineStartWidth) + Number.parseFloat(style.borderInlineEndWidth);
36
+ // Avoid shaving a fraction of a pixel from untouched labels when switching to fixed tracks.
37
+ return Math.ceil(borderBox * 100) / 100;
38
+ }
39
+ function ColumnResizeHandle({ column, label, width, disabled, onPreview, onResize }) {
40
+ const gesture = useRef(null);
41
+ const [dragging, setDragging] = useState(false);
42
+ const limits = bounds(column);
43
+ const direction = (node) => {
44
+ const rtl = node.ownerDocument.defaultView?.getComputedStyle(node).direction === "rtl";
45
+ return (rtl ? -1 : 1) * (column.pinned === "end" ? -1 : 1);
46
+ };
47
+ const cancel = () => {
48
+ const active = gesture.current;
49
+ if (!active)
50
+ return;
51
+ gesture.current = null;
52
+ if (active.handle.hasPointerCapture(active.id))
53
+ active.handle.releasePointerCapture(active.id);
54
+ setDragging(false);
55
+ onPreview(undefined);
56
+ };
57
+ useEffect(() => {
58
+ if (disabled)
59
+ cancel();
60
+ }, [disabled]);
61
+ useEffect(() => () => {
62
+ if (gesture.current)
63
+ onPreview(undefined);
64
+ }, []);
65
+ useEffect(() => {
66
+ if (!dragging)
67
+ return;
68
+ const document = gesture.current?.handle.ownerDocument;
69
+ const view = document?.defaultView;
70
+ const hide = () => { if (document?.hidden)
71
+ cancel(); };
72
+ view?.addEventListener("blur", cancel);
73
+ document?.addEventListener("visibilitychange", hide);
74
+ return () => {
75
+ view?.removeEventListener("blur", cancel);
76
+ document?.removeEventListener("visibilitychange", hide);
77
+ };
78
+ }, [dragging]);
79
+ const requestedWidth = (event) => {
80
+ const active = gesture.current;
81
+ if (active?.id !== event.pointerId)
82
+ return undefined;
83
+ return clampWidth(active.width + (event.clientX - active.origin) * active.direction / active.scale, limits);
84
+ };
85
+ const value = width === undefined ? limits.min : clampWidth(width, limits);
86
+ return _jsx("div", { role: "separator", "aria-label": label, title: label, "aria-orientation": "vertical", "aria-valuemin": limits.min, "aria-valuemax": limits.max, "aria-valuenow": value, "aria-valuetext": `${value} px`, tabIndex: disabled ? -1 : 0, "aria-disabled": disabled || undefined, className: "flood-data-table__resize", "data-dragging": dragging || undefined, onKeyDown: event => {
87
+ if (event.defaultPrevented || disabled || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
88
+ return;
89
+ if (event.key === "Escape" && gesture.current) {
90
+ event.preventDefault();
91
+ cancel();
92
+ return;
93
+ }
94
+ if (gesture.current)
95
+ return;
96
+ let next;
97
+ if (event.key === "Home")
98
+ next = limits.min;
99
+ else if (event.key === "End")
100
+ next = limits.max;
101
+ else if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
102
+ const step = event.shiftKey ? 24 : 8;
103
+ next = clampWidth((width ?? value) + (event.key === "ArrowRight" ? step : -step) * direction(event.currentTarget), limits);
104
+ }
105
+ else
106
+ return;
107
+ event.preventDefault();
108
+ if (next !== width)
109
+ onResize(next);
110
+ }, onPointerDown: event => {
111
+ const handle = event.currentTarget;
112
+ const header = handle.closest("th");
113
+ if (event.defaultPrevented || disabled || gesture.current || event.button !== 0 || !event.isPrimary || !header?.offsetWidth)
114
+ return;
115
+ try {
116
+ handle.setPointerCapture(event.pointerId);
117
+ }
118
+ catch {
119
+ return;
120
+ }
121
+ event.preventDefault();
122
+ handle.focus({ preventScroll: true });
123
+ gesture.current = {
124
+ handle, id: event.pointerId, origin: event.clientX, width: header.offsetWidth,
125
+ direction: direction(handle), scale: header.getBoundingClientRect().width / header.offsetWidth || 1,
126
+ };
127
+ setDragging(true);
128
+ }, onPointerMove: event => {
129
+ const next = requestedWidth(event);
130
+ if (next !== undefined && !disabled)
131
+ onPreview(event.clientX === gesture.current?.origin ? undefined : next);
132
+ }, onPointerUp: event => {
133
+ const next = requestedWidth(event);
134
+ if (next === undefined)
135
+ return;
136
+ const moved = event.clientX !== gesture.current?.origin && next !== gesture.current?.width;
137
+ cancel();
138
+ if (!disabled && moved)
139
+ onResize(next);
140
+ }, onPointerCancel: event => { if (gesture.current?.id === event.pointerId)
141
+ cancel(); }, onLostPointerCapture: event => { if (gesture.current?.id === event.pointerId)
142
+ cancel(); }, children: _jsx("span", { className: "flood-data-table__resize-icon", children: _jsx(IconSlot, { size: "sm", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "M3 12h18m-14-4-4 4 4 4m10-8 4 4-4 4" }) }) }) }) });
143
+ }
144
+ export function useDataTableColumnLayout(columns, visibleColumns, { columnWidths, defaultColumnWidths, onColumnWidthsChange, measureViewport = false, }) {
145
+ const [local, setLocal] = useState(defaultColumnWidths);
146
+ const [draft, setDraft] = useState();
147
+ const [metrics, setMetrics] = useState({ widths: new Map(), utilityKeys: [], viewport: 0 });
148
+ const table = useRef(null);
149
+ const baseline = useRef(new Map());
150
+ const accepted = normalizeWidths(columns, columnWidths ?? local);
151
+ const defaults = normalizeWidths(columns, defaultColumnWidths);
152
+ const activeDraft = draft && visibleColumns.some(column => column.resizable && column.id === draft.id) ? draft : undefined;
153
+ const visibleSizes = new Map(visibleColumns.flatMap(column => {
154
+ const width = activeDraft?.id === column.id ? activeDraft.width : accepted.get(column.id);
155
+ return width === undefined ? [] : [[columnKey(column.id), width]];
156
+ }));
157
+ const active = measureViewport || columns.some(column => column.resizable || column.pinned);
158
+ const hasSizes = visibleSizes.size > 0;
159
+ const columnOrder = JSON.stringify(visibleColumns.map(column => column.id));
160
+ useLayoutEffect(() => {
161
+ const node = table.current;
162
+ const viewport = node?.closest(".flood-scroll-area__viewport");
163
+ const headerRow = node?.tHead?.rows[0];
164
+ if (!active || !node || !viewport || !headerRow)
165
+ return;
166
+ let headers = Array.from(headerRow.cells);
167
+ const measure = () => {
168
+ const widths = new Map();
169
+ const utilityKeys = [];
170
+ for (const header of headers) {
171
+ const id = header.dataset.dataTableColumn;
172
+ const utility = header.dataset.dataTableUtility;
173
+ const key = id !== undefined ? columnKey(id) : utilityKey(utility ?? `unknown-${header.cellIndex}`);
174
+ if (id === undefined)
175
+ utilityKeys.push(key);
176
+ if (header.offsetWidth === 0)
177
+ continue;
178
+ widths.set(key, header.offsetWidth);
179
+ if (!hasSizes || !baseline.current.has(key))
180
+ baseline.current.set(key, measuredColumnWidth(header));
181
+ }
182
+ setMetrics(previous => previous.viewport === viewport.clientWidth && equalWidths(previous.widths, widths) &&
183
+ previous.utilityKeys.join("|") === utilityKeys.join("|")
184
+ ? previous : { widths, utilityKeys, viewport: viewport.clientWidth });
185
+ };
186
+ measure();
187
+ const Observer = node.ownerDocument.defaultView?.ResizeObserver;
188
+ if (!Observer)
189
+ return;
190
+ const observer = new Observer(measure);
191
+ const observeHeaders = () => {
192
+ observer.disconnect();
193
+ headers = Array.from(headerRow.cells);
194
+ observer.observe(viewport);
195
+ for (const header of headers)
196
+ observer.observe(header, { box: "border-box" });
197
+ measure();
198
+ };
199
+ observeHeaders();
200
+ const HeaderObserver = node.ownerDocument.defaultView?.MutationObserver;
201
+ const headerObserver = HeaderObserver && new HeaderObserver(observeHeaders);
202
+ headerObserver?.observe(headerRow, { childList: true });
203
+ return () => {
204
+ observer.disconnect();
205
+ headerObserver?.disconnect();
206
+ };
207
+ }, [active, hasSizes, columnOrder]);
208
+ const sizes = hasSizes ? new Map([...baseline.current, ...visibleSizes]) : metrics.widths;
209
+ const keys = [...metrics.utilityKeys, ...visibleColumns.map(column => columnKey(column.id))];
210
+ const fixedWidth = hasSizes && keys.every(key => sizes.has(key))
211
+ ? keys.reduce((total, key) => total + sizes.get(key), 0)
212
+ : undefined;
213
+ const start = visibleColumns.filter(column => column.pinned === "start").map(column => columnKey(column.id));
214
+ const end = visibleColumns.filter(column => column.pinned === "end").map(column => columnKey(column.id)).reverse();
215
+ if (start.length)
216
+ start.unshift(...metrics.utilityKeys);
217
+ const pinned = new Map();
218
+ const pinnedKeys = [...start, ...end];
219
+ const pinnedWidth = pinnedKeys.reduce((total, key) => total + (metrics.widths.get(key) ?? 0), 0);
220
+ if (pinnedKeys.every(key => metrics.widths.has(key)) && pinnedWidth + defaultMinWidth <= metrics.viewport) {
221
+ for (const [edge, group] of [["start", start], ["end", end]]) {
222
+ let offset = 0;
223
+ group.forEach((key, index) => {
224
+ pinned.set(key, { edge, offset, boundary: index === group.length - 1 });
225
+ offset += metrics.widths.get(key);
226
+ });
227
+ }
228
+ }
229
+ function cell(key, header, width) {
230
+ const pin = pinned.get(key);
231
+ const inlineSize = header ? (fixedWidth === undefined ? width : sizes.get(key)) : undefined;
232
+ return {
233
+ "data-pin-edge": pin?.edge,
234
+ "data-pin-boundary": pin?.boundary || undefined,
235
+ style: inlineSize !== undefined || pin
236
+ ? { inlineSize, "--flood-data-table-pin-offset": pin ? `${pin.offset}px` : undefined }
237
+ : undefined,
238
+ };
239
+ }
240
+ function publish(next) {
241
+ if (equalWidths(accepted, next))
242
+ return;
243
+ const widths = Object.fromEntries(next);
244
+ if (columnWidths === undefined)
245
+ setLocal(widths);
246
+ onColumnWidthsChange?.(widths);
247
+ }
248
+ return {
249
+ table, fixedWidth,
250
+ viewportWidth: metrics.viewport || undefined,
251
+ disclosureWidth: metrics.widths.get(utilityKey("expansion")),
252
+ atDefaults: equalWidths(accepted, defaults),
253
+ reset: () => { setDraft(undefined); publish(defaults); },
254
+ utilityCell: (id, header = false) => ({
255
+ ...cell(utilityKey(id), header),
256
+ "data-data-table-utility": id,
257
+ }),
258
+ columnCell: (column, header = false) => ({
259
+ ...cell(columnKey(column.id), header, header ? (accepted.get(column.id) ?? column.width) : undefined),
260
+ "data-data-table-column": column.id,
261
+ }),
262
+ renderHandle: (column, label, disabled) => column.resizable &&
263
+ _jsx(ColumnResizeHandle, { column: column, label: label, disabled: disabled, width: visibleSizes.get(columnKey(column.id)) ?? metrics.widths.get(columnKey(column.id)), onPreview: width => setDraft(previous => {
264
+ if (width === undefined)
265
+ return previous?.id === column.id ? undefined : previous;
266
+ if (previous?.id === column.id && previous.width === width)
267
+ return previous;
268
+ return { id: column.id, width };
269
+ }), onResize: width => publish(new Map(accepted).set(column.id, width)) }),
270
+ };
271
+ }
@@ -0,0 +1,21 @@
1
+ import { type ReactNode, type TdHTMLAttributes } from "react";
2
+ import { type DataTableLabels, type DataTableRow } from "./index.js";
3
+ export declare function useDataTableExpansion(rows: readonly DataTableRow[], expandedIds: readonly string[] | undefined, defaultExpandedIds: readonly string[] | undefined, onExpandedChange: ((ids: string[]) => void) | undefined): {
4
+ enabled: boolean;
5
+ expanded: Set<string>;
6
+ toggle: (id: string) => void;
7
+ };
8
+ interface DataTableExpandableRowProps {
9
+ row: DataTableRow;
10
+ showDisclosure: boolean;
11
+ expanded: boolean;
12
+ colSpan: number;
13
+ labels: DataTableLabels;
14
+ onToggle: () => void;
15
+ disclosureCellProps?: TdHTMLAttributes<HTMLTableCellElement>;
16
+ viewportWidth?: number;
17
+ disclosureWidth?: number;
18
+ children: (disclosureCell: ReactNode) => ReactNode;
19
+ }
20
+ export declare function DataTableExpandableRow({ row, showDisclosure, expanded, colSpan, labels, onToggle, disclosureCellProps, viewportWidth, disclosureWidth, children }: DataTableExpandableRowProps): import("react").JSX.Element;
21
+ export {};