@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,31 @@
|
|
|
1
|
+
export const emptyDateParts = () => ({ year: "", month: "", day: "" });
|
|
2
|
+
export function daysInMonth(year, month) {
|
|
3
|
+
return month === 2 ? (year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28) : [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
4
|
+
}
|
|
5
|
+
export function canonicalDate(parts) {
|
|
6
|
+
if (!/^\d{1,4}$/.test(parts.year) || !/^\d{1,2}$/.test(parts.month) || !/^\d{1,2}$/.test(parts.day))
|
|
7
|
+
return;
|
|
8
|
+
const y = Number(parts.year), m = Number(parts.month), d = Number(parts.day);
|
|
9
|
+
if (y < 1 || y > 9999 || m < 1 || m > 12 || d < 1 || d > daysInMonth(y, m))
|
|
10
|
+
return;
|
|
11
|
+
return `${String(y).padStart(4, "0")}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
|
|
12
|
+
}
|
|
13
|
+
export function parseDate(value) {
|
|
14
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
15
|
+
return;
|
|
16
|
+
const [year, month, day] = value.split("-");
|
|
17
|
+
const parts = { year, month, day };
|
|
18
|
+
return canonicalDate(parts) === value ? parts : undefined;
|
|
19
|
+
}
|
|
20
|
+
export const dateInRange = (value, min, max) => (!min || value >= min) && (!max || value <= max);
|
|
21
|
+
export const isEmptyDate = (parts) => !parts.year && !parts.month && !parts.day;
|
|
22
|
+
export function stepDatePart(parts, part, direction, edge) {
|
|
23
|
+
const next = { ...parts };
|
|
24
|
+
const max = part === "year" ? 9999 : part === "month" ? 12 : daysInMonth(Number(parts.year) || 2000, Number(parts.month) || 1);
|
|
25
|
+
const current = Number(parts[part]);
|
|
26
|
+
const number = edge === "start" ? 1 : edge === "end" ? max : current ? (current - 1 + direction + max) % max + 1 : direction > 0 ? 1 : max;
|
|
27
|
+
next[part] = String(number).padStart(part === "year" ? 4 : 2, "0");
|
|
28
|
+
if (part !== "day" && Number(next.day))
|
|
29
|
+
next.day = String(Math.min(Number(next.day), daysInMonth(Number(next.year) || 2000, Number(next.month) || 1))).padStart(2, "0");
|
|
30
|
+
return next;
|
|
31
|
+
}
|
package/dist/disclosure.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createElement, forwardRef, useEffect, useId, useLayoutEffect, useRef, u
|
|
|
3
3
|
import { Button } 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 = 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, 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);
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { EditableProps } from "./index.js";
|
|
2
|
+
/** A draft is private until explicitly committed; native forms receive committed text. */
|
|
3
|
+
export declare const InlineEditable: import("react").ForwardRefExoticComponent<EditableProps & import("react").RefAttributes<HTMLDivElement>>;
|
package/dist/editable.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
import { Button, IconButton, IconSlot, Input, InputGroup } from "./index.js";
|
|
4
|
+
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
5
|
+
const cx = (...values) => values.filter(Boolean).join(" ");
|
|
6
|
+
/** 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) {
|
|
8
|
+
if (![label, editLabel, saveLabel, cancelLabel].every(text => text.trim()))
|
|
9
|
+
throw new Error("Flood Editable requires nonempty control and action labels");
|
|
10
|
+
const initial = useRef(defaultValue), [local, setLocal] = useState(defaultValue);
|
|
11
|
+
const committed = value ?? local;
|
|
12
|
+
const [localEditing, setLocalEditing] = useState(defaultEditing);
|
|
13
|
+
const requested = editing ?? localEditing, active = requested && !disabled && !readOnly;
|
|
14
|
+
const [draft, setDraft] = useState(committed);
|
|
15
|
+
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 };
|
|
18
|
+
const composing = useRef(false), resolved = useRef(false), wasActive = useRef(false);
|
|
19
|
+
const generated = useId(), id = explicitId ?? `flood-editable-${generated}`, inputId = `${id}-input`, previewId = `${id}-preview`;
|
|
20
|
+
useImperativeHandle(forwardedRef, () => root.current, []);
|
|
21
|
+
useImperativeHandle(inputRef, () => editor.current, [active, readOnly]);
|
|
22
|
+
const requestEditing = (next) => {
|
|
23
|
+
if (current.current.editing === undefined)
|
|
24
|
+
setLocalEditing(next);
|
|
25
|
+
current.current.onEditingChange?.(next);
|
|
26
|
+
};
|
|
27
|
+
const start = () => {
|
|
28
|
+
if (disabled || readOnly || current.current.active || preview.current?.matches(":disabled"))
|
|
29
|
+
return;
|
|
30
|
+
setDraft(current.current.committed);
|
|
31
|
+
resolved.current = false;
|
|
32
|
+
requestEditing(true);
|
|
33
|
+
};
|
|
34
|
+
const finish = (action, restoreFocus) => {
|
|
35
|
+
if (!current.current.active || resolved.current || (composing.current && action === "commit") || editor.current?.matches(":disabled"))
|
|
36
|
+
return;
|
|
37
|
+
resolved.current = true;
|
|
38
|
+
const next = current.current.draft;
|
|
39
|
+
if (action === "commit") {
|
|
40
|
+
if (next !== current.current.committed) {
|
|
41
|
+
if (current.current.value === undefined)
|
|
42
|
+
setLocal(next);
|
|
43
|
+
current.current.onValueChange?.(next);
|
|
44
|
+
}
|
|
45
|
+
current.current.onCommit?.(next);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
setDraft(current.current.committed);
|
|
49
|
+
current.current.onCancel?.();
|
|
50
|
+
}
|
|
51
|
+
requestEditing(false);
|
|
52
|
+
if (restoreFocus) {
|
|
53
|
+
const document = root.current?.ownerDocument;
|
|
54
|
+
document?.defaultView?.requestAnimationFrame(() => {
|
|
55
|
+
if (current.current.active || !preview.current?.isConnected || preview.current.matches(":disabled"))
|
|
56
|
+
return;
|
|
57
|
+
const focused = document.activeElement;
|
|
58
|
+
if (focused === document.body || root.current?.contains(focused))
|
|
59
|
+
preview.current.focus({ preventScroll: true });
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// A controlled host may refuse to close and allow a later deliberate attempt.
|
|
63
|
+
queueMicrotask(() => { resolved.current = false; });
|
|
64
|
+
};
|
|
65
|
+
useBrowserLayoutEffect(() => {
|
|
66
|
+
let frame = 0;
|
|
67
|
+
if (active && !wasActive.current) {
|
|
68
|
+
setDraft(current.current.committed);
|
|
69
|
+
resolved.current = false;
|
|
70
|
+
composing.current = false;
|
|
71
|
+
const view = editor.current?.ownerDocument.defaultView;
|
|
72
|
+
if (view)
|
|
73
|
+
frame = view.requestAnimationFrame(() => { if (current.current.active && editor.current && !editor.current.matches(":disabled")) {
|
|
74
|
+
editor.current.focus();
|
|
75
|
+
editor.current.select();
|
|
76
|
+
} });
|
|
77
|
+
}
|
|
78
|
+
if (!active && wasActive.current) {
|
|
79
|
+
composing.current = false;
|
|
80
|
+
setDraft(current.current.committed);
|
|
81
|
+
}
|
|
82
|
+
wasActive.current = active;
|
|
83
|
+
return () => { editor.current?.ownerDocument.defaultView?.cancelAnimationFrame(frame); };
|
|
84
|
+
}, [active]);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if ((disabled || readOnly) && requested)
|
|
87
|
+
requestEditing(false);
|
|
88
|
+
}, [disabled, readOnly, requested]);
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const owner = bridge.current?.form;
|
|
91
|
+
if (!owner)
|
|
92
|
+
return;
|
|
93
|
+
let timer;
|
|
94
|
+
const reset = (event) => {
|
|
95
|
+
timer = setTimeout(() => {
|
|
96
|
+
if (event.defaultPrevented || !root.current)
|
|
97
|
+
return;
|
|
98
|
+
if (current.current.value === undefined)
|
|
99
|
+
setLocal(initial.current);
|
|
100
|
+
setDraft(current.current.value === undefined ? initial.current : current.current.committed);
|
|
101
|
+
if (current.current.active)
|
|
102
|
+
requestEditing(false);
|
|
103
|
+
}, 0);
|
|
104
|
+
};
|
|
105
|
+
owner.addEventListener("reset", reset);
|
|
106
|
+
return () => { owner.removeEventListener("reset", reset); clearTimeout(timer); };
|
|
107
|
+
}, [form]);
|
|
108
|
+
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 };
|
|
110
|
+
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
|
+
onKeyDown?.(event);
|
|
112
|
+
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)
|
|
113
|
+
return;
|
|
114
|
+
if (event.key === "Enter" || event.key === "Escape") {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
finish(event.key === "Enter" ? "commit" : "cancel", true);
|
|
117
|
+
}
|
|
118
|
+
}, onBlur: event => {
|
|
119
|
+
onBlur?.(event);
|
|
120
|
+
if (event.defaultPrevented || !current.current.active || event.currentTarget.contains(event.relatedTarget) || blurAction === "none")
|
|
121
|
+
return;
|
|
122
|
+
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") })] })] });
|
|
135
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ErrorSummaryProps } from "./index.js";
|
|
2
|
+
/** A host-owned validation summary; errors are never inferred from the form. */
|
|
3
|
+
export declare const ValidationErrorSummary: import("react").ForwardRefExoticComponent<ErrorSummaryProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useImperativeHandle, useRef } from "react";
|
|
3
|
+
import { Alert, Link, List, Text } from "./index.js";
|
|
4
|
+
import { hasFieldContent, mergeDescriptions } from "./field-state.js";
|
|
5
|
+
/** A host-owned validation summary; errors are never inferred from the form. */
|
|
6
|
+
export const ValidationErrorSummary = /* @__PURE__ */ forwardRef(function ValidationErrorSummary({ title, errors, description, announcement = "off", autoFocus = false, onSelect, className, id: explicitId, tabIndex = -1, "aria-labelledby": labelledBy, "aria-describedby": describedBy, ...props }, forwardedRef) {
|
|
7
|
+
if (!title.trim())
|
|
8
|
+
throw new Error("Flood ErrorSummary requires a nonempty title");
|
|
9
|
+
if (errors.some(error => !error.id.trim() || !error.fieldId.trim() || !error.message.trim()) || new Set(errors.map(error => error.id)).size !== errors.length)
|
|
10
|
+
throw new Error("Flood ErrorSummary requires unique nonempty error IDs, target IDs and messages");
|
|
11
|
+
if (!["off", "polite", "assertive"].includes(announcement))
|
|
12
|
+
throw new Error("Flood ErrorSummary announcement is unsupported");
|
|
13
|
+
const generated = useId(), id = explicitId ?? `flood-error-summary-${generated}`;
|
|
14
|
+
const root = useRef(null), present = errors.length > 0;
|
|
15
|
+
useImperativeHandle(forwardedRef, () => root.current, [present]);
|
|
16
|
+
const hasDescription = hasFieldContent(description), descriptionId = hasDescription ? `${id}-description` : undefined;
|
|
17
|
+
const live = autoFocus ? "off" : announcement;
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (autoFocus && present)
|
|
20
|
+
root.current?.focus({ preventScroll: true });
|
|
21
|
+
}, [autoFocus, present]);
|
|
22
|
+
if (!present)
|
|
23
|
+
return null;
|
|
24
|
+
return _jsx("div", { ...props, id: id, ref: root, role: "region", tabIndex: tabIndex, "aria-labelledby": labelledBy ?? `${id}-title`, "aria-describedby": mergeDescriptions(describedBy, descriptionId), "aria-live": live === "off" ? undefined : live, "aria-atomic": live === "off" ? undefined : true, className: ["flood-error-summary", className].filter(Boolean).join(" "), children: _jsxs(Alert, { tone: "error", announcement: "off", children: [_jsx(Text, { as: "div", size: "label", id: `${id}-title`, className: "flood-error-summary__title", children: title }), hasDescription && _jsx(Text, { as: "div", size: "compact", tone: "subtle", id: descriptionId, className: "flood-error-summary__description", children: description }), _jsx(List, { gap: "1", className: "flood-error-summary__list", children: errors.map(error => _jsx("li", { children: _jsx(Link, { href: `#${encodeURIComponent(error.fieldId)}`, onClick: event => {
|
|
25
|
+
onSelect?.(error, event);
|
|
26
|
+
if (event.defaultPrevented || event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
|
|
27
|
+
return;
|
|
28
|
+
const document = event.currentTarget.ownerDocument, target = document.getElementById(error.fieldId);
|
|
29
|
+
if (!target || typeof target.focus !== "function" || target.matches(":disabled") || target.closest("[inert]"))
|
|
30
|
+
return;
|
|
31
|
+
const before = document.activeElement;
|
|
32
|
+
target.focus({ preventScroll: true });
|
|
33
|
+
const focused = document.activeElement;
|
|
34
|
+
// Hidden native bridges can redirect focus to their visible Flood control.
|
|
35
|
+
if (focused && focused !== before && focused !== event.currentTarget && "scrollIntoView" in focused)
|
|
36
|
+
focused.scrollIntoView({ block: "nearest", behavior: "auto" });
|
|
37
|
+
// Keep native fragment navigation and modifier/new-tab behavior intact.
|
|
38
|
+
}, children: error.message }) }, error.id)) })] }) });
|
|
39
|
+
});
|
package/dist/feedback-actions.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
3
3
|
import { Button, IconButton, IconSlot, Spinner, VisuallyHidden } from "./index.js";
|
|
4
|
-
export const ClipboardAction = forwardRef(function ClipboardAction({ value, copyLabel = "Copy", copiedLabel = "Copied", errorLabel = "Failed", resetDelay = 2000, onCopied, onCopyError, className, disabled, size = "md", variant = "subtle", "aria-label": accessibleLabel, ...props }, ref) {
|
|
4
|
+
export const ClipboardAction = /* @__PURE__ */ forwardRef(function ClipboardAction({ value, copyLabel = "Copy", copiedLabel = "Copied", errorLabel = "Failed", resetDelay = 2000, onCopied, onCopyError, className, disabled, size = "md", variant = "subtle", "aria-label": accessibleLabel, ...props }, ref) {
|
|
5
5
|
if (!Number.isFinite(resetDelay) || resetDelay < 0)
|
|
6
6
|
throw new Error("Flood CopyButton resetDelay must be finite and nonnegative");
|
|
7
7
|
const [phase, setPhase] = useState("idle");
|
|
@@ -78,7 +78,7 @@ function TimedToast({ item, closeLabel, onDismiss }) {
|
|
|
78
78
|
return _jsxs("li", { ref: node, className: "flood-toast", "data-status": item.status ?? "neutral", onPointerEnter: () => setHover(true), onPointerLeave: () => setHover(false), onFocusCapture: () => setFocus(true), onBlurCapture: event => { if (!event.currentTarget.contains(event.relatedTarget))
|
|
79
79
|
setFocus(false); }, children: [_jsxs("div", { className: "flood-toast__message", role: item.status === "error" ? "alert" : "status", "aria-atomic": "true", children: [_jsx("div", { className: "flood-toast__title", children: item.title }), item.description && _jsx("div", { className: "flood-toast__description", children: item.description })] }), _jsx(IconButton, { label: `${closeLabel}: ${item.title}`, size: "sm", variant: "ghost", className: "flood-toast__close", onClick: dismiss, children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", children: _jsx("path", { d: "m6 6 12 12M18 6 6 18" }) }) }), item.action && _jsx(Button, { size: "sm", variant: "subtle", className: "flood-toast__action", onClick: item.action.onClick, children: item.action.label })] });
|
|
80
80
|
}
|
|
81
|
-
export const NotificationRegion = forwardRef(function NotificationRegion({ items, onDismiss, label = "Notifications", closeLabel = "Dismiss notification", placement = "bottom-end", limit = 3, inline = false, hotkey = true, className, ...props }, forwardedRef) {
|
|
81
|
+
export const NotificationRegion = /* @__PURE__ */ forwardRef(function NotificationRegion({ items, onDismiss, label = "Notifications", closeLabel = "Dismiss notification", placement = "bottom-end", limit = 3, inline = false, hotkey = true, className, ...props }, forwardedRef) {
|
|
82
82
|
if (!Number.isInteger(limit) || limit < 1)
|
|
83
83
|
throw new Error("Flood ToastRegion limit must be a positive integer");
|
|
84
84
|
const ids = new Set();
|
package/dist/feedback.js
CHANGED
|
@@ -9,7 +9,7 @@ export function validateRange(value, min, max, allowUnknown) {
|
|
|
9
9
|
if (value === undefined || !Number.isFinite(value) || value < min || value > max)
|
|
10
10
|
throw new RangeError("Flood indicator value must be finite and within its range");
|
|
11
11
|
}
|
|
12
|
-
export const ValueIndicator = forwardRef(function ValueIndicator({ kind, label, value, min, max, valueText, size, tone, hideLabel, className, ...props }, ref) {
|
|
12
|
+
export const ValueIndicator = /* @__PURE__ */ forwardRef(function ValueIndicator({ kind, label, value, min, max, valueText, size, tone, hideLabel, className, ...props }, ref) {
|
|
13
13
|
const labelId = `flood-indicator-${useId()}`;
|
|
14
14
|
validateRange(value, min, max, kind === "progress");
|
|
15
15
|
const unknown = value === undefined;
|
|
@@ -23,7 +23,7 @@ function StatusGlyph({ tone }) {
|
|
|
23
23
|
tone === "error" ? _jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "m9 9 6 6m0-6-6 6" })] }) :
|
|
24
24
|
_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M12 11v5m0-9h.01" })] }) });
|
|
25
25
|
}
|
|
26
|
-
export const FeedbackMessage = forwardRef(function FeedbackMessage({ title, tone, icon, announcement, children, className, ...props }, ref) {
|
|
26
|
+
export const FeedbackMessage = /* @__PURE__ */ forwardRef(function FeedbackMessage({ title, tone, icon, announcement, children, className, ...props }, ref) {
|
|
27
27
|
const artwork = icon === undefined ? _jsx(StatusGlyph, { tone: tone }) : icon;
|
|
28
28
|
return _jsxs("div", { ...props, ref: ref, className: ["flood-alert", className].filter(Boolean).join(" "), "data-tone": tone, role: announcement === "assertive" ? "alert" : announcement === "polite" ? "status" : undefined, "aria-atomic": announcement === "off" ? undefined : true, children: [artwork != null && _jsx("span", { className: "flood-alert__icon", "aria-hidden": "true", children: artwork }), _jsxs("div", { className: "flood-alert__content", children: [title && _jsx("p", { className: "flood-alert__title", children: title }), children != null && _jsx("div", { className: "flood-alert__body", children: children })] })] });
|
|
29
29
|
});
|
package/dist/file-input.js
CHANGED
|
@@ -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 { resolveFieldStatus } from "./field-state.js";
|
|
4
4
|
/** FileList and native form submission stay owned by the browser. */
|
|
5
|
-
export const FilePicker = forwardRef(function FilePicker({ actionComponent: Action, iconComponent: Icon, clearable = true, clearLabel = "Clear files", selectionLabel = "Selected files", removeFileLabel = "Remove file", placeholder, controlSize = "md", className, style, status, invalid, disabled, multiple, onChange, onFilesChange, ...props }, ref) {
|
|
5
|
+
export const FilePicker = /* @__PURE__ */ forwardRef(function FilePicker({ actionComponent: Action, iconComponent: Icon, clearable = true, clearLabel = "Clear files", selectionLabel = "Selected files", removeFileLabel = "Remove file", placeholder, controlSize = "md", className, style, status, invalid, disabled, multiple, onChange, onFilesChange, ...props }, ref) {
|
|
6
6
|
const input = useRef(null);
|
|
7
7
|
useImperativeHandle(ref, () => input.current, []);
|
|
8
8
|
const [files, setFiles] = useState([]);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type FileMetaProps, type FileItemProps, type UploadItemProps, type ImagePreviewProps } from "./index.js";
|
|
2
|
+
export declare const FileMetadata: import("react").ForwardRefExoticComponent<FileMetaProps & import("react").RefAttributes<HTMLSpanElement>>;
|
|
3
|
+
export declare const FileRecord: import("react").ForwardRefExoticComponent<FileItemProps & import("react").RefAttributes<HTMLLIElement>>;
|
|
4
|
+
export declare const UploadRecord: import("react").ForwardRefExoticComponent<UploadItemProps & import("react").RefAttributes<HTMLLIElement>>;
|
|
5
|
+
export declare const ModalImagePreview: import("react").ForwardRefExoticComponent<ImagePreviewProps & import("react").RefAttributes<HTMLDialogElement>>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useState } from "react";
|
|
3
|
+
import { DataListItem, IconSlot, IconButton, Link, Badge, FileMeta, StatusIndicator, Progress, Dialog, Image, Spinner, Text } from "./index.js";
|
|
4
|
+
import { hasFieldContent } from "./field-state.js";
|
|
5
|
+
const cx = (...parts) => parts.filter(Boolean).join(" ");
|
|
6
|
+
function sizeText(bytes, locale) {
|
|
7
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
8
|
+
throw new Error("Flood file size must be finite and nonnegative");
|
|
9
|
+
const units = ["B", "kB", "MB", "GB", "TB"], unit = bytes === 0 ? 0 : Math.max(0, Math.min(4, Math.floor(Math.log10(bytes) / 3)));
|
|
10
|
+
return `${new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(bytes / 1000 ** unit)} ${units[unit]}`;
|
|
11
|
+
}
|
|
12
|
+
export const FileMetadata = /* @__PURE__ */ forwardRef(function FileMetadata({ format, size, formatSize, className, ...props }, ref) {
|
|
13
|
+
if (format !== undefined && !format.trim())
|
|
14
|
+
throw new Error("Flood FileMeta format must be nonempty when supplied");
|
|
15
|
+
if (size !== undefined && (!Number.isFinite(size) || size < 0))
|
|
16
|
+
throw new Error("Flood file size must be finite and nonnegative");
|
|
17
|
+
if (format === undefined && size === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
const bytes = size === undefined ? undefined : (formatSize ?? ((value) => sizeText(value, props.lang)))(size);
|
|
20
|
+
return _jsxs(Badge, { ...props, ref: ref, size: "sm", tone: "neutral", className: cx("flood-file-meta", className), children: [format && _jsx("span", { children: format }), format && bytes && _jsx("span", { "aria-hidden": "true", children: "\u00B7" }), bytes && _jsx("span", { children: bytes })] });
|
|
21
|
+
});
|
|
22
|
+
function Glyph({ kind }) {
|
|
23
|
+
return _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: kind === "file" ? _jsxs(_Fragment, { children: [_jsx("path", { d: "M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9Z" }), _jsx("path", { d: "M14 3v6h6M8 13h8M8 17h5" })] }) :
|
|
24
|
+
kind === "preview" ? _jsxs(_Fragment, { children: [_jsx("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z" }), _jsx("circle", { cx: "12", cy: "12", r: "3" })] }) :
|
|
25
|
+
kind === "retry" ? _jsx(_Fragment, { children: _jsx("path", { d: "M3 10a9 9 0 1 1 2 8M3 4v6h6" }) }) : _jsx("path", { d: "m6 6 12 12M18 6 6 18" }) });
|
|
26
|
+
}
|
|
27
|
+
const FileRow = /* @__PURE__ */ forwardRef(function FileRow({ name, size, mimeType, format, thumbnail, description, href, download = false, onPreview, onRemove, disabled = false, labels = {}, formatSize, className, extraActions, ...props }, ref) {
|
|
28
|
+
if (!name.trim())
|
|
29
|
+
throw new Error("Flood FileItem requires a nonempty file name");
|
|
30
|
+
if (size !== undefined && (!Number.isFinite(size) || size < 0))
|
|
31
|
+
throw new Error("Flood file size must be finite and nonnegative");
|
|
32
|
+
const extension = name.lastIndexOf(".") > 0 ? name.slice(name.lastIndexOf(".") + 1) : "";
|
|
33
|
+
const displayedFormat = format === null ? undefined : format ?? mimeType ?? (/^[a-z0-9]{1,12}$/i.test(extension) ? extension.toUpperCase() : undefined);
|
|
34
|
+
return _jsx(DataListItem, { ...props, ref: ref, className: cx("flood-file-item", className), media: _jsx("span", { className: "flood-file-item__thumbnail", "aria-hidden": "true", children: hasFieldContent(thumbnail) ? thumbnail : _jsx(IconSlot, { size: "lg", children: _jsx(Glyph, { kind: "file" }) }) }), title: _jsxs("span", { className: "flood-file-item__heading", children: [href && !disabled ? _jsx(Link, { href: href, download: download ? name : undefined, className: "flood-file-item__name", children: name }) : _jsx("span", { className: "flood-file-item__name", dir: "auto", children: name }), _jsx(FileMeta, { format: displayedFormat, size: size, formatSize: formatSize, lang: props.lang })] }), description: hasFieldContent(description) ? description : undefined, actions: (onPreview || onRemove || hasFieldContent(extraActions)) ? _jsxs(_Fragment, { children: [onPreview && _jsx(IconButton, { label: `${labels.preview ?? "Preview"}: ${name}`, variant: "ghost", size: "sm", disabled: disabled, onClick: onPreview, children: _jsx(Glyph, { kind: "preview" }) }), extraActions, onRemove && _jsx(IconButton, { label: `${labels.remove ?? "Remove"}: ${name}`, variant: "ghost", size: "sm", disabled: disabled, onClick: onRemove, children: _jsx(Glyph, { kind: "remove" }) })] }) : undefined });
|
|
35
|
+
});
|
|
36
|
+
export const FileRecord = /* @__PURE__ */ forwardRef(function FileRecord(props, ref) { return _jsx(FileRow, { ...props, ref: ref }); });
|
|
37
|
+
export const UploadRecord = /* @__PURE__ */ forwardRef(function UploadRecord({ state, progress, error, onCancel, onRetry, onPreview, onRemove, href, labels = {}, disabled = false, className, ...props }, ref) {
|
|
38
|
+
if (!["queued", "uploading", "success", "error", "cancelled"].includes(state))
|
|
39
|
+
throw new Error("Flood UploadItem requires a supported state");
|
|
40
|
+
if (progress !== undefined && (!Number.isFinite(progress) || progress < 0 || progress > 100))
|
|
41
|
+
throw new Error("Flood upload progress must be between 0 and 100");
|
|
42
|
+
const active = state === "queued" || state === "uploading", retry = state === "error" || state === "cancelled";
|
|
43
|
+
const status = state === "error" ? (error || labels.error || "Upload failed") : labels[state] ?? ({ queued: "Waiting to upload", uploading: "Uploading", success: "Uploaded", error: "Upload failed", cancelled: "Cancelled" }[state]);
|
|
44
|
+
const percent = state === "uploading" && progress !== undefined ? ` · ${new Intl.NumberFormat(props.lang, { maximumFractionDigits: 0 }).format(progress)}%` : "";
|
|
45
|
+
return _jsx(FileRow, { ...props, ref: ref, labels: labels, disabled: disabled, href: state === "success" ? href : undefined, onPreview: state === "success" ? onPreview : undefined, onRemove: active ? undefined : onRemove, className: cx("flood-upload-item", className), "data-upload-state": state, description: _jsxs("div", { className: "flood-upload-item__status-row", "data-progress": state === "uploading" || undefined, children: [_jsx(StatusIndicator, { label: status + percent, tone: state === "error" ? "error" : state === "success" ? "success" : "neutral", size: "sm" }), state === "uploading" && _jsx(Progress, { label: `${labels.uploading ?? "Uploading"}: ${props.name}`, value: progress, size: "sm", hideLabel: true })] }), extraActions: (active && onCancel || retry && onRetry) ? _jsxs(_Fragment, { children: [active && onCancel && _jsx(IconButton, { label: `${labels.cancel ?? "Cancel upload"}: ${props.name}`, variant: "ghost", size: "sm", disabled: disabled, onClick: onCancel, children: _jsx(Glyph, { kind: "cancel" }) }), retry && onRetry && _jsx(IconButton, { label: `${labels.retry ?? "Retry upload"}: ${props.name}`, variant: "ghost", size: "sm", disabled: disabled, onClick: onRetry, children: _jsx(Glyph, { kind: "retry" }) })] }) : undefined });
|
|
46
|
+
});
|
|
47
|
+
function PreviewContent({ src, alt, caption, loadingLabel, errorLabel }) {
|
|
48
|
+
const [state, setState] = useState("loading");
|
|
49
|
+
return _jsxs("figure", { className: "flood-image-preview__figure", children: [_jsxs("div", { className: "flood-image-preview__media", "data-state": state, children: [_jsx(Image, { src: src, alt: alt, hidden: state === "error", className: "flood-image-preview__image", onLoad: () => setState("loaded"), onError: () => setState("error") }), state === "loading" && _jsx(Spinner, { label: loadingLabel }), state === "error" && _jsx(Text, { as: "p", size: "compact", tone: "subtle", role: "status", children: errorLabel })] }), hasFieldContent(caption) && _jsx("figcaption", { className: "flood-image-preview__caption", children: caption })] });
|
|
50
|
+
}
|
|
51
|
+
export const ModalImagePreview = /* @__PURE__ */ forwardRef(function ModalImagePreview({ src, alt, caption, downloadHref, downloadLabel = "Download image", loadingLabel = "Loading image…", errorLabel = "Could not load the image.", className, ...props }, ref) {
|
|
52
|
+
if (!src.trim() || typeof alt !== "string" || ![downloadLabel, loadingLabel, errorLabel].every(label => label.trim()))
|
|
53
|
+
throw new Error("Flood ImagePreview requires a source, explicit alt text and nonempty labels");
|
|
54
|
+
return _jsxs(Dialog, { ...props, ref: ref, className: cx("flood-image-preview", className), children: [_jsx(PreviewContent, { src: src, alt: alt, caption: caption, loadingLabel: loadingLabel, errorLabel: errorLabel }, src), downloadHref && _jsx("div", { className: "flood-image-preview__actions", children: _jsx(Link, { href: downloadHref, download: true, variant: "subtle", size: "sm", children: downloadLabel }) })] });
|
|
55
|
+
});
|
package/dist/form-controls.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from "react";
|
|
3
|
-
import { FileInput } from "./index.js";
|
|
2
|
+
import { forwardRef, useEffect, useLayoutEffect, useId, useImperativeHandle, useRef, useState } from "react";
|
|
3
|
+
import { FileInput, Label, Text } from "./index.js";
|
|
4
4
|
import { resolveFieldStatus, mergeDescriptions } from "./field-state.js";
|
|
5
5
|
const cx = (...values) => values.filter(Boolean).join(" ");
|
|
6
|
+
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
6
7
|
function useNativeReset(ref, form, reset) {
|
|
7
8
|
const callback = useRef(reset);
|
|
8
9
|
useEffect(() => { callback.current = reset; }, [reset]);
|
|
@@ -36,9 +37,12 @@ function useInheritedDisabled(ref, disabled) {
|
|
|
36
37
|
}, [ref, disabled]);
|
|
37
38
|
return disabled || inherited;
|
|
38
39
|
}
|
|
39
|
-
export const CollectionListBox = forwardRef(function CollectionListBox({ label, hideLabel = false, options, value, defaultValue, onValueChange, multiple = false, disabled = false, readOnly = false, name, form, controlSize = "md", id: explicitId, className, onKeyDown, onFocus, ...props }, forwardedRef) {
|
|
40
|
+
export const CollectionListBox = /* @__PURE__ */ forwardRef(function CollectionListBox({ label, hideLabel = false, options, value, defaultValue, onValueChange, multiple = false, selectionBehavior = "automatic", appearance = "surface", disabled = false, readOnly = false, name, form, controlSize = "md", id: explicitId, required = false, requiredMessage = "Choose at least one option.", validationMessage, description, message, status, invalid, className, onKeyDown, onFocus, onBlur, onInvalid, ...props }, forwardedRef) {
|
|
40
41
|
const generated = useId(), id = explicitId ?? `flood-list-box-${generated}`;
|
|
41
42
|
const root = useRef(null), bridge = useRef(null);
|
|
43
|
+
const [touched, setTouched] = useState(false);
|
|
44
|
+
if (!label.trim() || !requiredMessage.trim())
|
|
45
|
+
throw new Error("Flood ListBox requires a nonempty label and required message");
|
|
42
46
|
useImperativeHandle(forwardedRef, () => root.current, []);
|
|
43
47
|
const unique = new Set();
|
|
44
48
|
for (const option of options) {
|
|
@@ -56,11 +60,15 @@ export const CollectionListBox = forwardRef(function CollectionListBox({ label,
|
|
|
56
60
|
const first = Math.max(0, options.findIndex(option => !option.disabled));
|
|
57
61
|
const [active, setActive] = useState(() => { const index = options.findIndex(option => selected.includes(option.value) && !option.disabled); return index < 0 ? first : index; });
|
|
58
62
|
const effectiveDisabled = useInheritedDisabled(bridge, disabled);
|
|
63
|
+
const constraint = readOnly ? "" : validationMessage || (required && !selected.length ? requiredMessage : "");
|
|
64
|
+
const error = touched && !effectiveDisabled ? constraint : "", resolvedStatus = resolveFieldStatus(status, invalid || Boolean(error), props["aria-invalid"]);
|
|
65
|
+
const describedBy = mergeDescriptions(props["aria-describedby"], Boolean(description) && `${id}-description`, Boolean(error || message) && `${id}-message`);
|
|
66
|
+
useBrowserLayoutEffect(() => { bridge.current?.setCustomValidity(constraint); }, [constraint]);
|
|
59
67
|
const search = useRef({ text: "", time: 0 });
|
|
60
68
|
useNativeReset(bridge, form, () => { if (value === undefined) {
|
|
61
69
|
setLocal(normalize(initial.current));
|
|
62
70
|
setActive(first);
|
|
63
|
-
} });
|
|
71
|
+
} setTouched(false); });
|
|
64
72
|
const blocked = () => effectiveDisabled || bridge.current?.matches(":disabled");
|
|
65
73
|
const commit = (index) => {
|
|
66
74
|
const option = options[index];
|
|
@@ -71,15 +79,19 @@ export const CollectionListBox = forwardRef(function CollectionListBox({ label,
|
|
|
71
79
|
setLocal(next);
|
|
72
80
|
onValueChange?.(multiple ? [...next] : next[0] ?? "");
|
|
73
81
|
};
|
|
74
|
-
const move = (index, choose = !multiple) => {
|
|
82
|
+
const move = (index, choose = !multiple && selectionBehavior === "automatic") => {
|
|
75
83
|
setActive(index);
|
|
76
84
|
if (choose)
|
|
77
85
|
commit(index);
|
|
78
86
|
const target = root.current?.ownerDocument.getElementById(`${id}-option-${index}`);
|
|
79
87
|
target?.scrollIntoView({ block: "nearest" });
|
|
80
88
|
};
|
|
81
|
-
return _jsxs("div", { className: "flood-list-box-field", hidden: props.hidden, dir: props.dir, lang: props.lang, children: [_jsx(
|
|
82
|
-
|
|
89
|
+
return _jsxs("div", { className: "flood-list-box-field", hidden: props.hidden, dir: props.dir, lang: props.lang, children: [_jsx(Label, { htmlFor: `${id}-validation`, id: `${id}-label`, required: required, className: cx("flood-form-control__label", hideLabel && "flood-form-control__sr-only"), children: label }), _jsxs("div", { ...props, ref: root, id: id, role: "listbox", tabIndex: effectiveDisabled ? -1 : props.tabIndex ?? 0, "aria-label": props["aria-label"], "aria-labelledby": props["aria-label"] ? undefined : props["aria-labelledby"] ?? `${id}-label`, "aria-multiselectable": multiple || undefined, "aria-disabled": effectiveDisabled || undefined, "aria-readonly": readOnly || undefined, "aria-required": required || undefined, "aria-invalid": resolvedStatus === "error" || undefined, "aria-describedby": describedBy, "aria-activedescendant": options[active] ? `${id}-option-${active}` : undefined, "data-status": resolvedStatus, "data-appearance": appearance, className: cx("flood-list-box", `flood-control--${controlSize}`, className), onBlur: event => { onBlur?.(event); if (!event.defaultPrevented && !event.currentTarget.contains(event.relatedTarget))
|
|
90
|
+
setTouched(true); }, onInvalid: event => { onInvalid?.(event); if (event.defaultPrevented)
|
|
91
|
+
return; event.preventDefault(); setTouched(true); root.current?.focus(); }, onFocus: event => { onFocus?.(event); if (!options[active] || options[active].disabled)
|
|
92
|
+
setActive(first);
|
|
93
|
+
else
|
|
94
|
+
root.current?.querySelector(`[id="${id}-option-${active}"]`)?.scrollIntoView({ block: "nearest" }); }, onKeyDown: event => {
|
|
83
95
|
onKeyDown?.(event);
|
|
84
96
|
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing || blocked())
|
|
85
97
|
return;
|
|
@@ -108,10 +120,10 @@ export const CollectionListBox = forwardRef(function CollectionListBox({ label,
|
|
|
108
120
|
if (match !== undefined)
|
|
109
121
|
move(match);
|
|
110
122
|
}
|
|
111
|
-
}, children: [options.map((option, index) => _jsxs("div", { id: `${id}-option-${index}`, role: "option", "aria-selected": selected.includes(option.value), "aria-disabled": option.disabled || undefined, "data-active": index === active || undefined, className: "flood-list-box__option", onClick: () => { if (blocked())
|
|
112
|
-
return; root.current?.focus(); setActive(index); commit(index); }, children: [_jsxs("span", { className: "flood-list-box__copy", children: [_jsx("span", { children: option.label }), option.description && _jsx("span", { className: "flood-list-box__description", children: option.description })] }), _jsx("span", { className: "flood-list-box__check", "aria-hidden": "true", children: selected.includes(option.value) ? "✓" : "" })] }, option.value)), !options.length && _jsx("div", { role: "option", "aria-disabled": "true", className: "flood-list-box__option", children: "No options" })] }), _jsx("
|
|
123
|
+
}, children: [_jsx("input", { ref: bridge, id: `${id}-validation`, type: "text", className: "flood-form-control__sr-only", tabIndex: -1, "aria-hidden": "true", autoComplete: "off", form: form, disabled: disabled, readOnly: readOnly, required: required && !readOnly, value: selected.join("\u001f"), onChange: () => { }, onFocus: event => { event.stopPropagation(); root.current?.focus(); } }), options.map((option, index) => _jsxs("div", { id: `${id}-option-${index}`, role: "option", "aria-selected": selected.includes(option.value), "aria-disabled": option.disabled || undefined, "data-active": index === active || undefined, className: "flood-list-box__option", onClick: () => { if (blocked())
|
|
124
|
+
return; root.current?.focus(); setActive(index); commit(index); }, children: [_jsxs("span", { className: "flood-list-box__copy", children: [_jsx("span", { children: option.label }), option.description && _jsx("span", { className: "flood-list-box__description", children: option.description })] }), _jsx("span", { className: "flood-list-box__check", "aria-hidden": "true", children: selected.includes(option.value) ? "✓" : "" })] }, option.value)), !options.length && _jsx("div", { role: "option", "aria-disabled": "true", className: "flood-list-box__option", children: "No options" })] }), description && _jsx(Text, { as: "div", id: `${id}-description`, size: "helper", tone: "subtle", className: "flood-list-box__feedback", children: description }), (error || message) && _jsx(Text, { as: "div", id: `${id}-message`, size: "helper", tone: error ? "error" : resolvedStatus ?? "subtle", role: error ? "alert" : undefined, className: "flood-list-box__feedback", children: error || message }), name && selected.map(entry => _jsx("input", { type: "hidden", name: name, value: entry, form: form, disabled: disabled }, entry))] });
|
|
113
125
|
});
|
|
114
|
-
export const SegmentedPinInput = forwardRef(function SegmentedPinInput({ label, hideLabel = false, length = 6, mode = "numeric", mask = false, value, defaultValue = "", onValueChange, controlSize = "md", disabled, readOnly, className, style, status, invalid, id: explicitId, onFocus, onBlur, onSelect, onPointerDown, onPaste, onKeyUp, autoComplete = "one-time-code", inputMode, ...props }, forwardedRef) {
|
|
126
|
+
export const SegmentedPinInput = /* @__PURE__ */ forwardRef(function SegmentedPinInput({ label, hideLabel = false, length = 6, mode = "numeric", mask = false, value, defaultValue = "", onValueChange, controlSize = "md", disabled, readOnly, className, style, status, invalid, id: explicitId, onFocus, onBlur, onSelect, onPointerDown, onPaste, onKeyUp, autoComplete = "one-time-code", inputMode, ...props }, forwardedRef) {
|
|
115
127
|
if (!Number.isInteger(length) || length < 1 || length > 12)
|
|
116
128
|
throw new Error("Flood PinInput length must be an integer from 1 to 12");
|
|
117
129
|
const clean = (text) => text.replace(mode === "numeric" ? /[^0-9]/g : /[^a-zA-Z0-9]/g, "").slice(0, length);
|
|
@@ -160,7 +172,7 @@ export const SegmentedPinInput = forwardRef(function SegmentedPinInput({ label,
|
|
|
160
172
|
insert(next, Math.min(start + pasted.length, length));
|
|
161
173
|
} }), Array.from({ length }, (_, index) => _jsx("span", { "data-pin-slot": "", "aria-hidden": "true", className: "flood-pin-input__slot", "data-active": focused && index === active || undefined, children: text[index] ? mask ? "•" : text[index] : "" }, index))] })] });
|
|
162
174
|
});
|
|
163
|
-
export const StarRating = forwardRef(function StarRating({ label, hideLabel = false, value, defaultValue = 0, max = 5, onValueChange, getValueLabel, readOnly = false, required, name, form, size = "md", disabled, className, onKeyDown, ...props }, forwardedRef) {
|
|
175
|
+
export const StarRating = /* @__PURE__ */ forwardRef(function StarRating({ label, hideLabel = false, value, defaultValue = 0, max = 5, onValueChange, getValueLabel, readOnly = false, required, name, form, size = "md", disabled, className, onKeyDown, ...props }, forwardedRef) {
|
|
164
176
|
if (!name?.trim())
|
|
165
177
|
throw new Error("Flood Rating requires a nonempty native group name");
|
|
166
178
|
if (!Number.isInteger(max) || max < 1 || max > 10)
|
|
@@ -212,7 +224,7 @@ export const StarRating = forwardRef(function StarRating({ label, hideLabel = fa
|
|
|
212
224
|
event.preventDefault(); }, onChange: () => change(number) }), _jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinejoin: "round", children: _jsx("path", { d: "m12 3 2.8 5.8 6.4.9-4.6 4.5 1.1 6.4-5.7-3-5.7 3 1.1-6.4-4.6-4.5 6.4-.9L12 3Z" }) })] }, number);
|
|
213
225
|
}) })] });
|
|
214
226
|
});
|
|
215
|
-
export const FileDropzone = forwardRef(function FileDropzone({ label, description = "Drop files here or choose from your device.", multiple = true, disabled, className, style, id: explicitId, status, invalid, ...props }, forwardedRef) {
|
|
227
|
+
export const FileDropzone = /* @__PURE__ */ forwardRef(function FileDropzone({ label, description = "Drop files here or choose from your device.", multiple = true, disabled, className, style, id: explicitId, status, invalid, ...props }, forwardedRef) {
|
|
216
228
|
const input = useRef(null), generated = useId(), id = explicitId ?? `flood-dropzone-${generated}`;
|
|
217
229
|
useImperativeHandle(forwardedRef, () => input.current, []);
|
|
218
230
|
const [dragging, setDragging] = useState(false), depth = useRef(0);
|
|
@@ -240,7 +252,7 @@ export const FileDropzone = forwardRef(function FileDropzone({ label, descriptio
|
|
|
240
252
|
node.dispatchEvent(new node.ownerDocument.defaultView.Event("change", { bubbles: true }));
|
|
241
253
|
}, children: [_jsxs("div", { className: "flood-dropzone__intro", children: [_jsxs("label", { htmlFor: id, className: "flood-dropzone__label", children: [_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M12 16V3m-4 4 4-4 4 4M4 15v5a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-5" }) }), _jsx("span", { children: label })] }), description && _jsx("p", { id: `${id}-description`, className: "flood-dropzone__description", children: description })] }), _jsx(FileInput, { ...props, ref: input, id: id, multiple: multiple, disabled: disabled, status: status, invalid: invalid, "aria-describedby": mergeDescriptions(props["aria-describedby"], Boolean(description) && `${id}-description`) })] });
|
|
242
254
|
});
|
|
243
|
-
export const SupportingCharacterCount = forwardRef(function SupportingCharacterCount({ value = "", count, max, mode = "count", label = "Characters", overLimitLabel = "Over limit", className, ...props }, ref) {
|
|
255
|
+
export const SupportingCharacterCount = /* @__PURE__ */ forwardRef(function SupportingCharacterCount({ value = "", count, max, mode = "count", label = "Characters", overLimitLabel = "Over limit", className, ...props }, ref) {
|
|
244
256
|
const current = count ?? [...value].length;
|
|
245
257
|
if (!Number.isInteger(current) || current < 0 || max !== undefined && (!Number.isInteger(max) || max < 0))
|
|
246
258
|
throw new Error("Flood CharacterCount requires nonnegative integer counts and limits");
|