@flood-md/react 0.2.6 → 0.2.8

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 (85) hide show
  1. package/README.md +42 -1
  2. package/dist/alert-dialog.js +1 -1
  3. package/dist/calendar-period-grid.d.ts +17 -0
  4. package/dist/calendar-period-grid.js +13 -0
  5. package/dist/calendar-value.d.ts +10 -0
  6. package/dist/calendar-value.js +48 -0
  7. package/dist/calendar.d.ts +21 -0
  8. package/dist/calendar.js +259 -0
  9. package/dist/checkbox.js +1 -1
  10. package/dist/choice-group.js +1 -1
  11. package/dist/collection-cards.d.ts +4 -0
  12. package/dist/collection-cards.js +21 -0
  13. package/dist/color-controls.d.ts +5 -0
  14. package/dist/color-controls.js +110 -0
  15. package/dist/combobox.js +22 -13
  16. package/dist/command-list.d.ts +3 -0
  17. package/dist/command-list.js +93 -0
  18. package/dist/content-elements.js +7 -7
  19. package/dist/context-menu.d.ts +2 -0
  20. package/dist/context-menu.js +76 -0
  21. package/dist/data-display.js +2 -2
  22. package/dist/data-table.d.ts +2 -0
  23. package/dist/data-table.js +58 -0
  24. package/dist/date-field.d.ts +2 -0
  25. package/dist/date-field.js +172 -0
  26. package/dist/date-picker.d.ts +3 -0
  27. package/dist/date-picker.js +108 -0
  28. package/dist/date-range-field.d.ts +3 -0
  29. package/dist/date-range-field.js +84 -0
  30. package/dist/date-range-picker.d.ts +2 -0
  31. package/dist/date-range-picker.js +81 -0
  32. package/dist/date-range-value.d.ts +8 -0
  33. package/dist/date-range-value.js +13 -0
  34. package/dist/date-time-picker.d.ts +3 -0
  35. package/dist/date-time-picker.js +31 -0
  36. package/dist/date-time-range-picker.d.ts +2 -0
  37. package/dist/date-time-range-picker.js +51 -0
  38. package/dist/date-value.d.ts +10 -0
  39. package/dist/date-value.js +31 -0
  40. package/dist/disclosure.js +1 -1
  41. package/dist/editable.d.ts +3 -0
  42. package/dist/editable.js +135 -0
  43. package/dist/error-summary.d.ts +3 -0
  44. package/dist/error-summary.js +39 -0
  45. package/dist/feedback-actions.js +2 -2
  46. package/dist/feedback.js +2 -2
  47. package/dist/file-input.js +1 -1
  48. package/dist/file-records.d.ts +5 -0
  49. package/dist/file-records.js +55 -0
  50. package/dist/form-controls.js +25 -13
  51. package/dist/foundations.css +387 -1
  52. package/dist/hover-card.d.ts +3 -0
  53. package/dist/hover-card.js +144 -0
  54. package/dist/index.d.ts +661 -2
  55. package/dist/index.js +190 -86
  56. package/dist/menu.d.ts +27 -1
  57. package/dist/menu.js +327 -89
  58. package/dist/navigation-layout.js +6 -6
  59. package/dist/navigation-tree.d.ts +3 -0
  60. package/dist/navigation-tree.js +98 -0
  61. package/dist/navigation.js +3 -3
  62. package/dist/numeric-input.js +1 -1
  63. package/dist/overlays.js +11 -10
  64. package/dist/range-calendar.d.ts +2 -0
  65. package/dist/range-calendar.js +35 -0
  66. package/dist/resizable-panels.d.ts +3 -0
  67. package/dist/resizable-panels.js +205 -0
  68. package/dist/scroll-area.js +1 -1
  69. package/dist/select.js +1 -1
  70. package/dist/slider.js +1 -1
  71. package/dist/tag-input.d.ts +3 -0
  72. package/dist/tag-input.js +181 -0
  73. package/dist/text-inputs.js +2 -2
  74. package/dist/time-field.d.ts +2 -0
  75. package/dist/time-field.js +176 -0
  76. package/dist/time-picker.d.ts +2 -0
  77. package/dist/time-picker.js +83 -0
  78. package/dist/time-value.d.ts +12 -0
  79. package/dist/time-value.js +36 -0
  80. package/dist/toolbar.js +1 -1
  81. package/dist/tree-select.d.ts +3 -0
  82. package/dist/tree-select.js +113 -0
  83. package/dist/tree.d.ts +3 -0
  84. package/dist/tree.js +206 -0
  85. package/package.json +2 -2
package/dist/tree.js ADDED
@@ -0,0 +1,206 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
3
+ const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
4
+ const searchable = (value) => value.normalize("NFKD").replace(/\p{M}/gu, "").toLocaleLowerCase();
5
+ const values = (value) => [...new Set(typeof value === "string" ? (value ? [value] : []) : value ?? [])];
6
+ /** Application selection, not navigation or a form field; expansion and selection are independent. */
7
+ export const SelectionTree = /* @__PURE__ */ forwardRef(function SelectionTree({ label, hideLabel = false, items, value, defaultValue, onValueChange, multiple = false, expandedValues, defaultExpandedValues = [], onExpandedValuesChange, disabled = false, readOnly = false, controlSize = "md", emptyMessage = "No items.", loadingMessage = "Loading…", unloadedMessage = "Content not loaded.", readOnlyMessage = "Selection is read only.", id: explicitId, className, onKeyDown, onClick, onDoubleClick, onFocus, onBlur, ...props }, forwardedRef) {
8
+ if (![label, emptyMessage, loadingMessage, unloadedMessage, readOnlyMessage].every(text => text.trim()))
9
+ throw new Error("Flood Tree requires nonempty labels and state messages");
10
+ const all = new Map();
11
+ const collect = (siblings, parent, level) => {
12
+ if (level > 256)
13
+ throw new Error("Flood Tree nesting exceeds the supported depth");
14
+ siblings.forEach((item, index) => {
15
+ if (!item.value.trim() || !item.label.trim() || all.has(item.value))
16
+ throw new Error("Flood Tree items require globally unique nonempty values and labels");
17
+ all.set(item.value, { item, parent, level, position: index + 1, size: siblings.length, branch: !!item.hasChildren || !!item.loading || !!item.children?.length });
18
+ if (item.children)
19
+ collect(item.children, item.value, level + 1);
20
+ });
21
+ };
22
+ collect(items, undefined, 1);
23
+ const [localSelected, setLocalSelected] = useState(() => values(defaultValue));
24
+ const [localExpanded, setLocalExpanded] = useState(() => values(defaultExpandedValues));
25
+ const selectedValues = values(value === undefined ? localSelected : value).filter(key => all.has(key) && all.get(key).item.selectable !== false), selected = new Set(multiple ? selectedValues : selectedValues.slice(0, 1));
26
+ const expandedList = values(expandedValues ?? localExpanded), expanded = new Set(expandedList);
27
+ const visible = [];
28
+ for (const entry of all.values()) {
29
+ let parent = entry.parent, shown = true;
30
+ while (parent) {
31
+ if (!expanded.has(parent)) {
32
+ shown = false;
33
+ break;
34
+ }
35
+ parent = all.get(parent)?.parent;
36
+ }
37
+ if (shown)
38
+ visible.push(entry);
39
+ }
40
+ const visibleValues = new Set(visible.map(entry => entry.item.value));
41
+ const root = useRef(null), tree = useRef(null), nodes = useRef(new Map());
42
+ const previous = useRef({ all, visible });
43
+ const [focused, setFocused] = useState();
44
+ const [direction, setDirection] = useState("ltr");
45
+ const document = root.current?.ownerDocument, domFocused = document?.activeElement;
46
+ const wasFocused = !!domFocused && !!root.current?.contains(domFocused) && domFocused.hasAttribute("data-tree-value");
47
+ let active = focused && visibleValues.has(focused) ? focused : undefined;
48
+ if (!active && focused) {
49
+ let ancestor = all.get(focused)?.parent ?? previous.current.all.get(focused)?.parent;
50
+ while (ancestor) {
51
+ if (visibleValues.has(ancestor)) {
52
+ active = ancestor;
53
+ break;
54
+ }
55
+ ancestor = all.get(ancestor)?.parent ?? previous.current.all.get(ancestor)?.parent;
56
+ }
57
+ if (!active) {
58
+ const old = previous.current.visible.findIndex(entry => entry.item.value === focused);
59
+ active = visible[Math.min(Math.max(old, 0), visible.length - 1)]?.item.value;
60
+ }
61
+ }
62
+ active ??= visible.find(entry => selected.has(entry.item.value))?.item.value ?? visible[0]?.item.value;
63
+ const generated = useId(), id = explicitId ?? `flood-tree-${generated}`;
64
+ const itemId = (key) => `${id}-item-${encodeURIComponent(key)}`;
65
+ useImperativeHandle(forwardedRef, () => root.current, []);
66
+ useBrowserLayoutEffect(() => {
67
+ const node = root.current;
68
+ if (!node)
69
+ return;
70
+ const update = () => setDirection(node.ownerDocument.defaultView.getComputedStyle(node).direction);
71
+ update();
72
+ const observer = new node.ownerDocument.defaultView.MutationObserver(update);
73
+ let ancestor = node;
74
+ while (ancestor) {
75
+ observer.observe(ancestor, { attributes: true, attributeFilter: ["dir"] });
76
+ ancestor = ancestor.parentElement;
77
+ }
78
+ return () => observer.disconnect();
79
+ }, [props.dir]);
80
+ useBrowserLayoutEffect(() => {
81
+ previous.current = { all, visible };
82
+ if (focused !== active)
83
+ setFocused(active);
84
+ const current = document?.activeElement;
85
+ if (wasFocused && !disabled && (current === document?.body || !!current && !!root.current?.contains(current))) {
86
+ if (active && nodes.current.get(active) !== current)
87
+ nodes.current.get(active)?.focus({ preventScroll: true });
88
+ else if (!active && tree.current !== current)
89
+ tree.current?.focus({ preventScroll: true });
90
+ }
91
+ });
92
+ const search = useRef({ text: "", time: 0 });
93
+ const focusItem = (key) => { setFocused(key); nodes.current.get(key)?.focus({ preventScroll: true }); };
94
+ const toggle = (entry) => {
95
+ if (disabled || entry.item.disabled || !entry.branch)
96
+ return;
97
+ const next = expanded.has(entry.item.value) ? expandedList.filter(key => key !== entry.item.value) : [...expandedList, entry.item.value];
98
+ if (expandedValues === undefined)
99
+ setLocalExpanded(next);
100
+ onExpandedValuesChange?.(next);
101
+ };
102
+ const select = (entry) => {
103
+ if (disabled || readOnly || entry.item.disabled || entry.item.readOnly || entry.item.selectable === false)
104
+ return;
105
+ const key = entry.item.value, next = multiple ? (selected.has(key) ? selectedValues.filter(value => value !== key) : [...selectedValues, key]) : [key];
106
+ if (next.length === selectedValues.length && next.every((key, index) => key === selectedValues[index]))
107
+ return;
108
+ if (value === undefined)
109
+ setLocalSelected(next);
110
+ onValueChange?.(multiple ? next : key);
111
+ };
112
+ const eventEntry = (target) => {
113
+ if (!(target instanceof (root.current?.ownerDocument.defaultView?.Element ?? Element)))
114
+ return;
115
+ const node = target.closest("[data-tree-entry]");
116
+ if (!node || !tree.current?.contains(node))
117
+ return;
118
+ return all.get(node.dataset.treeValue);
119
+ };
120
+ const render = (siblings) => siblings.map(item => {
121
+ const entry = all.get(item.value), open = entry.branch && expanded.has(item.value), selectedItem = selected.has(item.value), status = open ? (item.loading ? loadingMessage : item.hasChildren && item.children === undefined ? unloadedMessage : entry.branch && !item.children?.length ? emptyMessage : undefined) : undefined;
122
+ return _jsxs("div", { className: "flood-tree__branch", "data-tree-entry": true, "data-tree-value": item.value, "data-has-icon": item.icon != null || undefined, style: { "--flood-tree-depth": entry.level - 1 }, children: [_jsx("div", { id: itemId(item.value), ref: node => { if (node)
123
+ nodes.current.set(item.value, node);
124
+ else
125
+ nodes.current.delete(item.value); }, className: "flood-tree__item", role: "treeitem", "data-tree-value": item.value, "data-disabled": disabled || item.disabled || undefined, "data-readonly": readOnly || item.readOnly || undefined, tabIndex: !disabled && active === item.value ? 0 : -1, "aria-labelledby": [`${itemId(item.value)}-label`, item.description ? `${itemId(item.value)}-description` : undefined].filter(Boolean).join(" "), "aria-describedby": [status ? `${itemId(item.value)}-status` : undefined, readOnly || item.readOnly ? `${id}-readonly` : undefined].filter(Boolean).join(" ") || undefined, "aria-owns": open && item.children?.length ? `${itemId(item.value)}-group` : undefined, "aria-level": entry.level, "aria-posinset": entry.position, "aria-setsize": entry.size, "aria-expanded": entry.branch ? open : undefined, "aria-selected": item.selectable === false ? undefined : selectedItem, "aria-disabled": disabled || item.disabled || undefined, "aria-busy": item.loading || undefined, children: _jsxs("div", { className: "flood-tree__row", "data-tree-row": true, "data-selected": selectedItem || undefined, children: [_jsx("span", { className: "flood-tree__disclosure", "data-tree-toggle": entry.branch ? "true" : undefined, "aria-hidden": "true", children: entry.branch && _jsx("svg", { viewBox: "0 0 24 24", fill: "none", className: "flood-tree__chevron", "data-expanded": open || undefined, children: _jsx("path", { d: "m9 5 7 7-7 7", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }) }), item.icon != null && _jsx("span", { className: "flood-tree__icon", "aria-hidden": "true", children: item.icon }), _jsxs("span", { className: "flood-tree__text", children: [_jsx("span", { id: `${itemId(item.value)}-label`, className: "flood-tree__label", children: item.label }), item.description && _jsxs(_Fragment, { children: [" ", _jsx("span", { id: `${itemId(item.value)}-description`, className: "flood-tree__description", children: item.description })] })] })] }) }), open && !!item.children?.length && _jsx("div", { id: `${itemId(item.value)}-group`, role: "group", className: "flood-tree__group", children: render(item.children) }), status && _jsx("div", { id: `${itemId(item.value)}-status`, className: "flood-tree__branch-status", role: "status", children: status })] }, item.value);
126
+ });
127
+ return _jsxs("div", { ...props, id: id, ref: root, className: ["flood-tree", `flood-control--${controlSize}`, className].filter(Boolean).join(" "), "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, "data-tree-direction": direction, onFocus: event => { onFocus?.(event); const entry = eventEntry(event.target); if (entry)
128
+ setFocused(entry.item.value); }, onBlur: onBlur, onClick: event => {
129
+ onClick?.(event);
130
+ if (event.defaultPrevented || disabled)
131
+ return;
132
+ const entry = eventEntry(event.target);
133
+ if (!entry)
134
+ return;
135
+ focusItem(entry.item.value);
136
+ if (event.detail > 1)
137
+ return;
138
+ if (event.target.closest("[data-tree-toggle='true']"))
139
+ toggle(entry);
140
+ else if (entry.item.selectable === false && entry.branch)
141
+ toggle(entry);
142
+ else
143
+ select(entry);
144
+ }, onDoubleClick: event => {
145
+ onDoubleClick?.(event);
146
+ if (event.defaultPrevented || disabled)
147
+ return;
148
+ const entry = eventEntry(event.target);
149
+ if (entry && entry.item.selectable !== false && !event.target.closest("[data-tree-toggle='true']"))
150
+ toggle(entry);
151
+ }, onKeyDown: event => {
152
+ onKeyDown?.(event);
153
+ if (event.defaultPrevented || disabled || event.nativeEvent.isComposing || event.keyCode === 229 || event.altKey || event.ctrlKey || event.metaKey || (event.shiftKey && event.key.length !== 1))
154
+ return;
155
+ const entry = eventEntry(event.target);
156
+ if (!entry)
157
+ return;
158
+ const index = visible.findIndex(item => item.item.value === entry.item.value);
159
+ const rtl = root.current?.ownerDocument.defaultView.getComputedStyle(root.current).direction === "rtl";
160
+ const forward = rtl ? "ArrowLeft" : "ArrowRight", back = rtl ? "ArrowRight" : "ArrowLeft";
161
+ if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Home" || event.key === "End") {
162
+ event.preventDefault();
163
+ const next = event.key === "Home" ? 0 : event.key === "End" ? visible.length - 1 : Math.max(0, Math.min(visible.length - 1, index + (event.key === "ArrowDown" ? 1 : -1)));
164
+ if (visible[next])
165
+ focusItem(visible[next].item.value);
166
+ return;
167
+ }
168
+ if (event.key === forward) {
169
+ event.preventDefault();
170
+ if (entry.branch && !expanded.has(entry.item.value))
171
+ toggle(entry);
172
+ else if (entry.branch && entry.item.children?.length)
173
+ focusItem(entry.item.children[0].value);
174
+ return;
175
+ }
176
+ if (event.key === back) {
177
+ event.preventDefault();
178
+ if (entry.branch && expanded.has(entry.item.value))
179
+ toggle(entry);
180
+ else if (entry.parent)
181
+ focusItem(entry.parent);
182
+ return;
183
+ }
184
+ if (event.key === "Enter" || event.key === " ") {
185
+ event.preventDefault();
186
+ if (entry.item.selectable === false && entry.branch)
187
+ toggle(entry);
188
+ else
189
+ select(entry);
190
+ return;
191
+ }
192
+ if (event.key.length === 1 && event.key.trim()) {
193
+ const now = Date.now(), text = searchable(event.key);
194
+ search.current = { text: now - search.current.time > 700 ? text : search.current.text + text, time: now };
195
+ const query = [...search.current.text].every(letter => letter === text) ? text : search.current.text;
196
+ for (let offset = 1; offset <= visible.length; offset++) {
197
+ const candidate = visible[(index + offset) % visible.length];
198
+ if (searchable(candidate.item.label).startsWith(query)) {
199
+ event.preventDefault();
200
+ focusItem(candidate.item.value);
201
+ break;
202
+ }
203
+ }
204
+ }
205
+ }, children: [_jsx("span", { className: ["flood-label", hideLabel ? "flood-visually-hidden" : ""].filter(Boolean).join(" "), id: `${id}-label`, children: label }), _jsx("span", { id: `${id}-readonly`, hidden: true, children: readOnlyMessage }), _jsx("div", { ref: tree, role: "tree", "aria-labelledby": `${id}-label`, "aria-multiselectable": multiple || undefined, "aria-disabled": disabled || undefined, "aria-describedby": [!items.length ? `${id}-empty` : undefined, readOnly ? `${id}-readonly` : undefined].filter(Boolean).join(" ") || undefined, tabIndex: !disabled && !items.length ? 0 : -1, className: "flood-tree__viewport", children: render(items) }), !items.length && _jsx("div", { id: `${id}-empty`, role: "status", className: "flood-tree__empty", children: emptyMessage })] });
206
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flood-md/react",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "type": "module",
5
5
  "description": "Original Flood components for React and Next.js.",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "react-dom": "^18.3.0 || ^19.0.0"
31
31
  },
32
32
  "dependencies": {
33
- "@flood-md/tokens": "0.2.6"
33
+ "@flood-md/tokens": "0.2.8"
34
34
  },
35
35
  "publishConfig": {
36
36
  "access": "public"