@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,390 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, use, useCallback, useEffect, useId, useMemo, useRef, useState, } from "react";
4
+ import { useIsomorphicLayoutEffect } from "../internal/iso-layout-effect.js";
5
+ import { useRenderElement } from "../internal/render/useRenderElement.js";
6
+ import { createShellStore, ShellStoreContext, useShellContextStore, useShellStore } from "./store.js";
7
+ /**
8
+ * The shell: a sidebar that collapses, floats out on hover, and can be dragged
9
+ * wider, next to the viewport it shares the screen with.
10
+ *
11
+ * The package ships no CSS. Parts publish their state as `data-*` attributes
12
+ * and the sidebar's measured width as a custom property; sizing, collapsing
13
+ * geometry and transitions are all yours. In particular **the sidebar's width
14
+ * and the range it may be dragged through are CSS**:
15
+ *
16
+ * [data-shell-sidebar] {
17
+ * width: var(--shell-sidebar-width, 240px);
18
+ * min-width: 200px;
19
+ * max-width: 400px;
20
+ * }
21
+ *
22
+ * The drag writes that custom property, the browser clamps it against your
23
+ * `min-width` / `max-width`, and the sidebar reports back whatever the browser
24
+ * settled on. Nothing here second-guesses your stylesheet.
25
+ *
26
+ * No global key binding either. `Shell.Trigger` is the click affordance; a
27
+ * Cmd/Ctrl+B — or whatever your app's shortcut registry says — is four lines
28
+ * around `useShell((shell) => shell.toggle)`, and a package has no business
29
+ * claiming a window-level `keydown` for a key it cannot know is free.
30
+ */
31
+ /** Written by the grip; read by your `width`. */
32
+ export const SHELL_SIDEBAR_WIDTH_VAR = "--shell-sidebar-width";
33
+ // Both delays are intent filters: the open delay stops a pointer merely
34
+ // crossing the strip from floating the panel out, and the close grace lets you
35
+ // clip a corner on the way from the strip to the panel without losing it.
36
+ const PEEK_OPEN_DELAY_MS = 200;
37
+ const PEEK_CLOSE_DELAY_MS = 250;
38
+ const selectOpen = (shell) => shell.open;
39
+ const selectHotspot = (shell) => shell.hotspot;
40
+ const selectResizing = (shell) => shell.resizing;
41
+ const selectWidth = (shell) => shell.width;
42
+ // Three primitive subscriptions rather than one selector returning an object:
43
+ // useSyncExternalStore compares snapshots by identity, so a selector that built
44
+ // a fresh object every call would never settle.
45
+ const usePartState = (store) => ({
46
+ state: useShellStore(store, selectOpen) ? "expanded" : "collapsed",
47
+ hotspot: useShellStore(store, selectHotspot),
48
+ resizing: useShellStore(store, selectResizing),
49
+ });
50
+ const ShellHotspotContext = createContext(null);
51
+ const useHotspotControls = (store) => {
52
+ const openTimer = useRef(undefined);
53
+ const closeTimer = useRef(undefined);
54
+ const controls = useMemo(() => ({
55
+ request: () => {
56
+ // Armed by the collapse that just happened under this pointer.
57
+ if (store.hotspotSuppressionRef.current)
58
+ return;
59
+ window.clearTimeout(closeTimer.current);
60
+ if (store.getSnapshot().hotspot)
61
+ return;
62
+ openTimer.current = window.setTimeout(() => {
63
+ // Re-checked on firing, not just on arming. A timer started while the
64
+ // sidebar was still expanded outlives a collapse that happens during
65
+ // the delay, and would then float the panel straight back out from
66
+ // under the pointer that just closed it.
67
+ if (store.hotspotSuppressionRef.current)
68
+ return;
69
+ store.getSnapshot().setHotspot(true);
70
+ }, PEEK_OPEN_DELAY_MS);
71
+ },
72
+ hold: () => window.clearTimeout(closeTimer.current),
73
+ release: () => {
74
+ // The pointer has moved on, so the bounce-back guard has done its job.
75
+ store.hotspotSuppressionRef.current = false;
76
+ // Darting through the strip never opens it.
77
+ window.clearTimeout(openTimer.current);
78
+ if (!store.getSnapshot().hotspot)
79
+ return;
80
+ closeTimer.current = window.setTimeout(() => store.getSnapshot().setHotspot(false), PEEK_CLOSE_DELAY_MS);
81
+ },
82
+ }), [store]);
83
+ // Cmd-Tab away mid-hotspot fires no pointerleave: without this the panel stays
84
+ // floated and a pending open timer fires into a backgrounded window.
85
+ useEffect(() => {
86
+ const dismiss = () => {
87
+ window.clearTimeout(openTimer.current);
88
+ window.clearTimeout(closeTimer.current);
89
+ store.getSnapshot().setHotspot(false);
90
+ };
91
+ window.addEventListener("blur", dismiss);
92
+ return () => {
93
+ window.removeEventListener("blur", dismiss);
94
+ window.clearTimeout(openTimer.current);
95
+ window.clearTimeout(closeTimer.current);
96
+ };
97
+ }, [store]);
98
+ return controls;
99
+ };
100
+ const useHotspotContext = () => {
101
+ const controls = use(ShellHotspotContext);
102
+ if (!controls)
103
+ throw new Error("Shell parts must be used within <Shell.Root>");
104
+ return controls;
105
+ };
106
+ /**
107
+ * The provider and container. Holds the store every other part reads, so
108
+ * `defaultOpen` and the width must arrive here rather than on a child.
109
+ * Renders a `<div>` element.
110
+ */
111
+ export const ShellRoot = ({ defaultOpen, open, onOpenChange, store: storeProp, className, render, style, ...elementProps }) => {
112
+ const sidebarId = useId();
113
+ const [store] = useState(() => {
114
+ const created = storeProp ?? createShellStore();
115
+ // Before anything has subscribed, so no notify and no write-back. The
116
+ // controlled value wins over the default: the layout effect below would
117
+ // otherwise correct it only after the server had already shipped the wrong
118
+ // markup, which is the pre-hydration flash `defaultOpen` exists to prevent.
119
+ created.hydrate({ open: open ?? defaultOpen });
120
+ created.sidebarId = sidebarId;
121
+ return created;
122
+ });
123
+ // Latest-ref bridges, written during render: idempotent, and they have to be
124
+ // current before any event handler can read them.
125
+ store.controlledRef.current = open !== undefined;
126
+ store.onOpenChangeRef.current = onOpenChange ?? null;
127
+ // Before paint, so a controlled change never shows the old state first.
128
+ useIsomorphicLayoutEffect(() => {
129
+ if (open !== undefined)
130
+ store.commitOpen(open);
131
+ }, [open, store]);
132
+ const hotspot = useHotspotControls(store);
133
+ const state = usePartState(store);
134
+ /*
135
+ * The bounce-back guard is armed by every collapse, but it is only *meant* to
136
+ * survive one where the pointer was sitting where the strip is about to
137
+ * appear. Any movement outside the strip proves it wasn't, so it is cleared
138
+ * here — without this, collapsing by keyboard or by a trigger anywhere else
139
+ * arms a guard that then swallows the next genuine hover.
140
+ *
141
+ * Ref write only, no re-render, and it returns immediately when nothing is
142
+ * armed — which is almost always.
143
+ */
144
+ const handlePointerMove = (event) => {
145
+ if (!store.hotspotSuppressionRef.current)
146
+ return;
147
+ const target = event.target;
148
+ if (target?.closest("[data-shell-hotspot]"))
149
+ return;
150
+ store.hotspotSuppressionRef.current = false;
151
+ };
152
+ const element = useRenderElement("div", { className, render, style }, {
153
+ state,
154
+ ref: store.rootRef,
155
+ props: [{ "data-shell": "", onPointerMove: handlePointerMove }, elementProps],
156
+ });
157
+ return (_jsx(ShellStoreContext, { value: store, children: _jsx(ShellHotspotContext, { value: hotspot, children: element }) }));
158
+ };
159
+ /**
160
+ * The panel, and the element whose width is measured and reported back.
161
+ * Carries the `id` the trigger's `aria-controls` points at.
162
+ * Renders a `<div>` element.
163
+ */
164
+ export const ShellSidebar = ({ side = "left", onResize, className, render, style, ...elementProps }) => {
165
+ const store = useShellContextStore();
166
+ // Latest-ref bridge, written during render: idempotent, and current before
167
+ // the measurement below can report anything.
168
+ store.onResizeRef.current = onResize ?? null;
169
+ const hotspot = useHotspotContext();
170
+ const partState = usePartState(store);
171
+ // Report the width the browser settled on, so persistence and the resize
172
+ // handle's aria-valuenow describe what is actually on screen.
173
+ useIsomorphicLayoutEffect(() => {
174
+ const element = store.sidebarRef.current;
175
+ if (!element)
176
+ return;
177
+ const measure = () => {
178
+ const measured = element.offsetWidth;
179
+ // Zero means the element is not laid out — display:none, a breakpoint
180
+ // that hides it, a test environment without layout. That carries no
181
+ // information, and persisting it would throw away the width the user
182
+ // actually chose.
183
+ if (measured > 0)
184
+ store.getSnapshot().setWidth(measured);
185
+ };
186
+ measure();
187
+ if (typeof ResizeObserver === "undefined")
188
+ return;
189
+ const observer = new ResizeObserver(measure);
190
+ observer.observe(element);
191
+ return () => observer.disconnect();
192
+ }, [store]);
193
+ return useRenderElement("div", { className, render, style }, {
194
+ state: { ...partState, side },
195
+ ref: store.sidebarRef,
196
+ props: [
197
+ {
198
+ "data-shell-sidebar": "",
199
+ id: store.sidebarId,
200
+ onPointerEnter: hotspot.hold,
201
+ onPointerLeave: hotspot.release,
202
+ },
203
+ elementProps,
204
+ ],
205
+ });
206
+ };
207
+ /**
208
+ * The content area beside the sidebar. Carries the same state attributes as
209
+ * the root, so it can react to the sidebar without a group selector.
210
+ * Renders a `<div>` element.
211
+ */
212
+ export const ShellViewport = ({ className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, {
213
+ state: usePartState(useShellContextStore()),
214
+ props: [{ "data-shell-viewport": "" }, elementProps],
215
+ });
216
+ /**
217
+ * The strip along the screen edge that floats a collapsed sidebar out on
218
+ * hover. Give it a width and a position in CSS. Omitting it is how you opt out
219
+ * of hotspot entirely — there is no prop to turn it off, because not rendering it
220
+ * already says that.
221
+ */
222
+ export const ShellHotspot = ({ className, render, style, ...elementProps }) => {
223
+ const hotspot = useHotspotContext();
224
+ return useRenderElement("div", { className, render, style }, {
225
+ state: usePartState(useShellContextStore()),
226
+ props: [
227
+ {
228
+ "data-shell-hotspot": "",
229
+ "aria-hidden": true,
230
+ onPointerEnter: hotspot.request,
231
+ onPointerLeave: hotspot.release,
232
+ },
233
+ elementProps,
234
+ ],
235
+ });
236
+ };
237
+ /**
238
+ * Toggles the sidebar, and pins a floated-out one open rather than closing it.
239
+ * Ships no copy — supply the label as children.
240
+ * Renders a `<button>` element.
241
+ */
242
+ export const ShellTrigger = ({ className, render, style, ...elementProps }) => {
243
+ const store = useShellContextStore();
244
+ const state = usePartState(store);
245
+ const handleClick = useCallback(() => {
246
+ const shell = store.getSnapshot();
247
+ // Clicking the trigger inside a floated sidebar pins it open.
248
+ shell.setOpen(shell.hotspot ? true : !shell.open);
249
+ }, [store]);
250
+ return useRenderElement("button", { className, render, style }, {
251
+ state,
252
+ props: [
253
+ {
254
+ type: "button",
255
+ "data-shell-trigger": "",
256
+ "aria-expanded": state.state === "expanded",
257
+ "aria-controls": store.sidebarId,
258
+ onClick: handleClick,
259
+ },
260
+ elementProps,
261
+ ],
262
+ });
263
+ };
264
+ /** The drag range, straight from the stylesheet. `max-width: none` means unbounded. */
265
+ const readBounds = (element) => {
266
+ const computed = getComputedStyle(element);
267
+ const min = Number.parseFloat(computed.minWidth);
268
+ const max = Number.parseFloat(computed.maxWidth);
269
+ return {
270
+ min: Number.isFinite(min) ? min : 0,
271
+ max: Number.isFinite(max) ? max : Number.POSITIVE_INFINITY,
272
+ };
273
+ };
274
+ const clamp = (value, { min, max }) => Math.min(Math.max(value, min), max);
275
+ /**
276
+ * Publish the width on the root, not the sidebar, so anything else in the
277
+ * shell can size itself to match. Clamped here rather than left to the
278
+ * browser: `min-width` would constrain only the sidebar, and a spacer reading
279
+ * the raw value would then disagree with it past either end of the range.
280
+ */
281
+ const publishWidth = (store, width, bounds) => {
282
+ store.rootRef.current?.style.setProperty(SHELL_SIDEBAR_WIDTH_VAR, `${clamp(width, bounds)}px`);
283
+ };
284
+ /** Dragging right widens a left sidebar and narrows a right one. */
285
+ const widenDirection = (sidebar) => (sidebar.dataset.side === "right" ? -1 : 1);
286
+ /**
287
+ * The drag affordance. Give it a width and a cursor in CSS; the drag range
288
+ * comes from the sidebar's own `min-width` and `max-width`.
289
+ * Renders a `<div>` element with `role="separator"`.
290
+ */
291
+ export const ShellGrip = ({ step = 16, className, render, style, ...elementProps }) => {
292
+ const store = useShellContextStore();
293
+ const state = usePartState(store);
294
+ const width = useShellStore(store, selectWidth);
295
+ const [bounds, setBounds] = useState({ min: 0, max: Number.POSITIVE_INFINITY });
296
+ const drag = useRef(null);
297
+ // Re-read after every commit rather than on a dependency, so a stylesheet
298
+ // that swaps the range at a breakpoint reaches aria-valuemin/max too. Cheap:
299
+ // the identity guard stops it looping, and it is skipped mid-drag, where the
300
+ // range cannot change and the drag holds its own copy anyway.
301
+ useIsomorphicLayoutEffect(() => {
302
+ const sidebar = store.sidebarRef.current;
303
+ if (!sidebar || state.resizing)
304
+ return;
305
+ const next = readBounds(sidebar);
306
+ setBounds((current) => (current.min === next.min && current.max === next.max ? current : next));
307
+ });
308
+ const endDrag = useCallback(() => {
309
+ if (!drag.current)
310
+ return;
311
+ drag.current = null;
312
+ store.getSnapshot().setResizing(false);
313
+ }, [store]);
314
+ // A drag can end without a pointerup: cmd-tab away, or the tab going to the
315
+ // background. Left unhandled the flag sticks and everything downstream of
316
+ // `data-resizing` stays wrong until a reload.
317
+ useEffect(() => {
318
+ if (!state.resizing)
319
+ return;
320
+ window.addEventListener("blur", endDrag);
321
+ document.addEventListener("visibilitychange", endDrag);
322
+ return () => {
323
+ window.removeEventListener("blur", endDrag);
324
+ document.removeEventListener("visibilitychange", endDrag);
325
+ };
326
+ }, [state.resizing, endDrag]);
327
+ const handlePointerDown = (event) => {
328
+ const sidebar = store.sidebarRef.current;
329
+ if (!sidebar || event.button !== 0)
330
+ return;
331
+ event.preventDefault();
332
+ // Not every environment implements pointer capture; without it the drag
333
+ // still works, it just stops tracking once the pointer leaves the handle.
334
+ if ("setPointerCapture" in event.currentTarget) {
335
+ event.currentTarget.setPointerCapture(event.pointerId);
336
+ }
337
+ drag.current = {
338
+ pointerId: event.pointerId,
339
+ startX: event.clientX,
340
+ startWidth: sidebar.offsetWidth,
341
+ // Read once, at the start: the range cannot change mid-drag, and
342
+ // re-reading computed style on every pointermove would be a needless
343
+ // style recalculation per frame.
344
+ bounds: readBounds(sidebar),
345
+ };
346
+ store.getSnapshot().setResizing(true);
347
+ };
348
+ const handlePointerMove = (event) => {
349
+ const sidebar = store.sidebarRef.current;
350
+ if (!drag.current || !sidebar || event.pointerId !== drag.current.pointerId)
351
+ return;
352
+ const travelled = (event.clientX - drag.current.startX) * widenDirection(sidebar);
353
+ publishWidth(store, drag.current.startWidth + travelled, drag.current.bounds);
354
+ };
355
+ const handleKeyDown = (event) => {
356
+ const sidebar = store.sidebarRef.current;
357
+ if (!sidebar)
358
+ return;
359
+ const towards = event.key === "ArrowLeft" ? -1 : event.key === "ArrowRight" ? 1 : 0;
360
+ if (towards === 0)
361
+ return;
362
+ event.preventDefault();
363
+ const next = sidebar.offsetWidth + towards * widenDirection(sidebar) * step;
364
+ publishWidth(store, next, readBounds(sidebar));
365
+ };
366
+ return useRenderElement("div", { className, render, style }, {
367
+ state,
368
+ props: [
369
+ {
370
+ "data-shell-grip": "",
371
+ role: "separator",
372
+ "aria-orientation": "vertical",
373
+ // A focusable separator must always carry a value; omitting it until
374
+ // the first measurement lands would leave the widget invalid for that
375
+ // window. The minimum is the least misleading stand-in, and it is
376
+ // corrected on the next frame.
377
+ "aria-valuenow": width ?? bounds.min,
378
+ "aria-valuemin": bounds.min,
379
+ "aria-valuemax": Number.isFinite(bounds.max) ? bounds.max : undefined,
380
+ tabIndex: 0,
381
+ onPointerDown: handlePointerDown,
382
+ onPointerMove: handlePointerMove,
383
+ onPointerUp: endDrag,
384
+ onPointerCancel: endDrag,
385
+ onKeyDown: handleKeyDown,
386
+ },
387
+ elementProps,
388
+ ],
389
+ });
390
+ };
@@ -0,0 +1,112 @@
1
+ import type { RefObject } from "react";
2
+ /**
3
+ * Shell store — all reactive shell state in one closure, so `useShell` can
4
+ * offer selectors and a component re-renders only for the value it reads.
5
+ * Not React state and not Zustand: a plain object with `subscribe` and
6
+ * `getSnapshot`, read through `useSyncExternalStore`.
7
+ *
8
+ * The factory takes no arguments. Everything configurable is either a
9
+ * `Shell.Root` prop (`defaultOpen`) seeded through `hydrate()` before the first
10
+ * paint, or — for anything dimensional — CSS.
11
+ *
12
+ * Nothing is persisted here. `open` goes out through `onOpenChange` and comes
13
+ * back as `defaultOpen`; the measured width goes out through `Shell.Sidebar`'s
14
+ * `onResize` and comes back as **CSS** — the custom property, which is
15
+ * where the width lives anyway. Where any of it is kept is the app's business,
16
+ * and the package needs no opinion about serialisation, keys or cookie flags.
17
+ *
18
+ * **Width is not owned here.** The sidebar's size, and the range it may be
19
+ * dragged through, are `width` / `min-width` / `max-width` in the consumer's
20
+ * stylesheet. The browser clamps; this store only records what the element
21
+ * actually measured, because persistence and `aria-valuenow` need a number.
22
+ * Measure in JS, size in CSS.
23
+ *
24
+ * Instance model: every `Shell.Root` owns a store — an explicit
25
+ * `Shell.createStore()` handle, or one created per mount — and provides it on
26
+ * `ShellStoreContext`. Inside the tree `useShell` resolves it implicitly;
27
+ * outside, `useShellStore(handle, selector)` takes the handle explicitly.
28
+ * There is deliberately **no global fallback**, so shell state is never read
29
+ * or driven by accident from somewhere that just happened to import this.
30
+ *
31
+ * SSR-safe by invariant: every write happens in an effect, an event handler,
32
+ * or `hydrate()` before anything has subscribed — so a server render only ever
33
+ * sees the pristine snapshot and one getter serves both sides of hydration.
34
+ */
35
+ export type ShellState = {
36
+ open: boolean;
37
+ /** The collapsed sidebar floating over the content on hover. Never persisted. */
38
+ hotspot: boolean;
39
+ /** True for the duration of a resize drag, so parts can suppress transitions. */
40
+ resizing: boolean;
41
+ /** Measured from the sidebar element. Null until it has mounted. */
42
+ width: number | null;
43
+ setOpen: (open: boolean) => void;
44
+ toggle: () => void;
45
+ setHotspot: (hotspot: boolean) => void;
46
+ setResizing: (resizing: boolean) => void;
47
+ /** Reports a measurement. Not a request to resize — CSS decides the size. */
48
+ setWidth: (width: number) => void;
49
+ };
50
+ export type ShellStore = {
51
+ subscribe: (listener: () => void) => () => void;
52
+ getSnapshot: () => ShellState;
53
+ /**
54
+ * Seed state before the first render commits: no notify and no report back,
55
+ * because nothing has subscribed yet and re-announcing what we just restored
56
+ * is pointless. This is how `defaultOpen` reaches the store without a flash.
57
+ */
58
+ hydrate: (state: {
59
+ open?: boolean;
60
+ }) => void;
61
+ /** Set while `open` is a controlled prop, which makes `setOpen` report-only. */
62
+ controlledRef: RefObject<boolean>;
63
+ onOpenChangeRef: RefObject<((open: boolean) => void) | null>;
64
+ /** Reports a settled size — never the per-frame values a drag produces. */
65
+ onResizeRef: RefObject<((width: number) => void) | null>;
66
+ /** Commits `open` past the controlled guard — how the Root syncs its prop in. */
67
+ commitOpen: (open: boolean) => void;
68
+ /**
69
+ * The mounted `Shell.Root`'s element. The width custom property is written
70
+ * here rather than on the sidebar, so siblings — an in-flow spacer holding
71
+ * the sidebar's place while it floats, say — can read the same value.
72
+ */
73
+ rootRef: RefObject<HTMLElement | null>;
74
+ /**
75
+ * The mounted `Shell.Sidebar`'s element, registered by its ref. The resize
76
+ * handle sizes and measures it; the trigger points `aria-controls` at its id.
77
+ */
78
+ sidebarRef: RefObject<HTMLElement | null>;
79
+ /**
80
+ * Stable per-instance id for the sidebar, assigned by the mounting
81
+ * `Shell.Root` from React's `useId` (SSR-stable). The factory can't mint it,
82
+ * because explicit handles are created outside React.
83
+ */
84
+ sidebarId: string;
85
+ /**
86
+ * Set while collapsing, so a panel sliding out from under a parked pointer
87
+ * doesn't bounce straight back as a hotspot. Cleared once the pointer leaves
88
+ * the edge strip. Imperative, not reactive.
89
+ */
90
+ hotspotSuppressionRef: RefObject<boolean>;
91
+ };
92
+ export declare const createShellStore: () => ShellStore;
93
+ export declare const ShellStoreContext: import("react").Context<ShellStore | null>;
94
+ /** Internal: parts resolve the store their Shell.Root provided. */
95
+ export declare const useShellContextStore: () => ShellStore;
96
+ /**
97
+ * Subscribe to an explicit `Shell.createStore()` handle — the
98
+ * outside-the-tree twin of `useShell`, for command palettes, global shortcut
99
+ * handlers and anything else that isn't a descendant. Wrap it once per
100
+ * instance to drop the argument at call sites:
101
+ *
102
+ * const useAppShell = <T,>(selector: (shell: ShellState) => T) =>
103
+ * useShellStore(appShellStore, selector);
104
+ */
105
+ export declare const useShellStore: <Selected = ShellState>(store: ShellStore, selector?: (shell: ShellState) => Selected) => Selected;
106
+ /**
107
+ * Subscribe to shell state from inside the tree. With a selector the component
108
+ * re-renders only when that value changes; without one it re-renders on any
109
+ * change. The actions are part of the snapshot and their identities never
110
+ * change, so selecting one never causes a render.
111
+ */
112
+ export declare const useShell: <Selected = ShellState>(selector?: (shell: ShellState) => Selected) => Selected;
@@ -0,0 +1,134 @@
1
+ "use client";
2
+ import { createContext, use, useSyncExternalStore } from "react";
3
+ export const createShellStore = () => {
4
+ const listeners = new Set();
5
+ const notify = () => {
6
+ for (const listener of listeners)
7
+ listener();
8
+ };
9
+ const controlledRef = { current: false };
10
+ const onOpenChangeRef = { current: null };
11
+ const onResizeRef = { current: null };
12
+ const hotspotSuppressionRef = { current: false };
13
+ const rootRef = { current: null };
14
+ const sidebarRef = { current: null };
15
+ let snapshot;
16
+ // A drag emits a measurement per frame, and reporting each one would hand the
17
+ // app a hundred sizes the user is still in the middle of choosing.
18
+ // `setResizing(false)` reports whatever it landed on.
19
+ const reportResize = () => {
20
+ if (snapshot.resizing || snapshot.width === null)
21
+ return;
22
+ onResizeRef.current?.(snapshot.width);
23
+ };
24
+ const commitOpen = (open) => {
25
+ if (snapshot.open === open)
26
+ return;
27
+ // Collapsing arms the bounce-back guard: a panel sliding out from under a
28
+ // parked pointer must not immediately float back as a hotspot.
29
+ if (!open)
30
+ hotspotSuppressionRef.current = true;
31
+ // Hotspot only means anything while collapsed, so opening ends it.
32
+ snapshot = { ...snapshot, open, hotspot: open ? false : snapshot.hotspot };
33
+ notify();
34
+ };
35
+ const setOpen = (open) => {
36
+ // Reported before the commit, and the only effect while controlled: whoever
37
+ // owns the prop decides, then pushes the result back through commitOpen.
38
+ onOpenChangeRef.current?.(open);
39
+ if (!controlledRef.current)
40
+ commitOpen(open);
41
+ };
42
+ const toggle = () => setOpen(!snapshot.open);
43
+ const setHotspot = (hotspot) => {
44
+ // There is nothing to hotspot at while the sidebar is already open.
45
+ const next = hotspot && !snapshot.open;
46
+ if (snapshot.hotspot === next)
47
+ return;
48
+ snapshot = { ...snapshot, hotspot: next };
49
+ notify();
50
+ };
51
+ const setWidth = (width) => {
52
+ if (snapshot.width === width)
53
+ return;
54
+ snapshot = { ...snapshot, width };
55
+ notify();
56
+ reportResize();
57
+ };
58
+ const setResizing = (resizing) => {
59
+ if (snapshot.resizing === resizing)
60
+ return;
61
+ snapshot = { ...snapshot, resizing };
62
+ notify();
63
+ if (!resizing)
64
+ reportResize();
65
+ };
66
+ snapshot = {
67
+ open: true,
68
+ hotspot: false,
69
+ resizing: false,
70
+ width: null,
71
+ setOpen,
72
+ toggle,
73
+ setHotspot,
74
+ setResizing,
75
+ setWidth,
76
+ };
77
+ return {
78
+ subscribe: (listener) => {
79
+ listeners.add(listener);
80
+ return () => {
81
+ listeners.delete(listener);
82
+ };
83
+ },
84
+ getSnapshot: () => snapshot,
85
+ hydrate: ({ open }) => {
86
+ // Width is deliberately not seedable: it is a *measurement*, and CSS has
87
+ // already decided the number before anything here runs.
88
+ snapshot = { ...snapshot, open: open ?? snapshot.open };
89
+ },
90
+ rootRef,
91
+ sidebarRef,
92
+ sidebarId: "",
93
+ controlledRef,
94
+ onOpenChangeRef,
95
+ onResizeRef,
96
+ commitOpen,
97
+ hotspotSuppressionRef,
98
+ };
99
+ };
100
+ // ---------------------------------------------------------------------------
101
+ // Instance resolution — the nearest Shell.Root, explicitly.
102
+ // ---------------------------------------------------------------------------
103
+ export const ShellStoreContext = createContext(null);
104
+ /** Internal: parts resolve the store their Shell.Root provided. */
105
+ export const useShellContextStore = () => {
106
+ const store = use(ShellStoreContext);
107
+ if (!store)
108
+ throw new Error("Shell parts must be used within <Shell.Root>");
109
+ return store;
110
+ };
111
+ /**
112
+ * Subscribe to an explicit `Shell.createStore()` handle — the
113
+ * outside-the-tree twin of `useShell`, for command palettes, global shortcut
114
+ * handlers and anything else that isn't a descendant. Wrap it once per
115
+ * instance to drop the argument at call sites:
116
+ *
117
+ * const useAppShell = <T,>(selector: (shell: ShellState) => T) =>
118
+ * useShellStore(appShellStore, selector);
119
+ */
120
+ export const useShellStore = (store, selector) => {
121
+ const getValue = () => {
122
+ const state = store.getSnapshot();
123
+ // Safe: without a selector, Selected defaults to ShellState.
124
+ return selector ? selector(state) : state;
125
+ };
126
+ return useSyncExternalStore(store.subscribe, getValue, getValue);
127
+ };
128
+ /**
129
+ * Subscribe to shell state from inside the tree. With a selector the component
130
+ * re-renders only when that value changes; without one it re-renders on any
131
+ * change. The actions are part of the snapshot and their identities never
132
+ * change, so selecting one never causes a render.
133
+ */
134
+ export const useShell = (selector) => useShellStore(useShellContextStore(), selector);
@@ -0,0 +1,2 @@
1
+ export * as Steps from "./index.parts.js";
2
+ export type { StepStatus, StepsIconProps, StepsItemProps, StepsLabelProps, StepsPanelProps, StepsRootProps, StepsStatusProps, StepsTriggerProps, } from "./steps.js";
@@ -0,0 +1,2 @@
1
+ // Public surface of @intentface/ui/steps. No directive — see index.parts.ts.
2
+ export * as Steps from "./index.parts.js";
@@ -0,0 +1 @@
1
+ export { StepsIcon as Icon, StepsItem as Item, StepsLabel as Label, StepsPanel as Panel, StepsRoot as Root, StepsStatus as Status, StepsTrigger as Trigger, } from "./steps.js";
@@ -0,0 +1,5 @@
1
+ // Part names for the `Steps` namespace. Deliberately carries no
2
+ // "use client" directive: this module and index.ts must stay server-resolvable
3
+ // so a React Server Component can reach Steps.Root and its siblings
4
+ // through them. The directive lives one level down, on steps.tsx.
5
+ export { StepsIcon as Icon, StepsItem as Item, StepsLabel as Label, StepsPanel as Panel, StepsRoot as Root, StepsStatus as Status, StepsTrigger as Trigger, } from "./steps.js";