@flood-md/react 0.2.10 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +275 -5
  2. package/dist/app-shell.d.ts +3 -0
  3. package/dist/app-shell.js +66 -0
  4. package/dist/bulk-action-bar.d.ts +3 -0
  5. package/dist/bulk-action-bar.js +17 -0
  6. package/dist/collection-toolbar.d.ts +3 -0
  7. package/dist/collection-toolbar.js +26 -0
  8. package/dist/collection-view-switcher.d.ts +3 -0
  9. package/dist/collection-view-switcher.js +19 -0
  10. package/dist/combobox.d.ts +6 -1
  11. package/dist/combobox.js +77 -29
  12. package/dist/command-list.d.ts +5 -1
  13. package/dist/command-list.js +2 -2
  14. package/dist/data-display.js +28 -4
  15. package/dist/data-table-columns.d.ts +25 -0
  16. package/dist/data-table-columns.js +271 -0
  17. package/dist/data-table-expansion.d.ts +21 -0
  18. package/dist/data-table-expansion.js +79 -0
  19. package/dist/data-table.js +140 -12
  20. package/dist/date-picker.js +2 -2
  21. package/dist/date-range-picker.js +2 -2
  22. package/dist/date-time-picker.js +2 -2
  23. package/dist/date-time-range-picker.js +2 -2
  24. package/dist/disclosure.d.ts +2 -1
  25. package/dist/disclosure.js +4 -4
  26. package/dist/editable.js +57 -18
  27. package/dist/filter-condition.d.ts +3 -0
  28. package/dist/filter-condition.js +21 -0
  29. package/dist/filter-group.d.ts +2 -0
  30. package/dist/filter-group.js +62 -0
  31. package/dist/foundations.css +1131 -93
  32. package/dist/index.d.ts +469 -5
  33. package/dist/index.js +72 -9
  34. package/dist/menu.js +5 -4
  35. package/dist/menubar.d.ts +2 -0
  36. package/dist/menubar.js +85 -0
  37. package/dist/navigation-group.d.ts +3 -0
  38. package/dist/navigation-group.js +13 -0
  39. package/dist/navigation-item.d.ts +3 -0
  40. package/dist/navigation-item.js +68 -0
  41. package/dist/navigation-layout.d.ts +2 -1
  42. package/dist/navigation-layout.js +12 -4
  43. package/dist/navigation-tree.js +51 -7
  44. package/dist/navigation.js +2 -2
  45. package/dist/overflow-actions.d.ts +2 -0
  46. package/dist/overflow-actions.js +75 -0
  47. package/dist/overlay-runtime.d.ts +6 -2
  48. package/dist/overlay-runtime.js +41 -5
  49. package/dist/overlays.d.ts +1 -0
  50. package/dist/overlays.js +34 -18
  51. package/dist/select.js +4 -1
  52. package/dist/sidebar.d.ts +2 -0
  53. package/dist/sidebar.js +58 -0
  54. package/dist/sparkline.d.ts +2 -0
  55. package/dist/sparkline.js +65 -0
  56. package/dist/toolbar.js +2 -2
  57. package/package.json +2 -2
@@ -1,16 +1,24 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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 { IconButton, IconSlot, Link } from "./index.js";
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 body = _jsxs(_Fragment, { children: [item.icon != null && _jsx(IconSlot, { size: size, children: item.icon }), _jsx("span", { className: "flood-navigation-tree__label", children: item.label })] });
94
- return _jsxs("li", { children: [_jsxs("div", { className: "flood-navigation-tree__row", "data-current": current || undefined, children: [item.href !== undefined && !item.disabled ? _jsx(Link, { ...item.linkProps, href: item.href, ref: bind(`link:${item.value}`, ancestors), variant: "ghost", size: size, "aria-current": current ? "page" : undefined, className: ["flood-navigation-tree__link", item.linkProps?.className].filter(Boolean).join(" "), children: body })
95
- : _jsx("span", { className: "flood-navigation-tree__text", "aria-disabled": item.disabled || undefined, "aria-current": current ? "page" : undefined, children: body }), expandable && _jsx(IconButton, { ref: bind(`disclosure:${item.value}`, ancestors), variant: "ghost", size: size, label: disclosureLabel, className: "flood-navigation-tree__disclosure", disabled: item.disabled, "aria-expanded": open, "aria-controls": open ? contentId : undefined, onClick: () => toggle(item.value), children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m9 5 7 7-7 7" }) }) })] }), 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);
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
  });
@@ -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;
@@ -0,0 +1,2 @@
1
+ import { type OverflowActionsProps } from "./index.js";
2
+ export declare const AdaptiveActions: import("react").ForwardRefExoticComponent<OverflowActionsProps & import("react").RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
3
+ import { Button, IconSlot, Menu, Toolbar } from "./index.js";
4
+ const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
5
+ export const AdaptiveActions = /* @__PURE__ */ forwardRef(function AdaptiveActions({ label, items, onAction, moreLabel = "More actions", size = "md", variant = "subtle", shape = "pill", containerVariant = "contained", disabled = false, leading, trailing, maxVisibleActions, className, ...props }, ref) {
6
+ if (!label.trim() || !moreLabel.trim() || items.some(item => !item.id.trim() || !item.label.trim() || (item.priority !== undefined && !Number.isFinite(item.priority))) || new Set(items.map(item => item.id)).size !== items.length)
7
+ throw new Error("Flood OverflowActions requires unique named actions and finite priorities");
8
+ if (maxVisibleActions !== undefined && (!Number.isInteger(maxVisibleActions) || maxVisibleActions < 0))
9
+ throw new Error("Flood OverflowActions maxVisibleActions must be a nonnegative integer");
10
+ const root = useRef(null), measure = useRef(null), more = useRef(null);
11
+ const before = useRef(null), after = useRef(null);
12
+ const menuAnchor = useRef(null);
13
+ const setMore = useCallback((node) => { more.current = node; menuAnchor.current?.(node); }, []);
14
+ useImperativeHandle(ref, () => root.current, []);
15
+ const [visibleIds, setVisibleIds] = useState(() => items.map(item => item.id));
16
+ const [open, setOpen] = useState(false), focusAfter = useRef(false);
17
+ const visible = items.filter(item => visibleIds.includes(item.id)), hidden = items.filter(item => !visibleIds.includes(item.id));
18
+ const action = (item, measuring = false) => _jsxs(Button, { size: size, variant: variant, tone: item.tone, disabled: disabled || item.disabled, "data-overflow-action": measuring ? undefined : item.id, "data-measure-action": measuring ? item.id : undefined, tabIndex: measuring ? -1 : undefined, onClick: measuring ? undefined : () => onAction?.(item.id), children: [item.icon && _jsx(IconSlot, { size: size, children: item.icon }), item.label] }, item.id);
19
+ const overflow = (measuring = false) => _jsxs(Button, { size: size, variant: variant, disabled: disabled, tabIndex: measuring ? -1 : undefined, "data-measure-more": measuring || undefined, children: [moreLabel, _jsx(IconSlot, { size: size, children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: [_jsx("circle", { cx: "5", cy: "12", r: "1.5" }), _jsx("circle", { cx: "12", cy: "12", r: "1.5" }), _jsx("circle", { cx: "19", cy: "12", r: "1.5" })] }) })] });
20
+ useBrowserLayoutEffect(() => {
21
+ const node = root.current, measured = measure.current, view = node?.ownerDocument.defaultView;
22
+ if (!node || !measured || !view)
23
+ return;
24
+ const update = () => {
25
+ const widths = new Map([...measured.querySelectorAll('[data-measure-action]')].map(item => [item.dataset.measureAction, item.getBoundingClientRect().width]));
26
+ const bar = node.querySelector('[role="toolbar"]'), gap = bar ? parseFloat(view.getComputedStyle(bar).columnGap) || 0 : 0;
27
+ const barStyle = bar ? view.getComputedStyle(bar) : null;
28
+ const rootStyle = view.getComputedStyle(node);
29
+ const available = Math.max(0, node.clientWidth - parseFloat(rootStyle.paddingLeft) - parseFloat(rootStyle.paddingRight) - (barStyle ? parseFloat(barStyle.paddingLeft) + parseFloat(barStyle.paddingRight) + parseFloat(barStyle.borderLeftWidth) + parseFloat(barStyle.borderRightWidth) : 0)), extra = measured.querySelector('[data-measure-more]')?.getBoundingClientRect().width ?? 0;
30
+ const fixed = [before.current, after.current].filter((item) => item !== null);
31
+ const fixedWidth = fixed.reduce((sum, item) => sum + item.getBoundingClientRect().width, 0);
32
+ const kept = new Set(items.map(item => item.id));
33
+ const width = (withMore) => fixedWidth + items.filter(item => kept.has(item.id)).reduce((sum, item) => sum + (widths.get(item.id) ?? 0), 0) + (withMore ? extra : 0) + Math.max(0, kept.size + fixed.length + (withMore ? 1 : 0) - 1) * gap;
34
+ const order = items.map((item, index) => ({ item, index })).sort((a, b) => (a.item.priority ?? 0) - (b.item.priority ?? 0) || b.index - a.index);
35
+ for (const { item } of order) {
36
+ if (kept.size <= (maxVisibleActions ?? items.length) && width(kept.size < items.length) <= available)
37
+ break;
38
+ kept.delete(item.id);
39
+ }
40
+ const next = items.filter(item => kept.has(item.id)).map(item => item.id);
41
+ const active = node.ownerDocument.activeElement;
42
+ const activeId = active?.dataset.overflowAction;
43
+ if ((activeId && !kept.has(activeId)) || (!items.some(item => !kept.has(item.id)) && more.current?.contains(active)))
44
+ focusAfter.current = true;
45
+ setVisibleIds(previous => previous.join("\0") === next.join("\0") ? previous : next);
46
+ };
47
+ update();
48
+ const observer = view.ResizeObserver ? new view.ResizeObserver(update) : undefined;
49
+ observer?.observe(node);
50
+ observer?.observe(measured);
51
+ if (before.current)
52
+ observer?.observe(before.current);
53
+ if (after.current)
54
+ observer?.observe(after.current);
55
+ let alive = true;
56
+ void node.ownerDocument.fonts?.ready.then(() => { if (alive)
57
+ update(); });
58
+ return () => { alive = false; observer?.disconnect(); };
59
+ }, [items, size, variant, moreLabel, disabled, leading, trailing, maxVisibleActions]);
60
+ useBrowserLayoutEffect(() => {
61
+ if (!hidden.length && open) {
62
+ setOpen(false);
63
+ focusAfter.current = true;
64
+ }
65
+ if (focusAfter.current) {
66
+ focusAfter.current = false;
67
+ (more.current ?? root.current?.querySelector('[data-overflow-action]:not(:disabled)'))?.focus({ preventScroll: true });
68
+ }
69
+ }, [visibleIds, hidden.length, open]);
70
+ return _jsxs("div", { ...props, ref: root, className: ["flood-overflow-actions", className].filter(Boolean).join(" "), children: [_jsxs(Toolbar, { label: label, shape: shape, variant: containerVariant, className: "flood-overflow-actions__bar", children: [leading != null && _jsx("div", { ref: before, className: "flood-overflow-actions__leading", children: leading }), visible.map(item => action(item)), hidden.length > 0 && _jsx(Menu, { label: moreLabel, items: hidden, open: open, onOpenChange: setOpen, onAction: onAction, disabled: disabled, controlSize: size, trigger: trigger => {
71
+ menuAnchor.current = trigger.ref;
72
+ return _jsxs(Button, { ...trigger, ref: setMore, size: size, variant: variant, children: [moreLabel, _jsx(IconSlot, { size: size, children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: [_jsx("circle", { cx: "5", cy: "12", r: "1.5" }), _jsx("circle", { cx: "12", cy: "12", r: "1.5" }), _jsx("circle", { cx: "19", cy: "12", r: "1.5" })] }) })] });
73
+ } }), trailing != null && _jsx("div", { ref: after, className: "flood-overflow-actions__trailing", children: trailing })] }), _jsxs("div", { ref: node => { measure.current = node; if (node)
74
+ node.inert = true; }, className: "flood-overflow-actions__measure", "aria-hidden": "true", children: [items.map(item => action(item, true)), overflow(true)] })] });
75
+ });
@@ -1,3 +1,4 @@
1
+ import { type RefObject } from "react";
1
2
  export type Side = "top" | "bottom" | "left" | "right";
2
3
  export type Align = "start" | "center" | "end";
3
4
  type Rect = {
@@ -17,7 +18,7 @@ export declare function placeOverlay(anchor: Rect, size: {
17
18
  top: number;
18
19
  width: number;
19
20
  height: number;
20
- }, side: Side, align: Align, gap: number, rtl?: boolean): {
21
+ }, side: Side, align: Align, gap: number, rtl?: boolean, overlap?: boolean): {
21
22
  left: number;
22
23
  top: number;
23
24
  side: Side;
@@ -28,11 +29,14 @@ type Layer = {
28
29
  anchor: () => HTMLElement | null;
29
30
  close: () => void;
30
31
  outside?: () => void;
32
+ closeOnFocusOutside?: boolean;
31
33
  modal?: boolean;
32
34
  tooltip?: boolean;
33
35
  };
34
36
  export declare function registerLayer(document: Document, layer: Layer): () => void;
35
37
  export declare function lockScroll(document: Document): () => void;
36
- export declare function positionLayer(anchor: HTMLElement, panel: HTMLElement, side: Side, align: Align): void;
38
+ export declare function positionLayer(anchor: HTMLElement, panel: HTMLElement, side: Side, align: Align, overlap?: boolean): void;
37
39
  export declare function returnFocus(panel: HTMLElement, anchor: HTMLElement | null): void;
40
+ /** Retain pixels for a CSS exit after the native layer has released focus. */
41
+ export declare function useLayerExitContent(panel: RefObject<HTMLElement | null>, open: boolean, enabled?: boolean, transitionProperty?: string): boolean;
38
42
  export {};
@@ -1,15 +1,18 @@
1
+ import { useEffect, useLayoutEffect, useState } from "react";
1
2
  /** Pure collision handling in layout-viewport coordinates. */
2
- export function placeOverlay(anchor, size, viewport, side, align, gap, rtl = false) {
3
+ export function placeOverlay(anchor, size, viewport, side, align, gap, rtl = false, overlap = false) {
3
4
  const edge = { left: viewport.left + gap, right: viewport.left + viewport.width - gap, top: viewport.top + gap, bottom: viewport.top + viewport.height - gap };
4
5
  const space = { top: anchor.top - edge.top, bottom: edge.bottom - anchor.bottom, left: anchor.left - edge.left, right: edge.right - anchor.right };
5
6
  const opposite = { top: "bottom", bottom: "top", left: "right", right: "left" };
6
7
  const needed = (side === "top" || side === "bottom" ? size.height : size.width) + gap;
7
- if (space[side] < needed && space[opposite[side]] > space[side])
8
+ if (!overlap && space[side] < needed && space[opposite[side]] > space[side])
8
9
  side = opposite[side];
9
10
  const horizontal = side === "top" || side === "bottom";
10
11
  const resolved = horizontal && rtl ? (align === "start" ? "end" : align === "end" ? "start" : align) : align;
11
12
  let left = horizontal ? (resolved === "start" ? anchor.left : resolved === "end" ? anchor.right - size.width : anchor.left + (anchor.width - size.width) / 2) : side === "left" ? anchor.left - size.width - gap : anchor.right + gap;
12
13
  let top = horizontal ? (side === "top" ? anchor.top - size.height - gap : anchor.bottom + gap) : (resolved === "start" ? anchor.top : resolved === "end" ? anchor.bottom - size.height : anchor.top + (anchor.height - size.height) / 2);
14
+ if (overlap)
15
+ top = anchor.top;
13
16
  left = Math.max(edge.left, Math.min(left, edge.right - size.width));
14
17
  top = Math.max(edge.top, Math.min(top, edge.bottom - size.height));
15
18
  return { left, top, side };
@@ -57,7 +60,7 @@ export function registerLayer(document, layer) {
57
60
  (layer.outside ?? layer.close)();
58
61
  };
59
62
  const focus = (event) => {
60
- if (top() !== layer || layer.modal)
63
+ if (top() !== layer || layer.modal || layer.closeOnFocusOutside === false)
61
64
  return;
62
65
  const target = event.target;
63
66
  if (!layer.panel.contains(target) && !layer.anchor()?.contains(target))
@@ -108,14 +111,19 @@ export function lockScroll(document) {
108
111
  locks.delete(document);
109
112
  };
110
113
  }
111
- export function positionLayer(anchor, panel, side, align) {
114
+ export function positionLayer(anchor, panel, side, align, overlap = false) {
112
115
  const view = panel.ownerDocument.defaultView;
113
116
  const visual = view.visualViewport;
114
117
  const viewport = { left: visual?.offsetLeft ?? 0, top: visual?.offsetTop ?? 0, width: visual?.width ?? view.innerWidth, height: visual?.height ?? view.innerHeight };
115
118
  const gap = parseFloat(getComputedStyle(panel).columnGap) || 8;
116
119
  panel.style.setProperty("--flood-overlay-available-width", `${Math.max(0, viewport.width - 2 * gap)}px`);
117
120
  panel.style.setProperty("--flood-overlay-available-height", `${Math.max(0, viewport.height - 2 * gap)}px`);
118
- const position = placeOverlay(anchor.getBoundingClientRect(), panel.getBoundingClientRect(), viewport, side, align, gap, getComputedStyle(anchor).direction === "rtl");
121
+ const bounds = anchor.getBoundingClientRect(), anchorWidth = `${bounds.width}px`, anchorHeight = `${bounds.height}px`;
122
+ if (panel.style.getPropertyValue("--flood-overlay-anchor-width") !== anchorWidth)
123
+ panel.style.setProperty("--flood-overlay-anchor-width", anchorWidth);
124
+ if (panel.style.getPropertyValue("--flood-overlay-anchor-height") !== anchorHeight)
125
+ panel.style.setProperty("--flood-overlay-anchor-height", anchorHeight);
126
+ const position = placeOverlay(bounds, panel.getBoundingClientRect(), viewport, side, align, gap, getComputedStyle(anchor).direction === "rtl", overlap);
119
127
  const left = `${position.left}px`, top = `${position.top}px`;
120
128
  if (panel.style.left !== left)
121
129
  panel.style.left = left;
@@ -129,3 +137,31 @@ export function returnFocus(panel, anchor) {
129
137
  if (anchor?.isConnected && !anchor.matches(":disabled") && (panel.contains(active) || active === panel.ownerDocument.body))
130
138
  anchor.focus({ preventScroll: true });
131
139
  }
140
+ /** Retain pixels for a CSS exit after the native layer has released focus. */
141
+ export function useLayerExitContent(panel, open, enabled = true, transitionProperty = "opacity") {
142
+ const [retained, setRetained] = useState(false);
143
+ useLayerLayoutEffect(() => {
144
+ const node = panel.current;
145
+ if (!enabled || !node) {
146
+ setRetained(false);
147
+ return;
148
+ }
149
+ if (open) {
150
+ setRetained(true);
151
+ return;
152
+ }
153
+ node.inert = true;
154
+ const transitions = node.getAnimations().filter(animation => "transitionProperty" in animation && animation.transitionProperty === transitionProperty);
155
+ let active = true;
156
+ if (!transitions.length)
157
+ setRetained(false);
158
+ else
159
+ void Promise.allSettled(transitions.map(animation => animation.finished)).then(() => {
160
+ if (active)
161
+ setRetained(false);
162
+ });
163
+ return () => { active = false; node.inert = false; };
164
+ }, [panel, open, enabled, transitionProperty]);
165
+ return open || enabled && retained;
166
+ }
167
+ const useLayerLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
@@ -7,6 +7,7 @@ interface ModalOverlayProps extends DialogProps {
7
7
  showCloseButton?: boolean;
8
8
  role?: "dialog" | "alertdialog";
9
9
  scrollBody?: boolean;
10
+ bodyContentInset?: "none" | "scrollbar";
10
11
  footer?: import("react").ReactNode;
11
12
  }
12
13
  export declare const ModalOverlay: import("react").ForwardRefExoticComponent<ModalOverlayProps & import("react").RefAttributes<HTMLDialogElement>>;
package/dist/overlays.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, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
3
3
  import { IconButton, ScrollArea, VisuallyHidden } from "./index.js";
4
- import { positionLayer, registerLayer, returnFocus, lockScroll, tabbables } from "./overlay-runtime.js";
4
+ import { positionLayer, registerLayer, returnFocus, lockScroll, tabbables, useLayerExitContent } from "./overlay-runtime.js";
5
5
  import { mergeDescriptions } from "./field-state.js";
6
6
  const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
7
7
  function useCurrent(value) { const ref = useRef(value); useBrowserLayoutEffect(() => { ref.current = value; }); return ref; }
@@ -19,7 +19,7 @@ function focusContent(panel, preferred) {
19
19
  }
20
20
  }
21
21
  function OverlayClose({ label, onClick }) {
22
- return _jsx(IconButton, { label: label, size: "sm", variant: "subtle", className: "flood-overlay-close", onClick: onClick, children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "m4 4 16 16M20 4 4 20" }) }) });
22
+ return _jsx(IconButton, { label: label, size: "sm", variant: "ghost", className: "flood-overlay-close", onClick: onClick, children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "m4 4 16 16M20 4 4 20" }) }) });
23
23
  }
24
24
  export const AnchoredOverlay = /* @__PURE__ */ forwardRef(function AnchoredOverlay(allProps, forwardedRef) {
25
25
  const { kind, trigger, side = "bottom", align = "start", disabled = false, id: explicitId, className, style, ...native } = allProps;
@@ -38,7 +38,9 @@ export const AnchoredOverlay = /* @__PURE__ */ forwardRef(function AnchoredOverl
38
38
  suppressed.current = true;
39
39
  overPanel.current = false;
40
40
  } request(false); };
41
- const current = useCurrent({ close, request, disabled, open, initialFocusRef: pop.initialFocusRef, positionReference: pop.positionReference });
41
+ const focusOnOpen = pop.focusOnOpen ?? true;
42
+ const current = useCurrent({ close, request, disabled, open, focusOnOpen, initialFocusRef: pop.initialFocusRef, positionReference: pop.positionReference });
43
+ const previousFocusOnOpen = useRef(focusOnOpen);
42
44
  const positionReference = () => { const node = current.current.positionReference?.current; return node?.isConnected && node.ownerDocument === panel.current?.ownerDocument ? node : anchor.current; };
43
45
  useImperativeHandle(forwardedRef, () => panel.current, []);
44
46
  useEffect(() => () => clearTimeout(timer.current), []);
@@ -74,7 +76,7 @@ export const AnchoredOverlay = /* @__PURE__ */ forwardRef(function AnchoredOverl
74
76
  element.showPopover();
75
77
  positionLayer(positionReference(), element, side, align);
76
78
  unregister = registerLayer(document, { panel: element, anchor: () => anchor.current, close: () => current.current.close(), tooltip: kind === "tooltip" });
77
- if (kind === "popover")
79
+ if (kind === "popover" && current.current.focusOnOpen)
78
80
  focusContent(element, current.current.initialFocusRef?.current);
79
81
  const update = () => { if (!alive)
80
82
  return; if (anchor.current?.isConnected)
@@ -89,32 +91,42 @@ export const AnchoredOverlay = /* @__PURE__ */ forwardRef(function AnchoredOverl
89
91
  queueMicrotask(() => { if (alive && current.current.open && element.isConnected && !element.matches(":popover-open") && anchor.current?.isConnected) {
90
92
  element.showPopover();
91
93
  positionLayer(positionReference(), element, side, align);
92
- if (kind === "popover")
94
+ if (kind === "popover" && current.current.focusOnOpen)
93
95
  focusContent(element, current.current.initialFocusRef?.current);
94
96
  } });
95
97
  };
96
98
  element.addEventListener("toggle", toggle);
97
- return () => { alive = false; view.cancelAnimationFrame(frame); unregister(); element.removeEventListener("toggle", toggle); if (kind === "popover")
99
+ return () => { alive = false; view.cancelAnimationFrame(frame); unregister(); element.removeEventListener("toggle", toggle); if (kind === "popover" && (previousFocusOnOpen.current || element.contains(document.activeElement)))
98
100
  returnFocus(element, anchor.current); if (element.matches(":popover-open"))
99
101
  element.hidePopover(); };
100
102
  }, [open, side, align, kind]);
101
- const triggerProps = { ref: setAnchor, type: "button", ...(kind === "tooltip" ? {
102
- "aria-describedby": disabled ? undefined : id,
103
- onPointerEnter: event => { if (event.pointerType === "touch")
104
- return; hovered.current = true; enter(); },
105
- onPointerLeave: () => { hovered.current = false; leave(); }, onFocus: () => { focused.current = true; enter(true); }, onBlur: () => { focused.current = false; leave(); },
106
- } : { disabled, "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": id, onClick: () => request(!open) }) };
103
+ const titleOnly = kind === "popover" && pop.chrome === "title-only";
104
+ const showContent = useLayerExitContent(panel, open, titleOnly);
105
+ useBrowserLayoutEffect(() => {
106
+ const element = panel.current;
107
+ if (focusOnOpen && !previousFocusOnOpen.current && element?.matches(":popover-open") && !element.contains(element.ownerDocument.activeElement)) {
108
+ focusContent(element, pop.initialFocusRef?.current);
109
+ }
110
+ previousFocusOnOpen.current = focusOnOpen;
111
+ }, [focusOnOpen]);
112
+ const tooltipTriggerProps = { ref: setAnchor,
113
+ "aria-describedby": disabled ? undefined : id,
114
+ onPointerEnter: event => { if (event.pointerType === "touch")
115
+ return; hovered.current = true; enter(); },
116
+ onPointerLeave: () => { hovered.current = false; leave(); }, onFocus: () => { focused.current = true; enter(true); }, onBlur: () => { focused.current = false; leave(); },
117
+ };
118
+ const popoverTriggerProps = { ref: setAnchor, type: "button", disabled, "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": id, onClick: () => request(!open) };
107
119
  // Remove component-only props before forwarding native div attributes.
108
- const { title: unusedTitle, chrome: unusedChrome, content: unusedContent, children: unusedChildren, open: unusedOpen, defaultOpen: unusedDefault, onOpenChange: unusedChange, delay: unusedDelay, closeLabel: unusedClose, initialFocusRef: unusedFocus, positionReference: unusedPosition, ...attributes } = native;
109
- return _jsxs(_Fragment, { children: [trigger(triggerProps), _jsx("div", { ...attributes, ref: panel, id: id, popover: "manual", role: kind === "tooltip" ? "tooltip" : "dialog", "aria-labelledby": kind === "popover" ? `${id}-title` : undefined, tabIndex: kind === "popover" ? -1 : undefined, "data-chrome": kind === "popover" ? pop.chrome : undefined, className: ["flood-overlay-panel", kind === "tooltip" ? "flood-tooltip" : "flood-popover", className].filter(Boolean).join(" "), style: style, onPointerEnter: event => { attributes.onPointerEnter?.(event); if (kind === "tooltip") {
120
+ const { title: unusedTitle, chrome: unusedChrome, content: unusedContent, children: unusedChildren, open: unusedOpen, defaultOpen: unusedDefault, onOpenChange: unusedChange, delay: unusedDelay, closeLabel: unusedClose, initialFocusRef: unusedFocus, positionReference: unusedPosition, focusOnOpen: unusedFocusOnOpen, ...attributes } = native;
121
+ return _jsxs(_Fragment, { children: [kind === "tooltip" ? tip.trigger(tooltipTriggerProps) : pop.trigger(popoverTriggerProps), _jsx("div", { ...attributes, ref: panel, id: id, popover: "manual", role: kind === "tooltip" ? "tooltip" : "dialog", "aria-labelledby": kind === "popover" ? `${id}-title` : undefined, tabIndex: kind === "popover" ? -1 : undefined, "aria-hidden": titleOnly && !open ? true : attributes["aria-hidden"], "data-chrome": kind === "popover" ? pop.chrome : undefined, className: ["flood-overlay-panel", kind === "tooltip" ? "flood-tooltip" : "flood-popover", className].filter(Boolean).join(" "), style: style, onPointerEnter: event => { attributes.onPointerEnter?.(event); if (kind === "tooltip") {
110
122
  overPanel.current = true;
111
123
  clear();
112
124
  } }, onPointerLeave: event => { attributes.onPointerLeave?.(event); if (kind === "tooltip") {
113
125
  overPanel.current = false;
114
126
  leave();
115
- } }, children: kind === "tooltip" ? tip.content : _jsxs(_Fragment, { children: [pop.chrome === "minimal" ? _jsx(VisuallyHidden, { children: _jsx("h2", { id: `${id}-title`, children: pop.title }) }) : _jsxs("div", { className: "flood-overlay-header", children: [_jsx("h2", { id: `${id}-title`, className: "flood-overlay-title", children: pop.title }), _jsx(OverlayClose, { label: pop.closeLabel ?? "Close", onClick: close })] }), open && _jsx("div", { className: "flood-overlay-body", children: pop.children })] }) })] });
127
+ } }, children: kind === "tooltip" ? tip.content : _jsxs(_Fragment, { children: [pop.chrome === "minimal" ? _jsx(VisuallyHidden, { children: _jsx("h2", { id: `${id}-title`, children: pop.title }) }) : _jsxs("div", { className: "flood-overlay-header", children: [_jsx("h2", { id: `${id}-title`, className: "flood-overlay-title", children: pop.title }), !titleOnly && _jsx(OverlayClose, { label: pop.closeLabel ?? "Close", onClick: close })] }), showContent && _jsx("div", { className: "flood-overlay-body", children: pop.children })] }) })] });
116
128
  });
117
- export const ModalOverlay = /* @__PURE__ */ forwardRef(function ModalOverlay({ showCloseButton = true, scrollBody = false, footer, title, description, trigger, children, open: controlled, defaultOpen = false, onOpenChange, disabled = false, closeLabel = "Close", closeOnOutsideClick = true, initialFocusRef, id: explicitId, className, onKeyDown, "aria-describedby": describedBy, ...props }, forwardedRef) {
129
+ export const ModalOverlay = /* @__PURE__ */ forwardRef(function ModalOverlay({ showCloseButton = true, scrollBody = false, bodyContentInset, footer, title, description, trigger, children, open: controlled, defaultOpen = false, onOpenChange, disabled = false, closeLabel = "Close", closeOnOutsideClick = true, initialFocusRef, id: explicitId, className, onKeyDown, "aria-describedby": describedBy, ...props }, forwardedRef) {
118
130
  const [local, setLocal] = useState(defaultOpen), open = controlled ?? local;
119
131
  const dialog = useRef(null), anchor = useRef(null), generated = useId(), id = explicitId ?? `flood-dialog-${generated}`;
120
132
  const request = (next) => { if (controlled === undefined)
@@ -161,7 +173,11 @@ export const ModalOverlay = /* @__PURE__ */ forwardRef(function ModalOverlay({ s
161
173
  start();
162
174
  return () => { alive = false; view.cancelAnimationFrame(frame); dispose(); };
163
175
  }, [open]);
164
- return _jsxs(_Fragment, { children: [trigger({ ref: setAnchor, type: "button", disabled, "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": id, onClick: () => request(true) }), _jsxs("dialog", { ...props, ref: dialog, id: id, "aria-modal": "true", "aria-labelledby": `${id}-title`, "aria-describedby": mergeDescriptions(describedBy, Boolean(description) && `${id}-description`), className: ["flood-dialog", className].filter(Boolean).join(" "), onKeyDown: event => {
176
+ return _jsxs(_Fragment, { children: [trigger({ ref: setAnchor, type: "button", disabled, "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": id, onClick: event => {
177
+ // WebKit can leave pointer-activated buttons unfocused before showModal().
178
+ event.currentTarget.focus({ preventScroll: true });
179
+ request(true);
180
+ } }), _jsxs("dialog", { ...props, ref: dialog, id: id, "aria-modal": "true", "aria-labelledby": `${id}-title`, "aria-describedby": mergeDescriptions(describedBy, Boolean(description) && `${id}-description`), className: ["flood-dialog", className].filter(Boolean).join(" "), onKeyDown: event => {
165
181
  onKeyDown?.(event);
166
182
  if (event.defaultPrevented || event.key !== "Tab")
167
183
  return;
@@ -178,5 +194,5 @@ export const ModalOverlay = /* @__PURE__ */ forwardRef(function ModalOverlay({ s
178
194
  event.preventDefault();
179
195
  first.focus();
180
196
  }
181
- }, children: [_jsxs("div", { className: "flood-overlay-header", children: [_jsx("h2", { id: `${id}-title`, className: "flood-overlay-title", children: title }), showCloseButton && _jsx(OverlayClose, { label: closeLabel, onClick: () => request(false) })] }), description && _jsx("p", { id: `${id}-description`, className: "flood-overlay-description", children: description }), open && (scrollBody ? _jsx("div", { className: "flood-drawer__body", children: _jsx(ScrollArea, { className: "flood-overlay-body", style: { height: "100%" }, children: children }) }) : _jsx("div", { className: "flood-overlay-body", children: children })), open && footer && _jsx("div", { className: "flood-drawer__footer", children: footer })] })] });
197
+ }, children: [_jsxs("div", { className: "flood-overlay-header", children: [_jsx("h2", { id: `${id}-title`, className: "flood-overlay-title", children: title }), showCloseButton && _jsx(OverlayClose, { label: closeLabel, onClick: () => request(false) })] }), description && _jsx("p", { id: `${id}-description`, className: "flood-overlay-description", children: description }), open && (scrollBody ? _jsx("div", { className: "flood-drawer__body", children: _jsx(ScrollArea, { className: "flood-overlay-body", contentInset: bodyContentInset, style: { height: "100%" }, children: children }) }) : _jsx("div", { className: "flood-overlay-body", children: children })), open && footer && _jsx("div", { className: "flood-drawer__footer", children: footer })] })] });
182
198
  });
package/dist/select.js CHANGED
@@ -187,7 +187,10 @@ export const SingleSelect = /* @__PURE__ */ forwardRef(function SingleSelect({ c
187
187
  const gap = length("--flood-sys-space-2") || 0;
188
188
  const below = Math.max(0, viewportTop + viewportHeight - rect.bottom - gap * 2), above = Math.max(0, rect.top - viewportTop - gap * 2);
189
189
  const maxHeight = Math.min(length("--flood-sys-control-md-height") * 8, Math.max(below, above));
190
- assign(popup, "width", `${Math.min(rect.width, Math.max(0, viewportWidth - gap * 2))}px`);
190
+ const availableWidth = Math.max(0, viewportWidth - gap * 2);
191
+ assign(popup, "width", "max-content");
192
+ assign(popup, "min-width", `${Math.min(rect.width, availableWidth)}px`);
193
+ assign(popup, "max-width", `${availableWidth}px`);
191
194
  assign(popup, "max-height", `${maxHeight}px`);
192
195
  // The shared positioner reads column-gap as its placement gap. On this
193
196
  // block listbox it does not add layout space between options.
@@ -0,0 +1,2 @@
1
+ import { type SidebarProps } from './index.js';
2
+ export declare const WorkspaceSidebar: import("react").ForwardRefExoticComponent<SidebarProps & import("react").RefAttributes<HTMLElement>>;
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react';
3
+ import { IconButton, NavigationTree, NavigationGroup, ScrollArea, Stack } from './index.js';
4
+ const useBrowserLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
5
+ export const WorkspaceSidebar = /* @__PURE__ */ forwardRef(function WorkspaceSidebar({ label, groups, currentValue, currentKind = "page", collapsed: controlled, defaultCollapsed = false, onCollapsedChange, collapsible = true, collapseLabel = 'Collapse sidebar', expandLabel = 'Expand sidebar', expandedValues, defaultExpandedValues = [], onExpandedValuesChange, expandItemLabel, size = 'md', variant = 'quiet', side = 'start', width = '16rem', header, headerLayout = 'stacked', footer, footerSeparator = false, headerSeparator = false, className, style, ...props }, ref) {
6
+ if (!label.trim() || !collapseLabel.trim() || !expandLabel.trim() || new Set(groups.map(group => group.id)).size !== groups.length || groups.some(group => !group.id.trim() || !group.label.trim()))
7
+ throw new Error('Flood Sidebar requires named unique groups and controls');
8
+ const known = new Set(), pending = groups.flatMap(group => [...group.items]);
9
+ while (pending.length) {
10
+ const item = pending.pop();
11
+ if (known.has(item.value))
12
+ throw new Error('Flood Sidebar item values must be globally unique');
13
+ known.add(item.value);
14
+ if (item.children)
15
+ pending.push(...item.children);
16
+ }
17
+ const [local, setLocal] = useState(defaultCollapsed), [expanded, setExpanded] = useState(defaultExpandedValues);
18
+ const collapsed = controlled ?? local, root = useRef(null), toggle = useRef(null), body = useRef(null);
19
+ const lastOwnedFocus = useRef(null);
20
+ const [rtl, setRtl] = useState(false);
21
+ useImperativeHandle(ref, () => root.current, []);
22
+ useBrowserLayoutEffect(() => {
23
+ const node = root.current;
24
+ if (!node)
25
+ return;
26
+ const view = node.ownerDocument.defaultView, update = () => setRtl(view.getComputedStyle(node).direction === 'rtl');
27
+ update();
28
+ const observer = new view.MutationObserver(update);
29
+ for (let ancestor = node; ancestor; ancestor = ancestor.parentElement)
30
+ observer.observe(ancestor, { attributes: true, attributeFilter: ['dir', 'style', 'class'] });
31
+ view.addEventListener('resize', update);
32
+ return () => { observer.disconnect(); view.removeEventListener('resize', update); };
33
+ }, []);
34
+ useBrowserLayoutEffect(() => {
35
+ const node = root.current;
36
+ if (!node)
37
+ return;
38
+ const document = node.ownerDocument;
39
+ const track = (event) => { const target = event.target; lastOwnedFocus.current = node.contains(target) ? target : null; };
40
+ document.addEventListener('focusin', track, true);
41
+ return () => document.removeEventListener('focusin', track, true);
42
+ }, []);
43
+ useBrowserLayoutEffect(() => {
44
+ const node = root.current, previous = lastOwnedFocus.current;
45
+ if (node && previous && !previous.isConnected && node.ownerDocument.activeElement === node.ownerDocument.body)
46
+ (toggle.current ?? node).focus({ preventScroll: true });
47
+ }, [collapsed]);
48
+ const slot = (value) => typeof value === 'function' ? value({ collapsed }) : value;
49
+ const headerContent = slot(header), footerContent = slot(footer), panelOnLeft = (side === 'start') !== rtl;
50
+ return _jsxs("aside", { ...props, ref: root, "aria-label": label, tabIndex: -1, "data-footer-separator": footerSeparator, "data-header-separator": headerSeparator, "data-header-layout": headerLayout, "data-flood-surface": "filled", "data-size": size, "data-variant": variant, "data-collapsed": collapsed, "data-side": side, className: ['flood-sidebar', className].filter(Boolean).join(' '), style: { '--flood-sidebar-width': typeof width === 'number' ? `${width}px` : width, ...style }, children: [(headerContent != null || collapsible) && _jsxs("div", { className: "flood-sidebar__header", "data-collapsible": collapsible, children: [collapsible && _jsx(IconButton, { ref: toggle, className: "flood-sidebar__toggle", label: collapsed ? expandLabel : collapseLabel, title: collapsed ? expandLabel : collapseLabel, size: size, variant: "ghost", "aria-expanded": !collapsed, onClick: () => { toggle.current?.focus({ preventScroll: true }); if (controlled === undefined)
51
+ setLocal(!collapsed); onCollapsedChange?.(!collapsed); }, children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.75", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "4" }), _jsx("path", { d: panelOnLeft ? 'M9 4v16' : 'M15 4v16' }), _jsx("path", { d: panelOnLeft ? (collapsed ? 'm14 9 3 3-3 3' : 'm17 9-3 3 3 3') : (collapsed ? 'm10 9-3 3 3 3' : 'm7 9 3 3-3 3') })] }) }), headerContent != null && _jsx("div", { className: "flood-sidebar__identity", children: headerContent })] }), _jsx(ScrollArea, { ref: body, className: "flood-sidebar__body", contentInset: "none", onFocus: event => {
52
+ // WebKit can focus the tree landmark on pointer down. Keep the row under
53
+ // the pointer instead of scrolling to reveal the entire tree.
54
+ if (event.target.matches('nav.flood-navigation-tree'))
55
+ event.preventDefault();
56
+ }, children: _jsx(Stack, { gap: "4", className: "flood-sidebar__navigation", children: groups.map(group => _jsx(NavigationGroup, { label: group.label, hideLabel: group.hideLabel, headingAction: group.headingAction, separator: group.separator, collapsible: group.collapsible, expanded: group.expanded, defaultExpanded: group.defaultExpanded, onExpandedChange: group.onExpandedChange, compact: collapsed, size: size, className: "flood-sidebar__group", "data-icon-column": group.items.some(item => item.icon != null) || undefined, children: _jsx(NavigationTree, { label: group.label, items: group.items, currentValue: currentValue, currentKind: currentKind, size: size, variant: variant, compact: collapsed, branchGuides: group.branchGuides, flyoutSide: panelOnLeft ? 'right' : 'left', expandedValues: expandedValues ?? expanded, onExpandedValuesChange: values => { if (expandedValues === undefined)
57
+ setExpanded(values); onExpandedValuesChange?.(values); }, expandLabel: expandItemLabel }) }, group.id)) }) }), footerContent != null && _jsx("div", { className: "flood-sidebar__footer", children: footerContent })] });
58
+ });
@@ -0,0 +1,2 @@
1
+ import { type SparklineProps } from "./index.js";
2
+ export declare const TrendPlot: import("react").ForwardRefExoticComponent<SparklineProps & import("react").RefAttributes<HTMLDivElement>>;