@intentface/ui 0.0.0 → 0.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.
Files changed (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -2
  3. package/dist/ask-user/ask-user.d.ts +104 -0
  4. package/dist/ask-user/ask-user.js +245 -0
  5. package/dist/ask-user/index.d.ts +3 -0
  6. package/dist/ask-user/index.js +3 -0
  7. package/dist/ask-user/index.parts.d.ts +1 -0
  8. package/dist/ask-user/index.parts.js +5 -0
  9. package/dist/attachments/attachments.d.ts +73 -0
  10. package/dist/attachments/attachments.js +121 -0
  11. package/dist/attachments/index.d.ts +3 -0
  12. package/dist/attachments/index.js +3 -0
  13. package/dist/attachments/index.parts.d.ts +1 -0
  14. package/dist/attachments/index.parts.js +5 -0
  15. package/dist/chip/chip.d.ts +45 -0
  16. package/dist/chip/chip.js +43 -0
  17. package/dist/chip/index.d.ts +2 -0
  18. package/dist/chip/index.js +2 -0
  19. package/dist/chip/index.parts.d.ts +1 -0
  20. package/dist/chip/index.parts.js +5 -0
  21. package/dist/chip-markdown.d.ts +25 -0
  22. package/dist/chip-markdown.js +54 -0
  23. package/dist/composer/actions.d.ts +35 -0
  24. package/dist/composer/actions.js +76 -0
  25. package/dist/composer/ask-user-machine.d.ts +52 -0
  26. package/dist/composer/ask-user-machine.js +164 -0
  27. package/dist/composer/attachments-machine.d.ts +34 -0
  28. package/dist/composer/attachments-machine.js +91 -0
  29. package/dist/composer/attachments.d.ts +16 -0
  30. package/dist/composer/attachments.js +32 -0
  31. package/dist/composer/command-list.d.ts +49 -0
  32. package/dist/composer/command-list.js +360 -0
  33. package/dist/composer/commands.d.ts +28 -0
  34. package/dist/composer/commands.js +56 -0
  35. package/dist/composer/container.d.ts +3 -0
  36. package/dist/composer/container.js +32 -0
  37. package/dist/composer/controller.d.ts +3 -0
  38. package/dist/composer/controller.js +6 -0
  39. package/dist/composer/editor-dom.d.ts +57 -0
  40. package/dist/composer/editor-dom.js +517 -0
  41. package/dist/composer/fuzzy.d.ts +4 -0
  42. package/dist/composer/fuzzy.js +49 -0
  43. package/dist/composer/index.d.ts +25 -0
  44. package/dist/composer/index.js +13 -0
  45. package/dist/composer/index.parts.d.ts +11 -0
  46. package/dist/composer/index.parts.js +15 -0
  47. package/dist/composer/internals.d.ts +30 -0
  48. package/dist/composer/internals.js +102 -0
  49. package/dist/composer/keyboard.d.ts +61 -0
  50. package/dist/composer/keyboard.js +75 -0
  51. package/dist/composer/panel.d.ts +28 -0
  52. package/dist/composer/panel.js +76 -0
  53. package/dist/composer/placeholder.d.ts +10 -0
  54. package/dist/composer/placeholder.js +8 -0
  55. package/dist/composer/popover.d.ts +12 -0
  56. package/dist/composer/popover.js +62 -0
  57. package/dist/composer/prefix-detection.d.ts +23 -0
  58. package/dist/composer/prefix-detection.js +67 -0
  59. package/dist/composer/root.d.ts +21 -0
  60. package/dist/composer/root.js +124 -0
  61. package/dist/composer/segments.d.ts +108 -0
  62. package/dist/composer/segments.js +366 -0
  63. package/dist/composer/store.d.ts +98 -0
  64. package/dist/composer/store.js +457 -0
  65. package/dist/composer/textarea.d.ts +39 -0
  66. package/dist/composer/textarea.js +64 -0
  67. package/dist/composer/trigger-tracker.d.ts +12 -0
  68. package/dist/composer/trigger-tracker.js +90 -0
  69. package/dist/composer/types.d.ts +127 -0
  70. package/dist/composer/types.js +4 -0
  71. package/dist/composer/use-composer-editor.d.ts +43 -0
  72. package/dist/composer/use-composer-editor.js +653 -0
  73. package/dist/generative/catalog.d.ts +177 -0
  74. package/dist/generative/catalog.js +20 -0
  75. package/dist/generative/index.d.ts +26 -0
  76. package/dist/generative/index.js +20 -0
  77. package/dist/generative/json-schema.d.ts +45 -0
  78. package/dist/generative/json-schema.js +216 -0
  79. package/dist/generative/markdown.d.ts +21 -0
  80. package/dist/generative/markdown.js +76 -0
  81. package/dist/generative/tree.d.ts +56 -0
  82. package/dist/generative/tree.js +365 -0
  83. package/dist/generative/use-tree.d.ts +12 -0
  84. package/dist/generative/use-tree.js +64 -0
  85. package/dist/internal/change-event-details.d.ts +12 -0
  86. package/dist/internal/change-event-details.js +16 -0
  87. package/dist/internal/collapsible.d.ts +22 -0
  88. package/dist/internal/collapsible.js +94 -0
  89. package/dist/internal/collection.d.ts +32 -0
  90. package/dist/internal/collection.js +72 -0
  91. package/dist/internal/iso-layout-effect.d.ts +2 -0
  92. package/dist/internal/iso-layout-effect.js +6 -0
  93. package/dist/internal/portal.d.ts +6 -0
  94. package/dist/internal/portal.js +18 -0
  95. package/dist/internal/primitive-props.d.ts +4 -0
  96. package/dist/internal/primitive-props.js +3 -0
  97. package/dist/internal/render/baseTypes.d.ts +18 -0
  98. package/dist/internal/render/baseTypes.js +4 -0
  99. package/dist/internal/render/empty.d.ts +3 -0
  100. package/dist/internal/render/empty.js +5 -0
  101. package/dist/internal/render/getReactElementRef.d.ts +5 -0
  102. package/dist/internal/render/getReactElementRef.js +15 -0
  103. package/dist/internal/render/getStateAttributesProps.d.ts +4 -0
  104. package/dist/internal/render/getStateAttributesProps.js +23 -0
  105. package/dist/internal/render/mergeObjects.d.ts +1 -0
  106. package/dist/internal/render/mergeObjects.js +14 -0
  107. package/dist/internal/render/mergeProps.d.ts +55 -0
  108. package/dist/internal/render/mergeProps.js +183 -0
  109. package/dist/internal/render/positioning.d.ts +33 -0
  110. package/dist/internal/render/positioning.js +112 -0
  111. package/dist/internal/render/reactVersion.d.ts +3 -0
  112. package/dist/internal/render/reactVersion.js +7 -0
  113. package/dist/internal/render/resolveClassName.d.ts +8 -0
  114. package/dist/internal/render/resolveClassName.js +12 -0
  115. package/dist/internal/render/resolveStyle.d.ts +8 -0
  116. package/dist/internal/render/resolveStyle.js +12 -0
  117. package/dist/internal/render/transition.d.ts +47 -0
  118. package/dist/internal/render/transition.js +259 -0
  119. package/dist/internal/render/types.d.ts +74 -0
  120. package/dist/internal/render/types.js +3 -0
  121. package/dist/internal/render/useMergedRefs.d.ts +21 -0
  122. package/dist/internal/render/useMergedRefs.js +102 -0
  123. package/dist/internal/render/useRefWithInit.d.ts +10 -0
  124. package/dist/internal/render/useRefWithInit.js +12 -0
  125. package/dist/internal/render/useRender.d.ts +71 -0
  126. package/dist/internal/render/useRender.js +12 -0
  127. package/dist/internal/render/useRenderElement.d.ts +62 -0
  128. package/dist/internal/render/useRenderElement.js +157 -0
  129. package/dist/internal/render/warn.d.ts +1 -0
  130. package/dist/internal/render/warn.js +15 -0
  131. package/dist/internal/safe-polygon.d.ts +21 -0
  132. package/dist/internal/safe-polygon.js +179 -0
  133. package/dist/internal/scroll-into-view-within.d.ts +1 -0
  134. package/dist/internal/scroll-into-view-within.js +26 -0
  135. package/dist/internal/standard-schema.d.ts +38 -0
  136. package/dist/internal/standard-schema.js +4 -0
  137. package/dist/internal/state-mappings.d.ts +8 -0
  138. package/dist/internal/state-mappings.js +17 -0
  139. package/dist/internal/use-controlled.d.ts +19 -0
  140. package/dist/internal/use-controlled.js +33 -0
  141. package/dist/message/index.d.ts +3 -0
  142. package/dist/message/index.js +3 -0
  143. package/dist/message/index.parts.d.ts +1 -0
  144. package/dist/message/index.parts.js +5 -0
  145. package/dist/message/message.d.ts +58 -0
  146. package/dist/message/message.js +93 -0
  147. package/dist/message-utils.d.ts +55 -0
  148. package/dist/message-utils.js +79 -0
  149. package/dist/nav/index.d.ts +4 -0
  150. package/dist/nav/index.js +5 -0
  151. package/dist/nav/index.parts.d.ts +3 -0
  152. package/dist/nav/index.parts.js +7 -0
  153. package/dist/nav/nav.d.ts +152 -0
  154. package/dist/nav/nav.js +660 -0
  155. package/dist/nav/store.d.ts +50 -0
  156. package/dist/nav/store.js +95 -0
  157. package/dist/reasoning/index.d.ts +3 -0
  158. package/dist/reasoning/index.js +3 -0
  159. package/dist/reasoning/index.parts.d.ts +1 -0
  160. package/dist/reasoning/index.parts.js +5 -0
  161. package/dist/reasoning/reasoning.d.ts +38 -0
  162. package/dist/reasoning/reasoning.js +67 -0
  163. package/dist/shell/index.d.ts +5 -0
  164. package/dist/shell/index.js +5 -0
  165. package/dist/shell/index.parts.d.ts +3 -0
  166. package/dist/shell/index.parts.js +7 -0
  167. package/dist/shell/shell.d.ts +108 -0
  168. package/dist/shell/shell.js +390 -0
  169. package/dist/shell/store.d.ts +112 -0
  170. package/dist/shell/store.js +134 -0
  171. package/dist/steps/index.d.ts +2 -0
  172. package/dist/steps/index.js +2 -0
  173. package/dist/steps/index.parts.d.ts +1 -0
  174. package/dist/steps/index.parts.js +5 -0
  175. package/dist/steps/steps.d.ts +90 -0
  176. package/dist/steps/steps.js +116 -0
  177. package/dist/tabs/anchored.d.ts +59 -0
  178. package/dist/tabs/anchored.js +109 -0
  179. package/dist/tabs/hover.d.ts +33 -0
  180. package/dist/tabs/hover.js +169 -0
  181. package/dist/tabs/index.d.ts +5 -0
  182. package/dist/tabs/index.js +4 -0
  183. package/dist/tabs/index.parts.d.ts +4 -0
  184. package/dist/tabs/index.parts.js +8 -0
  185. package/dist/tabs/store.d.ts +131 -0
  186. package/dist/tabs/store.js +248 -0
  187. package/dist/tabs/tabs.d.ts +217 -0
  188. package/dist/tabs/tabs.js +458 -0
  189. package/dist/thread/geometry.d.ts +23 -0
  190. package/dist/thread/geometry.js +48 -0
  191. package/dist/thread/index.d.ts +4 -0
  192. package/dist/thread/index.js +3 -0
  193. package/dist/thread/index.parts.d.ts +1 -0
  194. package/dist/thread/index.parts.js +5 -0
  195. package/dist/thread/stores.d.ts +21 -0
  196. package/dist/thread/stores.js +63 -0
  197. package/dist/thread/thread.d.ts +57 -0
  198. package/dist/thread/thread.js +663 -0
  199. package/dist/types.d.ts +58 -0
  200. package/dist/types.js +17 -0
  201. package/package.json +120 -4
@@ -0,0 +1,660 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, use, useCallback, useId, useMemo, useRef, useState, } from "react";
4
+ import { useIsomorphicLayoutEffect } from "../internal/iso-layout-effect.js";
5
+ import { useAnimationsFinished, useTransitionStatus, } from "../internal/render/transition.js";
6
+ import { useRenderElement } from "../internal/render/useRenderElement.js";
7
+ import { openStateMapping, transitionStatusMapping } from "../internal/state-mappings.js";
8
+ import { createNavStore, NavStoreContext, useNavContextStore, useNavStore } from "./store.js";
9
+ // ---------------------------------------------------------------------------
10
+ // Context
11
+ // ---------------------------------------------------------------------------
12
+ /** The depth of the current list. Root provides 0; each list provides its own + 1. */
13
+ const NavDepthContext = createContext(0);
14
+ /** The nearest enclosing group. Null at the top level, which is what `nested` means. */
15
+ const NavGroupContext = createContext(null);
16
+ const NavRovingContext = createContext(null);
17
+ const useRoving = () => {
18
+ const roving = use(NavRovingContext);
19
+ if (!roving)
20
+ throw new Error("Nav parts must be used within <Nav.Root>");
21
+ return roving;
22
+ };
23
+ /*
24
+ * `depth: 0` and `open: false` are both falsy, and the default derivation drops
25
+ * falsy values — which would silently delete the attribute for the top level
26
+ * and for every closed group, the two cases most worth styling.
27
+ */
28
+ const DEPTH_ATTRIBUTES = {
29
+ depth: (value) => ({ "data-depth": String(value) }),
30
+ };
31
+ /*
32
+ * One rung of state, three attributes — cumulative, because the rungs are.
33
+ * A stylesheet asking for `[data-rail]` gets branches too, which is what makes
34
+ * the ladder readable in CSS without `:is()` around every selector.
35
+ */
36
+ const GUIDE_ATTRIBUTES = {
37
+ guide: (value) => {
38
+ if (value === "none")
39
+ return null;
40
+ return {
41
+ "data-indent": "",
42
+ ...(value === "rail" || value === "branches" ? { "data-rail": "" } : {}),
43
+ ...(value === "branches" ? { "data-branches": "" } : {}),
44
+ };
45
+ },
46
+ };
47
+ const LIST_ATTRIBUTES = {
48
+ ...DEPTH_ATTRIBUTES,
49
+ ...openStateMapping,
50
+ ...GUIDE_ATTRIBUTES,
51
+ ...transitionStatusMapping,
52
+ };
53
+ /** Rows carry their value in the identity attribute — the DOM is the row order. */
54
+ const ROW_SELECTOR = "[data-nav-item],[data-nav-trigger]";
55
+ const visibleRows = (root) => Array.from(root.querySelectorAll(ROW_SELECTOR)).filter(
56
+ // A collapsed list unmounts, so its rows are already gone. `keepMounted`
57
+ // leaves them in the DOM behind `hidden`, where they are not navigable.
58
+ (row) => !row.closest("[data-nav-list][hidden]"));
59
+ const rowValue = (row) => row.getAttribute("data-nav-item") ?? row.getAttribute("data-nav-trigger");
60
+ /** A field inside the nav owns its own arrow keys, or the caret never moves. */
61
+ const ownsTheKeys = (target) => target instanceof HTMLElement &&
62
+ (target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName));
63
+ /**
64
+ * The provider and container. Owns the keyboard handling for the whole tree,
65
+ * so roving focus and typeahead work without any row registering itself.
66
+ * Renders a `<div>` element.
67
+ */
68
+ export const NavRoot = ({ defaultExpanded, expanded, onExpandedChange, store: storeProp, guide = "none", loop = false, disabled = false, typeahead = true, className, render, style, ...elementProps }) => {
69
+ const [fallback] = useState(createNavStore);
70
+ const store = storeProp ?? fallback;
71
+ const [highlighted, setHighlighted] = useState(null);
72
+ const search = useRef({ query: "", at: 0 });
73
+ store.controlledRef.current = expanded !== undefined;
74
+ store.onExpandedChangeRef.current = onExpandedChange ?? null;
75
+ // Applied before the first paint rather than in an effect: a group that opens
76
+ // one frame late is the flash this whole storage contract exists to avoid.
77
+ const hydrated = useRef(false);
78
+ if (!hydrated.current) {
79
+ hydrated.current = true;
80
+ store.hydrate({ expanded: expanded ?? defaultExpanded });
81
+ }
82
+ // A controlled Root's prop is the state, so keep the snapshot following it.
83
+ // `notify` here and nowhere else: this is the one hydrate that runs after
84
+ // mount, where a memoised subtree would otherwise never learn of the change.
85
+ useIsomorphicLayoutEffect(() => {
86
+ if (expanded === undefined)
87
+ return;
88
+ store.hydrate({ expanded }, { notify: true });
89
+ }, [expanded, store]);
90
+ const move = useCallback((rows, index) => {
91
+ const row = rows[index];
92
+ if (!row)
93
+ return;
94
+ setHighlighted(rowValue(row));
95
+ row.focus();
96
+ }, []);
97
+ const handleKeyDown = (event) => {
98
+ const root = store.rootRef.current;
99
+ if (!root || ownsTheKeys(event.target))
100
+ return;
101
+ const current = event.target.closest(ROW_SELECTOR);
102
+ if (!current)
103
+ return;
104
+ const rows = visibleRows(root);
105
+ const index = rows.indexOf(current);
106
+ if (index === -1)
107
+ return;
108
+ const last = rows.length - 1;
109
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
110
+ const delta = event.key === "ArrowDown" ? 1 : -1;
111
+ const next = index + delta;
112
+ event.preventDefault();
113
+ move(rows, loop ? (next + rows.length) % rows.length : Math.min(Math.max(next, 0), last));
114
+ return;
115
+ }
116
+ if (event.key === "Home" || event.key === "End") {
117
+ event.preventDefault();
118
+ move(rows, event.key === "Home" ? 0 : last);
119
+ return;
120
+ }
121
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
122
+ const forward = event.key === "ArrowRight";
123
+ const trigger = current.getAttribute("data-nav-trigger");
124
+ // From the store, not aria-expanded: a page row hands that to its Nav.Toggle.
125
+ const isOpen = trigger !== null && store.getSnapshot().expanded.has(trigger);
126
+ event.preventDefault();
127
+ // On a group: open it, then step into it. On a leaf, or a group already
128
+ // closed: step out to the parent — which is where the row came from.
129
+ if (trigger !== null && forward !== isOpen) {
130
+ store.getSnapshot().setOpen(trigger, forward);
131
+ return;
132
+ }
133
+ if (forward) {
134
+ if (trigger !== null)
135
+ move(rows, index + 1);
136
+ return;
137
+ }
138
+ const parent = current.parentElement?.closest("[data-nav-group]");
139
+ const parentTrigger = parent?.querySelector("[data-nav-trigger]");
140
+ if (!parentTrigger)
141
+ return;
142
+ setHighlighted(rowValue(parentTrigger));
143
+ parentTrigger.focus();
144
+ return;
145
+ }
146
+ // Typeahead. Single printable characters only, so shortcuts still work.
147
+ if (!typeahead || event.key.length !== 1 || event.metaKey || event.ctrlKey || event.altKey) {
148
+ return;
149
+ }
150
+ const now = Date.now();
151
+ search.current = {
152
+ query: (now - search.current.at > 500 ? "" : search.current.query) + event.key.toLowerCase(),
153
+ at: now,
154
+ };
155
+ // Search from the row after the current one, so repeating a letter walks
156
+ // through the matches rather than sticking to the first.
157
+ const ordered = [...rows.slice(index + 1), ...rows.slice(0, index + 1)];
158
+ const match = ordered.find((row) => (row.textContent ?? "").trim().toLowerCase().startsWith(search.current.query));
159
+ if (!match)
160
+ return;
161
+ event.preventDefault();
162
+ setHighlighted(rowValue(match));
163
+ match.focus();
164
+ };
165
+ /*
166
+ * A collapsed group unmounts its rows, so the highlighted one can simply
167
+ * cease to exist — and a tab stop pointing at nothing leaves the whole nav
168
+ * unreachable by keyboard. Re-home it on the first row instead. The same
169
+ * effect seeds the initial value, since before it runs the tab stop is on
170
+ * whichever row is active, and there may not be one.
171
+ */
172
+ useIsomorphicLayoutEffect(() => {
173
+ const root = store.rootRef.current;
174
+ if (!root)
175
+ return;
176
+ const rows = visibleRows(root);
177
+ if (highlighted !== null && rows.some((row) => rowValue(row) === highlighted))
178
+ return;
179
+ const first = rows[0];
180
+ setHighlighted(first ? rowValue(first) : null);
181
+ });
182
+ const roving = useMemo(() => ({ highlighted, setHighlighted, disabled, guide }), [highlighted, disabled, guide]);
183
+ const element = useRenderElement("div", { className, render, style }, {
184
+ state: { depth: 0, nested: false },
185
+ stateAttributesMapping: DEPTH_ATTRIBUTES,
186
+ ref: store.rootRef,
187
+ props: [{ "data-nav": "", onKeyDown: handleKeyDown }, elementProps],
188
+ });
189
+ return (_jsx(NavStoreContext, { value: store, children: _jsx(NavRovingContext, { value: roving, children: element }) }));
190
+ };
191
+ /**
192
+ * The collection of rows — at the top level, and again inside every group.
193
+ * One part for both is what makes the tree recursive: a group holds a list,
194
+ * and a list holds groups.
195
+ */
196
+ /**
197
+ * Measure the content's true height, with every pinned height lifted.
198
+ *
199
+ * Two reasons the lift cannot stop at this element.
200
+ *
201
+ * Reading `scrollHeight` through its own pinned height is a ratchet: with
202
+ * `overflow: hidden` it returns whichever of the content and the box is larger,
203
+ * so a value once too big measures itself as too big forever.
204
+ *
205
+ * And a *descendant* list may be pinned too — mid-transition, or holding a
206
+ * length it has not released yet. Measuring around one means the parent adopts
207
+ * a height that its content does not actually want, and jumps to the real one
208
+ * the moment the descendant releases. A recursive tree makes that the ordinary
209
+ * case rather than the edge case, which is why this goes further than the
210
+ * single-panel version it is modelled on.
211
+ */
212
+ const measure = (element) => {
213
+ const pinned = [element, ...element.querySelectorAll("[data-nav-list]")];
214
+ const restore = pinned.map((node) => {
215
+ const height = node.style.height;
216
+ const priority = node.style.getPropertyPriority("height");
217
+ node.style.setProperty("height", "auto", "important");
218
+ return () => {
219
+ if (height)
220
+ node.style.setProperty("height", height, priority);
221
+ else
222
+ node.style.removeProperty("height");
223
+ };
224
+ });
225
+ const size = { height: element.scrollHeight, width: element.scrollWidth };
226
+ for (const put of restore)
227
+ put();
228
+ return size;
229
+ };
230
+ /**
231
+ * A level of the tree. A list inside a group is that group's collapsible
232
+ * panel; a top-level list is not collapsible, having no trigger above it.
233
+ * Renders a `<div>` element.
234
+ */
235
+ export const NavList = ({ guide, keepMounted = false, className, render, style, ...elementProps }) => {
236
+ const depth = use(NavDepthContext);
237
+ const group = use(NavGroupContext);
238
+ const { guide: fallback } = useRoving();
239
+ const resolved = guide ?? fallback;
240
+ const listRef = useRef(null);
241
+ // Watched with its subtree, because the transition may be on the list or on
242
+ // the group wrapping it, and the primitive does not get to decide which.
243
+ const runWhenAnimationsFinish = useAnimationsFinished(group?.elementRef ?? listRef, false, true, true);
244
+ // A top-level list is not collapsible: there is no trigger above it.
245
+ const open = group ? group.open : true;
246
+ const { mounted, setMounted, transitionStatus } = useTransitionStatus(open);
247
+ /*
248
+ * The measured size, and only while a transition needs one.
249
+ *
250
+ * `height: auto` is not interpolable, so a collapse can only animate between
251
+ * lengths — but a list pinned to a length *permanently* cannot hold a group
252
+ * that expands inside it, and would clip the thing that just opened. So the
253
+ * pin is temporary: taken when a transition starts, released the moment the
254
+ * opening one finishes. An open, settled list is `auto` and grows freely.
255
+ *
256
+ * Base UI's Collapsible does exactly this, and it is the half of the
257
+ * behaviour that is invisible until you nest one inside another.
258
+ */
259
+ const [size, setSize] = useState(null);
260
+ /*
261
+ * The closing styles, one frame late.
262
+ *
263
+ * `useTransitionStatus` reports `ending` in the same render that closes the
264
+ * list, so pinning the height and setting `height: 0` would land together and
265
+ * the browser would have nothing to transition between. Holding the styles
266
+ * back for a frame lets the pinned length paint first.
267
+ */
268
+ const [closing, setClosing] = useState(false);
269
+ useIsomorphicLayoutEffect(() => {
270
+ const element = listRef.current;
271
+ if (!element || !mounted || transitionStatus === undefined)
272
+ return;
273
+ setSize(measure(element));
274
+ }, [transitionStatus, mounted]);
275
+ /*
276
+ * Keep the pin honest while it holds.
277
+ *
278
+ * A pinned length is a snapshot, and the content can change after it is
279
+ * taken — a nested list settling, an icon font arriving, a row wrapping to
280
+ * two lines. Nothing there involves a React render, so nothing re-measures,
281
+ * and a pin that ends up *short* strands the last row behind the clip for the
282
+ * whole transition. It then appears the instant the pin is released, which
283
+ * reads as the row popping in from the bottom rather than being revealed.
284
+ *
285
+ * Observing the children rather than the list: the list's own box is pinned,
286
+ * so it is precisely the thing that cannot report the change.
287
+ */
288
+ useIsomorphicLayoutEffect(() => {
289
+ const element = listRef.current;
290
+ if (!element || size === null || typeof ResizeObserver === "undefined")
291
+ return;
292
+ const observer = new ResizeObserver(() => {
293
+ const next = measure(element);
294
+ // Only when it actually moved, or the observer re-triggers on its own
295
+ // write and never settles.
296
+ setSize((current) => current && current.height === next.height && current.width === next.width ? current : next);
297
+ });
298
+ for (const child of element.children)
299
+ observer.observe(child);
300
+ return () => observer.disconnect();
301
+ }, [size]);
302
+ useIsomorphicLayoutEffect(() => {
303
+ if (transitionStatus !== "ending") {
304
+ setClosing(false);
305
+ return;
306
+ }
307
+ const frame = requestAnimationFrame(() => setClosing(true));
308
+ return () => cancelAnimationFrame(frame);
309
+ }, [transitionStatus]);
310
+ /*
311
+ * Released once the opening transition settles, so an ancestor is free to
312
+ * grow when a group inside it opens.
313
+ *
314
+ * Keyed off this element's own `height` transition ending rather than off
315
+ * "are any animations running", because at the moment this effect first runs
316
+ * the transition has been triggered in the same commit but not yet
317
+ * registered — so a poll can find nothing, release immediately, and turn the
318
+ * whole reveal into a snap to `auto`. `transitionend` cannot fire early by
319
+ * construction.
320
+ *
321
+ * The animation-frame path stays as the fallback for a consumer who animates
322
+ * with keyframes, or does not animate at all, where no `transitionend` is
323
+ * ever coming.
324
+ */
325
+ useIsomorphicLayoutEffect(() => {
326
+ const element = listRef.current;
327
+ if (!element || !open || !mounted || transitionStatus !== undefined || size === null)
328
+ return;
329
+ const controller = new AbortController();
330
+ const release = () => setSize(null);
331
+ const onTransitionEnd = (event) => {
332
+ if (event.target !== element || event.propertyName !== "height")
333
+ return;
334
+ release();
335
+ };
336
+ element.addEventListener("transitionend", onTransitionEnd, { signal: controller.signal });
337
+ // Nothing to wait for if no height transition was declared; the frame path
338
+ // resolves immediately in that case, which is the right answer.
339
+ if (getComputedStyle(element)
340
+ .transitionProperty.split(",")
341
+ .every((property) => {
342
+ const name = property.trim();
343
+ return name !== "height" && name !== "all";
344
+ })) {
345
+ runWhenAnimationsFinish(release, controller.signal);
346
+ }
347
+ return () => controller.abort();
348
+ }, [open, mounted, transitionStatus, size, runWhenAnimationsFinish]);
349
+ // Waits on `closing` rather than on `transitionStatus`, because at the moment the
350
+ // transitionStatus flips there is no animation running yet to wait for.
351
+ useIsomorphicLayoutEffect(() => {
352
+ if (!closing)
353
+ return;
354
+ const controller = new AbortController();
355
+ runWhenAnimationsFinish(() => setMounted(false), controller.signal);
356
+ return () => controller.abort();
357
+ }, [closing, runWhenAnimationsFinish, setMounted]);
358
+ const hidden = !open && !mounted;
359
+ // `until-found` is a string the DOM understands and React's boolean `hidden`
360
+ // cannot express, so a kept list gets the attribute imperatively. Without it
361
+ // the list stays navigable while collapsed and roving focus walks into rows
362
+ // the visitor just closed.
363
+ useIsomorphicLayoutEffect(() => {
364
+ const element = listRef.current;
365
+ if (!element || !keepMounted)
366
+ return;
367
+ if (hidden)
368
+ element.setAttribute("hidden", "until-found");
369
+ else
370
+ element.removeAttribute("hidden");
371
+ }, [keepMounted, hidden]);
372
+ // `ending` is withheld until the pinned height has had a frame to paint.
373
+ const published = transitionStatus === "ending" && !closing ? undefined : transitionStatus;
374
+ const element = useRenderElement("div", { className, render, style }, {
375
+ state: {
376
+ depth,
377
+ nested: group !== null,
378
+ guide: resolved,
379
+ open,
380
+ transitionStatus: published,
381
+ },
382
+ stateAttributesMapping: LIST_ATTRIBUTES,
383
+ ref: listRef,
384
+ props: [
385
+ {
386
+ "data-nav-list": "",
387
+ id: group?.listId,
388
+ // A kept list gets `hidden="until-found"` from the effect above; React
389
+ // can only express the boolean form, so it must not fight over it.
390
+ hidden: keepMounted ? undefined : hidden,
391
+ style: {
392
+ "--nav-depth": depth,
393
+ /*
394
+ * `initial` rather than absent, and that distinction is the whole
395
+ * bug it fixes.
396
+ *
397
+ * Custom properties inherit. Leaving this off a settled list lets it
398
+ * pick up an *ancestor's* pinned length — and since the rule that
399
+ * consumes it is `height: var(--nav-list-height)`, a nested list
400
+ * would take its grandparent's height instead of its own content's,
401
+ * shoving everything after it far down the page and snapping back the
402
+ * moment the ancestor releases.
403
+ *
404
+ * `initial` on an unregistered custom property is the
405
+ * guaranteed-invalid value, so `var()` fails, the declaration is
406
+ * invalid at computed-value time, and `height` lands on `auto` —
407
+ * which is what a settled list wants, without inheriting anything.
408
+ */
409
+ "--nav-list-height": size ? `${size.height}px` : "initial",
410
+ "--nav-list-width": size ? `${size.width}px` : "initial",
411
+ },
412
+ },
413
+ elementProps,
414
+ ],
415
+ });
416
+ if (!mounted && !keepMounted)
417
+ return null;
418
+ return _jsx(NavDepthContext, { value: depth + 1, children: element });
419
+ };
420
+ /**
421
+ * A collapsible branch. Recursive by construction — a group's list may hold
422
+ * further groups, to any depth, with no second set of parts for the nesting.
423
+ */
424
+ export const NavGroup = ({ value, disabled = false, className, render, style, ...elementProps }) => {
425
+ const store = useNavContextStore();
426
+ const depth = use(NavDepthContext);
427
+ const parent = use(NavGroupContext);
428
+ const { disabled: rootDisabled } = useRoving();
429
+ const listId = useId();
430
+ const elementRef = useRef(null);
431
+ const open = useNavStore(store, (nav) => nav.expanded.has(value));
432
+ const context = useMemo(() => ({ value, open, listId, disabled: disabled || rootDisabled, elementRef }), [value, open, listId, disabled, rootDisabled]);
433
+ const element = useRenderElement("div", { className, render, style }, {
434
+ state: { depth, nested: parent !== null, open },
435
+ stateAttributesMapping: { ...DEPTH_ATTRIBUTES, ...openStateMapping },
436
+ ref: elementRef,
437
+ props: [{ "data-nav-group": value }, elementProps],
438
+ });
439
+ return _jsx(NavGroupContext, { value: context, children: element });
440
+ };
441
+ // ---------------------------------------------------------------------------
442
+ // Rows — Item and Trigger
443
+ // ---------------------------------------------------------------------------
444
+ /** Elements the browser already activates on Enter or Space. */
445
+ /** A button handles Enter and Space itself; a link handles only Enter. */
446
+ const activatesItself = (element, key) => element.tagName === "BUTTON" ||
447
+ (key === "Enter" && element.tagName === "A" && element.hasAttribute("href"));
448
+ /**
449
+ * What `Item` and `Trigger` have in common: one tab stop for the whole nav,
450
+ * focus that follows the arrows, and hand-wired Enter/Space — the price of not
451
+ * being a real `<button>`, which is the price of holding an action inside.
452
+ */
453
+ const useRow = (value, disabled, active, activate) => {
454
+ const { highlighted, setHighlighted } = useRoving();
455
+ return {
456
+ role: "button",
457
+ // Exactly one row is tabbable. Before the roving state is seeded — a server
458
+ // render, or the frame before hydration — the active row carries it, which
459
+ // is both the conventional place for it and the one that keeps a row
460
+ // rendered as a real anchor reachable without JavaScript.
461
+ tabIndex: disabled ? -1 : (highlighted === null ? active : highlighted === value) ? 0 : -1,
462
+ "aria-disabled": disabled || undefined,
463
+ // `active` is the route being shown; your own `aria-current` still overrides it.
464
+ "aria-current": active ? "page" : undefined,
465
+ onFocus: () => setHighlighted(value),
466
+ onClick: disabled ? (event) => event.preventDefault() : undefined,
467
+ onKeyDown: (event) => {
468
+ if (event.key !== "Enter" && event.key !== " ")
469
+ return;
470
+ if (event.target !== event.currentTarget)
471
+ return;
472
+ if (activatesItself(event.currentTarget, event.key))
473
+ return;
474
+ // Space scrolls the page otherwise, and the row is the thing being
475
+ // pressed — not the document behind it.
476
+ event.preventDefault();
477
+ if (!disabled)
478
+ activate();
479
+ },
480
+ };
481
+ };
482
+ /**
483
+ * A leaf row. A `div[role="button"]` rather than an anchor, so an action can
484
+ * sit inside it — swap the element with `render={<Link href="…" />}` when you
485
+ * want a real navigation, or handle `onClick` and route yourself.
486
+ */
487
+ export const NavItem = ({ value, active = false, disabled, className, render, style, ...elementProps }) => {
488
+ const depth = use(NavDepthContext);
489
+ const group = use(NavGroupContext);
490
+ const { disabled: rootDisabled } = useRoving();
491
+ const isDisabled = disabled ?? rootDisabled;
492
+ const ref = useRef(null);
493
+ const row = useRow(value, isDisabled, active, () => ref.current?.click());
494
+ return useRenderElement("div", { className, render, style }, {
495
+ state: { depth, nested: group !== null, active, disabled: isDisabled },
496
+ stateAttributesMapping: DEPTH_ATTRIBUTES,
497
+ ref,
498
+ props: [{ ...row, "data-nav-item": value }, elementProps],
499
+ });
500
+ };
501
+ /** The trigger a `Nav.Toggle` sits in: its disabled state, and how the caret announces itself. */
502
+ const NavTriggerContext = createContext(null);
503
+ /**
504
+ * The row that opens a group — its heading and its disclosure in one, because
505
+ * in a sidebar they are the same thing you click. Takes no `value`: it belongs
506
+ * to the group it is written inside.
507
+ *
508
+ * Put a `Nav.Toggle` inside it and the two come apart: pressing the row
509
+ * activates it like `Nav.Item`, and the caret opens the group and carries
510
+ * `aria-expanded`. The arrow keys open and close it from the row either way.
511
+ */
512
+ export const NavTrigger = ({ active = false, disabled, className, render, style, ...elementProps }) => {
513
+ const store = useNavContextStore();
514
+ const depth = use(NavDepthContext);
515
+ const group = use(NavGroupContext);
516
+ if (!group)
517
+ throw new Error("<Nav.Trigger> must be used within <Nav.Group>");
518
+ const ref = useRef(null);
519
+ const isDisabled = disabled ?? group.disabled;
520
+ const toggle = () => store.getSnapshot().toggle(group.value);
521
+ // A row holding a Nav.Toggle is a page: a press activates it, and the caret is the disclosure.
522
+ const [hasToggle, setHasToggle] = useState(false);
523
+ // Enter and Space activate a page row as a click would, reaching your onClick or the link.
524
+ const row = useRow(group.value, isDisabled, active, () => hasToggle ? ref.current?.click() : toggle());
525
+ const context = useMemo(() => ({ disabled: isDisabled, setHasToggle }), [isDisabled]);
526
+ const element = useRenderElement("div", { className, render, style }, {
527
+ state: {
528
+ depth,
529
+ nested: depth > 0,
530
+ open: group.open,
531
+ active,
532
+ disabled: isDisabled,
533
+ },
534
+ stateAttributesMapping: { ...DEPTH_ATTRIBUTES, ...openStateMapping },
535
+ ref,
536
+ props: [
537
+ {
538
+ ...row,
539
+ "data-nav-trigger": group.value,
540
+ // On a page row the caret announces the open state, not the row.
541
+ "aria-expanded": hasToggle ? undefined : group.open,
542
+ "aria-controls": hasToggle ? undefined : group.listId,
543
+ onClick: (event) => {
544
+ if (isDisabled)
545
+ event.preventDefault();
546
+ else if (!hasToggle)
547
+ toggle();
548
+ },
549
+ },
550
+ elementProps,
551
+ ],
552
+ });
553
+ return _jsx(NavTriggerContext, { value: context, children: element });
554
+ };
555
+ /**
556
+ * The caret inside a row — the control that opens a branch whose row is also a
557
+ * page. Mounting it makes pressing the row activate the page. Out of the roving
558
+ * order, like `Nav.Action`, since the arrow keys already open the group from
559
+ * the row, and it stops every event it handles: anything that escaped would
560
+ * activate the row, or follow its link, on its way out of opening the group.
561
+ */
562
+ export const NavToggle = ({ className, render, style, ...elementProps }) => {
563
+ const store = useNavContextStore();
564
+ const depth = use(NavDepthContext);
565
+ const group = use(NavGroupContext);
566
+ const trigger = use(NavTriggerContext);
567
+ if (!group || !trigger)
568
+ throw new Error("<Nav.Toggle> must be used within <Nav.Trigger>");
569
+ // Mounting it is what makes its row a page.
570
+ const { setHasToggle } = trigger;
571
+ const ref = useCallback((element) => {
572
+ if (!element)
573
+ return;
574
+ setHasToggle(true);
575
+ return () => setHasToggle(false);
576
+ }, [setHasToggle]);
577
+ const stop = (event) => {
578
+ event.stopPropagation();
579
+ };
580
+ const toggle = (event) => {
581
+ stop(event);
582
+ event.preventDefault();
583
+ if (!trigger.disabled)
584
+ store.getSnapshot().toggle(group.value);
585
+ };
586
+ return useRenderElement("div", { className, render, style }, {
587
+ state: { depth, nested: depth > 0, open: group.open, disabled: trigger.disabled },
588
+ stateAttributesMapping: { ...DEPTH_ATTRIBUTES, ...openStateMapping },
589
+ ref,
590
+ props: [
591
+ {
592
+ "data-nav-toggle": "",
593
+ role: "button",
594
+ tabIndex: -1,
595
+ "aria-expanded": group.open,
596
+ "aria-controls": group.listId,
597
+ "aria-disabled": trigger.disabled || undefined,
598
+ onClick: toggle,
599
+ onPointerDown: stop,
600
+ onDoubleClick: stop,
601
+ onKeyDown: (event) => {
602
+ if (event.key === "Enter" || event.key === " ")
603
+ toggle(event);
604
+ },
605
+ },
606
+ elementProps,
607
+ ],
608
+ });
609
+ };
610
+ /** The row's text. Its own element so it can truncate while the row does not. */
611
+ export const NavLabel = ({ className, render, style, ...elementProps }) => {
612
+ const depth = use(NavDepthContext);
613
+ const group = use(NavGroupContext);
614
+ return useRenderElement("span", { className, render, style }, {
615
+ state: { depth, nested: group !== null },
616
+ stateAttributesMapping: DEPTH_ATTRIBUTES,
617
+ props: [{ "data-nav-label": "" }, elementProps],
618
+ });
619
+ };
620
+ /** Decoration. `aria-hidden`, so it stays out of the row's accessible name. */
621
+ export const NavIcon = ({ className, render, style, ...elementProps }) => {
622
+ const depth = use(NavDepthContext);
623
+ const group = use(NavGroupContext);
624
+ return useRenderElement("span", { className, render, style }, {
625
+ state: { depth, nested: group !== null },
626
+ stateAttributesMapping: DEPTH_ATTRIBUTES,
627
+ props: [{ "data-nav-icon": "", "aria-hidden": true }, elementProps],
628
+ });
629
+ };
630
+ /**
631
+ * A control inside a row — the "…" that opens a menu. Out of the roving order,
632
+ * so arrowing walks rows rather than stopping at every affordance, and it stops
633
+ * every event it handles: anything that escaped would activate the row on its
634
+ * way out of opening the menu.
635
+ */
636
+ export const NavAction = ({ className, render, style, ...elementProps }) => {
637
+ const depth = use(NavDepthContext);
638
+ const group = use(NavGroupContext);
639
+ const stop = (event) => {
640
+ event.stopPropagation();
641
+ };
642
+ return useRenderElement("div", { className, render, style }, {
643
+ state: { depth, nested: group !== null },
644
+ stateAttributesMapping: DEPTH_ATTRIBUTES,
645
+ props: [
646
+ {
647
+ "data-nav-action": "",
648
+ role: "button",
649
+ tabIndex: -1,
650
+ onClick: stop,
651
+ onPointerDown: stop,
652
+ onKeyDown: (event) => {
653
+ if (event.key === "Enter" || event.key === " ")
654
+ event.stopPropagation();
655
+ },
656
+ },
657
+ elementProps,
658
+ ],
659
+ });
660
+ };