@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
@@ -0,0 +1,79 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useRef, useState } from "react";
3
+ import { IconButton } from "./index.js";
4
+ import { useLayerExitContent } from "./overlay-runtime.js";
5
+ function hasExpandedContent(row) {
6
+ return row.expandedContent != null && typeof row.expandedContent !== "boolean";
7
+ }
8
+ export function useDataTableExpansion(rows, expandedIds, defaultExpandedIds, onExpandedChange) {
9
+ const [localExpandedIds, setLocalExpandedIds] = useState(defaultExpandedIds ?? []);
10
+ const expanded = new Set(expandedIds ?? localExpandedIds);
11
+ const enabled = expandedIds !== undefined || defaultExpandedIds !== undefined || rows.some(hasExpandedContent);
12
+ function toggle(id) {
13
+ const next = new Set(expanded);
14
+ if (next.has(id))
15
+ next.delete(id);
16
+ else
17
+ next.add(id);
18
+ const ids = [...next];
19
+ if (expandedIds === undefined)
20
+ setLocalExpandedIds(ids);
21
+ onExpandedChange?.(ids);
22
+ }
23
+ return { enabled, expanded, toggle };
24
+ }
25
+ export function DataTableExpandableRow({ row, showDisclosure, expanded, colSpan, labels, onToggle, disclosureCellProps, viewportWidth, disclosureWidth, children }) {
26
+ const detailId = useId();
27
+ const toggle = useRef(null);
28
+ const reveal = useRef(null);
29
+ const detail = useRef(null);
30
+ const lastFocusedDetail = useRef(null);
31
+ const expandable = showDisclosure && hasExpandedContent(row);
32
+ const open = expandable && expanded;
33
+ const showDetail = useLayerExitContent(reveal, open, expandable, "grid-template-rows");
34
+ useEffect(() => {
35
+ const content = detail.current;
36
+ const header = content?.querySelector("table")?.tHead?.rows[0];
37
+ if (!content || !header)
38
+ return;
39
+ const alignBranch = () => {
40
+ const bounds = content.getBoundingClientRect();
41
+ if (!bounds.height)
42
+ return;
43
+ const heading = header.getBoundingClientRect();
44
+ const center = (heading.top + heading.height / 2 - bounds.top) * content.offsetHeight / bounds.height;
45
+ const value = `${center}px`;
46
+ if (content.style.getPropertyValue("--flood-data-table-branch-target") !== value) {
47
+ content.style.setProperty("--flood-data-table-branch-target", value);
48
+ }
49
+ };
50
+ alignBranch();
51
+ const observer = new ResizeObserver(alignBranch);
52
+ observer.observe(content);
53
+ observer.observe(header);
54
+ return () => {
55
+ observer.disconnect();
56
+ content.style.removeProperty("--flood-data-table-branch-target");
57
+ };
58
+ }, [showDetail, row.expandedContent]);
59
+ useEffect(() => {
60
+ if (open)
61
+ return;
62
+ const previousFocus = lastFocusedDetail.current;
63
+ lastFocusedDetail.current = null;
64
+ const button = toggle.current;
65
+ const active = button?.ownerDocument.activeElement;
66
+ if (button && previousFocus && (active === button.ownerDocument.body || reveal.current?.contains(active ?? null))) {
67
+ button.focus({ preventScroll: true });
68
+ }
69
+ }, [open]);
70
+ const label = row.label ?? row.primaryAction?.title ?? row.id;
71
+ const actionLabel = open ? labels.collapseRow ?? (name => `Collapse ${name}`) : labels.expandRow ?? (name => `Expand ${name}`);
72
+ return _jsxs(_Fragment, { children: [children(showDisclosure ? _jsx("td", { ...disclosureCellProps, "data-data-table-utility": "expansion", "data-detail-visible": showDetail || undefined, className: "flood-data-table__expand", children: expandable && _jsx(IconButton, { ref: toggle, label: actionLabel(label), size: "sm", iconSize: "xs", variant: "ghost", className: "flood-data-table__disclosure", type: "button", "aria-expanded": open, "aria-controls": open ? detailId : undefined, onClick: onToggle, 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: "m6 9 6 6 6-6" }) }) }) }) : null), showDetail && _jsx("tr", { id: detailId, className: "flood-data-table__expanded-row", "aria-hidden": open ? undefined : true, onFocusCapture: event => {
73
+ if (event.currentTarget.contains(event.target))
74
+ lastFocusedDetail.current = event.target;
75
+ }, children: _jsx("td", { colSpan: colSpan, children: _jsx("div", { ref: reveal, className: "flood-data-table__detail-reveal", "data-open": open || undefined, children: _jsx("div", { className: "flood-data-table__detail-clip", children: _jsx("div", { ref: detail, className: "flood-data-table__expanded-content", style: {
76
+ maxInlineSize: viewportWidth ? `max(0px, calc(${viewportWidth}px - 2 * var(--flood-data-table-edge) - var(--flood-scroll-content-inline, 0px)))` : undefined,
77
+ "--flood-data-table-disclosure-center": disclosureWidth === undefined ? undefined : `${disclosureWidth / 2}px`,
78
+ }, children: row.expandedContent }) }) }) }) })] });
79
+ }
@@ -1,24 +1,129 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef, useMemo, useState } from "react";
3
- import { Table, Checkbox, Button, IconSlot, VisuallyHidden, Pagination, Text, Spinner, Alert, Stack } from "./index.js";
4
- export const ManagedDataTable = /* @__PURE__ */ forwardRef(function ManagedDataTable({ caption, hideCaption = false, columns, rows, density = "comfortable", striped = false, minWidth = "40rem", sort: controlledSort, defaultSort = null, onSortChange, locale = "en", selectable = false, selectedIds, defaultSelectedIds = [], onSelectionChange, page: controlledPage, defaultPage = 1, pageSize = 10, onPageChange, state = "ready", onRetry, labels = {}, className, ...props }, ref) {
1
+ import { createElement as _createElement } from "react";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
4
+ import { Table, Checkbox, Button, Badge, IconSlot, VisuallyHidden, Pagination, Text, Spinner, Alert, Stack, Inline, Menu, Tooltip } from "./index.js";
5
+ import { useDataTableColumnLayout } from "./data-table-columns.js";
6
+ import { DataTableExpandableRow, useDataTableExpansion } from "./data-table-expansion.js";
7
+ const columnName = (column) => column.unit ? `${column.label} ${column.unit}` : column.label;
8
+ function DataTableHeading({ column, sortLabel, disabled, descending, onSort }) {
9
+ const label = useRef(null);
10
+ const [truncated, setTruncated] = useState(false);
11
+ useEffect(() => {
12
+ const node = label.current;
13
+ if (!node)
14
+ return;
15
+ const measure = () => setTruncated(node.scrollWidth > node.clientWidth);
16
+ measure();
17
+ const Observer = node.ownerDocument.defaultView?.ResizeObserver;
18
+ const observer = Observer && new Observer(measure);
19
+ observer?.observe(node);
20
+ const fonts = node.ownerDocument.fonts;
21
+ fonts?.addEventListener("loadingdone", measure);
22
+ return () => {
23
+ observer?.disconnect();
24
+ fonts?.removeEventListener("loadingdone", measure);
25
+ };
26
+ }, [column.label, column.unit]);
27
+ const content = _jsxs(_Fragment, { children: [_jsx("span", { ref: label, className: "flood-data-table__header-label", children: column.label }), " ", column.unit && _jsx(Badge, { size: "sm", variant: "outline", className: "flood-data-table__header-unit", "aria-hidden": "true", children: column.unit })] });
28
+ return _jsx(Tooltip, { content: columnName(column), disabled: !truncated, trigger: trigger => column.sortable ? _jsxs(Button, { ...trigger, "aria-describedby": undefined, variant: "ghost", size: "sm", disabled: disabled, className: "flood-data-table__sort", "aria-label": sortLabel, onClick: onSort, children: [content, _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: descending ? _jsx("path", { d: "m7 14 5-5 5 5" }) : _jsx("path", { d: "m8 8 4-4 4 4M8 16l4 4 4-4" }) }) })] }) : _jsx("span", { ...trigger, "aria-describedby": undefined, className: "flood-data-table__header-content", tabIndex: truncated ? 0 : undefined, children: content }) });
29
+ }
30
+ function activatePrimaryRowAction(event) {
31
+ const target = event.target;
32
+ const row = event.currentTarget;
33
+ if (!(target instanceof Element) || target.closest("tr") !== row || event.defaultPrevented ||
34
+ event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
35
+ return;
36
+ const control = target.closest('button,a,input,label,select,textarea,summary,[role],[tabindex],[contenteditable]:not([contenteditable="false"])');
37
+ if (control && row.contains(control))
38
+ return;
39
+ const selection = row.ownerDocument.getSelection();
40
+ if (selection && !selection.isCollapsed && selection.containsNode(row, true))
41
+ return;
42
+ const button = row.querySelector(":scope > td > .flood-stack > .flood-data-table__primary-action");
43
+ if (!button || button.disabled || button.getAttribute("aria-disabled") === "true")
44
+ return;
45
+ button.focus({ preventScroll: true });
46
+ button.click();
47
+ }
48
+ export const ManagedDataTable = /* @__PURE__ */ forwardRef(function ManagedDataTable({ caption, hideCaption = false, columns, rows, density = "comfortable", striped = false, minWidth = "40rem", toolbar, inset = "standard", maxViewportHeight, stickyHeader, showColumnVisibility = false, hiddenColumnIds, defaultHiddenColumnIds = [], onHiddenColumnIdsChange, columnWidths, defaultColumnWidths, onColumnWidthsChange, expandedIds, defaultExpandedIds, onExpandedChange, sort: controlledSort, defaultSort = null, onSortChange, locale = "en", selectable = false, selectedIds, defaultSelectedIds = [], onSelectionChange, page: controlledPage, defaultPage = 1, pageSize = 10, onPageChange, showResults = true, showSelection = true, paginationMode = "client", totalRows, selectionScope, state = "ready", onRetry, emptyContent, labels = {}, className, ...props }, ref) {
5
49
  if (!caption.trim() || columns.length === 0 || columns.some(column => !column.id.trim() || !column.label.trim()) || new Set(columns.map(column => column.id)).size !== columns.length)
6
50
  throw new Error("Flood DataTable requires a caption and unique named columns");
7
51
  if (rows.some(row => !row.id.trim() || (row.label !== undefined && !row.label.trim())) || new Set(rows.map(row => row.id)).size !== rows.length)
8
52
  throw new Error("Flood DataTable rows require unique nonempty IDs and labels");
53
+ if (rows.some(({ primaryAction }) => primaryAction && (typeof primaryAction.title !== "string" || !primaryAction.title.trim() ||
54
+ !columns.some(column => column.id === primaryAction.column))))
55
+ throw new Error("Flood DataTable primary actions require a nonempty title and an existing column");
9
56
  if (!Number.isSafeInteger(pageSize) || pageSize < 1 || !Number.isSafeInteger(defaultPage) || defaultPage < 1 || (controlledPage !== undefined && (!Number.isSafeInteger(controlledPage) || controlledPage < 1)))
10
57
  throw new Error("Flood DataTable page and pageSize must be positive safe integers");
58
+ const server = paginationMode === "server", scope = selectionScope ?? (server ? "page" : "dataset");
59
+ if (server && (controlledPage === undefined || !onPageChange || !Number.isSafeInteger(totalRows) || (totalRows ?? -1) < 0))
60
+ throw new Error("Flood server DataTable requires page, onPageChange and a nonnegative totalRows");
61
+ if (server && columns.some(column => column.sortable) && (controlledSort === undefined || !onSortChange))
62
+ throw new Error("Flood server sorting requires sort (or null) and onSortChange");
63
+ if (server && selectable && scope === "dataset" && (selectedIds === undefined || !onSelectionChange))
64
+ throw new Error("Flood server dataset selection must be controlled");
11
65
  const [localSort, setSort] = useState(defaultSort), [localSelected, setSelected] = useState(defaultSelectedIds), [localPage, setPage] = useState(defaultPage);
12
66
  const requestedSort = controlledSort === undefined ? localSort : controlledSort;
13
67
  const sorting = requestedSort && columns.some(column => column.id === requestedSort.column && column.sortable) ? requestedSort : null;
68
+ const [localHiddenIds, setHiddenIds] = useState(defaultHiddenColumnIds);
69
+ const hidden = new Set(hiddenColumnIds ?? localHiddenIds);
70
+ const required = new Set(rows.flatMap(row => row.primaryAction ? [row.primaryAction.column] : []));
71
+ if (sorting)
72
+ required.add(sorting.column);
73
+ const displayed = columns.filter(column => column.hideable === false || required.has(column.id) || !hidden.has(column.id));
74
+ const retained = displayed.length > 0 ? displayed : columns.slice(0, 1);
75
+ const visibleColumns = [
76
+ ...retained.filter(column => column.pinned === "start"),
77
+ ...retained.filter(column => !column.pinned),
78
+ ...retained.filter(column => column.pinned === "end"),
79
+ ];
80
+ const expansion = useDataTableExpansion(rows, expandedIds, defaultExpandedIds, onExpandedChange);
81
+ const columnLayout = useDataTableColumnLayout(columns, visibleColumns, {
82
+ columnWidths, defaultColumnWidths, onColumnWidthsChange, measureViewport: expansion.enabled,
83
+ });
84
+ const columnCount = visibleColumns.length + Number(selectable) + Number(expansion.enabled);
85
+ const visibleIds = new Set(visibleColumns.map(column => column.id));
86
+ const normalizeHiddenIds = (ids) => columns.filter(column => column.hideable !== false && ids.has(column.id)).map(column => column.id);
87
+ const defaultIds = normalizeHiddenIds(new Set(defaultHiddenColumnIds));
88
+ const requestedIds = normalizeHiddenIds(hidden);
89
+ const visibilityAtDefaults = requestedIds.length === defaultIds.length && defaultIds.every(id => hidden.has(id));
90
+ const changeHiddenIds = (next) => {
91
+ if (hiddenColumnIds === undefined)
92
+ setHiddenIds(next);
93
+ onHiddenColumnIdsChange?.(next);
94
+ };
95
+ const columnItems = showColumnVisibility ? [
96
+ ...columns.map((column) => ({
97
+ id: `column:${column.id}`,
98
+ type: "checkbox",
99
+ label: sorting?.column === column.id
100
+ ? (labels.sortedColumn ?? (label => `${label} (sorted)`))(columnName(column))
101
+ : columnName(column),
102
+ checked: visibleIds.has(column.id),
103
+ disabled: column.hideable === false || required.has(column.id) || (visibleColumns.length === 1 && visibleIds.has(column.id)),
104
+ onCheckedChange: checked => {
105
+ const next = new Set(hidden);
106
+ if (checked)
107
+ next.delete(column.id);
108
+ else
109
+ next.add(column.id);
110
+ changeHiddenIds(normalizeHiddenIds(next));
111
+ },
112
+ })),
113
+ { id: "column-layout-separator", type: "separator" },
114
+ { id: "reset-columns", label: labels.resetColumns ?? "Reset columns",
115
+ disabled: visibilityAtDefaults && columnLayout.atDefaults },
116
+ ] : [];
14
117
  const sorted = useMemo(() => {
15
- if (!sorting)
118
+ if (server || !sorting)
16
119
  return [...rows];
17
120
  const collator = new Intl.Collator(locale, { numeric: true, sensitivity: "base" });
18
121
  const value = (row) => {
19
122
  const explicit = row.sortValues?.[sorting.column];
20
123
  if (explicit !== undefined)
21
124
  return explicit;
125
+ if (row.primaryAction?.column === sorting.column)
126
+ return row.primaryAction.title;
22
127
  const cell = row.cells[sorting.column];
23
128
  return typeof cell === "string" || typeof cell === "number" ? cell : null;
24
129
  };
@@ -30,14 +135,17 @@ export const ManagedDataTable = /* @__PURE__ */ forwardRef(function ManagedDataT
30
135
  const comparison = typeof left === "number" && typeof right === "number" ? left - right : collator.compare(String(left), String(right));
31
136
  return comparison === 0 ? a.index - b.index : comparison * (sorting.direction === "ascending" ? 1 : -1);
32
137
  }).map(entry => entry.row);
33
- }, [rows, sorting?.column, sorting?.direction, locale]);
34
- const pageCount = Math.max(1, Math.ceil(rows.length / pageSize)), page = Math.min(controlledPage ?? localPage, pageCount);
35
- const visible = sorted.slice((page - 1) * pageSize, page * pageSize), eligible = new Set(rows.filter(row => !row.selectionDisabled).map(row => row.id));
36
- const selection = [...new Set(selectedIds ?? localSelected)].filter(id => eligible.has(id)), chosen = new Set(selection);
138
+ }, [rows, sorting?.column, sorting?.direction, locale, server]);
139
+ const total = server ? totalRows : rows.length, pageCount = Math.max(1, Math.ceil(total / pageSize)), page = server ? controlledPage : Math.min(controlledPage ?? localPage, pageCount);
140
+ if (server && state === "ready" && (page > pageCount || rows.length > pageSize || (page - 1) * pageSize + rows.length > total))
141
+ throw new Error("Flood server rows and page must match totalRows/pageSize; reconcile the page after a query change");
142
+ const visible = server ? sorted : sorted.slice((page - 1) * pageSize, page * pageSize), eligible = new Set((scope === "page" ? visible : rows).filter(row => !row.selectionDisabled).map(row => row.id));
143
+ const accepts = (id) => server && scope === "dataset" ? !rows.some(row => row.id === id && row.selectionDisabled) : eligible.has(id);
144
+ const selection = [...new Set(selectedIds ?? localSelected)].filter(accepts), chosen = new Set(selection);
37
145
  const pageChoices = visible.filter(row => !row.selectionDisabled), all = pageChoices.length > 0 && pageChoices.every(row => chosen.has(row.id)), some = pageChoices.some(row => chosen.has(row.id));
38
146
  const blocked = state !== "ready";
39
147
  const select = (next) => { if (blocked)
40
- return; const accepted = [...new Set(next)].filter(id => eligible.has(id)); if (selectedIds === undefined)
148
+ return; const accepted = [...new Set(next)].filter(accepts); if (selectedIds === undefined)
41
149
  setSelected(accepted); onSelectionChange?.(accepted); };
42
150
  const changePage = (next) => { if (blocked)
43
151
  return; if (controlledPage === undefined)
@@ -53,6 +161,26 @@ export const ManagedDataTable = /* @__PURE__ */ forwardRef(function ManagedDataT
53
161
  changePage(1);
54
162
  };
55
163
  const resultLabel = labels.results ?? ((from, to, total) => `${from}–${to} of ${total}`);
56
- return _jsxs("div", { ...props, ref: ref, className: ["flood-data-table", className].filter(Boolean).join(" "), "aria-busy": state === "loading" || undefined, children: [_jsxs(Table, { caption: caption, hideCaption: hideCaption, density: density, striped: striped, style: { minWidth }, children: [_jsx("thead", { children: _jsxs("tr", { children: [selectable && _jsx("th", { scope: "col", className: "flood-data-table__choice", children: _jsx(Checkbox, { label: _jsx(VisuallyHidden, { children: labels.selectPage ?? "Select rows on this page" }), checked: all, indeterminate: some && !all, disabled: blocked || pageChoices.length === 0, onChange: event => { const ids = new Set(selection); for (const row of pageChoices)
57
- event.currentTarget.checked ? ids.add(row.id) : ids.delete(row.id); select([...ids]); } }) }), columns.map(column => _jsx("th", { scope: "col", "data-sortable": column.sortable || undefined, "aria-sort": sorting?.column === column.id ? sorting.direction : undefined, style: { textAlign: column.align ?? "start", width: column.width }, children: column.sortable ? _jsxs(Button, { variant: "ghost", size: "sm", disabled: blocked, className: "flood-data-table__sort", "aria-label": (labels.sort ?? ((label) => `Sort by ${label}`))(column.label), onClick: () => changeSort(column.id), children: [column.label, _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: sorting?.column === column.id ? _jsx("path", { d: sorting.direction === "ascending" ? "m7 14 5-5 5 5" : "m7 10 5 5 5-5" }) : _jsx("path", { d: "m8 8 4-4 4 4M8 16l4 4 4-4" }) }) })] }) : column.label }, column.id))] }) }), _jsx("tbody", { children: state === "ready" && visible.length > 0 ? visible.map(row => _jsxs("tr", { "data-selected": selectable && chosen.has(row.id) || undefined, children: [selectable && _jsx("td", { className: "flood-data-table__choice", children: _jsx(Checkbox, { label: _jsx(VisuallyHidden, { children: (labels.selectRow ?? ((label) => `Select ${label}`))(row.label ?? row.id) }), checked: chosen.has(row.id), disabled: Boolean(row.selectionDisabled), onChange: event => select(event.currentTarget.checked ? [...selection, row.id] : selection.filter(id => id !== row.id)) }) }), columns.map(column => _jsx("td", { style: { textAlign: column.align ?? "start" }, children: row.cells[column.id] ?? null }, column.id))] }, row.id)) : _jsx("tr", { children: _jsx("td", { colSpan: columns.length + (selectable ? 1 : 0), className: "flood-data-table__state", children: state === "loading" ? _jsx(Spinner, { label: labels.loading ?? "Loading rows…" }) : state === "error" ? _jsxs(Stack, { gap: "3", children: [_jsx(Alert, { tone: "error", announcement: "polite", children: labels.error ?? "Could not load the rows." }), onRetry && _jsx(Button, { variant: "subtle", size: "sm", onClick: onRetry, children: labels.retry ?? "Try again" })] }) : _jsx(Text, { as: "p", size: "compact", tone: "subtle", role: "status", children: labels.empty ?? "No results found." }) }) }) })] }), _jsxs("div", { className: "flood-data-table__footer", children: [_jsx(Text, { as: "p", size: "helper", tone: "subtle", children: state === "ready" ? resultLabel(rows.length ? (page - 1) * pageSize + 1 : 0, Math.min(page * pageSize, rows.length), rows.length) : "" }), selectable && _jsx(Text, { as: "p", size: "helper", tone: "subtle", role: "status", children: (labels.selection ?? ((count, total) => `${count} of ${total} selected`))(selection.length, eligible.size) }), pageCount > 1 && _jsx(Pagination, { page: page, pageCount: pageCount, onPageChange: changePage, disabled: blocked, label: labels.pagination, previousLabel: labels.previousPage, nextLabel: labels.nextPage, pageLabel: labels.page })] })] });
164
+ const selectionLabel = selectable
165
+ ? (labels.selection ?? ((count, total) => `${count} of ${total} selected`))(selection.length, server && scope === "dataset" ? total : eligible.size)
166
+ : null;
167
+ const renderToolbar = typeof toolbar === "function" ? toolbar : null;
168
+ const columnsControl = showColumnVisibility ? (_jsx(Menu, { label: labels.columns ?? "Columns", items: columnItems, align: "end", onAction: id => {
169
+ if (id !== "reset-columns")
170
+ return;
171
+ if (!visibilityAtDefaults)
172
+ changeHiddenIds(defaultIds);
173
+ columnLayout.reset();
174
+ }, trigger: trigger => _jsx(Button, { ...trigger, className: renderToolbar ? undefined : "flood-data-table__columns", variant: "subtle", size: "sm", children: labels.columns ?? "Columns" }) })) : null;
175
+ const toolbarContent = typeof toolbar === "function" ? toolbar({ columns: columnsControl }) : toolbar;
176
+ const sortLabel = labels.sort ?? ((label) => `Sort by ${label}`);
177
+ return _jsxs("div", { ...props, ref: ref, className: ["flood-data-table", className].filter(Boolean).join(" "), "data-inset": inset, "aria-busy": state === "loading" || undefined, children: [columnsControl && !renderToolbar ? _jsxs(Inline, { gap: "3", className: "flood-data-table__toolbar", children: [toolbarContent != null && _jsx("div", { className: "flood-data-table__toolbar-content", children: toolbarContent }), columnsControl] }) : toolbarContent != null && _jsx("div", { className: "flood-data-table__toolbar", children: toolbarContent }), _jsxs(Table, { ref: columnLayout.table, className: "flood-data-table__table", caption: caption, hideCaption: hideCaption, density: density, striped: striped, maxViewportHeight: maxViewportHeight, stickyHeader: stickyHeader, "data-inset": inset, "data-fixed-columns": columnLayout.fixedWidth !== undefined || undefined, style: columnLayout.fixedWidth === undefined ? { minWidth } : { minWidth, inlineSize: columnLayout.fixedWidth, tableLayout: "fixed" }, children: [_jsx("thead", { children: _jsxs("tr", { children: [expansion.enabled && _jsx("th", { ...columnLayout.utilityCell("expansion", true), scope: "col", "data-data-table-utility": "expansion", className: "flood-data-table__expand", children: _jsx(VisuallyHidden, { children: labels.rowDetails ?? "Row details" }) }), selectable && _jsx("th", { ...columnLayout.utilityCell("selection", true), scope: "col", className: "flood-data-table__choice", children: _jsx(Checkbox, { label: _jsx(VisuallyHidden, { children: labels.selectPage ?? "Select rows on this page" }), checked: all, indeterminate: some && !all, disabled: blocked || pageChoices.length === 0, onChange: event => { const ids = new Set(selection); for (const row of pageChoices)
178
+ event.currentTarget.checked ? ids.add(row.id) : ids.delete(row.id); select([...ids]); } }) }), visibleColumns.map(column => {
179
+ const name = columnName(column);
180
+ return _createElement("th", { ...columnLayout.columnCell(column, true), key: column.id, scope: "col", "data-numeric": column.numeric || undefined, "data-align": column.align, "data-sortable": column.sortable || undefined, "data-resize-edge": column.resizable ? column.pinned === "end" ? "start" : "end" : undefined, "aria-label": column.sortable ? sortLabel(name) : name, "aria-sort": sorting?.column === column.id ? sorting.direction : undefined },
181
+ _jsx(DataTableHeading, { column: column, sortLabel: sortLabel(name), disabled: blocked, descending: sorting?.column === column.id, onSort: () => changeSort(column.id) }),
182
+ column.resizable && columnLayout.renderHandle(column, (labels.resizeColumn ?? (label => `Resize ${label} column`))(name), blocked));
183
+ })] }) }), _jsx("tbody", { children: state === "ready" && visible.length > 0 ? visible.map(row => _jsx(DataTableExpandableRow, { row: row, showDisclosure: expansion.enabled, expanded: expansion.expanded.has(row.id), colSpan: columnCount, labels: labels, onToggle: () => expansion.toggle(row.id), disclosureCellProps: columnLayout.utilityCell("expansion"), viewportWidth: columnLayout.viewportWidth, disclosureWidth: columnLayout.disclosureWidth, children: disclosure => _jsxs("tr", { "data-selected": selectable && chosen.has(row.id) || undefined, "data-primary-action": row.primaryAction && !row.primaryAction.buttonProps.disabled && row.primaryAction.buttonProps["aria-disabled"] !== "true" || undefined, onClick: row.primaryAction ? activatePrimaryRowAction : undefined, children: [disclosure, selectable && _jsx("td", { ...columnLayout.utilityCell("selection"), className: "flood-data-table__choice", children: _jsx(Checkbox, { label: _jsx(VisuallyHidden, { children: (labels.selectRow ?? ((label) => `Select ${label}`))(row.label ?? row.id) }), checked: chosen.has(row.id), disabled: Boolean(row.selectionDisabled), onChange: event => select(event.currentTarget.checked ? [...selection, row.id] : selection.filter(id => id !== row.id)) }) }), visibleColumns.map(column => _createElement("td", { ...columnLayout.columnCell(column), key: column.id, "data-numeric": column.numeric || undefined, "data-align": column.align }, row.primaryAction?.column === column.id
184
+ ? _jsxs(Stack, { gap: "1", children: [_jsx(Button, { ...row.primaryAction.buttonProps, type: "button", variant: "ghost", size: "sm", className: ["flood-data-table__primary-action", row.primaryAction.buttonProps.className].filter(Boolean).join(" "), children: row.primaryAction.title }), row.primaryAction.description != null && _jsx(Text, { as: "div", size: "helper", tone: "subtle", children: row.primaryAction.description })] })
185
+ : row.cells[column.id] ?? null))] }) }, row.id)) : _jsx("tr", { children: _jsx("td", { colSpan: columnCount, className: "flood-data-table__state", children: state === "loading" ? _jsx(Spinner, { label: labels.loading ?? "Loading rows…" }) : state === "error" ? _jsxs(Stack, { gap: "3", children: [_jsx(Alert, { tone: "error", announcement: "polite", children: labels.error ?? "Could not load the rows." }), onRetry && _jsx(Button, { variant: "subtle", size: "sm", onClick: onRetry, children: labels.retry ?? "Try again" })] }) : emptyContent ?? _jsx(Text, { as: "p", size: "compact", tone: "subtle", role: "status", children: labels.empty ?? "No results found." }) }) }) })] }), selectable && !showSelection && _jsx(VisuallyHidden, { role: "status", children: selectionLabel }), (showResults || (selectable && showSelection) || pageCount > 1) && _jsxs("div", { className: "flood-data-table__footer", children: [showResults && _jsx(Text, { as: "p", size: "helper", tone: "subtle", children: state === "ready" ? resultLabel(visible.length ? (page - 1) * pageSize + 1 : 0, visible.length ? (page - 1) * pageSize + visible.length : 0, total) : "" }), selectable && showSelection && _jsx(Text, { as: "p", size: "helper", tone: "subtle", role: "status", children: selectionLabel }), pageCount > 1 && _jsx(Pagination, { page: Math.min(page, pageCount), pageCount: pageCount, onPageChange: changePage, disabled: blocked, label: labels.pagination, previousLabel: labels.previousPage, nextLabel: labels.nextPage, pageLabel: labels.page })] })] });
58
186
  });
@@ -4,7 +4,7 @@ import { Calendar, DateField, IconButton, Popover } from "./index.js";
4
4
  const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
5
5
  const triggerSizes = { sm: "xs", md: "sm", lg: "md", xl: "lg" };
6
6
  /** Calendar selection and keyboard date entry share one DateField value/form bridge. */
7
- export const CalendarDatePicker = /* @__PURE__ */ forwardRef(function CalendarDatePicker({ value, defaultValue = "", onValueChange, open: controlledOpen, defaultOpen = false, onOpenChange, calendarLabel, openCalendarLabel, locale, weekStartsOn, today, previousMonthLabel, nextMonthLabel, label, disabled = false, readOnly = false, controlSize = "md", min, max, form, className, onKeyDown, ...props }, forwardedRef) {
7
+ export const CalendarDatePicker = /* @__PURE__ */ forwardRef(function CalendarDatePicker({ value, defaultValue = "", onValueChange, open: controlledOpen, defaultOpen = false, onOpenChange, calendarLabel, openCalendarLabel, viewLabels, locale, weekStartsOn, today, previousMonthLabel, nextMonthLabel, label, disabled = false, readOnly = false, controlSize = "md", min, max, form, className, onKeyDown, ...props }, forwardedRef) {
8
8
  const panelLabel = calendarLabel ?? `${label} calendar`;
9
9
  const triggerLabel = openCalendarLabel ?? `Open calendar: ${label}`;
10
10
  if (!panelLabel.trim() || !triggerLabel.trim())
@@ -100,7 +100,7 @@ export const CalendarDatePicker = /* @__PURE__ */ forwardRef(function CalendarDa
100
100
  event.preventDefault();
101
101
  requestOpen(true);
102
102
  }
103
- }, end: _jsx(Popover, { title: panelLabel, className: "flood-date-picker__panel", align: "end", positionReference: positionReference, style: { "--flood-date-picker-width": fieldWidth === undefined ? undefined : `${fieldWidth}px` }, open: !blocked && requestedOpen, onOpenChange: requestOpen, disabled: blocked, trigger: trigger => _jsx(IconButton, { ...trigger, label: triggerLabel, variant: "ghost", size: triggerSizes[controlSize], children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { x: "3", y: "5", width: "18", height: "16", rx: "2" }), _jsx("path", { d: "M16 3v4M8 3v4M3 11h18" })] }) }), children: _jsx(Calendar, { label: panelLabel, value: committed, min: min, max: max, locale: locale, weekStartsOn: weekStartsOn, today: today, previousMonthLabel: previousMonthLabel, nextMonthLabel: nextMonthLabel, autoFocus: true, onValueChange: changeDate, onDateSelect: () => {
103
+ }, end: _jsx(Popover, { title: panelLabel, chrome: "minimal", className: "flood-date-picker__panel", align: "end", positionReference: positionReference, style: { "--flood-date-picker-width": fieldWidth === undefined ? undefined : `${fieldWidth}px` }, open: !blocked && requestedOpen, onOpenChange: requestOpen, disabled: blocked, trigger: trigger => _jsx(IconButton, { ...trigger, label: triggerLabel, variant: "ghost", size: triggerSizes[controlSize], children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { x: "3", y: "5", width: "18", height: "16", rx: "2" }), _jsx("path", { d: "M16 3v4M8 3v4M3 11h18" })] }) }), children: _jsx(Calendar, { label: panelLabel, viewLabels: viewLabels, value: committed, min: min, max: max, locale: locale, weekStartsOn: weekStartsOn, today: today, previousMonthLabel: previousMonthLabel, nextMonthLabel: nextMonthLabel, autoFocus: true, onValueChange: changeDate, onDateSelect: () => {
104
104
  if (blocked || nativeDisabled())
105
105
  return;
106
106
  requestOpen(false);
@@ -4,7 +4,7 @@ import { DateRangeField, IconButton, Popover, RangeCalendar } from "./index.js";
4
4
  import { emptyDateRange } from "./date-range-value.js";
5
5
  const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
6
6
  const triggerSizes = { sm: "xs", md: "sm", lg: "md", xl: "lg" };
7
- export const CalendarDateRangePicker = /* @__PURE__ */ forwardRef(function CalendarDateRangePicker({ value, defaultValue = emptyDateRange(), onValueChange, open: controlledOpen, defaultOpen = false, onOpenChange, label, calendarLabel, openCalendarLabel, locale, weekStartsOn, today, previousMonthLabel, nextMonthLabel, rangeLabels, disabled = false, readOnly = false, controlSize = "md", min, max, isDateDisabled, isRangeDisabled, form, className, onKeyDown, ...props }, forwardedRef) {
7
+ export const CalendarDateRangePicker = /* @__PURE__ */ forwardRef(function CalendarDateRangePicker({ value, defaultValue = emptyDateRange(), onValueChange, open: controlledOpen, defaultOpen = false, onOpenChange, label, calendarLabel, openCalendarLabel, viewLabels, locale, weekStartsOn, today, previousMonthLabel, nextMonthLabel, rangeLabels, disabled = false, readOnly = false, controlSize = "md", min, max, isDateDisabled, isRangeDisabled, form, className, onKeyDown, ...props }, forwardedRef) {
8
8
  const panelLabel = calendarLabel ?? `${label} calendar`, triggerLabel = openCalendarLabel ?? `Open calendar: ${label}`;
9
9
  if (!panelLabel.trim() || !triggerLabel.trim())
10
10
  throw new Error("Flood DateRangePicker requires nonempty calendar and trigger labels");
@@ -76,6 +76,6 @@ export const CalendarDateRangePicker = /* @__PURE__ */ forwardRef(function Calen
76
76
  event.preventDefault();
77
77
  requestOpen(true);
78
78
  }
79
- }, end: _jsx(Popover, { title: panelLabel, className: "flood-date-range-picker__panel", align: "end", positionReference: positionReference, style: { "--flood-date-range-picker-width": fieldWidth ? `${fieldWidth}px` : undefined }, open: !blocked && requestedOpen, onOpenChange: requestOpen, disabled: blocked, trigger: trigger => _jsx(IconButton, { ...trigger, label: triggerLabel, variant: "ghost", size: triggerSizes[controlSize], children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { x: "3", y: "5", width: "18", height: "16", rx: "2" }), _jsx("path", { d: "M16 3v4M8 3v4M3 11h18" })] }) }), children: _jsx(RangeCalendar, { label: panelLabel, value: committed, min: min, max: max, isDateDisabled: isDateDisabled, isRangeDisabled: isRangeDisabled, locale: locale, weekStartsOn: weekStartsOn, today: today, previousMonthLabel: previousMonthLabel, nextMonthLabel: nextMonthLabel, rangeLabels: rangeLabels, autoFocus: true, onValueChange: change, onRangeSelect: () => { if (!blocked && !nativeDisabled())
79
+ }, end: _jsx(Popover, { title: panelLabel, chrome: "minimal", className: "flood-date-range-picker__panel", align: "end", positionReference: positionReference, style: { "--flood-date-range-picker-width": fieldWidth ? `${fieldWidth}px` : undefined }, open: !blocked && requestedOpen, onOpenChange: requestOpen, disabled: blocked, trigger: trigger => _jsx(IconButton, { ...trigger, label: triggerLabel, variant: "ghost", size: triggerSizes[controlSize], children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { x: "3", y: "5", width: "18", height: "16", rx: "2" }), _jsx("path", { d: "M16 3v4M8 3v4M3 11h18" })] }) }), children: _jsx(RangeCalendar, { label: panelLabel, viewLabels: viewLabels, value: committed, min: min, max: max, isDateDisabled: isDateDisabled, isRangeDisabled: isRangeDisabled, locale: locale, weekStartsOn: weekStartsOn, today: today, previousMonthLabel: previousMonthLabel, nextMonthLabel: nextMonthLabel, rangeLabels: rangeLabels, autoFocus: true, onValueChange: change, onRangeSelect: () => { if (!blocked && !nativeDisabled())
80
80
  requestOpen(false); } }) }) });
81
81
  });
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
3
3
  import { DatePicker, TimePicker, Fieldset } from "./index.js";
4
4
  /** A civil date and wall-clock time. The host owns timezone conversion. */
5
- export const CivilDateTimePicker = /* @__PURE__ */ forwardRef(function CivilDateTimePicker({ label, validationMessage, timeInterval, hideLabel = false, hideFieldLabels = false, value, defaultValue = { date: "", time: "" }, onValueChange, dateLabel = "Date", timeLabel = "Time", dateName, timeName, form, required = false, disabled = false, readOnly = false, controlSize = "md", precision = "minute", order = "month-day-year", locale = "en-US", minDate, maxDate, minTime, maxTime, step, status, description, className, ...props }, ref) {
5
+ export const CivilDateTimePicker = /* @__PURE__ */ forwardRef(function CivilDateTimePicker({ label, validationMessage, timeInterval, hideLabel = false, hideFieldLabels = false, value, defaultValue = { date: "", time: "" }, onValueChange, dateLabel = "Date", timeLabel = "Time", datePickerLabels, timePickerLabels, dateName, timeName, form, required = false, disabled = false, readOnly = false, controlSize = "md", precision = "minute", order = "month-day-year", locale = "en-US", minDate, maxDate, minTime, maxTime, step, status, description, className, ...props }, ref) {
6
6
  const [local, setLocal] = useState(defaultValue);
7
7
  const root = useRef(null), initial = useRef(defaultValue);
8
8
  useImperativeHandle(ref, () => root.current, []);
@@ -27,5 +27,5 @@ export const CivilDateTimePicker = /* @__PURE__ */ forwardRef(function CivilDate
27
27
  };
28
28
  // Each field owns native validity, reset and submission. Values are deliberately
29
29
  // separate: neither a timezone nor an instant is inferred from this pair.
30
- return _jsx("div", { ...props, ref: root, className: ["flood-date-time-picker", className].filter(Boolean).join(" "), children: _jsx(Fieldset, { legend: label, hideLabel: hideLabel, hint: description, disabled: disabled, children: _jsxs("div", { className: "flood-date-time-picker__fields", children: [_jsx(DatePicker, { label: dateLabel, hideLabel: hideFieldLabels, value: value === undefined ? undefined : selected.date, defaultValue: defaultValue.date, onValueChange: next => change("date", next), name: dateName, form: form, required: required, disabled: disabled, readOnly: readOnly, controlSize: controlSize, order: order, locale: locale, min: minDate, max: maxDate, status: status, validationMessage: validationMessage }), _jsx(TimePicker, { label: timeLabel, hideLabel: hideFieldLabels, value: value === undefined ? undefined : selected.time, defaultValue: defaultValue.time, onValueChange: next => change("time", next), name: timeName, form: form, required: required, disabled: disabled, readOnly: readOnly, controlSize: controlSize, precision: precision, min: minTime, max: maxTime, step: step, interval: timeInterval, status: status })] }) }) });
30
+ return _jsx("div", { ...props, ref: root, className: ["flood-date-time-picker", className].filter(Boolean).join(" "), children: _jsx(Fieldset, { legend: label, hideLabel: hideLabel, hint: description, disabled: disabled, children: _jsxs("div", { className: "flood-date-time-picker__fields", children: [_jsx(DatePicker, { ...datePickerLabels, label: dateLabel, hideLabel: hideFieldLabels, value: value === undefined ? undefined : selected.date, defaultValue: defaultValue.date, onValueChange: next => change("date", next), name: dateName, form: form, required: required, disabled: disabled, readOnly: readOnly, controlSize: controlSize, order: order, locale: locale, min: minDate, max: maxDate, status: status, validationMessage: validationMessage }), _jsx(TimePicker, { ...timePickerLabels, label: timeLabel, hideLabel: hideFieldLabels, value: value === undefined ? undefined : selected.time, defaultValue: defaultValue.time, onValueChange: next => change("time", next), name: timeName, form: form, required: required, disabled: disabled, readOnly: readOnly, controlSize: controlSize, precision: precision, min: minTime, max: maxTime, step: step, interval: timeInterval, status: status })] }) }) });
31
31
  });
@@ -6,7 +6,7 @@ import { parseTime, timeOnStep } from "./time-value.js";
6
6
  const empty = () => ({ start: { date: "", time: "" }, end: { date: "", time: "" } });
7
7
  const copy = (value) => ({ start: { ...value.start }, end: { ...value.end } });
8
8
  const signature = (value) => [value.start.date, value.start.time, value.end.date, value.end.time].join("|");
9
- export const CivilDateTimeRangePicker = /* @__PURE__ */ forwardRef(function CivilDateTimeRangePicker({ label, hideLabel = false, value, defaultValue = empty(), onValueChange, startDateName, startTimeName, endDateName, endTimeName, startLabel = "Start", endLabel = "End", startDateLabel = "Start date", startTimeLabel = "Start time", endDateLabel = "End date", endTimeLabel = "End time", form, required = false, disabled = false, readOnly = false, controlSize = "md", precision = "minute", order = "month-day-year", locale = "en-US", minDate, maxDate, step: explicitStep, interval, status, description, incompleteMessage = "Complete both dates and times.", reversedMessage = "End must not precede start.", className, onBlur, ...props }, ref) {
9
+ export const CivilDateTimeRangePicker = /* @__PURE__ */ forwardRef(function CivilDateTimeRangePicker({ label, hideLabel = false, value, defaultValue = empty(), onValueChange, startDateName, startTimeName, endDateName, endTimeName, startLabel = "Start", endLabel = "End", startDateLabel = "Start date", startTimeLabel = "Start time", endDateLabel = "End date", endTimeLabel = "End time", startDatePickerLabels, endDatePickerLabels, startTimePickerLabels, endTimePickerLabels, form, required = false, disabled = false, readOnly = false, controlSize = "md", precision = "minute", order = "month-day-year", locale = "en-US", minDate, maxDate, step: explicitStep, interval, status, description, incompleteMessage = "Complete both dates and times.", reversedMessage = "End must not precede start.", className, onBlur, ...props }, ref) {
10
10
  if (![label, startLabel, endLabel, startDateLabel, startTimeLabel, endDateLabel, endTimeLabel, incompleteMessage, reversedMessage].every(text => text.trim()))
11
11
  throw new Error("Flood DateTimeRangePicker requires nonempty labels and messages");
12
12
  const initial = useRef(copy(defaultValue)), [local, setLocal] = useState(() => copy(defaultValue)), [proposal, setProposal] = useState();
@@ -47,5 +47,5 @@ export const CivilDateTimeRangePicker = /* @__PURE__ */ forwardRef(function Civi
47
47
  onBlur?.(event);
48
48
  if (!event.defaultPrevented && !event.currentTarget.contains(event.relatedTarget))
49
49
  setProposal(undefined);
50
- }, children: [_jsx(Fieldset, { legend: label, hideLabel: hideLabel, hint: description, disabled: disabled, children: _jsxs(Stack, { gap: "4", children: [_jsx(DateTimePicker, { ...shared, label: startLabel, dateLabel: startDateLabel, timeLabel: startTimeLabel, value: selected.start, defaultValue: initial.current.start, onValueChange: next => change("start", next), validationMessage: constraint }), _jsx(DateTimePicker, { ...shared, label: endLabel, dateLabel: endDateLabel, timeLabel: endTimeLabel, value: selected.end, defaultValue: initial.current.end, onValueChange: next => change("end", next) })] }) }), [[startDateName, selected.start.date], [startTimeName, selected.start.time], [endDateName, selected.end.date], [endTimeName, selected.end.time]].map(([name, part], index) => name && _jsx("input", { type: "hidden", name: name, form: form, value: accepted ? part : "", disabled: disabled }, index))] });
50
+ }, children: [_jsx(Fieldset, { legend: label, hideLabel: hideLabel, hint: description, disabled: disabled, children: _jsxs(Stack, { gap: "4", children: [_jsx(DateTimePicker, { ...shared, datePickerLabels: startDatePickerLabels, timePickerLabels: startTimePickerLabels, label: startLabel, dateLabel: startDateLabel, timeLabel: startTimeLabel, value: selected.start, defaultValue: initial.current.start, onValueChange: next => change("start", next), validationMessage: constraint }), _jsx(DateTimePicker, { ...shared, datePickerLabels: endDatePickerLabels, timePickerLabels: endTimePickerLabels, label: endLabel, dateLabel: endDateLabel, timeLabel: endTimeLabel, value: selected.end, defaultValue: initial.current.end, onValueChange: next => change("end", next) })] }) }), [[startDateName, selected.start.date], [startTimeName, selected.start.time], [endDateName, selected.end.date], [endTimeName, selected.end.time]].map(([name, part], index) => name && _jsx("input", { type: "hidden", name: name, form: form, value: accepted ? part : "", disabled: disabled }, index))] });
51
51
  });
@@ -1,4 +1,4 @@
1
- import { type HTMLAttributes } from "react";
1
+ import { type HTMLAttributes, type ReactNode } from "react";
2
2
  interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
3
3
  title: string;
4
4
  expanded: boolean;
@@ -6,6 +6,7 @@ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
6
6
  disabled?: boolean;
7
7
  controlSize: "sm" | "md" | "lg" | "xl";
8
8
  headingLevel?: 2 | 3 | 4 | 5 | 6;
9
+ headerAction?: ReactNode;
9
10
  }
10
11
  /** Shared original disclosure anatomy. The closed panel stays mounted but hidden. */
11
12
  export declare const Disclosure: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<HTMLDivElement>>;
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createElement, forwardRef, useEffect, useId, useLayoutEffect, useRef, useImperativeHandle } from "react";
3
- import { Button } from "./index.js";
3
+ import { Button, IconSlot } from "./index.js";
4
4
  const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
5
5
  /** Shared original disclosure anatomy. The closed panel stays mounted but hidden. */
6
- export const Disclosure = /* @__PURE__ */ forwardRef(function Disclosure({ title, expanded, onExpandedChange, disabled, controlSize, headingLevel, className, children, id: explicitId, ...props }, ref) {
6
+ export const Disclosure = /* @__PURE__ */ forwardRef(function Disclosure({ title, expanded, onExpandedChange, disabled, controlSize, headingLevel, headerAction, className, children, id: explicitId, ...props }, ref) {
7
7
  const generatedId = useId(), id = explicitId ?? `flood-disclosure-${generatedId}`;
8
8
  const root = useRef(null), panel = useRef(null), trigger = useRef(null);
9
9
  const focusedInside = useRef(false);
@@ -15,7 +15,7 @@ export const Disclosure = /* @__PURE__ */ forwardRef(function Disclosure({ title
15
15
  if (!expanded)
16
16
  focusedInside.current = false;
17
17
  }, [expanded, disabled]);
18
- const button = _jsxs(Button, { ref: trigger, variant: "ghost", size: controlSize, id: `${id}-trigger`, type: "button", disabled: disabled, "aria-expanded": expanded, "aria-controls": `${id}-panel`, className: "flood-disclosure__trigger", onClick: () => onExpandedChange(!expanded), children: [_jsx("span", { className: "flood-disclosure__title", children: title }), _jsx("span", { className: "flood-disclosure__indicator", "aria-hidden": "true" })] });
19
- return _jsxs("div", { ...props, id: id, ref: root, "data-open": expanded, className: ["flood-disclosure", `flood-control--${controlSize}`, className].filter(Boolean).join(" "), children: [createElement(headingLevel ? `h${headingLevel}` : "div", { className: "flood-disclosure__heading" }, button), _jsx("div", { ref: panel, id: `${id}-panel`, className: "flood-disclosure__panel", hidden: !expanded, onFocusCapture: () => { focusedInside.current = true; }, onBlurCapture: event => { if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget))
18
+ const button = _jsxs(Button, { ref: trigger, variant: "ghost", size: controlSize, id: `${id}-trigger`, type: "button", disabled: disabled, "aria-expanded": expanded, "aria-controls": `${id}-panel`, className: "flood-disclosure__trigger", onClick: () => onExpandedChange(!expanded), children: [_jsx("span", { className: "flood-disclosure__title", children: title }), _jsx(IconSlot, { size: "2xs", className: "flood-disclosure__indicator", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m3 8 9 8 9-8" }) }) })] });
19
+ return _jsxs("div", { ...props, id: id, ref: root, "data-open": expanded, className: ["flood-disclosure", `flood-control--${controlSize}`, className].filter(Boolean).join(" "), children: [createElement(headingLevel ? `h${headingLevel}` : "div", { className: "flood-disclosure__heading" }, button, headerAction != null && _jsx("div", { className: "flood-disclosure__action", children: headerAction })), _jsx("div", { ref: panel, id: `${id}-panel`, className: "flood-disclosure__panel", hidden: !expanded, onFocusCapture: () => { focusedInside.current = true; }, onBlurCapture: event => { if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget))
20
20
  focusedInside.current = false; }, children: children })] });
21
21
  });
package/dist/editable.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
3
- import { Button, IconButton, IconSlot, Input, InputGroup } from "./index.js";
3
+ import { Button, IconButton, IconSlot, Input, InputGroup, Text } from "./index.js";
4
+ import { mergeDescriptions } from "./field-state.js";
5
+ import { positionLayer, registerLayer } from "./overlay-runtime.js";
4
6
  const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
5
7
  const cx = (...values) => values.filter(Boolean).join(" ");
6
8
  /** A draft is private until explicitly committed; native forms receive committed text. */
7
- export const InlineEditable = /* @__PURE__ */ forwardRef(function InlineEditable({ label, hideLabel = false, value, defaultValue = "", onValueChange, editing, defaultEditing = false, onEditingChange, onCommit, onCancel, blurAction = "commit", readOnly = false, disabled = false, placeholder = "Empty", controlSize = "md", showActions = true, editLabel = "Edit", saveLabel = "Save", cancelLabel = "Cancel", inputRef, name, form, id: explicitId, className, onBlur, onKeyDown, ...props }, forwardedRef) {
9
+ export const InlineEditable = /* @__PURE__ */ forwardRef(function InlineEditable({ label, hideLabel = false, value, defaultValue = "", onValueChange, editing, defaultEditing = false, onEditingChange, onCommit, onCancel, validate, inputMode, previewVariant = "subtle", editorOverlayWidth, blurAction = "commit", readOnly = false, disabled = false, placeholder = "Empty", controlSize = "md", showActions = true, editLabel = "Edit", saveLabel = "Save", cancelLabel = "Cancel", inputRef, name, form, id: explicitId, className, onBlur, onKeyDown, ...props }, forwardedRef) {
8
10
  if (![label, editLabel, saveLabel, cancelLabel].every(text => text.trim()))
9
11
  throw new Error("Flood Editable requires nonempty control and action labels");
10
12
  const initial = useRef(defaultValue), [local, setLocal] = useState(defaultValue);
@@ -12,9 +14,13 @@ export const InlineEditable = /* @__PURE__ */ forwardRef(function InlineEditable
12
14
  const [localEditing, setLocalEditing] = useState(defaultEditing);
13
15
  const requested = editing ?? localEditing, active = requested && !disabled && !readOnly;
14
16
  const [draft, setDraft] = useState(committed);
17
+ const [validationError, setValidationError] = useState();
15
18
  const root = useRef(null), editor = useRef(null), bridge = useRef(null), preview = useRef(null);
16
- const current = useRef({ committed, active, draft, value, editing, onValueChange, onEditingChange, onCommit, onCancel });
17
- current.current = { committed, active, draft, value, editing, onValueChange, onEditingChange, onCommit, onCancel };
19
+ const layer = useRef(null);
20
+ const floating = editorOverlayWidth !== undefined;
21
+ const fitDraft = editorOverlayWidth === "fit-content";
22
+ const current = useRef({ committed, active, draft, value, editing, onValueChange, onEditingChange, onCommit, onCancel, validate });
23
+ current.current = { committed, active, draft, value, editing, onValueChange, onEditingChange, onCommit, onCancel, validate };
18
24
  const composing = useRef(false), resolved = useRef(false), wasActive = useRef(false);
19
25
  const generated = useId(), id = explicitId ?? `flood-editable-${generated}`, inputId = `${id}-input`, previewId = `${id}-preview`;
20
26
  useImperativeHandle(forwardedRef, () => root.current, []);
@@ -34,8 +40,12 @@ export const InlineEditable = /* @__PURE__ */ forwardRef(function InlineEditable
34
40
  const finish = (action, restoreFocus) => {
35
41
  if (!current.current.active || resolved.current || (composing.current && action === "commit") || editor.current?.matches(":disabled"))
36
42
  return;
37
- resolved.current = true;
38
43
  const next = current.current.draft;
44
+ const message = action === "commit" ? current.current.validate?.(next) : undefined;
45
+ setValidationError(message);
46
+ if (message)
47
+ return;
48
+ resolved.current = true;
39
49
  if (action === "commit") {
40
50
  if (next !== current.current.committed) {
41
51
  if (current.current.value === undefined)
@@ -62,10 +72,35 @@ export const InlineEditable = /* @__PURE__ */ forwardRef(function InlineEditable
62
72
  // A controlled host may refuse to close and allow a later deliberate attempt.
63
73
  queueMicrotask(() => { resolved.current = false; });
64
74
  };
75
+ useBrowserLayoutEffect(() => {
76
+ const panel = layer.current, anchor = preview.current;
77
+ if (!active || !floating || !panel || !anchor)
78
+ return;
79
+ const document = panel.ownerDocument, view = document.defaultView;
80
+ panel.showPopover();
81
+ let frame = 0;
82
+ const position = () => {
83
+ positionLayer(anchor, panel, "bottom", "end", true);
84
+ frame = view.requestAnimationFrame(position);
85
+ };
86
+ position();
87
+ const unregister = registerLayer(document, {
88
+ panel, anchor: () => anchor, close: () => finish("cancel", true), closeOnFocusOutside: false,
89
+ outside: () => { if (blurAction !== "none")
90
+ finish(blurAction, false); },
91
+ });
92
+ return () => {
93
+ unregister();
94
+ view.cancelAnimationFrame(frame);
95
+ if (panel.matches(":popover-open"))
96
+ panel.hidePopover();
97
+ };
98
+ }, [active, floating, blurAction]);
65
99
  useBrowserLayoutEffect(() => {
66
100
  let frame = 0;
67
101
  if (active && !wasActive.current) {
68
102
  setDraft(current.current.committed);
103
+ setValidationError(undefined);
69
104
  resolved.current = false;
70
105
  composing.current = false;
71
106
  const view = editor.current?.ownerDocument.defaultView;
@@ -106,7 +141,20 @@ export const InlineEditable = /* @__PURE__ */ forwardRef(function InlineEditable
106
141
  return () => { owner.removeEventListener("reset", reset); clearTimeout(timer); };
107
142
  }, [form]);
108
143
  const glyph = (kind) => _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: kind === "save" ? _jsx("path", { d: "m5 12 4 4L19 6" }) : kind === "cancel" ? _jsx("path", { d: "m6 6 12 12M18 6 6 18" }) : _jsx(_Fragment, { children: _jsx("path", { d: "m15 5 4 4M4 20l4-1L20 7a2.1 2.1 0 0 0-3-3L5 16l-1 4Z" }) }) });
109
- const editorProps = { id: inputId, "aria-describedby": props["aria-describedby"], "aria-invalid": props["aria-invalid"], controlSize, disabled };
144
+ const errorId = `${inputId}-error`;
145
+ const editorProps = { id: inputId, inputMode, "aria-describedby": mergeDescriptions(props["aria-describedby"], active && validationError ? errorId : undefined),
146
+ "aria-invalid": active && validationError ? true : props["aria-invalid"], controlSize, disabled };
147
+ const editButton = _jsxs(Button, { ref: preview, id: previewId, type: "button", variant: previewVariant, size: controlSize, disabled: disabled, className: "flood-editable__preview", "aria-describedby": props["aria-describedby"], "aria-label": `${editLabel}: ${label}. ${committed || placeholder}`, "aria-hidden": active && floating || undefined, tabIndex: active && floating ? -1 : undefined, onClick: start, children: [_jsx("span", { className: "flood-editable__value", "data-empty": !committed || undefined, children: committed || placeholder }), _jsx(IconSlot, { size: controlSize, children: glyph("edit") })] });
148
+ const editorContent = active && _jsxs(_Fragment, { children: [_jsx(InputGroup, { ...editorProps, ref: editor, value: draft, placeholder: placeholder, size: fitDraft ? Math.max(4, (draft || placeholder).length + 1) : undefined, onChange: event => { setValidationError(undefined); setDraft(event.currentTarget.value); current.current.draft = event.currentTarget.value; }, onCompositionStart: () => { composing.current = true; }, onCompositionEnd: event => {
149
+ composing.current = false;
150
+ setDraft(event.currentTarget.value);
151
+ current.current.draft = event.currentTarget.value;
152
+ const document = event.currentTarget.ownerDocument;
153
+ if (blurAction !== "none" && !root.current?.contains(document.activeElement))
154
+ queueMicrotask(() => finish(blurAction, false));
155
+ }, end: showActions ? _jsxs("div", { className: "flood-editable__actions", children: [_jsx(IconButton, { type: "button", label: saveLabel, size: controlSize, variant: "ghost", onPointerDown: event => { if (event.pointerType === "mouse")
156
+ event.preventDefault(); }, onMouseDown: event => event.preventDefault(), onClick: () => finish("commit", true), children: glyph("save") }), _jsx(IconButton, { type: "button", label: cancelLabel, size: controlSize, variant: "ghost", onPointerDown: event => { if (event.pointerType === "mouse")
157
+ event.preventDefault(); }, onMouseDown: event => event.preventDefault(), onClick: () => finish("cancel", true), children: glyph("cancel") })] }) : undefined }), validationError && _jsx(Text, { as: "p", id: errorId, role: "alert", size: "helper", tone: "error", className: "flood-editable__error", children: validationError })] });
110
158
  return _jsxs("div", { ...props, ref: root, id: id, className: cx("flood-editable", className), "data-editing": active || undefined, "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, onKeyDown: event => {
111
159
  onKeyDown?.(event);
112
160
  if (event.defaultPrevented || !current.current.active || event.target !== editor.current || event.nativeEvent.isComposing || composing.current || event.keyCode === 229 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
@@ -120,16 +168,7 @@ export const InlineEditable = /* @__PURE__ */ forwardRef(function InlineEditable
120
168
  if (event.defaultPrevented || !current.current.active || event.currentTarget.contains(event.relatedTarget) || blurAction === "none")
121
169
  return;
122
170
  finish(blurAction, false);
123
- }, children: [_jsx("label", { htmlFor: active || readOnly ? inputId : previewId, className: cx("flood-label", hideLabel && "flood-visually-hidden"), children: label }), _jsx("input", { ref: bridge, type: "hidden", name: name, form: form, value: committed, disabled: disabled }), active ? _jsx(InputGroup, { ...editorProps, ref: editor, value: draft, placeholder: placeholder, onChange: event => { setDraft(event.currentTarget.value); current.current.draft = event.currentTarget.value; }, onCompositionStart: () => { composing.current = true; }, onCompositionEnd: event => {
124
- composing.current = false;
125
- setDraft(event.currentTarget.value);
126
- current.current.draft = event.currentTarget.value;
127
- const document = event.currentTarget.ownerDocument;
128
- if (blurAction !== "none" && !root.current?.contains(document.activeElement))
129
- queueMicrotask(() => finish(blurAction, false));
130
- }, end: showActions ? _jsxs("div", { className: "flood-editable__actions", children: [_jsx(IconButton, { type: "button", label: saveLabel, size: controlSize, variant: "ghost", onPointerDown: event => { if (event.pointerType === "mouse")
131
- event.preventDefault(); }, onMouseDown: event => event.preventDefault(), onClick: () => finish("commit", true), children: glyph("save") }), _jsx(IconButton, { type: "button", label: cancelLabel, size: controlSize, variant: "ghost", onPointerDown: event => { if (event.pointerType === "mouse")
132
- event.preventDefault(); }, onMouseDown: event => event.preventDefault(), onClick: () => finish("cancel", true), children: glyph("cancel") })] }) : undefined })
133
- : readOnly ? _jsx(Input, { ...editorProps, ref: editor, value: committed, readOnly: true })
134
- : _jsxs(Button, { ref: preview, id: previewId, type: "button", variant: "subtle", size: controlSize, disabled: disabled, className: "flood-editable__preview", "aria-describedby": props["aria-describedby"], "aria-label": `${editLabel}: ${label}. ${committed || placeholder}`, onClick: start, children: [_jsx("span", { className: "flood-editable__value", "data-empty": !committed || undefined, children: committed || placeholder }), _jsx(IconSlot, { size: controlSize, children: glyph("edit") })] })] });
171
+ }, children: [_jsx("label", { htmlFor: active || readOnly ? inputId : previewId, className: cx("flood-label", hideLabel && "flood-visually-hidden"), children: label }), _jsx("input", { ref: bridge, type: "hidden", name: name, form: form, value: committed, disabled: disabled }), readOnly ? _jsx(Input, { ...editorProps, ref: editor, value: committed, readOnly: true }) : (!active || floating) && editButton, active && (floating
172
+ ? _jsx("div", { ref: layer, popover: "manual", className: "flood-overlay-panel flood-editable__overlay", "data-fit-draft": fitDraft || undefined, style: { inlineSize: editorOverlayWidth }, children: editorContent })
173
+ : editorContent)] });
135
174
  });
@@ -0,0 +1,3 @@
1
+ import { type FilterConditionProps } from './index.js';
2
+ /** One controlled finite-choice predicate. Query execution and draft/application belong to the host. */
3
+ export declare const PredicateRow: import("react").ForwardRefExoticComponent<FilterConditionProps & import("react").RefAttributes<HTMLFieldSetElement>>;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import { Field, Fieldset, Grid, IconButton, MultiSelect, Select } from './index.js';
4
+ /** One controlled finite-choice predicate. Query execution and draft/application belong to the host. */
5
+ export const PredicateRow = /* @__PURE__ */ forwardRef(function PredicateRow({ label, properties, value, onValueChange, onRemove, controlSize = 'sm', labels, disabled = false, hideLabel = true, className, ...props }, ref) {
6
+ const property = properties.find(property => property.id === value.property);
7
+ if (!label.trim() || !properties.length || new Set(properties.map(property => property.id)).size !== properties.length ||
8
+ properties.some(property => !property.id.trim() || !property.label.trim()) || !property ||
9
+ !['is', 'is-not'].includes(value.operator))
10
+ throw new Error('Flood FilterCondition requires named unique properties and a supported predicate');
11
+ const names = {
12
+ property: `${label} property`, operator: `${label} operator`, values: `${label} values`,
13
+ include: 'is any of', exclude: 'is none of', placeholder: 'Choose values…', remove: `Remove ${label.charAt(0).toLowerCase()}${label.slice(1)}`,
14
+ ...labels,
15
+ };
16
+ return _jsx(Fieldset, { ...props, ref: ref, legend: label, hideLabel: hideLabel, disabled: disabled, className: ['flood-filter-condition', className].filter(Boolean).join(' '), "data-removable": Boolean(onRemove), children: _jsxs(Grid, { gap: "2", className: "flood-filter-condition__row", children: [_jsx(Field, { label: names.property, hideLabel: true, children: field => _jsx(Select, { ...field, controlSize: controlSize, value: value.property, disabled: disabled, onChange: event => { if (!disabled)
17
+ onValueChange({ ...value, property: event.currentTarget.value, values: [] }); }, children: properties.map(property => _jsx("option", { value: property.id, disabled: property.disabled, children: property.label }, property.id)) }) }), _jsx(Field, { label: names.operator, hideLabel: true, children: field => _jsxs(Select, { ...field, controlSize: controlSize, value: value.operator, disabled: disabled, onChange: event => { if (!disabled)
18
+ onValueChange({ ...value, operator: event.currentTarget.value }); }, children: [_jsx("option", { value: "is", children: names.include }), _jsx("option", { value: "is-not", children: names.exclude })] }) }), _jsx("div", { className: "flood-filter-condition__values", children: _jsx(Field, { label: names.values, hideLabel: true, children: field => _jsx(MultiSelect, { ...field, controlSize: controlSize, options: property.options, value: value.values, placeholder: names.placeholder, disabled: disabled, onValueChange: values => { if (!disabled)
19
+ onValueChange({ ...value, values }); } }) }) }), onRemove && _jsx(IconButton, { label: names.remove, size: controlSize, variant: "subtle", disabled: disabled, className: "flood-filter-condition__remove", onClick: () => { if (!disabled)
20
+ onRemove(); }, children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "m6 6 12 12M18 6 6 18" }) }) })] }) });
21
+ });
@@ -0,0 +1,2 @@
1
+ import { type FilterGroupProps } from './index.js';
2
+ export declare const PredicateGroup: import("react").ForwardRefExoticComponent<FilterGroupProps & import("react").RefAttributes<HTMLFieldSetElement>>;