@flood-md/react 0.2.5 → 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.
- package/README.md +42 -1
- package/dist/alert-dialog.js +1 -1
- package/dist/calendar-period-grid.d.ts +17 -0
- package/dist/calendar-period-grid.js +13 -0
- package/dist/calendar-value.d.ts +10 -0
- package/dist/calendar-value.js +48 -0
- package/dist/calendar.d.ts +21 -0
- package/dist/calendar.js +259 -0
- package/dist/checkbox.js +1 -1
- package/dist/choice-group.js +1 -1
- package/dist/collection-cards.d.ts +4 -0
- package/dist/collection-cards.js +21 -0
- package/dist/color-controls.d.ts +5 -0
- package/dist/color-controls.js +110 -0
- package/dist/combobox.js +22 -13
- package/dist/command-list.d.ts +3 -0
- package/dist/command-list.js +93 -0
- package/dist/content-elements.js +7 -7
- package/dist/context-menu.d.ts +2 -0
- package/dist/context-menu.js +76 -0
- package/dist/data-display.js +2 -2
- package/dist/data-table.d.ts +2 -0
- package/dist/data-table.js +58 -0
- package/dist/date-field.d.ts +2 -0
- package/dist/date-field.js +172 -0
- package/dist/date-picker.d.ts +3 -0
- package/dist/date-picker.js +108 -0
- package/dist/date-range-field.d.ts +3 -0
- package/dist/date-range-field.js +84 -0
- package/dist/date-range-picker.d.ts +2 -0
- package/dist/date-range-picker.js +81 -0
- package/dist/date-range-value.d.ts +8 -0
- package/dist/date-range-value.js +13 -0
- package/dist/date-time-picker.d.ts +3 -0
- package/dist/date-time-picker.js +31 -0
- package/dist/date-time-range-picker.d.ts +2 -0
- package/dist/date-time-range-picker.js +51 -0
- package/dist/date-value.d.ts +10 -0
- package/dist/date-value.js +31 -0
- package/dist/disclosure.js +1 -1
- package/dist/editable.d.ts +3 -0
- package/dist/editable.js +135 -0
- package/dist/error-summary.d.ts +3 -0
- package/dist/error-summary.js +39 -0
- package/dist/feedback-actions.js +2 -2
- package/dist/feedback.js +2 -2
- package/dist/file-input.js +1 -1
- package/dist/file-records.d.ts +5 -0
- package/dist/file-records.js +55 -0
- package/dist/form-controls.js +25 -13
- package/dist/foundations.css +388 -2
- package/dist/hover-card.d.ts +3 -0
- package/dist/hover-card.js +144 -0
- package/dist/index.d.ts +661 -2
- package/dist/index.js +190 -86
- package/dist/menu.d.ts +27 -1
- package/dist/menu.js +327 -89
- package/dist/navigation-layout.js +6 -6
- package/dist/navigation-tree.d.ts +3 -0
- package/dist/navigation-tree.js +98 -0
- package/dist/navigation.js +3 -3
- package/dist/numeric-input.js +1 -1
- package/dist/overlays.js +11 -10
- package/dist/range-calendar.d.ts +2 -0
- package/dist/range-calendar.js +35 -0
- package/dist/resizable-panels.d.ts +3 -0
- package/dist/resizable-panels.js +205 -0
- package/dist/scroll-area.js +1 -1
- package/dist/select.js +1 -1
- package/dist/slider.js +1 -1
- package/dist/tag-input.d.ts +3 -0
- package/dist/tag-input.js +181 -0
- package/dist/text-inputs.js +2 -2
- package/dist/time-field.d.ts +2 -0
- package/dist/time-field.js +176 -0
- package/dist/time-picker.d.ts +2 -0
- package/dist/time-picker.js +83 -0
- package/dist/time-value.d.ts +12 -0
- package/dist/time-value.js +36 -0
- package/dist/toolbar.js +1 -1
- package/dist/tree-select.d.ts +3 -0
- package/dist/tree-select.js +113 -0
- package/dist/tree.d.ts +3 -0
- package/dist/tree.js +206 -0
- package/package.json +2 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { Input, Label, Slider } from "./index.js";
|
|
4
|
+
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
5
|
+
/** The first color contract deliberately accepts only opaque RGB hex values. */
|
|
6
|
+
export function normalizeHexColor(value) {
|
|
7
|
+
const text = value.trim().toLowerCase();
|
|
8
|
+
if (/^#[0-9a-f]{6}$/.test(text))
|
|
9
|
+
return text;
|
|
10
|
+
if (/^#[0-9a-f]{3}$/.test(text))
|
|
11
|
+
return `#${[...text.slice(1)].map(character => character + character).join("")}`;
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
function checkedColor(value) {
|
|
15
|
+
const normalized = normalizeHexColor(value);
|
|
16
|
+
if (!normalized)
|
|
17
|
+
throw new Error("Flood color values must be opaque #rgb or #rrggbb hex strings");
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
const cx = (...values) => values.filter(Boolean).join(" ");
|
|
21
|
+
export const ColorPreview = /* @__PURE__ */ forwardRef(function ColorPreview({ value, label, size = "md", className, style, ...props }, ref) {
|
|
22
|
+
if (!label.trim())
|
|
23
|
+
throw new Error("Flood ColorSwatch requires a nonempty label");
|
|
24
|
+
const color = checkedColor(value);
|
|
25
|
+
return _jsx("span", { ...props, ref: ref, role: "img", "aria-label": label, className: cx("flood-color-swatch", `flood-color-swatch--${size}`, className), style: { ...style, backgroundColor: color } });
|
|
26
|
+
});
|
|
27
|
+
export const ColorEditor = /* @__PURE__ */ forwardRef(function ColorEditor({ label, hideLabel = false, value, defaultValue = "", onValueChange, name, form, required = false, disabled = false, readOnly = false, controlSize = "md", presets = [], invalidMessage = "Enter a color in #rrggbb format.", hexLabel = "Hex", channelLabels = ["Red", "Green", "Blue"], presetsLabel = "Preset colors", id: explicitId, className, ...props }, ref) {
|
|
28
|
+
if (![label, invalidMessage, hexLabel, presetsLabel, ...channelLabels].every(text => text.trim()) || channelLabels.length !== 3)
|
|
29
|
+
throw new Error("Flood ColorPicker requires nonempty labels and three channel names");
|
|
30
|
+
const canonical = (color) => color === "" ? "" : checkedColor(color);
|
|
31
|
+
const initial = useRef(canonical(defaultValue));
|
|
32
|
+
const [local, setLocal] = useState(initial.current);
|
|
33
|
+
const current = value === undefined ? local : canonical(value);
|
|
34
|
+
const [draft, setDraft] = useState(current), [showError, setShowError] = useState(false);
|
|
35
|
+
const pending = useRef(undefined);
|
|
36
|
+
const previous = useRef({ current, readOnly });
|
|
37
|
+
const input = useRef(null);
|
|
38
|
+
const generated = useId(), id = explicitId ?? `flood-color-picker-${generated}`;
|
|
39
|
+
const choices = [...new Set(presets.map(checkedColor))];
|
|
40
|
+
useImperativeHandle(ref, () => input.current, []);
|
|
41
|
+
useBrowserLayoutEffect(() => {
|
|
42
|
+
// Keep the user's short valid spelling while they continue typing six digits.
|
|
43
|
+
// A refused controlled request restores the host's accepted value instead.
|
|
44
|
+
if (pending.current !== undefined) {
|
|
45
|
+
if (pending.current !== current)
|
|
46
|
+
setDraft(current);
|
|
47
|
+
pending.current = undefined;
|
|
48
|
+
}
|
|
49
|
+
else if (previous.current.current !== current || previous.current.readOnly !== readOnly)
|
|
50
|
+
setDraft(current);
|
|
51
|
+
previous.current = { current, readOnly };
|
|
52
|
+
});
|
|
53
|
+
const invalidDraft = draft !== "" && !normalizeHexColor(draft);
|
|
54
|
+
const message = readOnly ? "" : invalidDraft || (required && !current) ? invalidMessage : "";
|
|
55
|
+
useBrowserLayoutEffect(() => { input.current?.setCustomValidity(message); });
|
|
56
|
+
const controlled = useRef(value !== undefined);
|
|
57
|
+
controlled.current = value !== undefined;
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const owner = input.current?.form;
|
|
60
|
+
if (!owner)
|
|
61
|
+
return;
|
|
62
|
+
const timers = new Set();
|
|
63
|
+
const reset = (event) => {
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
timers.delete(timer);
|
|
66
|
+
if (event.defaultPrevented || !input.current)
|
|
67
|
+
return;
|
|
68
|
+
if (!controlled.current) {
|
|
69
|
+
setLocal(initial.current);
|
|
70
|
+
setDraft(initial.current);
|
|
71
|
+
}
|
|
72
|
+
else
|
|
73
|
+
setDraft(input.current.dataset.acceptedValue ?? "");
|
|
74
|
+
setShowError(false);
|
|
75
|
+
}, 0);
|
|
76
|
+
timers.add(timer);
|
|
77
|
+
};
|
|
78
|
+
owner.addEventListener("reset", reset);
|
|
79
|
+
return () => { owner.removeEventListener("reset", reset); timers.forEach(clearTimeout); };
|
|
80
|
+
}, [form]);
|
|
81
|
+
const blocked = () => disabled || readOnly || input.current?.matches(":disabled");
|
|
82
|
+
const commit = (next, raw = next) => {
|
|
83
|
+
if (blocked())
|
|
84
|
+
return;
|
|
85
|
+
pending.current = next;
|
|
86
|
+
setDraft(raw);
|
|
87
|
+
if (value === undefined)
|
|
88
|
+
setLocal(next);
|
|
89
|
+
setShowError(false);
|
|
90
|
+
if (next !== current)
|
|
91
|
+
onValueChange?.(next);
|
|
92
|
+
};
|
|
93
|
+
const channels = [0, 1, 2].map(index => parseInt((current || "#000000").slice(index * 2 + 1, index * 2 + 3), 16));
|
|
94
|
+
return _jsxs("div", { ...props, id: id, className: cx("flood-color-picker", className), role: "group", "aria-labelledby": props["aria-label"] ? undefined : props["aria-labelledby"] ?? `${id}-label`, "data-disabled": disabled || undefined, "data-readonly": readOnly || undefined, children: [_jsx(Label, { id: `${id}-label`, htmlFor: `${id}-hex`, required: required, className: hideLabel ? "flood-visually-hidden" : undefined, children: label }), _jsxs("div", { className: "flood-color-picker__panel", children: [_jsxs("div", { className: "flood-color-picker__value", children: [current ? _jsx(ColorPreview, { value: current, label: current, size: "lg" }) : _jsx("span", { className: "flood-color-picker__empty", "aria-hidden": "true" }), _jsxs("div", { className: "flood-color-picker__hex", children: [_jsx(Label, { htmlFor: `${id}-hex`, children: hexLabel }), _jsx(Input, { ref: input, id: `${id}-hex`, form: form, value: draft, "data-accepted-value": current, autoComplete: "off", autoCapitalize: "off", spellCheck: false, dir: "ltr", placeholder: "#rrggbb", controlSize: controlSize, readOnly: readOnly, disabled: disabled, required: required, "aria-invalid": showError && Boolean(message) || undefined, "aria-describedby": [props["aria-describedby"], showError && message ? `${id}-error` : undefined].filter(Boolean).join(" ") || undefined, onChange: event => {
|
|
95
|
+
if (blocked())
|
|
96
|
+
return;
|
|
97
|
+
const text = event.currentTarget.value;
|
|
98
|
+
setDraft(text);
|
|
99
|
+
setShowError(false);
|
|
100
|
+
const normalized = normalizeHexColor(text);
|
|
101
|
+
if (!text || normalized)
|
|
102
|
+
commit(normalized ?? "", text);
|
|
103
|
+
}, onBlur: () => { if (!message)
|
|
104
|
+
setDraft(current);
|
|
105
|
+
else if (!readOnly)
|
|
106
|
+
setShowError(true); }, onInvalid: () => setShowError(true) })] })] }), _jsx("div", { className: "flood-color-picker__channels", children: channelLabels.map((channel, index) => _jsx(Slider, { label: channel, min: 0, max: 255, step: 1, value: [channels[index]], controlSize: "sm", disabled: disabled, readOnly: readOnly, form: form, onValueChange: next => {
|
|
107
|
+
const numbers = channels.map((number, position) => position === index ? next[0] : number);
|
|
108
|
+
commit(`#${numbers.map(number => number.toString(16).padStart(2, "0")).join("")}`);
|
|
109
|
+
} }, index)) }), choices.length > 0 && _jsx("div", { className: "flood-color-picker__presets", role: "group", "aria-label": presetsLabel, children: choices.map(color => _jsx("button", { type: "button", className: "flood-color-picker__preset", "aria-label": color, "aria-pressed": current === color, "aria-disabled": readOnly || undefined, disabled: disabled, onClick: () => commit(color), children: _jsx(ColorPreview, { value: color, label: color, size: "sm", "aria-hidden": "true" }) }, color)) })] }), name && _jsx("input", { type: "hidden", name: name, form: form, value: invalidDraft && !readOnly ? "" : current, disabled: disabled }), showError && message && _jsx("span", { id: `${id}-error`, className: "flood-error", role: "alert", children: message })] });
|
|
110
|
+
});
|
package/dist/combobox.js
CHANGED
|
@@ -6,7 +6,7 @@ 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 = 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, 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
12
|
if (![emptyMessage, listLabel, toggleLabel, clearLabel, invalidSelectionMessage].every(text => text.trim()))
|
|
@@ -74,23 +74,32 @@ export const SearchSelection = forwardRef(function SearchSelection({ options, se
|
|
|
74
74
|
setOpen(true);
|
|
75
75
|
};
|
|
76
76
|
useBrowserLayoutEffect(() => {
|
|
77
|
-
input.current
|
|
78
|
-
|
|
77
|
+
const control = input.current;
|
|
78
|
+
control?.setCustomValidity(required && !selections.length ? invalidSelectionMessage : "");
|
|
79
|
+
if (validation && control?.validity.valid)
|
|
80
|
+
setValidation("");
|
|
81
|
+
});
|
|
79
82
|
useEffect(() => {
|
|
80
83
|
const form = input.current?.form;
|
|
81
84
|
if (!form)
|
|
82
85
|
return;
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
const timers = new Set();
|
|
87
|
+
const reset = (event) => {
|
|
88
|
+
// React may cancel reset in its delegated handler after this native listener.
|
|
89
|
+
const timer = setTimeout(() => {
|
|
90
|
+
timers.delete(timer);
|
|
91
|
+
if (event.defaultPrevented || !input.current)
|
|
92
|
+
return;
|
|
93
|
+
if (current.current.value === undefined)
|
|
94
|
+
setLocal(initial.current);
|
|
95
|
+
setQuery(null);
|
|
96
|
+
setOpen(false);
|
|
97
|
+
setValidation("");
|
|
98
|
+
}, 0);
|
|
99
|
+
timers.add(timer);
|
|
100
|
+
};
|
|
92
101
|
form.addEventListener("reset", reset);
|
|
93
|
-
return () => form.removeEventListener("reset", reset);
|
|
102
|
+
return () => { form.removeEventListener("reset", reset); timers.forEach(clearTimeout); };
|
|
94
103
|
}, [props.form]);
|
|
95
104
|
useEffect(() => {
|
|
96
105
|
const control = input.current, Observer = control?.ownerDocument.defaultView?.MutationObserver;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type CommandListProps } from "./index.js";
|
|
2
|
+
/** Searchable action suggestions. No selected form value or modal lifecycle. */
|
|
3
|
+
export declare const SearchCommandList: import("react").ForwardRefExoticComponent<CommandListProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from "react";
|
|
3
|
+
import { Input, Label, ScrollArea, Surface, Text, VisuallyHidden } from "./index.js";
|
|
4
|
+
const searchable = (value) => value.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase();
|
|
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) {
|
|
7
|
+
if (![label, emptyMessage, loadingMessage].every(value => value.trim()))
|
|
8
|
+
throw new Error("Flood CommandList requires nonempty accessible labels and state messages");
|
|
9
|
+
if (new Set(items.map(item => item.id)).size !== items.length || items.some(item => !item.id.trim() || !item.label.trim()))
|
|
10
|
+
throw new Error("Flood CommandList items require unique nonempty IDs and labels");
|
|
11
|
+
const generated = useId(), rootId = explicitId ?? `flood-command-list-${generated}`, inputId = `${rootId}-input`, listId = `${rootId}-list`;
|
|
12
|
+
const input = useRef(null), viewport = useRef(null), composing = useRef(false);
|
|
13
|
+
useImperativeHandle(forwardedRef, () => input.current, []);
|
|
14
|
+
const [localQuery, setLocalQuery] = useState(defaultQuery), [activeId, setActiveId] = useState(), [inheritedDisabled, setInheritedDisabled] = useState(false);
|
|
15
|
+
const text = query ?? localQuery;
|
|
16
|
+
const terms = searchable(text).trim().split(/\s+/).filter(Boolean);
|
|
17
|
+
const filtered = items.filter(item => {
|
|
18
|
+
const haystack = searchable([item.label, item.description ?? "", item.group ?? "", ...(item.keywords ?? [])].join(" "));
|
|
19
|
+
return terms.every(term => haystack.includes(term));
|
|
20
|
+
});
|
|
21
|
+
const groups = new Map();
|
|
22
|
+
for (const item of filtered) {
|
|
23
|
+
const key = item.group?.trim() ?? "", group = groups.get(key);
|
|
24
|
+
if (group)
|
|
25
|
+
group.push(item);
|
|
26
|
+
else
|
|
27
|
+
groups.set(key, [item]);
|
|
28
|
+
}
|
|
29
|
+
const effectiveDisabled = disabled || inheritedDisabled;
|
|
30
|
+
const enabled = loading || effectiveDisabled ? [] : [...groups.values()].flat().filter(item => !item.disabled);
|
|
31
|
+
const active = enabled.find(item => item.id === activeId) ?? enabled[0];
|
|
32
|
+
const positions = new Map(items.map((item, index) => [item.id, index]));
|
|
33
|
+
const optionId = (item) => `${listId}-option-${positions.get(item.id)}`;
|
|
34
|
+
const blocked = () => Boolean(disabled || loading || input.current?.matches(":disabled"));
|
|
35
|
+
const invoke = (item) => {
|
|
36
|
+
if (blocked() || item.disabled)
|
|
37
|
+
return;
|
|
38
|
+
input.current?.focus({ preventScroll: true });
|
|
39
|
+
onAction?.(item.id);
|
|
40
|
+
};
|
|
41
|
+
useEffect(() => { setActiveId(undefined); }, [text]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const node = input.current, view = node?.ownerDocument.defaultView;
|
|
44
|
+
if (!node || !view)
|
|
45
|
+
return;
|
|
46
|
+
const update = () => setInheritedDisabled(node.matches(":disabled"));
|
|
47
|
+
update();
|
|
48
|
+
const observer = new view.MutationObserver(update);
|
|
49
|
+
for (let ancestor = node.parentElement; ancestor; ancestor = ancestor.parentElement)
|
|
50
|
+
if (ancestor.localName === "fieldset")
|
|
51
|
+
observer.observe(ancestor, { attributes: true, attributeFilter: ["disabled"] });
|
|
52
|
+
return () => observer.disconnect();
|
|
53
|
+
}, [disabled]);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!active || !viewport.current)
|
|
56
|
+
return;
|
|
57
|
+
const option = input.current?.ownerDocument.getElementById(optionId(active));
|
|
58
|
+
if (!option)
|
|
59
|
+
return;
|
|
60
|
+
const area = viewport.current, a = option.getBoundingClientRect(), b = area.getBoundingClientRect();
|
|
61
|
+
if (a.top < b.top)
|
|
62
|
+
area.scrollTop -= b.top - a.top;
|
|
63
|
+
else if (a.bottom > b.bottom)
|
|
64
|
+
area.scrollTop += a.bottom - b.bottom;
|
|
65
|
+
}, [active?.id, text, loading, items]);
|
|
66
|
+
const status = loading ? loadingMessage : filtered.length ? "" : emptyMessage;
|
|
67
|
+
return _jsxs("div", { ...props, id: rootId, className: ["flood-command-list", `flood-control--${controlSize}`, className].filter(Boolean).join(" "), "data-disabled": effectiveDisabled || undefined, onKeyDown: event => {
|
|
68
|
+
onKeyDown?.(event);
|
|
69
|
+
if (event.defaultPrevented || event.target !== input.current || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.nativeEvent.isComposing || composing.current)
|
|
70
|
+
return;
|
|
71
|
+
if (event.key === "Enter") {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
if (active)
|
|
74
|
+
invoke(active);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (blocked() || !enabled.length)
|
|
78
|
+
return;
|
|
79
|
+
if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key))
|
|
80
|
+
return;
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
const index = enabled.findIndex(item => item.id === active?.id);
|
|
83
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? enabled.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + enabled.length) % enabled.length;
|
|
84
|
+
setActiveId(enabled[next].id);
|
|
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
|
+
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) => {
|
|
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
|
+
setActiveId(item.id); }, onPointerDown: event => { if (event.pointerType === "mouse")
|
|
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));
|
|
91
|
+
return group ? _jsxs("div", { role: "group", "aria-labelledby": `${listId}-group-${groupIndex}`, className: "flood-command-list__group", children: [_jsx(Text, { as: "div", size: "helper", tone: "subtle", id: `${listId}-group-${groupIndex}`, className: "flood-command-list__group-label", children: group }), content] }, `group:${group}`) : _jsx("div", { role: "group", className: "flood-command-list__group", children: content }, "ungrouped");
|
|
92
|
+
}) }) }), _jsx(VisuallyHidden, { role: "status", children: status })] });
|
|
93
|
+
});
|
package/dist/content-elements.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
3
|
import { Avatar, VisuallyHidden } from "./index.js";
|
|
4
4
|
const cx = (...parts) => parts.filter(Boolean).join(" ");
|
|
5
|
-
export const GroupedAvatars = forwardRef(function GroupedAvatars({ items, label, max = 4, size = "md", overflowLabel, className, ...props }, ref) {
|
|
5
|
+
export const GroupedAvatars = /* @__PURE__ */ forwardRef(function GroupedAvatars({ items, label, max = 4, size = "md", overflowLabel, className, ...props }, ref) {
|
|
6
6
|
if (!Number.isInteger(max) || max < 1)
|
|
7
7
|
throw new Error("Flood AvatarGroup max must be a positive integer");
|
|
8
8
|
if (!label.trim() || items.some(item => !item.id.trim() || !item.name.trim()) || new Set(items.map(item => item.id)).size !== items.length)
|
|
@@ -12,27 +12,27 @@ export const GroupedAvatars = forwardRef(function GroupedAvatars({ items, label,
|
|
|
12
12
|
throw new Error("Flood AvatarGroup overflowLabel must be nonempty");
|
|
13
13
|
return _jsxs("ul", { ...props, ref: ref, "aria-label": label, className: cx("flood-avatar-group", className), "data-size": size, children: [visible.map(item => _jsx("li", { children: _jsx(Avatar, { name: item.name, src: item.src, fallback: item.fallback, size: size }) }, item.id)), remaining > 0 && _jsx("li", { children: _jsxs("span", { className: "flood-avatar flood-avatar-group__overflow", "data-size": size, role: "img", "aria-label": overflowLabel ?? `${remaining} more people`, children: ["+", remaining] }) })] });
|
|
14
14
|
});
|
|
15
|
-
export const NamedStatus = forwardRef(function NamedStatus({ label, tone = "neutral", size = "md", announce = false, className, role, ...props }, ref) {
|
|
15
|
+
export const NamedStatus = /* @__PURE__ */ forwardRef(function NamedStatus({ label, tone = "neutral", size = "md", announce = false, className, role, ...props }, ref) {
|
|
16
16
|
if (!label.trim())
|
|
17
17
|
throw new Error("Flood StatusIndicator requires a visible label");
|
|
18
18
|
return _jsxs("span", { ...props, ref: ref, role: announce ? "status" : role, className: cx("flood-status-indicator", className), "data-tone": tone, "data-size": size, children: [_jsx("span", { className: "flood-status-indicator__dot", "aria-hidden": "true" }), label] });
|
|
19
19
|
});
|
|
20
|
-
export const CaptionedFigure = forwardRef(function CaptionedFigure({ caption, captionPosition = "after", children, className, ...props }, ref) {
|
|
20
|
+
export const CaptionedFigure = /* @__PURE__ */ forwardRef(function CaptionedFigure({ caption, captionPosition = "after", children, className, ...props }, ref) {
|
|
21
21
|
const content = caption !== undefined && caption !== null && caption !== false ? _jsx("figcaption", { className: "flood-figure__caption", children: caption }) : null;
|
|
22
22
|
return _jsxs("figure", { ...props, ref: ref, className: cx("flood-figure", className), children: [captionPosition === "before" && content, children, captionPosition === "after" && content] });
|
|
23
23
|
});
|
|
24
|
-
export const AttributedQuote = forwardRef(function AttributedQuote({ attribution, children, className, ...props }, ref) {
|
|
24
|
+
export const AttributedQuote = /* @__PURE__ */ forwardRef(function AttributedQuote({ attribution, children, className, ...props }, ref) {
|
|
25
25
|
return _jsxs("blockquote", { ...props, ref: ref, className: cx("flood-blockquote", className), children: [_jsx("div", { className: "flood-blockquote__quote", children: children }), attribution !== undefined && attribution !== null && attribution !== false && _jsx("footer", { className: "flood-blockquote__attribution", children: attribution })] });
|
|
26
26
|
});
|
|
27
|
-
export const InlineCodeText = forwardRef(function InlineCodeText({ size = "md", wrap = true, className, ...props }, ref) {
|
|
27
|
+
export const InlineCodeText = /* @__PURE__ */ forwardRef(function InlineCodeText({ size = "md", wrap = true, className, ...props }, ref) {
|
|
28
28
|
return _jsx("code", { ...props, ref: ref, className: cx("flood-inline-code", className), "data-size": size, "data-wrap": wrap });
|
|
29
29
|
});
|
|
30
|
-
export const MeasuredValue = forwardRef(function MeasuredValue({ label, value, hint, trend, size = "md", className, ...props }, ref) {
|
|
30
|
+
export const MeasuredValue = /* @__PURE__ */ forwardRef(function MeasuredValue({ label, value, hint, trend, size = "md", className, ...props }, ref) {
|
|
31
31
|
if (!label.trim() || (trend && !trend.label.trim()))
|
|
32
32
|
throw new Error("Flood Metric requires a label and meaningful trend text");
|
|
33
33
|
return _jsxs("dl", { ...props, ref: ref, className: cx("flood-metric", className), "data-size": size, children: [_jsx("dt", { className: "flood-metric__label", children: label }), _jsx("dd", { className: "flood-metric__value", children: value }), trend && _jsxs("dd", { className: "flood-metric__trend", "data-tone": trend.tone ?? "neutral", children: [_jsx("span", { "aria-hidden": "true", children: trend.direction === "up" ? "↑" : trend.direction === "down" ? "↓" : "→" }), trend.label] }), hint !== undefined && hint !== null && hint !== false && _jsx("dd", { className: "flood-metric__hint", children: hint })] });
|
|
34
34
|
});
|
|
35
|
-
export const EventTimeline = forwardRef(function EventTimeline({ items, label, stateLabels, className, ...props }, ref) {
|
|
35
|
+
export const EventTimeline = /* @__PURE__ */ forwardRef(function EventTimeline({ items, label, stateLabels, className, ...props }, ref) {
|
|
36
36
|
if (!label.trim() || items.some(item => !item.id.trim() || !item.title.trim()) || new Set(items.map(item => item.id)).size !== items.length)
|
|
37
37
|
throw new Error("Flood Timeline needs a label and unique nonempty item IDs/titles");
|
|
38
38
|
if (items.filter(item => item.state === "current").length > 1)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
3
|
+
import { focusMenuEdge, MenuLayer, validateMenuTree } from "./menu.js";
|
|
4
|
+
import { placeOverlay, positionLayer } from "./overlay-runtime.js";
|
|
5
|
+
export const ContextActionMenu = /* @__PURE__ */ forwardRef(function ContextActionMenu({ label, items, children, onAction, open: controlled, defaultOpen = false, onOpenChange, disabled = false, controlSize = "sm", className, tabIndex = 0, onContextMenu, onKeyDown, ...props }, forwardedRef) {
|
|
6
|
+
if (!label.trim())
|
|
7
|
+
throw new Error("Flood ContextMenu requires a nonempty label");
|
|
8
|
+
validateMenuTree(items);
|
|
9
|
+
const root = useRef(null), panel = useRef(null), source = useRef(null);
|
|
10
|
+
useImperativeHandle(forwardedRef, () => root.current, []);
|
|
11
|
+
const [local, setLocal] = useState(defaultOpen), [point, setPoint] = useState(undefined);
|
|
12
|
+
const requestedOpen = controlled ?? local, open = !disabled && requestedOpen;
|
|
13
|
+
const pending = useRef(undefined);
|
|
14
|
+
const current = useRef({ controlled, onOpenChange });
|
|
15
|
+
current.current = { controlled, onOpenChange };
|
|
16
|
+
const request = (next) => {
|
|
17
|
+
if (pending.current === next)
|
|
18
|
+
return;
|
|
19
|
+
pending.current = next;
|
|
20
|
+
queueMicrotask(() => { pending.current = undefined; });
|
|
21
|
+
if (current.current.controlled === undefined)
|
|
22
|
+
setLocal(next);
|
|
23
|
+
current.current.onOpenChange?.(next);
|
|
24
|
+
};
|
|
25
|
+
useEffect(() => { if (disabled && requestedOpen)
|
|
26
|
+
request(false); }, [disabled, requestedOpen]);
|
|
27
|
+
const anchor = () => source.current?.isConnected ? source.current : root.current;
|
|
28
|
+
const targetOf = (target) => {
|
|
29
|
+
const element = target && "nodeType" in target && target.nodeType === 1 ? target : null;
|
|
30
|
+
const focusable = element?.closest('button,a[href],input,select,textarea,[tabindex],[contenteditable="true"]');
|
|
31
|
+
return focusable && root.current?.contains(focusable) && typeof focusable.focus === "function" ? focusable : root.current;
|
|
32
|
+
};
|
|
33
|
+
const position = (element) => {
|
|
34
|
+
const target = anchor();
|
|
35
|
+
if (!target)
|
|
36
|
+
return;
|
|
37
|
+
if (!point) {
|
|
38
|
+
positionLayer(target, element, "bottom", "start");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const view = element.ownerDocument.defaultView, visual = view.visualViewport;
|
|
42
|
+
const viewport = { left: visual?.offsetLeft ?? 0, top: visual?.offsetTop ?? 0, width: visual?.width ?? view.innerWidth, height: visual?.height ?? view.innerHeight };
|
|
43
|
+
const gap = parseFloat(view.getComputedStyle(element).columnGap) || 8;
|
|
44
|
+
element.style.setProperty("--flood-overlay-available-width", `${Math.max(0, viewport.width - 2 * gap)}px`);
|
|
45
|
+
element.style.setProperty("--flood-overlay-available-height", `${Math.max(0, viewport.height - 2 * gap)}px`);
|
|
46
|
+
const rect = { left: point.x, right: point.x, top: point.y, bottom: point.y, width: 0, height: 0 };
|
|
47
|
+
const placed = placeOverlay(rect, element.getBoundingClientRect(), viewport, "bottom", "start", gap, view.getComputedStyle(target).direction === "rtl");
|
|
48
|
+
element.style.left = `${placed.left}px`;
|
|
49
|
+
element.style.top = `${placed.top}px`;
|
|
50
|
+
element.dataset.side = placed.side;
|
|
51
|
+
element.style.visibility = "visible";
|
|
52
|
+
};
|
|
53
|
+
return _jsxs("div", { ...props, ref: root, role: "group", "aria-label": label, tabIndex: tabIndex, className: ["flood-context-menu", className].filter(Boolean).join(" "), onContextMenu: event => {
|
|
54
|
+
onContextMenu?.(event);
|
|
55
|
+
if (event.defaultPrevented || disabled || panel.current?.contains(event.target))
|
|
56
|
+
return;
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
source.current = targetOf(event.target);
|
|
59
|
+
setPoint({ x: event.clientX, y: event.clientY });
|
|
60
|
+
request(true);
|
|
61
|
+
if (open)
|
|
62
|
+
focusMenuEdge(panel.current);
|
|
63
|
+
}, onKeyDown: event => {
|
|
64
|
+
onKeyDown?.(event);
|
|
65
|
+
if (event.defaultPrevented || disabled || event.altKey || event.ctrlKey || event.metaKey || panel.current?.contains(event.target))
|
|
66
|
+
return;
|
|
67
|
+
if (event.key !== "ContextMenu" && !(event.shiftKey && event.key === "F10"))
|
|
68
|
+
return;
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
source.current = targetOf(event.target);
|
|
71
|
+
setPoint(undefined);
|
|
72
|
+
request(true);
|
|
73
|
+
if (open)
|
|
74
|
+
focusMenuEdge(panel.current);
|
|
75
|
+
}, children: [children, _jsx(MenuLayer, { ref: panel, label: label, items: items, open: open, requestOpen: request, closeTree: () => request(false), anchor: anchor, rootAnchor: anchor, position: position, onAction: onAction, controlSize: controlSize })] });
|
|
76
|
+
});
|
package/dist/data-display.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef } from "react";
|
|
3
3
|
import { ScrollArea } from "./index.js";
|
|
4
|
-
export const DescribedData = forwardRef(function DescribedData({ items, layout, density, dividers, className, ...props }, ref) {
|
|
4
|
+
export const DescribedData = /* @__PURE__ */ forwardRef(function DescribedData({ items, layout, density, dividers, className, ...props }, ref) {
|
|
5
5
|
if (items.some(item => !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 = forwardRef(function TabularData({ caption, children, hideCaption, density, striped, scrollLabel, className, hidden, dir, lang, ...props }, ref) {
|
|
9
|
+
export const TabularData = /* @__PURE__ */ forwardRef(function TabularData({ caption, children, hideCaption, density, striped, scrollLabel, className, hidden, dir, lang, ...props }, ref) {
|
|
10
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, children: [_jsx("caption", { className: hideCaption ? "flood-visually-hidden" : undefined, children: caption }), children] }) }) });
|
|
11
11
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
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) {
|
|
5
|
+
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
|
+
throw new Error("Flood DataTable requires a caption and unique named columns");
|
|
7
|
+
if (rows.some(row => !row.id.trim() || (row.label !== undefined && !row.label.trim())) || new Set(rows.map(row => row.id)).size !== rows.length)
|
|
8
|
+
throw new Error("Flood DataTable rows require unique nonempty IDs and labels");
|
|
9
|
+
if (!Number.isSafeInteger(pageSize) || pageSize < 1 || !Number.isSafeInteger(defaultPage) || defaultPage < 1 || (controlledPage !== undefined && (!Number.isSafeInteger(controlledPage) || controlledPage < 1)))
|
|
10
|
+
throw new Error("Flood DataTable page and pageSize must be positive safe integers");
|
|
11
|
+
const [localSort, setSort] = useState(defaultSort), [localSelected, setSelected] = useState(defaultSelectedIds), [localPage, setPage] = useState(defaultPage);
|
|
12
|
+
const requestedSort = controlledSort === undefined ? localSort : controlledSort;
|
|
13
|
+
const sorting = requestedSort && columns.some(column => column.id === requestedSort.column && column.sortable) ? requestedSort : null;
|
|
14
|
+
const sorted = useMemo(() => {
|
|
15
|
+
if (!sorting)
|
|
16
|
+
return [...rows];
|
|
17
|
+
const collator = new Intl.Collator(locale, { numeric: true, sensitivity: "base" });
|
|
18
|
+
const value = (row) => {
|
|
19
|
+
const explicit = row.sortValues?.[sorting.column];
|
|
20
|
+
if (explicit !== undefined)
|
|
21
|
+
return explicit;
|
|
22
|
+
const cell = row.cells[sorting.column];
|
|
23
|
+
return typeof cell === "string" || typeof cell === "number" ? cell : null;
|
|
24
|
+
};
|
|
25
|
+
return rows.map((row, index) => ({ row, index })).sort((a, b) => {
|
|
26
|
+
const left = value(a.row), right = value(b.row);
|
|
27
|
+
// Missing values stay last in both directions; ties retain source order.
|
|
28
|
+
if (left === null || right === null)
|
|
29
|
+
return left === right ? a.index - b.index : left === null ? 1 : -1;
|
|
30
|
+
const comparison = typeof left === "number" && typeof right === "number" ? left - right : collator.compare(String(left), String(right));
|
|
31
|
+
return comparison === 0 ? a.index - b.index : comparison * (sorting.direction === "ascending" ? 1 : -1);
|
|
32
|
+
}).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);
|
|
37
|
+
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
|
+
const blocked = state !== "ready";
|
|
39
|
+
const select = (next) => { if (blocked)
|
|
40
|
+
return; const accepted = [...new Set(next)].filter(id => eligible.has(id)); if (selectedIds === undefined)
|
|
41
|
+
setSelected(accepted); onSelectionChange?.(accepted); };
|
|
42
|
+
const changePage = (next) => { if (blocked)
|
|
43
|
+
return; if (controlledPage === undefined)
|
|
44
|
+
setPage(next); onPageChange?.(next); };
|
|
45
|
+
const changeSort = (column) => {
|
|
46
|
+
if (blocked)
|
|
47
|
+
return;
|
|
48
|
+
const next = sorting?.column !== column ? { column, direction: "ascending" } : sorting.direction === "ascending" ? { column, direction: "descending" } : null;
|
|
49
|
+
if (controlledSort === undefined)
|
|
50
|
+
setSort(next);
|
|
51
|
+
onSortChange?.(next);
|
|
52
|
+
if (page !== 1)
|
|
53
|
+
changePage(1);
|
|
54
|
+
};
|
|
55
|
+
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 })] })] });
|
|
58
|
+
});
|