@flood-md/react 0.2.10 → 0.2.11
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 +239 -1
- package/dist/app-shell.d.ts +3 -0
- package/dist/app-shell.js +66 -0
- package/dist/bulk-action-bar.d.ts +3 -0
- package/dist/bulk-action-bar.js +17 -0
- package/dist/collection-toolbar.d.ts +3 -0
- package/dist/collection-toolbar.js +26 -0
- package/dist/collection-view-switcher.d.ts +3 -0
- package/dist/collection-view-switcher.js +19 -0
- package/dist/combobox.d.ts +6 -1
- package/dist/combobox.js +77 -29
- package/dist/command-list.d.ts +5 -1
- package/dist/command-list.js +2 -2
- package/dist/data-display.js +1 -1
- package/dist/data-table.js +44 -9
- package/dist/date-picker.js +2 -2
- package/dist/date-range-picker.js +2 -2
- package/dist/date-time-picker.js +2 -2
- package/dist/date-time-range-picker.js +2 -2
- package/dist/disclosure.d.ts +2 -1
- package/dist/disclosure.js +4 -4
- package/dist/filter-condition.d.ts +3 -0
- package/dist/filter-condition.js +21 -0
- package/dist/filter-group.d.ts +2 -0
- package/dist/filter-group.js +62 -0
- package/dist/foundations.css +752 -76
- package/dist/index.d.ts +399 -3
- package/dist/index.js +61 -7
- package/dist/menu.js +5 -4
- package/dist/menubar.d.ts +2 -0
- package/dist/menubar.js +85 -0
- package/dist/navigation-group.d.ts +3 -0
- package/dist/navigation-group.js +13 -0
- package/dist/navigation-item.d.ts +3 -0
- package/dist/navigation-item.js +68 -0
- package/dist/navigation-layout.d.ts +2 -1
- package/dist/navigation-layout.js +12 -4
- package/dist/navigation-tree.js +51 -7
- package/dist/navigation.js +2 -2
- package/dist/overflow-actions.d.ts +2 -0
- package/dist/overflow-actions.js +75 -0
- package/dist/overlay-runtime.d.ts +3 -0
- package/dist/overlay-runtime.js +35 -1
- package/dist/overlays.js +32 -16
- package/dist/select.js +4 -1
- package/dist/sidebar.d.ts +2 -0
- package/dist/sidebar.js +58 -0
- package/dist/toolbar.js +2 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { forwardRef, createElement, useId, useState, } from "react";
|
|
3
|
+
import { forwardRef, createElement, useId, useRef, useState, } from "react";
|
|
4
4
|
import { useFocusableHidden } from "./focusable-hidden.js";
|
|
5
|
+
import { PredicateRow } from "./filter-condition.js";
|
|
6
|
+
import { PredicateGroup } from "./filter-group.js";
|
|
7
|
+
import { CollectionQueryBar } from "./collection-toolbar.js";
|
|
8
|
+
import { CollectionViewSelector } from "./collection-view-switcher.js";
|
|
9
|
+
import { WorkspaceSidebar } from "./sidebar.js";
|
|
10
|
+
import { NavigationRow } from "./navigation-item.js";
|
|
11
|
+
import { NavigationSection } from "./navigation-group.js";
|
|
5
12
|
import { resolveFieldStatus, hasFieldContent, mergeDescriptions } from "./field-state.js";
|
|
6
13
|
import { SingleSelect } from "./select.js";
|
|
7
14
|
import { MixedCheckbox } from "./checkbox.js";
|
|
@@ -18,7 +25,8 @@ import { RovingToolbar } from "./toolbar.js";
|
|
|
18
25
|
import { NumericStepper } from "./numeric-input.js";
|
|
19
26
|
import { RangeControl } from "./slider.js";
|
|
20
27
|
import { GroupedAvatars, NamedStatus, CaptionedFigure, AttributedQuote, InlineCodeText, MeasuredValue, EventTimeline } from "./content-elements.js";
|
|
21
|
-
import { NavigationMenu, StepSequence, ContentSection, SplitLayout, CenterLayout, SkipNavigation } from "./navigation-layout.js";
|
|
28
|
+
import { NavigationMenu, StepSequence, ContentSection, PageHeading, SplitLayout, CenterLayout, SkipNavigation } from "./navigation-layout.js";
|
|
29
|
+
import { ApplicationShell, ApplicationTopBar } from "./app-shell.js";
|
|
22
30
|
import { CollectionListBox, SegmentedPinInput, StarRating, FileDropzone, SupportingCharacterCount } from "./form-controls.js";
|
|
23
31
|
import { ClipboardAction, NotificationRegion } from "./feedback-actions.js";
|
|
24
32
|
import { ConfirmationDialog } from "./alert-dialog.js";
|
|
@@ -49,6 +57,23 @@ import { HoverPreview } from "./hover-card.js";
|
|
|
49
57
|
import { HierarchicalSelect } from "./tree-select.js";
|
|
50
58
|
import { CivilDateTimePicker } from "./date-time-picker.js";
|
|
51
59
|
import { HierarchicalNavigation } from "./navigation-tree.js";
|
|
60
|
+
import { AdaptiveActions } from "./overflow-actions.js";
|
|
61
|
+
import { SelectionActionBar } from "./bulk-action-bar.js";
|
|
62
|
+
import { ApplicationMenubar } from "./menubar.js";
|
|
63
|
+
/** Priority-based overflow composed from Toolbar, Button and Menu. @since 0.2.11 */
|
|
64
|
+
export const OverflowActions = /* @__PURE__ */ forwardRef(function OverflowActions(props, ref) { return _jsx(AdaptiveActions, { ...props, ref: ref }); });
|
|
65
|
+
/** One finite-choice filter predicate; the host owns application and query semantics. @since 0.2.11 */
|
|
66
|
+
export const FilterCondition = /* @__PURE__ */ forwardRef(function FilterCondition(props, ref) { return _jsx(PredicateRow, { ...props, ref: ref }); });
|
|
67
|
+
/** A flat controlled group of finite-choice predicates, without query execution or Apply/Cancel ownership. @since 0.2.11 */
|
|
68
|
+
export const FilterGroup = /* @__PURE__ */ forwardRef(function FilterGroup(props, ref) { return _jsx(PredicateGroup, { ...props, ref: ref }); });
|
|
69
|
+
/** Search, accepted summaries and staged filter disclosure; saved views and query semantics stay host-owned. @since 0.2.11 */
|
|
70
|
+
export const CollectionToolbar = /* @__PURE__ */ forwardRef(function CollectionToolbar(props, ref) { return _jsx(CollectionQueryBar, { ...props, ref: ref }); });
|
|
71
|
+
/** Controlled saved/preset view choice with optional adaptive commands. @since 0.2.11 */
|
|
72
|
+
export const CollectionViewSwitcher = /* @__PURE__ */ forwardRef(function CollectionViewSwitcher(props, ref) { return _jsx(CollectionViewSelector, { ...props, ref: ref }); });
|
|
73
|
+
/** A bounded single-row selection command capsule. The host owns positioning and updates. @since 0.2.11 */
|
|
74
|
+
export const BulkActionBar = /* @__PURE__ */ forwardRef(function BulkActionBar(props, ref) { return _jsx(SelectionActionBar, { ...props, ref: ref }); });
|
|
75
|
+
/** Desktop application commands; destinations use native links instead. @since 0.2.11 */
|
|
76
|
+
export const Menubar = /* @__PURE__ */ forwardRef(function Menubar(props, ref) { return _jsx(ApplicationMenubar, { ...props, ref: ref }); });
|
|
52
77
|
/** @since 0.2.8 */
|
|
53
78
|
export const TimePicker = /* @__PURE__ */ forwardRef(function TimePicker(props, ref) { return _jsx(ClockTimePicker, { ...props, ref: ref }); });
|
|
54
79
|
/** Four atomic civil date/time form values, without timezone conversion. @since 0.2.8 */
|
|
@@ -99,10 +124,16 @@ export const DateTimePicker = /* @__PURE__ */ forwardRef(function DateTimePicker
|
|
|
99
124
|
export const Tree = /* @__PURE__ */ forwardRef(function Tree(props, ref) {
|
|
100
125
|
return _jsx(SelectionTree, { ...props, ref: ref });
|
|
101
126
|
});
|
|
127
|
+
/** Shared navigation row geometry and native destination/action behavior. @since 0.2.11 */
|
|
128
|
+
export const NavigationItem = /* @__PURE__ */ forwardRef(function NavigationItem(props, ref) { return _jsx(NavigationRow, { ...props, ref: ref }); });
|
|
129
|
+
/** Related navigation with independently configurable presentation. @since 0.2.11 */
|
|
130
|
+
export const NavigationGroup = /* @__PURE__ */ forwardRef(function NavigationGroup(props, ref) { return _jsx(NavigationSection, { ...props, ref: ref }); });
|
|
102
131
|
/** @since 0.2.7 */
|
|
103
132
|
export const NavigationTree = /* @__PURE__ */ forwardRef(function NavigationTree(props, ref) {
|
|
104
133
|
return _jsx(HierarchicalNavigation, { ...props, ref: ref });
|
|
105
134
|
});
|
|
135
|
+
/** Bounded desktop navigation composed from NavigationTree and ScrollArea. @since 0.2.11 */
|
|
136
|
+
export const Sidebar = /* @__PURE__ */ forwardRef(function Sidebar(props, ref) { return _jsx(WorkspaceSidebar, { ...props, ref: ref }); });
|
|
106
137
|
/** @since 0.2.7 */
|
|
107
138
|
export const ErrorSummary = /* @__PURE__ */ forwardRef(function ErrorSummary({ announcement = "off", autoFocus = false, ...props }, ref) { return _jsx(ValidationErrorSummary, { ...props, ref: ref, announcement: announcement, autoFocus: autoFocus }); });
|
|
108
139
|
/** @since 0.2.7 */
|
|
@@ -128,8 +159,8 @@ export const TagInput = /* @__PURE__ */ forwardRef(function TagInput({ controlSi
|
|
|
128
159
|
/** @since 0.2.7 */
|
|
129
160
|
export const Editable = /* @__PURE__ */ forwardRef(function Editable({ controlSize = "md", blurAction = "commit", showActions = true, ...props }, ref) { return _jsx(InlineEditable, { ...props, ref: ref, controlSize: controlSize, blurAction: blurAction, showActions: showActions }); });
|
|
130
161
|
const classes = (...values) => values.filter(Boolean).join(" ");
|
|
131
|
-
export const Button = /* @__PURE__ */ forwardRef(function Button({ variant = "solid", size = "md", loading = false, disabled, type = "button", className, children, ...props }, ref) {
|
|
132
|
-
return (_jsxs("button", { ...props, ref: ref, type: type, disabled: disabled || loading, "aria-busy": loading || undefined, className: classes("flood-button", `flood-button--${variant}`, `flood-button--${size}`, className), children: [loading && _jsx(Spinner, { decorative: true }), children] }));
|
|
162
|
+
export const Button = /* @__PURE__ */ forwardRef(function Button({ variant = "solid", tone = "default", size = "md", loading = false, disabled, type = "button", className, children, ...props }, ref) {
|
|
163
|
+
return (_jsxs("button", { ...props, ref: ref, type: type, "data-tone": tone === "danger" ? tone : undefined, disabled: disabled || loading, "aria-busy": loading || undefined, className: classes("flood-button", `flood-button--${variant}`, `flood-button--${size}`, className), children: [loading && _jsx(Spinner, { decorative: true }), children] }));
|
|
133
164
|
});
|
|
134
165
|
/** @since 0.2.5 */
|
|
135
166
|
export const Tag = /* @__PURE__ */ forwardRef(function Tag({ size = "md", icon, disabled = false, onRemove, removeLabel, removeButtonProps, className, children, ...props }, ref) {
|
|
@@ -197,6 +228,17 @@ export const Menu = /* @__PURE__ */ forwardRef(function Menu({ defaultOpen = fal
|
|
|
197
228
|
export const Tooltip = /* @__PURE__ */ forwardRef(function Tooltip({ side = "top", align = "center", delay = 300, disabled = false, ...props }, ref) { return _jsx(AnchoredOverlay, { ...props, ref: ref, kind: "tooltip", side: side, align: align, delay: delay, disabled: disabled }); });
|
|
198
229
|
export const Popover = /* @__PURE__ */ forwardRef(function Popover({ side = "bottom", align = "start", defaultOpen = false, disabled = false, closeLabel = "Close", ...props }, ref) { return _jsx(AnchoredOverlay, { ...props, ref: ref, kind: "popover", side: side, align: align, defaultOpen: defaultOpen, disabled: disabled, closeLabel: closeLabel }); });
|
|
199
230
|
export const Dialog = /* @__PURE__ */ forwardRef(function Dialog({ defaultOpen = false, disabled = false, closeLabel = "Close", closeOnOutsideClick = true, ...props }, ref) { return _jsx(ModalOverlay, { ...props, ref: ref, defaultOpen: defaultOpen, disabled: disabled, closeLabel: closeLabel, closeOnOutsideClick: closeOnOutsideClick }); });
|
|
231
|
+
/** @since 0.2.11 */
|
|
232
|
+
export const CommandPalette = /* @__PURE__ */ forwardRef(function CommandPalette({ searchLabel, items, density = "comfortable", query, defaultQuery, onQueryChange, onAction, loading, placeholder, emptyMessage, loadingMessage, open, defaultOpen = false, onOpenChange, className, ...props }, ref) {
|
|
233
|
+
const input = useRef(null);
|
|
234
|
+
const [localOpen, setLocalOpen] = useState(defaultOpen);
|
|
235
|
+
const requestOpen = (next) => {
|
|
236
|
+
if (open === undefined)
|
|
237
|
+
setLocalOpen(next);
|
|
238
|
+
onOpenChange?.(next);
|
|
239
|
+
};
|
|
240
|
+
return _jsxs(Dialog, { ...props, ref: ref, open: open ?? localOpen, onOpenChange: requestOpen, initialFocusRef: input, className: classes("flood-command-palette", className), children: [_jsx("div", { className: "flood-command-palette__search-icon", children: _jsx(IconSlot, { size: "sm", children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", "aria-hidden": "true", focusable: "false", children: [_jsx("circle", { cx: "10.5", cy: "10.5", r: "5.75" }), _jsx("path", { d: "m15 15 4 4" })] }) }) }), _jsx(SearchCommandList, { ref: input, label: searchLabel, hideLabel: true, integrated: true, items: items, controlSize: density === "compact" ? "sm" : "md", query: query, defaultQuery: defaultQuery, onQueryChange: onQueryChange, loading: loading, placeholder: placeholder, emptyMessage: emptyMessage, loadingMessage: loadingMessage, onAction: id => { requestOpen(false); onAction(id); } })] });
|
|
241
|
+
});
|
|
200
242
|
export const Drawer = /* @__PURE__ */ forwardRef(function Drawer({ side = "end", size = "md", defaultOpen = false, closeLabel = "Close", closeOnOutsideClick = true, className, footer, ...props }, ref) {
|
|
201
243
|
return _jsx(ModalOverlay, { ...props, ref: ref, defaultOpen: defaultOpen, closeLabel: closeLabel, closeOnOutsideClick: closeOnOutsideClick, className: classes("flood-drawer", className), "data-side": side, "data-size": size, scrollBody: true, footer: footer });
|
|
202
244
|
});
|
|
@@ -304,6 +346,10 @@ export const SearchInput = /* @__PURE__ */ forwardRef(function SearchInput({ cle
|
|
|
304
346
|
export const PasswordInput = /* @__PURE__ */ forwardRef(function PasswordInput({ defaultVisible = false, showPasswordLabel = "Show password", hidePasswordLabel = "Hide password", controlSize = "md", ...props }, ref) {
|
|
305
347
|
return _jsx(PasswordField, { ...props, ref: ref, defaultVisible: defaultVisible, showPasswordLabel: showPasswordLabel, hidePasswordLabel: hidePasswordLabel, controlSize: controlSize, group: InputGroup, action: IconButton, icon: IconSlot });
|
|
306
348
|
});
|
|
349
|
+
/** A free text field with suggestions; FormData contains text rather than an option ID. @since 0.2.11 */
|
|
350
|
+
export const Autocomplete = /* @__PURE__ */ forwardRef(function Autocomplete({ value, defaultValue = "", onValueChange, onOptionSelect, ...props }, ref) {
|
|
351
|
+
return _jsx(SearchSelection, { ...props, ref: ref, freeText: true, textValue: value, initialText: defaultValue, onTextChange: onValueChange, onOptionSelect: onOptionSelect, initialValues: [] });
|
|
352
|
+
});
|
|
307
353
|
/** @since 0.2.5 */
|
|
308
354
|
export const Combobox = /* @__PURE__ */ forwardRef(function Combobox({ value, defaultValue = "", onValueChange, clearable = false, emptyMessage = "No results found.", listLabel = "Options", toggleLabel = "Show options", clearLabel = "Clear selection", invalidSelectionMessage = "Select an option.", controlSize = "md", ...props }, ref) {
|
|
309
355
|
return _jsx(SearchSelection, { ...props, ref: ref, multiple: false, selectedValues: value === undefined ? undefined : value ? [value] : [], initialValues: defaultValue ? [defaultValue] : [], onSelectionChange: next => onValueChange?.(next[0] ?? ""), clearable: clearable, emptyMessage: emptyMessage, listLabel: listLabel, toggleLabel: toggleLabel, clearLabel: clearLabel, invalidSelectionMessage: invalidSelectionMessage, controlSize: controlSize });
|
|
@@ -360,7 +406,9 @@ export const RadioGroup = /* @__PURE__ */ forwardRef(function RadioGroup({ value
|
|
|
360
406
|
export const SegmentedControl = /* @__PURE__ */ forwardRef(function SegmentedControl({ label, options, controlSize = "md", iconOnly = false, fullWidth = false, orientation = "horizontal", className, onKeyDown, ...props }, ref) {
|
|
361
407
|
if (iconOnly && options.some(option => !option.icon))
|
|
362
408
|
throw new Error("Flood icon-only segments require icons");
|
|
363
|
-
return _jsx(RadioGroup, { ...props, ref: ref, legend: label, orientation: orientation, className: classes("flood-segmented", `flood-control--${controlSize}`, iconOnly && "flood-segmented--icons", fullWidth && "flood-segmented--full", className), options: options.map(option => ({ ...option, label: _jsxs(_Fragment, { children: [option.icon && _jsx(IconSlot, { size: controlSize, children: option.icon }), iconOnly ? _jsx("span", { className: "flood-segmented__name", children: option.label })
|
|
409
|
+
return _jsx(RadioGroup, { ...props, ref: ref, legend: label, orientation: orientation, className: classes("flood-segmented", `flood-control--${controlSize}`, iconOnly && "flood-segmented--icons", fullWidth && "flood-segmented--full", className), options: options.map(option => ({ ...option, label: _jsxs(_Fragment, { children: [option.icon && _jsx(IconSlot, { size: controlSize, children: option.icon }), iconOnly ? _jsx("span", { className: "flood-segmented__name", children: option.label })
|
|
410
|
+
: option.count == null || option.count === "" ? option.label
|
|
411
|
+
: _jsxs("span", { className: "flood-segmented__counted-label", children: [option.label, " ", _jsx("span", { className: "flood-segmented__count", children: option.count })] })] }) })), onKeyDown: event => {
|
|
364
412
|
onKeyDown?.(event);
|
|
365
413
|
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
|
|
366
414
|
return;
|
|
@@ -476,8 +524,8 @@ export const Card = /* @__PURE__ */ forwardRef(function Card({ title, descriptio
|
|
|
476
524
|
export const IconSlot = /* @__PURE__ */ forwardRef(function IconSlot({ size = "md", label, className, children, ...props }, ref) {
|
|
477
525
|
return _jsx("span", { ...props, ref: ref, role: label ? "img" : undefined, "aria-label": label, "aria-hidden": label ? undefined : true, className: classes("flood-icon", `flood-icon--${size}`, className), children: children });
|
|
478
526
|
});
|
|
479
|
-
export const IconButton = /* @__PURE__ */ forwardRef(function IconButton({ label, size = "md", variant = "subtle", className, children, ...props }, ref) {
|
|
480
|
-
return _jsx(Button, { ...props, ref: ref, size: size, variant: variant, "aria-label": label, className: classes("flood-icon-button", className), children: _jsx(IconSlot, { size:
|
|
527
|
+
export const IconButton = /* @__PURE__ */ forwardRef(function IconButton({ label, size = "md", iconSize = size, variant = "subtle", className, children, ...props }, ref) {
|
|
528
|
+
return _jsx(Button, { ...props, ref: ref, size: size, variant: variant, "aria-label": label, className: classes("flood-icon-button", className), children: _jsx(IconSlot, { size: iconSize, children: children }) });
|
|
481
529
|
});
|
|
482
530
|
export const Divider = /* @__PURE__ */ forwardRef(function Divider({ className, ...props }, ref) {
|
|
483
531
|
return _jsx("hr", { ...props, ref: ref, className: classes("flood-divider", className) });
|
|
@@ -510,6 +558,12 @@ export const Timeline = /* @__PURE__ */ forwardRef(function Timeline(props, ref)
|
|
|
510
558
|
export const NavigationList = /* @__PURE__ */ forwardRef(function NavigationList({ size = "md", ...props }, ref) { return _jsx(NavigationMenu, { ...props, ref: ref, size: size }); });
|
|
511
559
|
export const Steps = /* @__PURE__ */ forwardRef(function Steps({ orientation = "horizontal", ...props }, ref) { return _jsx(StepSequence, { ...props, ref: ref, orientation: orientation }); });
|
|
512
560
|
export const Section = /* @__PURE__ */ forwardRef(function Section({ headingLevel = 2, headingSize = "section", ...props }, ref) { return _jsx(ContentSection, { ...props, ref: ref, headingLevel: headingLevel, headingSize: headingSize }); });
|
|
561
|
+
/** The primary heading and associated actions inside a page's main region. @since 0.2.11 */
|
|
562
|
+
export const PageHeader = /* @__PURE__ */ forwardRef(function PageHeader({ headingSize = "large", headingLevel = 1, ...props }, ref) { return _jsx(PageHeading, { ...props, ref: ref, headingSize: headingSize, headingLevel: headingLevel }); });
|
|
563
|
+
/** Persistent application chrome with independent context and action slots. @since 0.2.11 */
|
|
564
|
+
export const AppTopBar = /* @__PURE__ */ forwardRef(function AppTopBar({ bordered = false, ...props }, ref) { return _jsx(ApplicationTopBar, { ...props, ref: ref, bordered: bordered }); });
|
|
565
|
+
/** Composes application navigation and a main landmark with optional bounded scrolling. @since 0.2.11 */
|
|
566
|
+
export const AppShell = /* @__PURE__ */ forwardRef(function AppShell({ navigationSide = "start", scroll = "content", mainAs = "main", ...props }, ref) { return _jsx(ApplicationShell, { ...props, ref: ref, navigationSide: navigationSide, scroll: scroll, mainAs: mainAs }); });
|
|
513
567
|
export const Split = /* @__PURE__ */ forwardRef(function Split({ side = "end", ratio = "main", collapseAt = "md", gap = "6", ...props }, ref) { return _jsx(SplitLayout, { ...props, ref: ref, side: side, ratio: ratio, collapseAt: collapseAt, gap: gap }); });
|
|
514
568
|
export const Center = /* @__PURE__ */ forwardRef(function Center({ inline = false, ...props }, ref) { return _jsx(CenterLayout, { ...props, ref: ref, inline: inline }); });
|
|
515
569
|
export const SkipLink = /* @__PURE__ */ forwardRef(function SkipLink({ children = "Skip to content", ...props }, ref) { return _jsx(SkipNavigation, { ...props, ref: ref, children: children }); });
|
package/dist/menu.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 { Fragment, forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
3
|
import { Button, Kbd, ScrollArea } from "./index.js";
|
|
4
|
-
import { positionLayer, registerLayer, returnFocus, tabbables } from "./overlay-runtime.js";
|
|
4
|
+
import { positionLayer, registerLayer, returnFocus, tabbables, useLayerExitContent } from "./overlay-runtime.js";
|
|
5
5
|
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
6
6
|
/** Validate each level while retaining globally unique action IDs. */
|
|
7
7
|
export function validateMenuTree(items) {
|
|
@@ -171,6 +171,7 @@ export const MenuLayer = /* @__PURE__ */ forwardRef(function MenuLayerRender({ l
|
|
|
171
171
|
element.hidePopover();
|
|
172
172
|
};
|
|
173
173
|
}, [open, side, align, nested]);
|
|
174
|
+
const showContent = useLayerExitContent(panel, open);
|
|
174
175
|
useBrowserLayoutEffect(() => {
|
|
175
176
|
if (openChild && !items.some(item => item.id === openChild && item.type === "submenu" && !item.disabled))
|
|
176
177
|
setOpenChild(undefined);
|
|
@@ -211,7 +212,7 @@ export const MenuLayer = /* @__PURE__ */ forwardRef(function MenuLayerRender({ l
|
|
|
211
212
|
if (item.type === "separator")
|
|
212
213
|
return _jsx("div", { role: "separator", className: "flood-menu__separator" }, item.id);
|
|
213
214
|
const submenu = item.type === "submenu", childId = `${id}-submenu-${encodeURIComponent(item.id)}`;
|
|
214
|
-
return _jsxs(Fragment, { children: [_jsxs(Button, { ref: anchorRef(item.id), role: item.type === "checkbox" ? "menuitemcheckbox" : item.type === "radio" ? "menuitemradio" : "menuitem", "aria-checked": item.type === "checkbox" || item.type === "radio" ? item.checked : undefined, "aria-haspopup": submenu ? "menu" : undefined, "aria-expanded": submenu ? openChild === item.id && !item.disabled : undefined, "aria-controls": submenu ? childId : undefined, tabIndex: -1, variant: "ghost", size: controlSize, "aria-label": item.label, "aria-disabled": item.disabled || undefined, "data-label": item.label, "data-menu-id": item.id,
|
|
215
|
+
return _jsxs(Fragment, { children: [_jsxs(Button, { ref: anchorRef(item.id), role: item.type === "checkbox" ? "menuitemcheckbox" : item.type === "radio" ? "menuitemradio" : "menuitem", "aria-checked": item.type === "checkbox" || item.type === "radio" ? item.checked : undefined, "aria-haspopup": submenu ? "menu" : undefined, "aria-expanded": submenu ? openChild === item.id && !item.disabled : undefined, "aria-controls": submenu ? childId : undefined, tabIndex: -1, variant: "ghost", size: controlSize, "aria-label": item.label, "aria-disabled": item.disabled || undefined, "data-label": item.label, "data-menu-id": item.id, tone: "tone" in item ? item.tone : undefined, "data-submenu": submenu || undefined, className: "flood-menu__item", onPointerDown: event => { pointerType.current = event.pointerType; }, onPointerEnter: event => { if (event.pointerType === "mouse")
|
|
215
216
|
scheduleHover(item, event.currentTarget); }, onPointerLeave: () => clearHover(), onPointerMove: event => {
|
|
216
217
|
if (event.pointerType === "touch" || openChild)
|
|
217
218
|
return;
|
|
@@ -262,7 +263,7 @@ export const MenuLayer = /* @__PURE__ */ forwardRef(function MenuLayerRender({ l
|
|
|
262
263
|
group.push(items[++index]);
|
|
263
264
|
rows.push(_jsxs("div", { role: "group", "aria-label": item.group, children: [_jsx("div", { className: "flood-menu__group-label", "aria-hidden": "true", children: item.group }), group.map(renderItem)] }, `radio-${item.id}`));
|
|
264
265
|
}
|
|
265
|
-
return _jsx("div", { ...props, ref: panel, id: id, popover: "manual", role: "menu", "aria-label": label, "aria-orientation": "vertical", tabIndex: -1, className: ["flood-overlay-panel", "flood-menu", nested && "flood-menu--submenu", className].filter(Boolean).join(" "), onKeyDown: event => {
|
|
266
|
+
return _jsx("div", { ...props, ref: panel, id: id, popover: "manual", role: "menu", "aria-label": label, "aria-orientation": "vertical", "aria-hidden": !open || undefined, tabIndex: -1, className: ["flood-overlay-panel", "flood-menu", nested && "flood-menu--submenu", className].filter(Boolean).join(" "), onKeyDown: event => {
|
|
266
267
|
if (event.target.closest('[role="menu"]') !== panel.current)
|
|
267
268
|
return;
|
|
268
269
|
onKeyDown?.(event);
|
|
@@ -325,7 +326,7 @@ export const MenuLayer = /* @__PURE__ */ forwardRef(function MenuLayerRender({ l
|
|
|
325
326
|
all[next].focus({ preventScroll: true });
|
|
326
327
|
all[next].scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
327
328
|
}
|
|
328
|
-
}, children:
|
|
329
|
+
}, children: showContent && _jsx(ScrollArea, { orientation: "vertical", role: "group", tabIndex: -1, className: "flood-menu__viewport", children: rows }) });
|
|
329
330
|
});
|
|
330
331
|
export const ActionMenu = /* @__PURE__ */ forwardRef(function ActionMenu({ label, trigger, items, onAction, open: controlled, defaultOpen, onOpenChange, disabled, side = "bottom", align = "start", controlSize = "sm", ...props }, forwardedRef) {
|
|
331
332
|
if (!label.trim())
|
package/dist/menubar.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { Button, Menu } from "./index.js";
|
|
4
|
+
import { tabbables } from "./overlay-runtime.js";
|
|
5
|
+
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
6
|
+
export const ApplicationMenubar = /* @__PURE__ */ forwardRef(function ApplicationMenubar({ label, menus, openValue, defaultOpenValue = null, onOpenValueChange, onAction, controlSize = "sm", disabled = false, className, onKeyDown, onKeyDownCapture, ...props }, ref) {
|
|
7
|
+
if (!label.trim() || menus.some(menu => !menu.id.trim() || !menu.label.trim()) || new Set(menus.map(menu => menu.id)).size !== menus.length)
|
|
8
|
+
throw new Error("Flood Menubar requires unique named menus");
|
|
9
|
+
const root = useRef(null);
|
|
10
|
+
useImperativeHandle(ref, () => root.current, []);
|
|
11
|
+
const [local, setLocal] = useState(defaultOpenValue), [active, setActive] = useState();
|
|
12
|
+
const requested = openValue === undefined ? local : openValue;
|
|
13
|
+
const enabled = menus.filter(menu => !disabled && !menu.disabled), open = enabled.some(menu => menu.id === requested) ? requested : null;
|
|
14
|
+
const current = useRef(open);
|
|
15
|
+
current.current = open;
|
|
16
|
+
const activeId = enabled.some(menu => menu.id === active) ? active : enabled[0]?.id;
|
|
17
|
+
const request = (next) => { if (openValue === undefined)
|
|
18
|
+
setLocal(next); onOpenValueChange?.(next); };
|
|
19
|
+
const trigger = (id) => [...root.current?.querySelectorAll('[data-menubar-trigger]') ?? []].find(node => node.dataset.menubarTrigger === id);
|
|
20
|
+
const move = (id, delta) => {
|
|
21
|
+
const at = enabled.findIndex(menu => menu.id === id), next = enabled[(at + delta + enabled.length) % enabled.length];
|
|
22
|
+
if (!next)
|
|
23
|
+
return;
|
|
24
|
+
setActive(next.id);
|
|
25
|
+
if (open)
|
|
26
|
+
request(next.id);
|
|
27
|
+
else
|
|
28
|
+
trigger(next.id)?.focus({ preventScroll: true });
|
|
29
|
+
};
|
|
30
|
+
useBrowserLayoutEffect(() => { if (open)
|
|
31
|
+
setActive(open); }, [open]);
|
|
32
|
+
useEffect(() => { if (requested && !open)
|
|
33
|
+
request(null); }, [requested, open]);
|
|
34
|
+
return _jsx("div", { ...props, ref: root, role: "menubar", "aria-label": label, "aria-orientation": "horizontal", "aria-disabled": disabled || undefined, tabIndex: enabled.length ? undefined : 0, className: ["flood-menubar", className].filter(Boolean).join(" "), onKeyDownCapture: event => {
|
|
35
|
+
onKeyDownCapture?.(event);
|
|
36
|
+
if (event.defaultPrevented || event.key !== "Tab" || event.altKey || event.ctrlKey || event.metaKey || !open)
|
|
37
|
+
return;
|
|
38
|
+
// Capture also sees nested menus; their ordinary Menu Tab contract returns
|
|
39
|
+
// to its trigger, while an application menubar exits the whole bar.
|
|
40
|
+
const anchor = trigger(open), scope = anchor?.closest('dialog[open]') ?? anchor?.ownerDocument.body;
|
|
41
|
+
if (anchor && scope) {
|
|
42
|
+
const stops = tabbables(scope), at = stops.indexOf(anchor);
|
|
43
|
+
const next = event.shiftKey ? stops.slice(0, at).reverse().find(node => !root.current?.contains(node)) : stops.slice(at + 1).find(node => !root.current?.contains(node));
|
|
44
|
+
if (next) {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
event.stopPropagation();
|
|
47
|
+
next.focus();
|
|
48
|
+
request(null);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}, onKeyDown: event => {
|
|
52
|
+
onKeyDown?.(event);
|
|
53
|
+
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
|
|
54
|
+
return;
|
|
55
|
+
const source = event.target.closest('[data-menubar-trigger]');
|
|
56
|
+
if (!source)
|
|
57
|
+
return;
|
|
58
|
+
const id = source.dataset.menubarTrigger, rtl = getComputedStyle(event.currentTarget).direction === "rtl";
|
|
59
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
move(id, event.key === (rtl ? "ArrowLeft" : "ArrowRight") ? 1 : -1);
|
|
62
|
+
}
|
|
63
|
+
else if (event.key === "Home" || event.key === "End") {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
const next = event.key === "Home" ? enabled[0] : enabled.at(-1);
|
|
66
|
+
if (next) {
|
|
67
|
+
setActive(next.id);
|
|
68
|
+
trigger(next.id)?.focus();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, children: menus.map(menu => _jsx(Menu, { label: menu.label, items: menu.items, open: open === menu.id, disabled: disabled || menu.disabled, controlSize: controlSize, onOpenChange: next => { if (next)
|
|
72
|
+
request(menu.id);
|
|
73
|
+
else if (current.current === menu.id)
|
|
74
|
+
request(null); }, onAction: id => onAction?.(menu.id, id), onKeyDown: event => {
|
|
75
|
+
if (event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
|
|
76
|
+
return;
|
|
77
|
+
const rtl = getComputedStyle(root.current).direction === "rtl", forward = event.key === (rtl ? "ArrowLeft" : "ArrowRight");
|
|
78
|
+
if ((forward || (event.key === (rtl ? "ArrowRight" : "ArrowLeft"))) && !(forward && event.target.closest('[aria-haspopup="menu"]'))) {
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
event.stopPropagation();
|
|
81
|
+
move(menu.id, forward ? 1 : -1);
|
|
82
|
+
}
|
|
83
|
+
}, trigger: binding => _jsx(Button, { ...binding, role: "menuitem", "data-menubar-trigger": menu.id, tabIndex: menu.id === activeId ? 0 : -1, variant: "ghost", size: controlSize, onFocus: () => setActive(menu.id), onPointerEnter: () => { if (open && open !== menu.id && !disabled && !menu.disabled)
|
|
84
|
+
request(menu.id); }, children: menu.label }) }, menu.id)) });
|
|
85
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { NavigationGroupProps } from "./index.js";
|
|
2
|
+
/** Related navigation with optional presentation and an independent disclosure owner. */
|
|
3
|
+
export declare const NavigationSection: import("react").ForwardRefExoticComponent<NavigationGroupProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { Collapsible, Text } from "./index.js";
|
|
4
|
+
/** Related navigation with optional presentation and an independent disclosure owner. */
|
|
5
|
+
export const NavigationSection = /* @__PURE__ */ forwardRef(function NavigationSection({ label, hideLabel = false, headingAction, separator = false, collapsible = false, expanded, defaultExpanded = true, onExpandedChange, compact = false, size = "md", collapseDisabled = false, className, children, ...props }, ref) {
|
|
6
|
+
if (label !== undefined && !label.trim())
|
|
7
|
+
throw new Error("Flood NavigationGroup label must be nonempty when provided");
|
|
8
|
+
if (collapsible && !label?.trim())
|
|
9
|
+
throw new Error("Flood collapsible NavigationGroup requires a nonempty label");
|
|
10
|
+
const action = headingAction == null ? undefined : _jsx("div", { className: "flood-navigation-group__action", children: headingAction });
|
|
11
|
+
const body = _jsx("div", { className: "flood-navigation-group__body", children: children });
|
|
12
|
+
return _jsx("div", { ...props, ref: ref, role: label ? "group" : props.role, "aria-label": props["aria-label"] ?? label, "data-size": size, "data-compact": compact || undefined, "data-hide-label": hideLabel || undefined, "data-separator": separator || undefined, "data-collapsible": collapsible || undefined, className: ["flood-navigation-group", className].filter(Boolean).join(" "), children: collapsible ? _jsx(Collapsible, { title: label, open: compact ? true : expanded, defaultOpen: defaultExpanded, onOpenChange: compact ? undefined : onExpandedChange, disabled: collapseDisabled, controlSize: size, headerAction: action, className: "flood-navigation-group__disclosure", children: body }) : _jsxs(_Fragment, { children: [(!compact && label && !hideLabel || action) && _jsxs("div", { className: "flood-navigation-group__heading", children: [!compact && label && !hideLabel && _jsx(Text, { size: "helper", tone: "subtle", className: "flood-navigation-group__label", children: label }), action] }), body] }) });
|
|
13
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { NavigationItemProps } from "./index.js";
|
|
2
|
+
/** Shared anatomy for destinations, commands and navigation disclosures. */
|
|
3
|
+
export declare const NavigationRow: import("react").ForwardRefExoticComponent<NavigationItemProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useCallback, useRef } from 'react';
|
|
3
|
+
import { Button, IconButton, IconSlot, Link, Text, Tooltip } from './index.js';
|
|
4
|
+
function assignRef(ref, node) { if (typeof ref === 'function')
|
|
5
|
+
ref(node);
|
|
6
|
+
else if (ref)
|
|
7
|
+
ref.current = node; }
|
|
8
|
+
const classes = (...values) => values.filter(Boolean).join(' ');
|
|
9
|
+
/** Shared anatomy for destinations, commands and navigation disclosures. */
|
|
10
|
+
export const NavigationRow = /* @__PURE__ */ forwardRef(function NavigationRow({ label, description, icon, reserveIcon = false, trailing, trailingAction, href, linkProps, onAction, actionProps, disclosure, current = false, currentKind = 'page', ancestorCurrent = false, disabled = false, size = 'md', compact = false, variant = 'quiet', tooltip = true, tooltipSide = 'right', controlRef, disclosureRef, className, ...props }, ref) {
|
|
11
|
+
if (!label.trim())
|
|
12
|
+
throw new Error('Flood NavigationItem requires a nonempty label');
|
|
13
|
+
if (href !== undefined && !href.trim())
|
|
14
|
+
throw new Error('Flood NavigationItem requires a nonempty destination');
|
|
15
|
+
if (href !== undefined && onAction)
|
|
16
|
+
throw new Error('Flood NavigationItem uses either a destination or a command');
|
|
17
|
+
if (compact && disclosure && (href !== undefined || onAction || actionProps?.onClick))
|
|
18
|
+
throw new Error('Flood compact rows with separate actions and disclosure require a branch composition such as NavigationTree');
|
|
19
|
+
if (disclosure && !disclosure.label.trim())
|
|
20
|
+
throw new Error('Flood NavigationItem requires a named disclosure');
|
|
21
|
+
const toggleOnly = Boolean(disclosure && href === undefined && !onAction);
|
|
22
|
+
const hasAction = !compact && trailingAction != null;
|
|
23
|
+
const unavailable = disabled || Boolean(toggleOnly && disclosure?.disabled) || Boolean(href === undefined && actionProps?.disabled);
|
|
24
|
+
const hintRef = useRef(undefined);
|
|
25
|
+
const setControl = useCallback((node) => { assignRef(controlRef, node); hintRef.current?.(node); }, [controlRef]);
|
|
26
|
+
const glyph = icon ?? (compact ? _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", "aria-hidden": "true", children: _jsx("rect", { x: "5", y: "5", width: "14", height: "14", rx: "4" }) }) : null);
|
|
27
|
+
const chevron = _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m9 6 6 6-6 6" }) });
|
|
28
|
+
const content = _jsxs(_Fragment, { children: [(icon != null || reserveIcon || compact) && _jsx(IconSlot, { size: size, children: glyph }), _jsxs("span", { className: "flood-navigation-item__content", children: [_jsx(Text, { as: "span", className: "flood-navigation-item__label", children: label }), !compact && description && _jsx(Text, { as: "span", size: "helper", tone: "subtle", className: "flood-navigation-item__description", children: description })] }), !compact && !hasAction && trailing != null && _jsx("span", { className: "flood-navigation-item__trailing", children: trailing }), !compact && toggleOnly && _jsx("span", { className: "flood-navigation-item__indicator", children: _jsx(IconSlot, { size: size === 'lg' ? 'xs' : '2xs', children: chevron }) })] });
|
|
29
|
+
const primary = (hint) => {
|
|
30
|
+
hintRef.current = hint?.ref;
|
|
31
|
+
const hintEvents = {
|
|
32
|
+
onPointerEnter: (event) => { if (href !== undefined)
|
|
33
|
+
linkProps?.onPointerEnter?.(event);
|
|
34
|
+
else
|
|
35
|
+
actionProps?.onPointerEnter?.(event); if (!event.defaultPrevented)
|
|
36
|
+
hint?.onPointerEnter?.(event); },
|
|
37
|
+
onPointerLeave: (event) => { if (href !== undefined)
|
|
38
|
+
linkProps?.onPointerLeave?.(event);
|
|
39
|
+
else
|
|
40
|
+
actionProps?.onPointerLeave?.(event); hint?.onPointerLeave?.(event); },
|
|
41
|
+
onFocus: (event) => { if (href !== undefined)
|
|
42
|
+
linkProps?.onFocus?.(event);
|
|
43
|
+
else
|
|
44
|
+
actionProps?.onFocus?.(event); if (!event.defaultPrevented)
|
|
45
|
+
hint?.onFocus?.(event); },
|
|
46
|
+
onBlur: (event) => { if (href !== undefined)
|
|
47
|
+
linkProps?.onBlur?.(event);
|
|
48
|
+
else
|
|
49
|
+
actionProps?.onBlur?.(event); hint?.onBlur?.(event); },
|
|
50
|
+
};
|
|
51
|
+
const source = href !== undefined ? linkProps : actionProps;
|
|
52
|
+
const descriptionIds = [source?.['aria-describedby'], hint?.['aria-describedby']].filter(Boolean).join(' ') || undefined;
|
|
53
|
+
const controlClass = classes('flood-navigation-item__control', source?.className);
|
|
54
|
+
if (href !== undefined) {
|
|
55
|
+
if (unavailable)
|
|
56
|
+
return _jsx("span", { ref: setControl, role: "link", "aria-disabled": "true", "aria-label": compact ? linkProps?.['aria-label'] ?? label : linkProps?.['aria-label'], "aria-current": current ? currentKind : undefined, className: controlClass, children: content });
|
|
57
|
+
return _jsx(Link, { ...linkProps, ...hintEvents, ref: setControl, href: href, variant: "ghost", size: size, className: controlClass, title: hint ? undefined : linkProps?.title, "aria-label": compact ? linkProps?.['aria-label'] ?? label : linkProps?.['aria-label'], "aria-describedby": descriptionIds, "aria-current": current ? currentKind : undefined, children: content });
|
|
58
|
+
}
|
|
59
|
+
if (onAction || toggleOnly || actionProps?.onClick)
|
|
60
|
+
return _jsx(Button, { ...actionProps, ...hintEvents, ref: setControl, type: "button", variant: "ghost", size: size, className: controlClass, disabled: unavailable || actionProps?.disabled, "aria-label": actionProps?.['aria-label'] ?? (toggleOnly ? disclosure.label : compact ? label : undefined), "aria-describedby": descriptionIds, "aria-current": current ? currentKind : undefined, "aria-expanded": toggleOnly ? disclosure.expanded : actionProps?.['aria-expanded'], "aria-controls": toggleOnly ? disclosure.controls : actionProps?.['aria-controls'], onClick: event => { actionProps?.onClick?.(event); if (event.defaultPrevented)
|
|
61
|
+
return; if (toggleOnly)
|
|
62
|
+
disclosure.onExpandedChange(!disclosure.expanded);
|
|
63
|
+
else
|
|
64
|
+
onAction?.(); }, children: content });
|
|
65
|
+
return _jsx("span", { ref: setControl, className: controlClass, "aria-disabled": unavailable || undefined, "aria-current": current ? currentKind : undefined, children: content });
|
|
66
|
+
};
|
|
67
|
+
return _jsxs("div", { ...props, ref: ref, className: classes('flood-navigation-item', className), "data-size": size, "data-variant": variant, "data-compact": compact || undefined, "data-current": current || undefined, "data-ancestor-current": ancestorCurrent || undefined, "data-disabled": unavailable || undefined, "data-disclosure": disclosure && !toggleOnly && !compact || undefined, "data-trailing-action": hasAction || undefined, "data-expanded": disclosure?.expanded, children: [compact ? _jsx(Tooltip, { content: label, side: tooltipSide, disabled: !tooltip || unavailable, trigger: hint => primary(hint) }) : primary(), hasAction && _jsxs("div", { className: "flood-navigation-item__accessories", children: [trailing != null && _jsx("span", { className: "flood-navigation-item__metadata", children: trailing }), _jsx("div", { className: "flood-navigation-item__action", children: trailingAction })] }), disclosure && !toggleOnly && !compact && _jsx(IconButton, { ref: disclosureRef, label: disclosure.label, size: size, iconSize: size === 'lg' ? 'xs' : '2xs', variant: "ghost", className: "flood-navigation-item__disclosure", disabled: disabled || disclosure.disabled, "aria-expanded": disclosure.expanded, "aria-controls": disclosure.controls, onClick: () => disclosure.onExpandedChange(!disclosure.expanded), children: chevron })] });
|
|
68
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { NavigationListProps, StepsProps, SectionProps, SplitProps, CenterProps, SkipLinkProps } from "./index.js";
|
|
1
|
+
import type { NavigationListProps, StepsProps, SectionProps, PageHeaderProps, SplitProps, CenterProps, SkipLinkProps } from "./index.js";
|
|
2
2
|
export declare const NavigationMenu: import("react").ForwardRefExoticComponent<NavigationListProps & import("react").RefAttributes<HTMLElement>>;
|
|
3
3
|
export declare const StepSequence: import("react").ForwardRefExoticComponent<StepsProps & import("react").RefAttributes<HTMLOListElement>>;
|
|
4
|
+
export declare const PageHeading: import("react").ForwardRefExoticComponent<PageHeaderProps & import("react").RefAttributes<HTMLElement>>;
|
|
4
5
|
export declare const ContentSection: import("react").ForwardRefExoticComponent<SectionProps & import("react").RefAttributes<HTMLElement>>;
|
|
5
6
|
export declare const SplitLayout: import("react").ForwardRefExoticComponent<SplitProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
6
7
|
export declare const CenterLayout: import("react").ForwardRefExoticComponent<CenterProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useId } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { Heading, NavigationGroup, NavigationItem } from "./index.js";
|
|
4
4
|
const classes = (...names) => names.filter(Boolean).join(" ");
|
|
5
5
|
function uniqueValues(items, component) {
|
|
6
6
|
if (items.some(item => !item.value.trim()) || new Set(items.map(item => item.value)).size !== items.length)
|
|
@@ -19,8 +19,8 @@ export const NavigationMenu = /* @__PURE__ */ forwardRef(function NavigationMenu
|
|
|
19
19
|
else
|
|
20
20
|
groups.push({ label: item.group ?? "", items: [item] });
|
|
21
21
|
}
|
|
22
|
-
const links = (entries) => entries.map(item => _jsx("li", { children:
|
|
23
|
-
return _jsx("nav", { ...props, ref: ref, "aria-label": label, className: classes("flood-navigation-list", className), "data-size": size, children: _jsx("ul", { role: "list", className: "flood-navigation-list__groups", children: groups.map((group, index) => group.label ?
|
|
22
|
+
const links = (entries) => entries.map(item => _jsx("li", { children: _jsx(NavigationItem, { label: item.label, description: item.description, href: item.href, icon: item.icon, reserveIcon: entries.some(entry => entry.icon != null), current: item.value === currentValue, size: size, linkProps: { ...item.linkProps, className: classes("flood-navigation-list__link", item.linkProps?.className) } }) }, item.value));
|
|
23
|
+
return _jsx("nav", { ...props, ref: ref, "aria-label": label, className: classes("flood-navigation-list", className), "data-size": size, children: _jsx("ul", { role: "list", className: "flood-navigation-list__groups", children: groups.map((group, index) => group.label ? _jsx("li", { className: "flood-navigation-list__group", children: _jsx(NavigationGroup, { label: group.label, id: `${id}-${index}`, size: size, children: _jsx("ul", { role: "list", "aria-labelledby": `${id}-${index}`, children: links(group.items) }) }) }, index) : links(group.items)) }) });
|
|
24
24
|
});
|
|
25
25
|
export const StepSequence = /* @__PURE__ */ forwardRef(function StepSequence({ items, label, currentValue = items[0]?.value, orientation = "horizontal", onStepChange, statusLabels = { complete: "Completed", current: "Current step", upcoming: "Upcoming", error: "Needs attention" }, className, ...props }, ref) {
|
|
26
26
|
uniqueValues(items, "Steps");
|
|
@@ -36,11 +36,19 @@ export const StepSequence = /* @__PURE__ */ forwardRef(function StepSequence({ i
|
|
|
36
36
|
return _jsx("li", { "data-state": state, "aria-current": current ? "step" : undefined, children: onStepChange ? _jsx("button", { type: "button", className: "flood-steps__content flood-steps__action", disabled: item.disabled, onClick: () => onStepChange(item.value), children: content }) : _jsx("div", { className: "flood-steps__content", children: content }) }, item.value);
|
|
37
37
|
}) });
|
|
38
38
|
});
|
|
39
|
+
const ContentHeader = /* @__PURE__ */ forwardRef(function ContentHeader({ title, description, actions, headingId, headingLevel, headingSize, className, ...props }, ref) {
|
|
40
|
+
return _jsxs("header", { ...props, ref: ref, className: classes("flood-section__header", className), children: [_jsxs("div", { className: "flood-section__heading", children: [_jsx(Heading, { id: headingId, level: headingLevel, size: headingSize, className: "flood-section__title", children: title }), description != null && _jsx("div", { className: "flood-section__description", children: description })] }), actions != null && _jsx("div", { className: "flood-section__actions", children: actions })] });
|
|
41
|
+
});
|
|
42
|
+
export const PageHeading = /* @__PURE__ */ forwardRef(function PageHeader({ title, headingSize = "large", headingLevel = 1, className, ...props }, ref) {
|
|
43
|
+
if (!title.trim())
|
|
44
|
+
throw new Error("Flood PageHeader requires a nonempty title");
|
|
45
|
+
return _jsx(ContentHeader, { ...props, ref: ref, title: title, headingLevel: headingLevel, headingSize: headingSize, className: classes("flood-page-header", className) });
|
|
46
|
+
});
|
|
39
47
|
export const ContentSection = /* @__PURE__ */ forwardRef(function ContentSection({ title, description, actions, headingLevel = 2, headingSize = "section", children, id: explicitId, className, "aria-labelledby": labelledBy, ...props }, ref) {
|
|
40
48
|
if (!title.trim() || ![2, 3, 4, 5, 6].includes(headingLevel))
|
|
41
49
|
throw new Error("Flood Section requires a nonempty title and headingLevel from 2 to 6");
|
|
42
50
|
const generated = useId(), headingId = `${explicitId ?? `flood-section-${generated}`}-title`;
|
|
43
|
-
return _jsxs("section", { ...props, ref: ref, id: explicitId, "aria-labelledby": labelledBy ?? headingId, className: classes("flood-section", className), children: [
|
|
51
|
+
return _jsxs("section", { ...props, ref: ref, id: explicitId, "aria-labelledby": labelledBy ?? headingId, className: classes("flood-section", className), children: [_jsx(ContentHeader, { title: title, description: description, actions: actions, headingId: headingId, headingLevel: headingLevel, headingSize: headingSize }), children != null && _jsx("div", { className: "flood-section__body", children: children })] });
|
|
44
52
|
});
|
|
45
53
|
export const SplitLayout = /* @__PURE__ */ forwardRef(function SplitLayout({ main, aside, side = "end", ratio = "main", collapseAt = "md", gap = "6", className, style, ...props }, ref) {
|
|
46
54
|
const primary = _jsx("div", { className: "flood-split__main", children: main }, "main"), secondary = _jsx("div", { className: "flood-split__aside", children: aside }, "aside");
|
package/dist/navigation-tree.js
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { NavigationItem, Popover, NavigationTree } from "./index.js";
|
|
4
4
|
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
5
5
|
const branch = (item) => item.children !== undefined || item.content !== undefined;
|
|
6
6
|
/** Ordinary website navigation uses disclosure lists, not application tree selection semantics. */
|
|
7
|
-
export const HierarchicalNavigation = /* @__PURE__ */ forwardRef(function HierarchicalNavigation({ label, items, currentValue, expandedValues, defaultExpandedValues = [], onExpandedValuesChange, expandLabel = (item, expanded) => `${expanded ? "Collapse" : "Expand"} ${item.label}`, size = "md", id: explicitId, className, tabIndex = -1, ...props }, forwardedRef) {
|
|
7
|
+
export const HierarchicalNavigation = /* @__PURE__ */ forwardRef(function HierarchicalNavigation({ label, items, currentValue, currentKind = "page", expandedValues, defaultExpandedValues = [], onExpandedValuesChange, expandLabel = (item, expanded) => `${expanded ? "Collapse" : "Expand"} ${item.label}`, size = "md", variant = "quiet", compact = false, flyoutSide = "right", branchGuides = false, id: explicitId, className, tabIndex = -1, ...props }, forwardedRef) {
|
|
8
8
|
if (!label.trim())
|
|
9
9
|
throw new Error("Flood NavigationTree requires a nonempty label");
|
|
10
10
|
const generated = useId(), id = explicitId ?? `flood-navigation-tree-${generated}`;
|
|
11
11
|
const root = useRef(null);
|
|
12
12
|
useImperativeHandle(forwardedRef, () => root.current, []);
|
|
13
13
|
const [local, setLocal] = useState(() => [...new Set(defaultExpandedValues)]);
|
|
14
|
+
const [flyout, setFlyout] = useState(null);
|
|
15
|
+
const hoverTimer = useRef(undefined);
|
|
16
|
+
const clearHover = () => clearTimeout(hoverTimer.current);
|
|
17
|
+
useEffect(() => { setFlyout(null); return clearHover; }, [compact, currentValue]);
|
|
18
|
+
const leaveFlyout = (value) => {
|
|
19
|
+
clearHover();
|
|
20
|
+
hoverTimer.current = setTimeout(() => setFlyout(current => current?.value === value && current.preview ? null : current), 240);
|
|
21
|
+
};
|
|
14
22
|
const expanded = new Set(expandedValues ?? local);
|
|
15
23
|
const known = new Set(), visible = new Set();
|
|
16
24
|
const parents = new Map();
|
|
@@ -32,11 +40,17 @@ export const HierarchicalNavigation = /* @__PURE__ */ forwardRef(function Hierar
|
|
|
32
40
|
pending.push({ items: item.children, parent: item.value, shown: level.shown && expanded.has(item.value), ancestors });
|
|
33
41
|
}
|
|
34
42
|
}
|
|
43
|
+
const currentAncestors = new Set();
|
|
44
|
+
for (let parent = currentValue === undefined ? undefined : parents.get(currentValue); parent !== undefined; parent = parents.get(parent)) {
|
|
45
|
+
currentAncestors.add(parent);
|
|
46
|
+
}
|
|
35
47
|
const latest = useRef({ visible, parents });
|
|
36
48
|
latest.current = { visible, parents };
|
|
37
49
|
const destinations = useRef(new Map());
|
|
38
50
|
const refs = useRef(new Map());
|
|
39
51
|
const refAncestors = useRef(new Map());
|
|
52
|
+
const railAnchors = useRef(new Map());
|
|
53
|
+
const railRefs = useRef(new Map());
|
|
40
54
|
const repair = (removed, ancestors) => {
|
|
41
55
|
const active = removed.ownerDocument.activeElement;
|
|
42
56
|
if (!active || !removed.contains(active))
|
|
@@ -66,11 +80,21 @@ export const HierarchicalNavigation = /* @__PURE__ */ forwardRef(function Hierar
|
|
|
66
80
|
}
|
|
67
81
|
return refs.current.get(key);
|
|
68
82
|
};
|
|
83
|
+
const bindRail = (value, anchor) => {
|
|
84
|
+
const key = `disclosure:${value}`;
|
|
85
|
+
railAnchors.current.set(key, anchor);
|
|
86
|
+
bind(key, []);
|
|
87
|
+
if (!railRefs.current.has(key))
|
|
88
|
+
railRefs.current.set(key, node => { bind(key, [])(node); railAnchors.current.get(key)?.(node); });
|
|
89
|
+
return railRefs.current.get(key);
|
|
90
|
+
};
|
|
69
91
|
useBrowserLayoutEffect(() => {
|
|
70
92
|
for (const key of refs.current.keys())
|
|
71
93
|
if (!known.has(key.slice(key.indexOf(":") + 1))) {
|
|
72
94
|
refs.current.delete(key);
|
|
73
95
|
refAncestors.current.delete(key);
|
|
96
|
+
railRefs.current.delete(key);
|
|
97
|
+
railAnchors.current.delete(key);
|
|
74
98
|
}
|
|
75
99
|
}, [items]);
|
|
76
100
|
const toggle = (value) => {
|
|
@@ -90,9 +114,29 @@ export const HierarchicalNavigation = /* @__PURE__ */ forwardRef(function Hierar
|
|
|
90
114
|
const disclosureLabel = expandable ? expandLabel(item, open) : "";
|
|
91
115
|
if (expandable && !disclosureLabel.trim())
|
|
92
116
|
throw new Error("Flood NavigationTree disclosure labels must be nonempty");
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
117
|
+
const ancestorCurrent = currentAncestors.has(item.value) && (compact ? flyout?.value !== item.value : !open);
|
|
118
|
+
const linkProps = { ...item.linkProps, className: ["flood-navigation-tree__link", item.linkProps?.className].filter(Boolean).join(" ") };
|
|
119
|
+
const shared = { label: item.label, icon: item.icon, reserveIcon: entries.some(entry => entry.icon != null), size, variant, current, currentKind, ancestorCurrent, disabled: item.disabled, className: "flood-navigation-tree__row" };
|
|
120
|
+
if (compact) {
|
|
121
|
+
const shown = flyout?.value === item.value;
|
|
122
|
+
const preview = shown && flyout.preview;
|
|
123
|
+
const hover = item.flyout?.openOnHover && !item.disabled;
|
|
124
|
+
return _jsx("li", { children: expandable ? _jsxs(Popover, { title: item.label, chrome: "title-only", side: flyoutSide, align: "start", open: shown, disabled: item.disabled, positionReference: item.flyout?.positionReference, focusOnOpen: !preview, onOpenChange: open => { clearHover(); setFlyout(open ? { value: item.value, preview: false } : null); }, className: "flood-navigation-tree__flyout", onPointerEnter: hover ? clearHover : undefined, onPointerLeave: hover ? () => leaveFlyout(item.value) : undefined, onFocusCapture: hover && preview ? () => { clearHover(); setFlyout({ value: item.value, preview: false }); } : undefined, onClick: event => { if (!event.defaultPrevented && event.target.closest('a[href]')) {
|
|
125
|
+
clearHover();
|
|
126
|
+
setFlyout(null);
|
|
127
|
+
} }, trigger: ({ ref: anchor, ...trigger }) => _jsx(NavigationItem, { ...shared, compact: true, actionProps: { ...trigger,
|
|
128
|
+
onPointerEnter: hover ? event => {
|
|
129
|
+
if (event.pointerType !== "mouse")
|
|
130
|
+
return;
|
|
131
|
+
clearHover();
|
|
132
|
+
hoverTimer.current = setTimeout(() => setFlyout(current => current && !current.preview ? current : { value: item.value, preview: true }), 130);
|
|
133
|
+
} : undefined,
|
|
134
|
+
onPointerLeave: hover ? () => leaveFlyout(item.value) : undefined,
|
|
135
|
+
onClick: preview ? () => { clearHover(); setFlyout({ value: item.value, preview: false }); } : trigger.onClick,
|
|
136
|
+
}, controlRef: bindRail(item.value, anchor), tooltip: !hover && !shown, tooltipSide: flyoutSide }), children: [(item.href || item.children?.length) ? _jsx(NavigationTree, { label: item.label, size: size, variant: variant, currentValue: currentValue, currentKind: currentKind, branchGuides: branchGuides, items: [...(item.href ? [{ ...item, children: undefined, content: undefined }] : []), ...(item.children ?? [])], expandedValues: expandedValues ?? local, onExpandedValuesChange: values => { if (expandedValues === undefined)
|
|
137
|
+
setLocal(values); onExpandedValuesChange?.(values); }, expandLabel: expandLabel }) : null, item.content] }) : _jsx(NavigationItem, { ...shared, compact: true, href: item.href, linkProps: linkProps, controlRef: bind(`link:${item.value}`, []), tooltipSide: flyoutSide }) }, item.value);
|
|
138
|
+
}
|
|
139
|
+
return _jsxs("li", { children: [_jsx(NavigationItem, { ...shared, href: item.href, linkProps: linkProps, trailing: item.trailing, trailingAction: item.trailingAction, controlRef: bind(`${item.href !== undefined ? 'link' : 'disclosure'}:${item.value}`, ancestors), disclosureRef: bind(`disclosure:${item.value}`, ancestors), disclosure: expandable ? { expanded: open, onExpandedChange: () => toggle(item.value), label: disclosureLabel, controls: open ? contentId : undefined } : undefined }), expandable && open && _jsxs("div", { id: contentId, ref: bind(`group:${item.value}`, [item.value, ...ancestors]), className: "flood-navigation-tree__branch", children: [item.children && item.children.length > 0 && _jsx("ul", { children: render(item.children, [item.value, ...ancestors]) }), item.content != null && _jsx("div", { className: "flood-navigation-tree__content", children: item.content })] })] }, item.value);
|
|
96
140
|
});
|
|
97
|
-
return _jsx("nav", { ...props, ref: root, id: id, "aria-label": label, tabIndex: tabIndex, "data-size": size, className: ["flood-navigation-tree", className].filter(Boolean).join(" "), children: _jsx("ul", { children: render(items) }) });
|
|
141
|
+
return _jsx("nav", { ...props, ref: root, id: id, "aria-label": label, tabIndex: tabIndex, "data-size": size, "data-compact": compact || undefined, "data-branch-guides": branchGuides || undefined, className: ["flood-navigation-tree", className].filter(Boolean).join(" "), children: _jsx("ul", { children: render(items) }) });
|
|
98
142
|
});
|
package/dist/navigation.js
CHANGED
|
@@ -3,7 +3,7 @@ import { forwardRef, useEffect, useId, useImperativeHandle, useLayoutEffect, use
|
|
|
3
3
|
import { Button, IconSlot, Link, ScrollArea } from "./index.js";
|
|
4
4
|
import { pageWindow } from "./page-window.js";
|
|
5
5
|
const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
6
|
-
export const TabNavigation = /* @__PURE__ */ forwardRef(function TabNavigation({ label, items, value, defaultValue, onValueChange, activationMode = "automatic", orientation = "horizontal", controlSize = "md", disabled = false, iconOnly = false, className, onKeyDown, id: explicitId, ...props }, ref) {
|
|
6
|
+
export const TabNavigation = /* @__PURE__ */ forwardRef(function TabNavigation({ label, items, value, defaultValue, onValueChange, activationMode = "automatic", orientation = "horizontal", variant = "contained", controlSize = "md", disabled = false, iconOnly = false, className, onKeyDown, id: explicitId, ...props }, ref) {
|
|
7
7
|
const generated = useId(), id = explicitId ?? `flood-tabs-${generated}`;
|
|
8
8
|
const [local, setLocal] = useState(defaultValue), [focused, setFocused] = useState();
|
|
9
9
|
const root = useRef(null), list = useRef(null), focusedPanel = useRef(undefined);
|
|
@@ -29,7 +29,7 @@ export const TabNavigation = /* @__PURE__ */ forwardRef(function TabNavigation({
|
|
|
29
29
|
}
|
|
30
30
|
focusedPanel.current = undefined;
|
|
31
31
|
}, [selected, id, items]);
|
|
32
|
-
return _jsxs("div", { ...props, ref: root, id: id, className: ["flood-tabs", className].filter(Boolean).join(" "), "data-orientation": orientation, onKeyDown: event => {
|
|
32
|
+
return _jsxs("div", { ...props, ref: root, id: id, className: ["flood-tabs", className].filter(Boolean).join(" "), "data-orientation": orientation, "data-variant": variant, onKeyDown: event => {
|
|
33
33
|
onKeyDown?.(event);
|
|
34
34
|
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing)
|
|
35
35
|
return;
|