@tensor_1/react 2.6.0 → 2.7.0

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.
@@ -1,12 +1,18 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cloneElement, isValidElement, useId } from "react";
2
3
  import { cx } from "../cx.js";
3
4
  function Field({ label, hint, error, success, required, children, className, htmlFor }) {
5
+ const uid = useId();
6
+ const msgId = `${uid}-msg`;
7
+ const describedBy = error || success || hint ? msgId : void 0;
8
+ const control = isValidElement(children) ? cloneElement(children, {
9
+ "aria-describedby": [children.props["aria-describedby"], describedBy].filter(Boolean).join(" ") || void 0,
10
+ "aria-invalid": error ? true : children.props["aria-invalid"]
11
+ }) : children;
4
12
  return /* @__PURE__ */ jsxs("div", { className: cx("uix-field", className), children: [
5
13
  label && /* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor, "data-required": required || void 0, children: label }),
6
- children,
7
- error && /* @__PURE__ */ jsx("span", { className: "uix-field__error", children: error }),
8
- success && !error && /* @__PURE__ */ jsx("span", { className: "uix-field__success", children: success }),
9
- hint && !error && !success && /* @__PURE__ */ jsx("span", { className: "uix-field__hint", children: hint })
14
+ control,
15
+ /* @__PURE__ */ jsx("div", { className: "uix-field__msg", id: msgId, children: error ? /* @__PURE__ */ jsx("span", { className: "uix-field__error", role: "alert", children: error }) : success ? /* @__PURE__ */ jsx("span", { className: "uix-field__success", children: success }) : hint ? /* @__PURE__ */ jsx("span", { className: "uix-field__hint", children: hint }) : null })
10
16
  ] });
11
17
  }
12
18
  export {
@@ -1,5 +1,6 @@
1
1
  import * as react from 'react';
2
- import { HTMLAttributes, ReactNode } from 'react';
2
+ import { HTMLAttributes, RefObject, ReactNode } from 'react';
3
+ import { Placement } from '../overlay-position.js';
3
4
 
4
5
  interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
5
6
  /**
@@ -7,12 +8,25 @@ interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
7
8
  * wires to it via `popoverTarget={id}`. Pass `"manual"` to control open state yourself.
8
9
  */
9
10
  popover?: 'auto' | 'manual';
11
+ /**
12
+ * Element (or ref) to anchor against. When set, the popover is placed with
13
+ * cross-browser JS positioning — flip when it won't fit, shift to stay on-screen —
14
+ * instead of relying on CSS `anchor()`, which is Chromium-only and detaches the
15
+ * popover from its trigger everywhere else. The native Popover API still provides
16
+ * the top layer (so it escapes `overflow` clipping) and light-dismiss.
17
+ */
18
+ anchor?: RefObject<HTMLElement | null> | HTMLElement | null;
19
+ /** Preferred placement when `anchor` is set. Default `'bottom-start'`. */
20
+ placement?: Placement;
21
+ /** Gap between the trigger and the popover, in px. Default 6. */
22
+ offset?: number;
10
23
  children?: ReactNode;
11
24
  }
12
25
  /**
13
- * Anchored surface over `.uix-popover`, using the native Popover API.
14
- * Compose ITSM things like a filter popover by putting controls inside.
26
+ * Anchored surface over `.uix-popover`, using the native Popover API. Compose ITSM
27
+ * things like a filter popover, menu, or rich select by putting controls inside.
28
+ * Pass `anchor` to get collision-aware, cross-browser placement (UIX-FIX-02).
15
29
  */
16
- declare function Popover({ popover, children, className, ...props }: PopoverProps): react.JSX.Element;
30
+ declare function Popover({ popover, anchor, placement, offset, className, children, ...props }: PopoverProps): react.JSX.Element;
17
31
 
18
32
  export { Popover, type PopoverProps };
@@ -1,8 +1,37 @@
1
+ "use client";
1
2
  import { jsx } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "react";
2
4
  import { cx } from "../cx.js";
3
- function Popover({ popover = "auto", children, className, ...props }) {
5
+ import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
6
+ function Popover({
7
+ popover = "auto",
8
+ anchor,
9
+ placement = "bottom-start",
10
+ offset = 6,
11
+ className,
12
+ children,
13
+ ...props
14
+ }) {
15
+ const ref = useRef(null);
16
+ const [open, setOpen] = useState(false);
4
17
  const popoverAttr = { popover };
5
- return /* @__PURE__ */ jsx("div", { className: cx("uix-popover", className), ...popoverAttr, ...props, children });
18
+ const reposition = useAnchoredPosition(anchor ?? null, ref, {
19
+ open: open && !!anchor,
20
+ placement,
21
+ offset
22
+ });
23
+ useEffect(() => {
24
+ const el = ref.current;
25
+ if (!el || !anchor) return;
26
+ const sync = () => {
27
+ const isOpen = el.matches(":popover-open");
28
+ if (isOpen) reposition();
29
+ setOpen(isOpen);
30
+ };
31
+ el.addEventListener("toggle", sync);
32
+ return () => el.removeEventListener("toggle", sync);
33
+ }, [anchor, reposition]);
34
+ return /* @__PURE__ */ jsx("div", { ref, className: cx("uix-popover", className), ...popoverAttr, ...props, children });
6
35
  }
7
36
  export {
8
37
  Popover
@@ -1,16 +1,17 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { cx } from "../cx.js";
3
3
  function Toast({ title, message, tone, icon, onClose, leaving, className, ...props }) {
4
+ const assertive = tone === "danger";
4
5
  return /* @__PURE__ */ jsxs(
5
6
  "div",
6
7
  {
7
8
  className: cx("uix-toast", tone && `uix-toast--${tone}`, className),
8
9
  "data-leaving": leaving || void 0,
9
- role: "status",
10
- "aria-live": "polite",
10
+ role: assertive ? "alert" : "status",
11
+ "aria-live": assertive ? "assertive" : "polite",
11
12
  ...props,
12
13
  children: [
13
- icon && /* @__PURE__ */ jsx("div", { className: "uix-toast__icon", children: icon }),
14
+ icon && /* @__PURE__ */ jsx("div", { className: "uix-toast__icon", "aria-hidden": "true", children: icon }),
14
15
  /* @__PURE__ */ jsxs("div", { className: "uix-toast__body", children: [
15
16
  title && /* @__PURE__ */ jsx("div", { className: "uix-toast__title", children: title }),
16
17
  message && /* @__PURE__ */ jsx("div", { className: "uix-toast__msg", children: message })
@@ -21,7 +22,7 @@ function Toast({ title, message, tone, icon, onClose, leaving, className, ...pro
21
22
  );
22
23
  }
23
24
  function Toaster({ children, className, ...props }) {
24
- return /* @__PURE__ */ jsx("div", { className: cx("uix-toaster", className), "aria-live": "polite", "aria-atomic": "false", ...props, children });
25
+ return /* @__PURE__ */ jsx("div", { className: cx("uix-toaster", className), ...props, children });
25
26
  }
26
27
  export {
27
28
  Toast,
@@ -1,16 +1,20 @@
1
1
  import * as react from 'react';
2
2
  import { HTMLAttributes, ReactNode } from 'react';
3
+ import { Placement } from '../overlay-position.js';
3
4
 
4
5
  interface TooltipProps extends HTMLAttributes<HTMLSpanElement> {
5
- /** Tooltip text. Rendered via the CSS `[data-uix-tip]` hover/focus tooltip. */
6
+ /** Tooltip text. */
6
7
  label: string;
8
+ /** Preferred placement. Default `'top'`. */
9
+ placement?: Placement;
7
10
  children?: ReactNode;
8
11
  }
9
12
  /**
10
- * Thin wrapper over the CSS-only tooltip (`[data-uix-tip]`). Wraps its children
11
- * in a span carrying the attribute; the bubble is drawn entirely in CSS on
12
- * hover/focus. For keyboard reveal, ensure a focusable child or pass `tabIndex`.
13
+ * Tooltip whose bubble renders in the top layer via the native Popover API, so it
14
+ * escapes any `overflow: hidden/scroll` ancestor that would clip the old CSS-only
15
+ * `[data-uix-tip]` tooltip (UIX-FIX-02). Positioned by the shared overlay engine
16
+ * (flip/shift), shown on hover and keyboard focus, dismissed on blur or Escape.
13
17
  */
14
- declare function Tooltip({ label, children, ...props }: TooltipProps): react.JSX.Element;
18
+ declare function Tooltip({ label, placement, className, children, ...props }: TooltipProps): react.JSX.Element;
15
19
 
16
20
  export { Tooltip, type TooltipProps };
@@ -1,6 +1,64 @@
1
- import { jsx } from "react/jsx-runtime";
2
- function Tooltip({ label, children, ...props }) {
3
- return /* @__PURE__ */ jsx("span", { "data-uix-tip": label, ...props, children });
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef, useState } from "react";
4
+ import { cx } from "../cx.js";
5
+ import { useAnchoredPosition } from "../hooks/useAnchoredPosition.js";
6
+ function Tooltip({ label, placement = "top", className, children, ...props }) {
7
+ const triggerRef = useRef(null);
8
+ const bubbleRef = useRef(null);
9
+ const [open, setOpen] = useState(false);
10
+ const id = useId();
11
+ useAnchoredPosition(triggerRef, bubbleRef, { open, placement, offset: 6 });
12
+ const show = () => {
13
+ const b = bubbleRef.current;
14
+ if (b && !b.matches(":popover-open")) {
15
+ try {
16
+ b.showPopover();
17
+ } catch {
18
+ }
19
+ }
20
+ setOpen(true);
21
+ };
22
+ const hide = () => {
23
+ const b = bubbleRef.current;
24
+ if (b && b.matches(":popover-open")) {
25
+ try {
26
+ b.hidePopover();
27
+ } catch {
28
+ }
29
+ }
30
+ setOpen(false);
31
+ };
32
+ useEffect(() => () => {
33
+ const b = bubbleRef.current;
34
+ if (b && b.matches(":popover-open")) {
35
+ try {
36
+ b.hidePopover();
37
+ } catch {
38
+ }
39
+ }
40
+ }, []);
41
+ const bubbleAttr = { popover: "manual" };
42
+ return /* @__PURE__ */ jsxs(
43
+ "span",
44
+ {
45
+ ref: triggerRef,
46
+ className: cx("uix-tooltip", className),
47
+ "aria-describedby": id,
48
+ onMouseEnter: show,
49
+ onMouseLeave: hide,
50
+ onFocus: show,
51
+ onBlur: hide,
52
+ onKeyDown: (e) => {
53
+ if (e.key === "Escape") hide();
54
+ },
55
+ ...props,
56
+ children: [
57
+ children,
58
+ /* @__PURE__ */ jsx("span", { ref: bubbleRef, id, role: "tooltip", className: "uix-tooltip__bubble", ...bubbleAttr, children: label })
59
+ ]
60
+ }
61
+ );
4
62
  }
5
63
  export {
6
64
  Tooltip
@@ -11,8 +11,11 @@ interface TreeNodeProps {
11
11
  node: TreeNodeData;
12
12
  expanded: Set<string>;
13
13
  selected?: string;
14
+ selectable: boolean;
15
+ tabbableId?: string;
14
16
  onToggle: (id: string) => void;
15
17
  onSelect?: (id: string) => void;
18
+ onFocusNode: (id: string) => void;
16
19
  level?: number;
17
20
  }
18
21
  interface TreeProps extends Omit<HTMLAttributes<HTMLUListElement>, 'onSelect' | 'onChange'> {
@@ -22,7 +25,18 @@ interface TreeProps extends Omit<HTMLAttributes<HTMLUListElement>, 'onSelect' |
22
25
  selected?: string;
23
26
  onToggle?: (id: string) => void;
24
27
  onSelect?: (id: string) => void;
28
+ /**
29
+ * Virtualize a large tree — render only the rows in view (UIX-FIX-05). Defaults to auto
30
+ * (on past the engine's `shouldVirtualize` threshold of visible rows). When on, the tree is
31
+ * flattened to a scrollable list of `maxHeight` with fixed-height rows; a11y is preserved via
32
+ * `aria-level` / `aria-setsize` / `aria-posinset` instead of nested `role="group"`s.
33
+ */
34
+ virtualize?: boolean;
35
+ /** Fixed row height (px) used for virtualization. Default 32. */
36
+ rowHeight?: number;
37
+ /** Scroll-viewport height (px) when virtualizing. Default 384. */
38
+ maxHeight?: number;
25
39
  }
26
- declare function Tree({ nodes, expanded: controlledExpanded, defaultExpanded, selected, onToggle, onSelect, className, ...props }: TreeProps): react.JSX.Element;
40
+ declare function Tree({ nodes, expanded: controlledExpanded, defaultExpanded, selected, onToggle, onSelect, virtualize, rowHeight, maxHeight, className, ...props }: TreeProps): react.JSX.Element;
27
41
 
28
42
  export { Tree, type TreeNodeData, type TreeNodeProps, type TreeProps };
@@ -1,47 +1,76 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useState } from "react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { cx } from "../cx.js";
5
+ import { flattenTree, treeNav } from "../tree-model.js";
6
+ import { virtualWindow, shouldVirtualize } from "../table-engine.js";
5
7
  const ChevronIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M2 4l4 4 4-4" }) });
6
- function TreeNode({ node, expanded, selected, onToggle, onSelect, level = 0 }) {
8
+ function TreeNode({ node, expanded, selected, selectable, tabbableId, onToggle, onSelect, onFocusNode, level = 1 }) {
7
9
  const hasChildren = Array.isArray(node.children) && node.children.length > 0;
8
10
  const isExpanded = expanded.has(node.id);
9
- return /* @__PURE__ */ jsxs("li", { children: [
10
- /* @__PURE__ */ jsxs(
11
- "button",
12
- {
13
- className: "uix-tree__row",
14
- "aria-expanded": hasChildren ? isExpanded : void 0,
15
- "aria-selected": selected === node.id,
16
- onClick: () => {
17
- if (hasChildren) onToggle(node.id);
18
- onSelect?.(node.id);
19
- },
20
- children: [
11
+ return /* @__PURE__ */ jsxs(
12
+ "li",
13
+ {
14
+ role: "treeitem",
15
+ "aria-level": level,
16
+ "aria-expanded": hasChildren ? isExpanded : void 0,
17
+ "aria-selected": selectable ? selected === node.id : void 0,
18
+ "data-id": node.id,
19
+ tabIndex: tabbableId === node.id ? 0 : -1,
20
+ className: "uix-tree__item",
21
+ onFocus: (e) => {
22
+ if (e.target === e.currentTarget) onFocusNode(node.id);
23
+ },
24
+ onClick: (e) => {
25
+ e.stopPropagation();
26
+ if (hasChildren) onToggle(node.id);
27
+ onSelect?.(node.id);
28
+ onFocusNode(node.id);
29
+ },
30
+ children: [
31
+ /* @__PURE__ */ jsxs("span", { className: "uix-tree__row", children: [
21
32
  /* @__PURE__ */ jsx("span", { className: "uix-tree__toggle", children: hasChildren && /* @__PURE__ */ jsx(ChevronIcon, {}) }),
22
33
  node.icon && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: node.icon }),
23
- node.label
24
- ]
25
- }
26
- ),
27
- hasChildren && isExpanded && /* @__PURE__ */ jsx("ul", { children: node.children.map((child) => /* @__PURE__ */ jsx(
28
- TreeNode,
29
- {
30
- node: child,
31
- expanded,
32
- selected,
33
- onToggle,
34
- onSelect,
35
- level: level + 1
36
- },
37
- child.id
38
- )) })
39
- ] });
34
+ /* @__PURE__ */ jsx("span", { className: "uix-tree__label", children: node.label })
35
+ ] }),
36
+ hasChildren && isExpanded && /* @__PURE__ */ jsx("ul", { role: "group", className: "uix-tree__group", children: node.children.map((child) => /* @__PURE__ */ jsx(
37
+ TreeNode,
38
+ {
39
+ node: child,
40
+ expanded,
41
+ selected,
42
+ selectable,
43
+ tabbableId,
44
+ onToggle,
45
+ onSelect,
46
+ onFocusNode,
47
+ level: level + 1
48
+ },
49
+ child.id
50
+ )) })
51
+ ]
52
+ }
53
+ );
40
54
  }
41
- function Tree({ nodes, expanded: controlledExpanded, defaultExpanded, selected, onToggle, onSelect, className, ...props }) {
55
+ function Tree({
56
+ nodes,
57
+ expanded: controlledExpanded,
58
+ defaultExpanded,
59
+ selected,
60
+ onToggle,
61
+ onSelect,
62
+ virtualize,
63
+ rowHeight = 32,
64
+ maxHeight = 384,
65
+ className,
66
+ ...props
67
+ }) {
42
68
  const [internalExpanded, setInternalExpanded] = useState(defaultExpanded ?? /* @__PURE__ */ new Set());
43
69
  const expanded = controlledExpanded ?? internalExpanded;
44
- function handleToggle(id) {
70
+ const rootRef = useRef(null);
71
+ const [focusedId, setFocusedId] = useState(void 0);
72
+ const selectable = selected !== void 0 || onSelect != null;
73
+ const handleToggle = useCallback((id) => {
45
74
  if (!controlledExpanded) {
46
75
  setInternalExpanded((prev) => {
47
76
  const next = new Set(prev);
@@ -51,19 +80,202 @@ function Tree({ nodes, expanded: controlledExpanded, defaultExpanded, selected,
51
80
  });
52
81
  }
53
82
  onToggle?.(id);
83
+ }, [controlledExpanded, onToggle]);
84
+ const flat = useMemo(() => flattenTree(nodes, expanded), [nodes, expanded]);
85
+ const useVirtual = virtualize ?? shouldVirtualize(flat.length);
86
+ const tabbableId = focusedId ?? selected ?? nodes[0]?.id;
87
+ if (useVirtual) {
88
+ return /* @__PURE__ */ jsx(
89
+ VirtualTreeView,
90
+ {
91
+ flat,
92
+ rowHeight,
93
+ maxHeight,
94
+ selectable,
95
+ selected,
96
+ tabbableId,
97
+ onToggle: handleToggle,
98
+ onSelect,
99
+ focusedId,
100
+ setFocusedId,
101
+ className,
102
+ ...props
103
+ }
104
+ );
54
105
  }
55
- return /* @__PURE__ */ jsx("ul", { className: cx("uix-tree", className), role: "tree", ...props, children: nodes.map((node) => /* @__PURE__ */ jsx(
106
+ const treeItems = () => Array.from(rootRef.current?.querySelectorAll('[role="treeitem"]') ?? []);
107
+ const focusItem = (el) => {
108
+ if (el) {
109
+ el.focus();
110
+ setFocusedId(el.dataset.id);
111
+ }
112
+ };
113
+ const onKeyDown = (e) => {
114
+ const cur = e.target.closest('[role="treeitem"]');
115
+ if (!cur || !rootRef.current?.contains(cur)) return;
116
+ const list = treeItems();
117
+ const idx = list.indexOf(cur);
118
+ const id = cur.dataset.id;
119
+ const exp = cur.getAttribute("aria-expanded");
120
+ switch (e.key) {
121
+ case "ArrowDown":
122
+ e.preventDefault();
123
+ focusItem(list[Math.min(idx + 1, list.length - 1)]);
124
+ break;
125
+ case "ArrowUp":
126
+ e.preventDefault();
127
+ focusItem(list[Math.max(idx - 1, 0)]);
128
+ break;
129
+ case "Home":
130
+ e.preventDefault();
131
+ focusItem(list[0]);
132
+ break;
133
+ case "End":
134
+ e.preventDefault();
135
+ focusItem(list[list.length - 1]);
136
+ break;
137
+ case "ArrowRight":
138
+ e.preventDefault();
139
+ if (exp === "false") handleToggle(id);
140
+ else if (exp === "true") focusItem(cur.querySelector(':scope > [role="group"] > [role="treeitem"]'));
141
+ break;
142
+ case "ArrowLeft":
143
+ e.preventDefault();
144
+ if (exp === "true") handleToggle(id);
145
+ else focusItem(cur.parentElement?.closest('[role="treeitem"]'));
146
+ break;
147
+ case "Enter":
148
+ case " ":
149
+ e.preventDefault();
150
+ if (exp !== null) handleToggle(id);
151
+ onSelect?.(id);
152
+ setFocusedId(id);
153
+ break;
154
+ default:
155
+ break;
156
+ }
157
+ };
158
+ return /* @__PURE__ */ jsx("ul", { ref: rootRef, className: cx("uix-tree", className), role: "tree", onKeyDown, ...props, children: nodes.map((node) => /* @__PURE__ */ jsx(
56
159
  TreeNode,
57
160
  {
58
161
  node,
59
162
  expanded,
60
163
  selected,
164
+ selectable,
165
+ tabbableId,
61
166
  onToggle: handleToggle,
62
- onSelect
167
+ onSelect,
168
+ onFocusNode: setFocusedId,
169
+ level: 1
63
170
  },
64
171
  node.id
65
172
  )) });
66
173
  }
174
+ function VirtualTreeView({
175
+ flat,
176
+ rowHeight,
177
+ maxHeight,
178
+ selectable,
179
+ selected,
180
+ tabbableId,
181
+ onToggle,
182
+ onSelect,
183
+ focusedId,
184
+ setFocusedId,
185
+ className,
186
+ ...props
187
+ }) {
188
+ const scrollRef = useRef(null);
189
+ const [scrollTop, setScrollTop] = useState(0);
190
+ const pendingFocus = useRef(false);
191
+ const win = virtualWindow(scrollTop, maxHeight, rowHeight, flat.length);
192
+ const rows = flat.slice(win.start, win.end);
193
+ useEffect(() => {
194
+ if (!pendingFocus.current || focusedId == null) return;
195
+ const el = scrollRef.current?.querySelector(`[role="treeitem"][data-id="${CSS.escape(focusedId)}"]`);
196
+ if (el) {
197
+ el.focus();
198
+ pendingFocus.current = false;
199
+ }
200
+ }, [focusedId, win.start, win.end]);
201
+ const moveTo = (id) => {
202
+ const i = flat.findIndex((f) => f.node.id === id);
203
+ if (i < 0) return;
204
+ const top = i * rowHeight;
205
+ let next = scrollTop;
206
+ if (top < scrollTop) next = top;
207
+ else if (top + rowHeight > scrollTop + maxHeight) next = top + rowHeight - maxHeight;
208
+ if (next !== scrollTop) {
209
+ if (scrollRef.current) scrollRef.current.scrollTop = next;
210
+ setScrollTop(next);
211
+ }
212
+ pendingFocus.current = true;
213
+ setFocusedId(id);
214
+ };
215
+ const onKeyDown = (e) => {
216
+ const curId = e.target.closest('[role="treeitem"]')?.dataset.id ?? focusedId;
217
+ const act = treeNav(flat, curId, e.key);
218
+ if (act.focusId === void 0 && act.toggleId === void 0 && act.selectId === void 0) return;
219
+ e.preventDefault();
220
+ if (act.toggleId !== void 0) onToggle(act.toggleId);
221
+ if (act.selectId !== void 0) {
222
+ onSelect?.(act.selectId);
223
+ if (act.focusId === void 0) {
224
+ pendingFocus.current = true;
225
+ setFocusedId(act.selectId);
226
+ }
227
+ }
228
+ if (act.focusId !== void 0) moveTo(act.focusId);
229
+ };
230
+ return /* @__PURE__ */ jsxs(
231
+ "ul",
232
+ {
233
+ ref: scrollRef,
234
+ className: cx("uix-tree", "uix-tree--virtual", className),
235
+ role: "tree",
236
+ style: { maxHeight, overflow: "auto" },
237
+ onScroll: (e) => setScrollTop(e.currentTarget.scrollTop),
238
+ onKeyDown,
239
+ ...props,
240
+ children: [
241
+ win.padTop > 0 && /* @__PURE__ */ jsx("li", { "aria-hidden": "true", style: { height: win.padTop } }),
242
+ rows.map((f) => {
243
+ const rowStyle = { height: rowHeight, paddingLeft: `calc(${f.level - 1} * var(--uix-space-5))` };
244
+ return /* @__PURE__ */ jsx(
245
+ "li",
246
+ {
247
+ role: "treeitem",
248
+ "aria-level": f.level,
249
+ "aria-setsize": f.setsize,
250
+ "aria-posinset": f.posinset,
251
+ "aria-expanded": f.hasChildren ? f.isExpanded : void 0,
252
+ "aria-selected": selectable ? selected === f.node.id : void 0,
253
+ "data-id": f.node.id,
254
+ tabIndex: tabbableId === f.node.id ? 0 : -1,
255
+ className: "uix-tree__item",
256
+ onFocus: (e) => {
257
+ if (e.target === e.currentTarget) setFocusedId(f.node.id);
258
+ },
259
+ onClick: (e) => {
260
+ e.stopPropagation();
261
+ if (f.hasChildren) onToggle(f.node.id);
262
+ onSelect?.(f.node.id);
263
+ setFocusedId(f.node.id);
264
+ },
265
+ children: /* @__PURE__ */ jsxs("span", { className: "uix-tree__row", style: rowStyle, children: [
266
+ /* @__PURE__ */ jsx("span", { className: "uix-tree__toggle", children: f.hasChildren && /* @__PURE__ */ jsx(ChevronIcon, {}) }),
267
+ f.node.icon && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: f.node.icon }),
268
+ /* @__PURE__ */ jsx("span", { className: "uix-tree__label", children: f.node.label })
269
+ ] })
270
+ },
271
+ f.node.id
272
+ );
273
+ }),
274
+ win.padBottom > 0 && /* @__PURE__ */ jsx("li", { "aria-hidden": "true", style: { height: win.padBottom } })
275
+ ]
276
+ }
277
+ );
278
+ }
67
279
  export {
68
280
  Tree
69
281
  };
@@ -0,0 +1,22 @@
1
+ import { RefObject } from 'react';
2
+ import { Placement } from '../overlay-position.js';
3
+
4
+ interface UseAnchoredPositionOptions {
5
+ /** Reposition while true, and (re)attach scroll/resize listeners. */
6
+ open: boolean;
7
+ placement?: Placement;
8
+ offset?: number;
9
+ padding?: number;
10
+ }
11
+ /**
12
+ * Position a floating element (a native-popover overlay) against an anchor with
13
+ * cross-browser flip/shift — the DOM half of the overlay-position engine (UIX-FIX-02).
14
+ *
15
+ * Measures both elements with getBoundingClientRect, applies `position: fixed` + left/top
16
+ * (overriding the popover's default centered `inset:0; margin:auto`), and keeps the overlay
17
+ * glued to its anchor on scroll/resize while `open`. Returns a `reposition` fn for callers
18
+ * that want to nudge it manually (e.g. right when a native popover's `toggle` fires).
19
+ */
20
+ declare function useAnchoredPosition(anchor: RefObject<HTMLElement | null> | HTMLElement | null | undefined, floatingRef: RefObject<HTMLElement | null>, { open, placement, offset, padding }: UseAnchoredPositionOptions): () => void;
21
+
22
+ export { type UseAnchoredPositionOptions, useAnchoredPosition };
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
3
+ import { computePosition } from "../overlay-position.js";
4
+ const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
5
+ const resolve = (a) => a && typeof a === "object" && "current" in a ? a.current : a ?? null;
6
+ function useAnchoredPosition(anchor, floatingRef, { open, placement = "bottom-start", offset = 6, padding = 8 }) {
7
+ const opts = useRef({ placement, offset, padding });
8
+ opts.current = { placement, offset, padding };
9
+ const reposition = useCallback(() => {
10
+ const anchorEl = resolve(anchor);
11
+ const floating = floatingRef.current;
12
+ if (!anchorEl || !floating) return;
13
+ const s = floating.style;
14
+ s.position = "fixed";
15
+ s.inset = "auto";
16
+ s.margin = "0";
17
+ const a = anchorEl.getBoundingClientRect();
18
+ const { placement: p, offset: o, padding: pad } = opts.current;
19
+ const pos = computePosition(
20
+ { x: a.x, y: a.y, width: a.width, height: a.height },
21
+ { width: floating.offsetWidth, height: floating.offsetHeight },
22
+ { width: window.innerWidth, height: window.innerHeight },
23
+ { placement: p, offset: o, padding: pad }
24
+ );
25
+ s.left = `${Math.round(pos.x)}px`;
26
+ s.top = `${Math.round(pos.y)}px`;
27
+ floating.dataset.placement = pos.placement;
28
+ }, [anchor, floatingRef]);
29
+ useIsomorphicLayoutEffect(() => {
30
+ if (!open) return;
31
+ reposition();
32
+ const onScroll = () => reposition();
33
+ window.addEventListener("scroll", onScroll, { passive: true, capture: true });
34
+ window.addEventListener("resize", onScroll);
35
+ return () => {
36
+ window.removeEventListener("scroll", onScroll, true);
37
+ window.removeEventListener("resize", onScroll);
38
+ };
39
+ }, [open, reposition]);
40
+ return reposition;
41
+ }
42
+ export {
43
+ useAnchoredPosition
44
+ };