@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,458 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { createContext, Fragment, use, useCallback, useId, useMemo, useState } from "react";
4
+ import { createChangeEventDetails } from "../internal/change-event-details.js";
5
+ import { step } from "../internal/collection.js";
6
+ import { useIsomorphicLayoutEffect } from "../internal/iso-layout-effect.js";
7
+ import { useRenderElement } from "../internal/render/useRenderElement.js";
8
+ import { createTabsStore, TabsStoreContext, useTabsContextStore, useTabsStore } from "./store.js";
9
+ /**
10
+ * `direction` would otherwise emit `data-direction` — the default derivation
11
+ * lowercases rather than kebab-cases — and `none` is not worth an attribute.
12
+ */
13
+ const PART_ATTRIBUTES = {
14
+ direction: (value) => value === "none" ? null : { "data-activation-direction": value },
15
+ };
16
+ const selectValue = (tabs) => tabs.value;
17
+ const selectDirection = (tabs) => tabs.direction;
18
+ const selectHasViewport = (tabs) => tabs.hasViewport;
19
+ const selectItems = (tabs) => tabs.items;
20
+ const TabsConfigContext = createContext(null);
21
+ const useTabsConfig = () => {
22
+ const config = use(TabsConfigContext);
23
+ if (!config)
24
+ throw new Error("Tabs parts must be used within <Tabs.Root>");
25
+ return config;
26
+ };
27
+ const TabsListContext = createContext(null);
28
+ const TabsItemContext = createContext(null);
29
+ /**
30
+ * Disabled cascades: the Root disables everything, an Item disables its own
31
+ * entry, and a part can disable just itself.
32
+ */
33
+ const useDisabled = (own = false) => {
34
+ const { disabled: rootDisabled } = useTabsConfig();
35
+ const item = use(TabsItemContext);
36
+ return rootDisabled || (item?.disabled ?? false) || own;
37
+ };
38
+ /** The enclosing item's value, if there is one. Does not insist on it. */
39
+ const useOptionalItemValue = (explicit) => explicit ?? use(TabsItemContext)?.value ?? null;
40
+ const useItemValue = (explicit) => {
41
+ const inherited = use(TabsItemContext)?.value ?? null;
42
+ const value = explicit ?? inherited;
43
+ if (value === null || value === undefined) {
44
+ throw new Error("Tabs parts need a `value`, either their own or from <Tabs.Trigger>");
45
+ }
46
+ return value;
47
+ };
48
+ const usePartState = (store, orientation, disabled = false) => ({
49
+ orientation,
50
+ disabled,
51
+ empty: useTabsStore(store, selectValue) === null,
52
+ direction: useTabsStore(store, selectDirection),
53
+ });
54
+ /**
55
+ * True while a text field should keep an arrow key for itself: there is a
56
+ * selection, Shift is held, or the caret has not yet reached the end it is
57
+ * travelling towards.
58
+ */
59
+ const ownsTheArrowKeys = (event, forward, backward) => {
60
+ const target = event.target;
61
+ if (!target || !(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) {
62
+ return false;
63
+ }
64
+ if (target.disabled)
65
+ return false;
66
+ const { selectionStart, selectionEnd, value } = target;
67
+ if (selectionStart === null || event.shiftKey || selectionStart !== selectionEnd)
68
+ return true;
69
+ if (event.key !== backward && selectionStart < value.length)
70
+ return true;
71
+ if (event.key !== forward && selectionStart > 0)
72
+ return true;
73
+ return false;
74
+ };
75
+ const tabId = (store, value) => `${store.baseId}-tab-${value}`;
76
+ const viewportId = (store) => `${store.baseId}-viewport`;
77
+ /**
78
+ * The provider and container. Owns the item order, the selection and the
79
+ * roving focus, so every other part reads rather than holds state.
80
+ * Renders a `<div>` element.
81
+ */
82
+ export const TabsRoot = ({ defaultValue, value, onValueChange, defaultItems, items, onItemsChange, selectOnClose, store: storeProp, orientation = "horizontal", loop = true, activateOnFocus = false, disabled = false, dismissOnEscape = true, className, render, style, ...elementProps }) => {
83
+ const baseId = useId();
84
+ const [store] = useState(() => {
85
+ const created = storeProp ?? createTabsStore();
86
+ // Before anything has subscribed, so no notify and no write-back.
87
+ created.hydrate({ items: items ?? defaultItems, value: value ?? defaultValue });
88
+ created.baseId = baseId;
89
+ return created;
90
+ });
91
+ // Latest-ref bridges, written during render: idempotent, and current before
92
+ // any event handler can read them.
93
+ store.selectOnCloseRef.current = selectOnClose;
94
+ store.orientationRef.current = orientation;
95
+ store.valueControlledRef.current = value !== undefined;
96
+ store.itemsControlledRef.current = items !== undefined;
97
+ store.onValueChangeRef.current = onValueChange;
98
+ store.onItemsChangeRef.current = onItemsChange;
99
+ // Before paint, so a controlled change never shows the old state first.
100
+ useIsomorphicLayoutEffect(() => {
101
+ if (value !== undefined)
102
+ store.commitValue(value);
103
+ }, [value, store]);
104
+ useIsomorphicLayoutEffect(() => {
105
+ if (items !== undefined)
106
+ store.commitItems(items);
107
+ }, [items, store]);
108
+ useIsomorphicLayoutEffect(() => {
109
+ if (!dismissOnEscape)
110
+ return;
111
+ const handleKeyDown = (event) => {
112
+ const { value } = store.getSnapshot();
113
+ // The window hears Escape last, so one handled inside (a composer stopping) is spent.
114
+ if (event.key !== "Escape" || event.defaultPrevented || value === null)
115
+ return;
116
+ event.preventDefault();
117
+ // From inside the popup, focus would drop to the body as it closes; it goes back to the tab.
118
+ const fromPopup = store.popupRef.current?.contains(document.activeElement) ?? false;
119
+ store.selectWithDetails(null, createChangeEventDetails("escape-key", event));
120
+ if (fromPopup)
121
+ store.elements.get(value)?.focus();
122
+ };
123
+ window.addEventListener("keydown", handleKeyDown);
124
+ return () => window.removeEventListener("keydown", handleKeyDown);
125
+ }, [dismissOnEscape, store]);
126
+ const config = useMemo(() => ({ orientation, loop, activateOnFocus, disabled }), [orientation, loop, activateOnFocus, disabled]);
127
+ // Nothing hover started outlives the strip.
128
+ const releaseHoverOnUnmount = useCallback(() => () => store.hover.release(), [store]);
129
+ const element = useRenderElement("div", { className, render, style }, {
130
+ state: usePartState(store, orientation),
131
+ stateAttributesMapping: PART_ATTRIBUTES,
132
+ ref: releaseHoverOnUnmount,
133
+ props: [{ "data-tabs": "" }, elementProps],
134
+ });
135
+ return (_jsx(TabsStoreContext, { value: store, children: _jsx(TabsConfigContext, { value: config, children: element }) }));
136
+ };
137
+ /**
138
+ * The strip. A `toolbar` of disclosures rather than a `tablist`, because a
139
+ * tablist may hold only tabs and must always have one selected.
140
+ * Renders a `<div>` element.
141
+ */
142
+ export const TabsList = ({ children, className, render, style, ...elementProps }) => {
143
+ const store = useTabsContextStore();
144
+ const { orientation, loop, activateOnFocus } = useTabsConfig();
145
+ const items = useTabsStore(store, selectItems);
146
+ const value = useTabsStore(store, selectValue);
147
+ // Which tab the roving tabindex sits on. Follows the open tab until the
148
+ // arrows move it somewhere else, which is what manual activation means.
149
+ const [highlighted, setHighlighted] = useState(null);
150
+ // Validated against `items`, not trusted: closing the focused tab with no
151
+ // `selectOnClose` policy leaves nothing to focus, so no `onFocus` fires to
152
+ // re-seed this. A stale id would leave every trigger at `tabIndex: -1` and
153
+ // the strip unreachable from the keyboard.
154
+ const live = highlighted !== null && items.includes(highlighted) ? highlighted : null;
155
+ const current = live ?? value ?? items[0] ?? null;
156
+ const listContext = useMemo(() => ({ highlighted: current, setHighlighted }), [current]);
157
+ const handleKeyDown = (event) => {
158
+ const forward = orientation === "horizontal" ? "ArrowRight" : "ArrowDown";
159
+ const backward = orientation === "horizontal" ? "ArrowLeft" : "ArrowUp";
160
+ // A text field inside the strip — renaming a tab in place — owns its own
161
+ // arrow keys until the caret reaches the end it is heading for. Without
162
+ // this the roving focus steals them and the caret never moves.
163
+ if (ownsTheArrowKeys(event, forward, backward))
164
+ return;
165
+ const next = event.key === forward
166
+ ? step(items, current, 1, { loop })
167
+ : event.key === backward
168
+ ? step(items, current, -1, { loop })
169
+ : event.key === "Home"
170
+ ? (items[0] ?? null)
171
+ : event.key === "End"
172
+ ? (items[items.length - 1] ?? null)
173
+ : undefined;
174
+ if (next === undefined)
175
+ return;
176
+ event.preventDefault();
177
+ if (next === null)
178
+ return;
179
+ setHighlighted(next);
180
+ const element = store.elements.get(next);
181
+ element?.focus();
182
+ if (!activateOnFocus)
183
+ return;
184
+ store.selectWithDetails(next, createChangeEventDetails("list-navigation", event.nativeEvent, element));
185
+ };
186
+ const element = useRenderElement("div", { className, render, style }, {
187
+ state: usePartState(store, orientation),
188
+ stateAttributesMapping: PART_ATTRIBUTES,
189
+ props: [
190
+ {
191
+ "data-tabs-list": "",
192
+ role: "toolbar",
193
+ "aria-orientation": orientation,
194
+ onKeyDown: handleKeyDown,
195
+ },
196
+ elementProps,
197
+ {
198
+ children: typeof children === "function"
199
+ ? items.map((value, index) => (_jsx(Fragment, { children: children(value, index) }, value)))
200
+ : children,
201
+ },
202
+ ],
203
+ });
204
+ return _jsx(TabsListContext, { value: listContext, children: element });
205
+ };
206
+ /**
207
+ * A tab: the whole pill, and the thing you press.
208
+ *
209
+ * It is a `div` with `role="button"` rather than a `<button>`, so that a close
210
+ * button can live inside it. That nesting is what Linear does and it is what
211
+ * makes the entire pill a click target, but it is not free: `role="button"`
212
+ * has children-presentational in ARIA, so `axe` reports `nested-interactive`
213
+ * and the suite suppresses that one rule with a note. A real `<button>` would
214
+ * be worse — the HTML parser closes an outer button when it meets an inner
215
+ * one, so server-rendered markup would come back reshaped and hydration would
216
+ * mismatch.
217
+ *
218
+ * The price of not being a native button is that activation is ours to
219
+ * implement: Enter and Space, by hand.
220
+ *
221
+ * One part, two situations, decided by where it sits rather than by a prop:
222
+ *
223
+ * - **Inside a `Tabs.List`** it is one of the collection, joins the roving
224
+ * focus, and selects. `Delete` closes it.
225
+ * - **Outside one** it takes an explicit value, keeps its own tab stop, and
226
+ * toggles — a panel with no tab behind it, opened anchored to the button
227
+ * that owns it and promoted to a real tab only when you call `open()`.
228
+ *
229
+ * With `openOnHover`, leaving closes the tab again unless the mouse heads into
230
+ * the popup. A press on the tab, or a press or focus inside the popup, keeps it.
231
+ */
232
+ export const TabsTrigger = ({ value: explicitValue, disabled: ownDisabled, openOnHover = false, openDelay = 50, closeDelay = 50, className, render, style, ...elementProps }) => {
233
+ const store = useTabsContextStore();
234
+ const { orientation, disabled: rootDisabled } = useTabsConfig();
235
+ const list = use(TabsListContext);
236
+ const inList = list !== null;
237
+ const value = useItemValue(explicitValue);
238
+ const disabled = rootDisabled || (ownDisabled ?? false);
239
+ const selected = useTabsStore(store, selectValue) === value;
240
+ const hasViewport = useTabsStore(store, selectHasViewport);
241
+ const context = useMemo(() => ({ value, disabled }), [value, disabled]);
242
+ const hovers = openOnHover && !disabled;
243
+ const ref = useCallback((element) => {
244
+ // The same element is both the focus target and what a floating surface
245
+ // lines up with, now that the pill and the button are one thing.
246
+ store.registerElement(value, element);
247
+ store.registerAnchor(value, element);
248
+ const unregisterHover = element && hovers ? store.hover.register(element) : undefined;
249
+ return () => {
250
+ store.registerElement(value, null);
251
+ store.registerAnchor(value, null);
252
+ unregisterHover?.();
253
+ };
254
+ }, [store, value, hovers]);
255
+ useIsomorphicLayoutEffect(() => {
256
+ store.registerDisabled(value, disabled);
257
+ return () => store.registerDisabled(value, false);
258
+ }, [store, value, disabled]);
259
+ const activate = (event) => {
260
+ if (disabled)
261
+ return;
262
+ // A press on a tab the mouse opened keeps it open, rather than toggling it shut.
263
+ const hoverOpened = store.hover.owns(value);
264
+ store.hover.claim();
265
+ store.selectWithDetails(!inList && selected && !hoverOpened ? null : value, createChangeEventDetails("trigger-press", event.nativeEvent, event.currentTarget));
266
+ };
267
+ const handleKeyDown = (event) => {
268
+ if (event.key === "Enter" || event.key === " ") {
269
+ // A div does not do this for us.
270
+ event.preventDefault();
271
+ activate(event);
272
+ return;
273
+ }
274
+ // Delete closes a tab — the keyboard equivalent of the × beside it.
275
+ if (!inList || disabled)
276
+ return;
277
+ if (event.key !== "Delete" && event.key !== "Backspace")
278
+ return;
279
+ event.preventDefault();
280
+ closeAndRefocus(store, value, createChangeEventDetails("keyboard", event.nativeEvent, event.currentTarget));
281
+ };
282
+ const element = useRenderElement("div", { className, render, style }, {
283
+ state: { ...usePartState(store, orientation, disabled), selected },
284
+ stateAttributesMapping: PART_ATTRIBUTES,
285
+ ref,
286
+ props: [
287
+ {
288
+ "data-tabs-trigger": "",
289
+ role: "button",
290
+ id: tabId(store, value),
291
+ "aria-expanded": selected,
292
+ // Only claim a viewport that is really in the document.
293
+ "aria-controls": hasViewport ? viewportId(store) : undefined,
294
+ "aria-disabled": disabled || undefined,
295
+ tabIndex: inList ? (list.highlighted === value ? 0 : -1) : 0,
296
+ onClick: activate,
297
+ onFocus: inList ? () => list.setHighlighted(value) : undefined,
298
+ onKeyDown: handleKeyDown,
299
+ ...(hovers && {
300
+ onPointerEnter: (event) => store.hover.enterTab({ value, openDelay, closeDelay }, event),
301
+ onPointerMove: store.hover.moveOnTab,
302
+ onPointerLeave: store.hover.leaveTab,
303
+ }),
304
+ },
305
+ elementProps,
306
+ ],
307
+ });
308
+ return _jsx(TabsItemContext, { value: context, children: element });
309
+ };
310
+ /**
311
+ * Close, then put focus somewhere deliberate.
312
+ *
313
+ * The store commits synchronously, so the tab that takes over is already known
314
+ * by the time this reads it back — and focus moves there before React unmounts
315
+ * the tab that was closed. Left alone, focus would fall to the body.
316
+ */
317
+ const closeAndRefocus = (store, value, eventDetails) => {
318
+ store.closeWithDetails(value, eventDetails);
319
+ if (eventDetails.isCanceled)
320
+ return;
321
+ const successor = store.getSnapshot().value;
322
+ if (successor)
323
+ store.elements.get(successor)?.focus();
324
+ };
325
+ /**
326
+ * A decorative mark inside a trigger.
327
+ *
328
+ * `aria-hidden`, deliberately: the trigger already has an accessible name, and
329
+ * an icon that repeats it only makes the announcement longer. It carries the
330
+ * part state too, so the mark can respond to its tab being open without the
331
+ * consumer reaching for a group selector.
332
+ */
333
+ export const TabsIcon = ({ value: explicitValue, className, render, style, ...elementProps }) => {
334
+ const store = useTabsContextStore();
335
+ const { orientation } = useTabsConfig();
336
+ const value = useOptionalItemValue(explicitValue);
337
+ const disabled = useDisabled();
338
+ const selected = useTabsStore(store, selectValue) === value && value !== null;
339
+ return useRenderElement("span", { className, render, style }, {
340
+ state: { ...usePartState(store, orientation, disabled), selected },
341
+ stateAttributesMapping: PART_ATTRIBUTES,
342
+ props: [{ "data-tabs-icon": "", "aria-hidden": true }, elementProps],
343
+ });
344
+ };
345
+ /**
346
+ * The trailing slot inside a tab — where the close button lives.
347
+ *
348
+ * Its own part rather than a div of yours for one reason: a tab narrow enough
349
+ * to truncate has nowhere to put a control. Give this `position: absolute` and
350
+ * a background that fades to the tab's own colour, and the label runs the full
351
+ * width and disappears under it instead of colliding with it. A flex box with
352
+ * padding then centres whatever is inside.
353
+ *
354
+ * All of that is your CSS. What the part supplies is the element and the
355
+ * trigger's state on it — `data-selected` above all, since the fade has to end
356
+ * in whatever colour the tab currently is.
357
+ */
358
+ export const TabsAction = ({ value: explicitValue, className, render, style, ...elementProps }) => {
359
+ const store = useTabsContextStore();
360
+ const { orientation } = useTabsConfig();
361
+ const value = useOptionalItemValue(explicitValue);
362
+ const disabled = useDisabled();
363
+ const selected = useTabsStore(store, selectValue) === value && value !== null;
364
+ return useRenderElement("div", { className, render, style }, {
365
+ state: { ...usePartState(store, orientation, disabled), selected },
366
+ stateAttributesMapping: PART_ATTRIBUTES,
367
+ props: [{ "data-tabs-action": "" }, elementProps],
368
+ });
369
+ };
370
+ /**
371
+ * The × inside a tab.
372
+ *
373
+ * Also a `div` with `role="button"`, for the same reason its parent is. It
374
+ * keeps out of the roving order — arrowing along a strip should walk tabs, not
375
+ * alternate between each tab and its close button — so the keyboard route to
376
+ * closing is `Delete` on the tab itself. Every event it handles stops there:
377
+ * nested inside the trigger, anything that escaped would open the tab on its
378
+ * way out of closing it.
379
+ */
380
+ export const TabsClose = ({ value: explicitValue, disabled: ownDisabled, className, render, style, ...elementProps }) => {
381
+ const store = useTabsContextStore();
382
+ const { orientation } = useTabsConfig();
383
+ const disabled = useDisabled(ownDisabled);
384
+ const value = useItemValue(explicitValue);
385
+ const selected = useTabsStore(store, selectValue) === value;
386
+ const close = (event) => {
387
+ event.stopPropagation();
388
+ event.preventDefault();
389
+ if (disabled)
390
+ return;
391
+ closeAndRefocus(store, value, createChangeEventDetails("close-press", event.nativeEvent, event.currentTarget));
392
+ };
393
+ return useRenderElement("div", { className, render, style }, {
394
+ state: { ...usePartState(store, orientation, disabled), selected },
395
+ stateAttributesMapping: PART_ATTRIBUTES,
396
+ props: [
397
+ {
398
+ "data-tabs-close": "",
399
+ role: "button",
400
+ "aria-disabled": disabled || undefined,
401
+ tabIndex: -1,
402
+ onClick: close,
403
+ onKeyDown: (event) => {
404
+ if (event.key !== "Enter" && event.key !== " ")
405
+ return;
406
+ close(event);
407
+ },
408
+ },
409
+ elementProps,
410
+ ],
411
+ });
412
+ };
413
+ /**
414
+ * The one box that shows a tab's content.
415
+ *
416
+ * There is no per-tab element and nothing mounts or unmounts as you switch —
417
+ * the same box stays put and re-renders its children for whichever value is
418
+ * open. That is what makes a floating surface simply move rather than tear
419
+ * itself down, and it is why nothing here needs overlaying or a mount flag.
420
+ *
421
+ * The corollary, and it is deliberate: nothing off-screen exists. A tab you
422
+ * are not looking at has no component, so anything that must keep running
423
+ * while you are elsewhere — a reply still streaming — belongs in a store
424
+ * rather than in the panel's own state.
425
+ */
426
+ export const TabsViewport = ({ children, className, render, style, ...elementProps }) => {
427
+ const store = useTabsContextStore();
428
+ const { orientation } = useTabsConfig();
429
+ const value = useTabsStore(store, selectValue);
430
+ // A trigger only claims a viewport that is really in the document.
431
+ useIsomorphicLayoutEffect(() => {
432
+ store.registerViewport(true);
433
+ return () => store.registerViewport(false);
434
+ }, [store]);
435
+ return useRenderElement("div", { className, render, style }, {
436
+ state: { ...usePartState(store, orientation), value },
437
+ stateAttributesMapping: {
438
+ ...PART_ATTRIBUTES,
439
+ // The open tab's id is not a styling hook; keep it out of the DOM.
440
+ value: () => null,
441
+ },
442
+ props: [
443
+ {
444
+ "data-tabs-viewport": "",
445
+ id: viewportId(store),
446
+ // A bare div maps to the `generic` role, whose name is prohibited, so
447
+ // `aria-labelledby` on one is discarded. `group` is the lightest role
448
+ // that accepts a name without claiming the tabpanel semantics this
449
+ // widget deliberately does not have.
450
+ role: value === null ? undefined : "group",
451
+ // Named by whichever tab is open, so the box is not anonymous.
452
+ "aria-labelledby": value === null ? undefined : tabId(store, value),
453
+ },
454
+ elementProps,
455
+ { children: value === null ? null : children?.(value) },
456
+ ],
457
+ });
458
+ };
@@ -0,0 +1,23 @@
1
+ export declare const prefersReducedMotion: () => boolean;
2
+ export declare const resolveScrollBehavior: (behavior: ScrollBehavior) => ScrollBehavior;
3
+ export declare const scrollContainerTo: (el: HTMLElement, top: number, behavior: ScrollBehavior) => void;
4
+ export type ViewportBox = {
5
+ width: number;
6
+ height: number;
7
+ };
8
+ export declare const readViewportBox: (el: HTMLElement | null) => ViewportBox;
9
+ export declare const sameViewportBox: (a: ViewportBox, b: ViewportBox) => boolean;
10
+ export declare const wasPrepended: (mutations: MutationRecord[], previousFirst: Element | null, content: HTMLElement) => boolean;
11
+ export declare const DEFAULT_BOTTOM_OFFSET = 128;
12
+ export declare const DOCK_SELECTOR = "[data-thread-composer]";
13
+ export declare const queryDock: (root: HTMLElement) => HTMLElement | null;
14
+ /**
15
+ * Height (px) to reserve at the bottom for the dock: its top edge down to the
16
+ * root's bottom, plus the content gap. Measured against the root's bottom
17
+ * rather than the slot's own height, so a dock that floats above the bottom
18
+ * edge still reserves the space beneath it. Parts that must NOT reserve space
19
+ * — the command-list / ask-user panel, the scroll button — are positioned out
20
+ * of the slot's flow. Returns null when the slot isn't mounted yet.
21
+ */
22
+ export declare const measureDockInset: (root: HTMLElement) => number | null;
23
+ export declare const measureTopInset: (root: HTMLElement) => number;
@@ -0,0 +1,48 @@
1
+ // Pure DOM geometry + measurement for the thread scroll subsystem. No React —
2
+ // unit-testable with stubbed elements/rects.
3
+ // Queried per call, not cached — the OS setting can change mid-session and
4
+ // none of these scrolls are hot paths. Guarded for non-DOM (pure-test) callers.
5
+ export const prefersReducedMotion = () => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
6
+ // "smooth" downgrades to "auto" under prefers-reduced-motion — deliberate
7
+ // movements still happen, they just don't animate.
8
+ export const resolveScrollBehavior = (behavior) => behavior === "smooth" && prefersReducedMotion() ? "auto" : behavior;
9
+ // Single place that performs the scroll, so callers just choose the behavior:
10
+ // 'instant' for jumps that must not animate, 'smooth' for deliberate movements.
11
+ export const scrollContainerTo = (el, top, behavior) => {
12
+ el.scrollTo({ top, behavior: resolveScrollBehavior(behavior) });
13
+ };
14
+ export const readViewportBox = (el) => el ? { width: el.clientWidth, height: el.clientHeight } : { width: 0, height: 0 };
15
+ export const sameViewportBox = (a, b) => a.width === b.width && a.height === b.height;
16
+ // A prepend = rows were added, nothing removed, and the previously-first row
17
+ // is still connected but no longer first (older history loading in above).
18
+ export const wasPrepended = (mutations, previousFirst, content) => previousFirst?.isConnected === true &&
19
+ content.firstElementChild !== previousFirst &&
20
+ mutations.some((m) => m.addedNodes.length > 0) &&
21
+ mutations.every((m) => m.removedNodes.length === 0);
22
+ // Fallback (px) until the composer is measured.
23
+ export const DEFAULT_BOTTOM_OFFSET = 128;
24
+ // Breathing room between the last line of content and the composer dock. The
25
+ // bottom overlay spans it in the styled layer.
26
+ const COMPOSER_GAP = 32;
27
+ // The dock is the Thread.Composer slot — the one element the thread reserves
28
+ // space for. Null until it mounts.
29
+ export const DOCK_SELECTOR = "[data-thread-composer]";
30
+ export const queryDock = (root) => root.querySelector(DOCK_SELECTOR);
31
+ /**
32
+ * Height (px) to reserve at the bottom for the dock: its top edge down to the
33
+ * root's bottom, plus the content gap. Measured against the root's bottom
34
+ * rather than the slot's own height, so a dock that floats above the bottom
35
+ * edge still reserves the space beneath it. Parts that must NOT reserve space
36
+ * — the command-list / ask-user panel, the scroll button — are positioned out
37
+ * of the slot's flow. Returns null when the slot isn't mounted yet.
38
+ */
39
+ export const measureDockInset = (root) => {
40
+ const dock = queryDock(root);
41
+ if (!dock)
42
+ return null;
43
+ const dockTop = dock.getBoundingClientRect().top;
44
+ return Math.round(root.getBoundingClientRect().bottom - dockTop + COMPOSER_GAP);
45
+ };
46
+ // Top inset reserved by the top overlay, measured straight off the rendered
47
+ // element (px) — no getComputedStyle / rem→px conversion. 0 if no top overlay.
48
+ export const measureTopInset = (root) => root.querySelector('[data-thread-overlay="top"]')?.getBoundingClientRect().height ?? 0;
@@ -0,0 +1,4 @@
1
+ export * as Thread from "./index.parts.js";
2
+ export type { ThreadVisibilityState } from "./stores.js";
3
+ export type { ThreadAutoScrollMode, ThreadComposerProps, ThreadContentProps, ThreadOverlayProps, ThreadOverlayState, ThreadPlaceholderProps, ThreadRootProps, ThreadScrollToMessageOptions, ThreadViewportProps, } from "./thread.js";
4
+ export { useThread, useThreadVisibility } from "./thread.js";
@@ -0,0 +1,3 @@
1
+ // Public surface of @intentface/ui/thread. No directive — see index.parts.ts.
2
+ export * as Thread from "./index.parts.js";
3
+ export { useThread, useThreadVisibility } from "./thread.js";
@@ -0,0 +1 @@
1
+ export { ThreadComposer as Composer, ThreadContent as Content, ThreadOverlay as Overlay, ThreadPlaceholder as Placeholder, ThreadRoot as Root, ThreadViewport as Viewport, } from "./thread.js";
@@ -0,0 +1,5 @@
1
+ // Part names for the `Thread` 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 Thread.Root and its siblings
4
+ // through them. The directive lives one level down, on thread.tsx.
5
+ export { ThreadComposer as Composer, ThreadContent as Content, ThreadOverlay as Overlay, ThreadPlaceholder as Placeholder, ThreadRoot as Root, ThreadViewport as Viewport, } from "./thread.js";
@@ -0,0 +1,21 @@
1
+ export declare const createEdgeStore: () => {
2
+ getSnapshot: () => boolean;
3
+ setSnapshot: (next: boolean) => void;
4
+ subscribe: (listener: () => void) => () => void;
5
+ };
6
+ export type EdgeStore = ReturnType<typeof createEdgeStore>;
7
+ export type ThreadVisibilityState = {
8
+ /** data-message-id values intersecting the viewport, in document order. */
9
+ visibleMessageIds: string[];
10
+ /** The topmost visible row — the one being read. */
11
+ currentMessageId: string | null;
12
+ };
13
+ export declare const EMPTY_VISIBILITY: ThreadVisibilityState;
14
+ export declare const visibilityStatesEqual: (a: ThreadVisibilityState, b: ThreadVisibilityState) => boolean;
15
+ export declare const createVisibilityStore: () => {
16
+ getSnapshot: () => ThreadVisibilityState;
17
+ hasListeners: () => boolean;
18
+ setSnapshot: (next: ThreadVisibilityState) => void;
19
+ subscribe: (listener: () => void, onFirstSubscribe: () => void, onLastUnsubscribe: () => void) => () => void;
20
+ };
21
+ export type VisibilityStore = ReturnType<typeof createVisibilityStore>;
@@ -0,0 +1,63 @@
1
+ // Scroll-edge + visibility stores for the thread. External (hand-rolled
2
+ // useSyncExternalStore stores) so a change re-renders only the components that
3
+ // read it, never the Thread tree — the context value stays referentially stable
4
+ // for the thread's lifetime. Pure; no React, no DOM.
5
+ // One boolean store per edge (top / bottom). Two independent stores rather than
6
+ // a single { atTop, atBottom } snapshot, so a flip at one edge never notifies
7
+ // the other's readers.
8
+ export const createEdgeStore = () => {
9
+ let snapshot = true;
10
+ const listeners = new Set();
11
+ return {
12
+ getSnapshot: () => snapshot,
13
+ setSnapshot: (next) => {
14
+ if (snapshot === next)
15
+ return;
16
+ snapshot = next;
17
+ for (const listener of listeners)
18
+ listener();
19
+ },
20
+ subscribe: (listener) => {
21
+ listeners.add(listener);
22
+ return () => {
23
+ listeners.delete(listener);
24
+ };
25
+ },
26
+ };
27
+ };
28
+ export const EMPTY_VISIBILITY = {
29
+ visibleMessageIds: [],
30
+ currentMessageId: null,
31
+ };
32
+ export const visibilityStatesEqual = (a, b) => a.currentMessageId === b.currentMessageId &&
33
+ a.visibleMessageIds.length === b.visibleMessageIds.length &&
34
+ a.visibleMessageIds.every((id, index) => id === b.visibleMessageIds[index]);
35
+ // Which data-message-id rows intersect the viewport. Lazy + ref-counted: the
36
+ // observers behind it exist only while at least one useThreadVisibility
37
+ // subscriber is mounted, so unsubscribed threads pay nothing.
38
+ export const createVisibilityStore = () => {
39
+ let snapshot = EMPTY_VISIBILITY;
40
+ const listeners = new Set();
41
+ return {
42
+ getSnapshot: () => snapshot,
43
+ hasListeners: () => listeners.size > 0,
44
+ setSnapshot: (next) => {
45
+ if (visibilityStatesEqual(snapshot, next))
46
+ return;
47
+ snapshot = next;
48
+ for (const listener of listeners)
49
+ listener();
50
+ },
51
+ subscribe: (listener, onFirstSubscribe, onLastUnsubscribe) => {
52
+ const wasEmpty = listeners.size === 0;
53
+ listeners.add(listener);
54
+ if (wasEmpty)
55
+ onFirstSubscribe();
56
+ return () => {
57
+ listeners.delete(listener);
58
+ if (listeners.size === 0)
59
+ onLastUnsubscribe();
60
+ };
61
+ },
62
+ };
63
+ };