@intentface/chat 0.1.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 (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/dist/ask-user/ask-user.d.ts +104 -0
  4. package/dist/ask-user/ask-user.js +226 -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 +41 -0
  10. package/dist/attachments/attachments.js +91 -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 +25 -0
  16. package/dist/chip/chip.js +25 -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 +52 -0
  23. package/dist/composer/actions.d.ts +35 -0
  24. package/dist/composer/actions.js +75 -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 +357 -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 +55 -0
  40. package/dist/composer/editor-dom.js +431 -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 +29 -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 +80 -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 +63 -0
  57. package/dist/composer/prefix-detection.d.ts +23 -0
  58. package/dist/composer/prefix-detection.js +58 -0
  59. package/dist/composer/root.d.ts +21 -0
  60. package/dist/composer/root.js +107 -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 +97 -0
  64. package/dist/composer/store.js +451 -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 +126 -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 +649 -0
  73. package/dist/internal/collapsible.d.ts +22 -0
  74. package/dist/internal/collapsible.js +90 -0
  75. package/dist/internal/primitive-props.d.ts +4 -0
  76. package/dist/internal/primitive-props.js +4 -0
  77. package/dist/internal/render/baseTypes.d.ts +18 -0
  78. package/dist/internal/render/baseTypes.js +4 -0
  79. package/dist/internal/render/empty.d.ts +3 -0
  80. package/dist/internal/render/empty.js +5 -0
  81. package/dist/internal/render/getReactElementRef.d.ts +5 -0
  82. package/dist/internal/render/getReactElementRef.js +15 -0
  83. package/dist/internal/render/getStateAttributesProps.d.ts +4 -0
  84. package/dist/internal/render/getStateAttributesProps.js +23 -0
  85. package/dist/internal/render/mergeObjects.d.ts +1 -0
  86. package/dist/internal/render/mergeObjects.js +14 -0
  87. package/dist/internal/render/mergeProps.d.ts +55 -0
  88. package/dist/internal/render/mergeProps.js +183 -0
  89. package/dist/internal/render/positioning.d.ts +22 -0
  90. package/dist/internal/render/positioning.js +78 -0
  91. package/dist/internal/render/reactVersion.d.ts +3 -0
  92. package/dist/internal/render/reactVersion.js +7 -0
  93. package/dist/internal/render/resolveClassName.d.ts +8 -0
  94. package/dist/internal/render/resolveClassName.js +12 -0
  95. package/dist/internal/render/resolveStyle.d.ts +8 -0
  96. package/dist/internal/render/resolveStyle.js +12 -0
  97. package/dist/internal/render/transition.d.ts +48 -0
  98. package/dist/internal/render/transition.js +203 -0
  99. package/dist/internal/render/types.d.ts +74 -0
  100. package/dist/internal/render/types.js +3 -0
  101. package/dist/internal/render/useMergedRefs.d.ts +21 -0
  102. package/dist/internal/render/useMergedRefs.js +102 -0
  103. package/dist/internal/render/useRefWithInit.d.ts +10 -0
  104. package/dist/internal/render/useRefWithInit.js +12 -0
  105. package/dist/internal/render/useRender.d.ts +71 -0
  106. package/dist/internal/render/useRender.js +12 -0
  107. package/dist/internal/render/useRenderElement.d.ts +62 -0
  108. package/dist/internal/render/useRenderElement.js +157 -0
  109. package/dist/internal/render/warn.d.ts +1 -0
  110. package/dist/internal/render/warn.js +15 -0
  111. package/dist/internal/state-mappings.d.ts +8 -0
  112. package/dist/internal/state-mappings.js +17 -0
  113. package/dist/message/index.d.ts +3 -0
  114. package/dist/message/index.js +3 -0
  115. package/dist/message/index.parts.d.ts +1 -0
  116. package/dist/message/index.parts.js +5 -0
  117. package/dist/message/message.d.ts +43 -0
  118. package/dist/message/message.js +78 -0
  119. package/dist/message-utils.d.ts +55 -0
  120. package/dist/message-utils.js +79 -0
  121. package/dist/reasoning/index.d.ts +3 -0
  122. package/dist/reasoning/index.js +3 -0
  123. package/dist/reasoning/index.parts.d.ts +1 -0
  124. package/dist/reasoning/index.parts.js +5 -0
  125. package/dist/reasoning/reasoning.d.ts +20 -0
  126. package/dist/reasoning/reasoning.js +49 -0
  127. package/dist/steps/index.d.ts +2 -0
  128. package/dist/steps/index.js +2 -0
  129. package/dist/steps/index.parts.d.ts +1 -0
  130. package/dist/steps/index.parts.js +5 -0
  131. package/dist/steps/steps.d.ts +53 -0
  132. package/dist/steps/steps.js +79 -0
  133. package/dist/thread/geometry.d.ts +18 -0
  134. package/dist/thread/geometry.js +59 -0
  135. package/dist/thread/index.d.ts +4 -0
  136. package/dist/thread/index.js +3 -0
  137. package/dist/thread/index.parts.d.ts +1 -0
  138. package/dist/thread/index.parts.js +5 -0
  139. package/dist/thread/stores.d.ts +21 -0
  140. package/dist/thread/stores.js +63 -0
  141. package/dist/thread/thread.d.ts +66 -0
  142. package/dist/thread/thread.js +639 -0
  143. package/dist/types.d.ts +58 -0
  144. package/dist/types.js +17 -0
  145. package/package.json +106 -0
@@ -0,0 +1,49 @@
1
+ import { type ComponentProps, type ReactNode } from "react";
2
+ import type { PrimitiveProps } from "../internal/primitive-props.js";
3
+ import { Commands } from "./commands.js";
4
+ import type { CommandItemData } from "./types.js";
5
+ export type CommandListState = "loading" | "empty" | "ready";
6
+ export declare const optionDomId: (listboxId: string, value: string) => string;
7
+ export type ComposerCommandState = {
8
+ /** Present as data-loading while an async items callback is in flight. */
9
+ loading: boolean;
10
+ /** Present as data-empty when nothing matches the query. */
11
+ empty: boolean;
12
+ };
13
+ export type ComposerCommandProps = PrimitiveProps<"div", ComposerCommandState> & {
14
+ prefix: string;
15
+ };
16
+ export declare const ComposerCommand: ({ prefix, className, render, style, children, ...elementProps }: ComposerCommandProps) => import("react").JSX.Element | null;
17
+ export declare const useCommandListItems: <Item extends CommandItemData = CommandItemData>() => {
18
+ items: Item[];
19
+ state: CommandListState;
20
+ };
21
+ export type ComposerCommandListProps<Item extends CommandItemData> = Omit<PrimitiveProps<"div">, "children"> & {
22
+ children: (item: Item) => ReactNode;
23
+ };
24
+ export declare const ComposerCommandList: <Item extends CommandItemData>({ className, render, style, children: renderItem, ...elementProps }: ComposerCommandListProps<Item>) => ReactNode;
25
+ export type ComposerCommandLoadingProps = PrimitiveProps<"div">;
26
+ export declare const ComposerCommandLoading: ({ children, className, render, style, ...elementProps }: ComposerCommandLoadingProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
27
+ export type ComposerCommandEmptyProps = PrimitiveProps<"div">;
28
+ export declare const ComposerCommandEmpty: ({ children, className, render, style, ...elementProps }: ComposerCommandEmptyProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
29
+ export type ComposerCommandDismissProps = PrimitiveProps<"button">;
30
+ export declare const ComposerCommandDismiss: ({ children, className, render, style, ...elementProps }: ComposerCommandDismissProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
31
+ export type ComposerCommandItemProps = Omit<ComponentProps<typeof Commands.Item>, "highlighted" | "disabled"> & {
32
+ value: string;
33
+ };
34
+ export declare const ComposerCommandItem: ({ value, ...props }: ComposerCommandItemProps) => import("react").JSX.Element;
35
+ export type ComposerCommandItemIconProps = PrimitiveProps<"span">;
36
+ export declare const ComposerCommandItemIcon: ({ className, render, style, ...elementProps }: ComposerCommandItemIconProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
37
+ export type ComposerCommandItemLabelProps = PrimitiveProps<"span">;
38
+ export declare const ComposerCommandItemLabel: ({ className, render, style, ...elementProps }: ComposerCommandItemLabelProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
39
+ export type ComposerCommandItemDescriptionProps = PrimitiveProps<"span">;
40
+ export declare const ComposerCommandItemDescription: ({ className, render, style, ...elementProps }: ComposerCommandItemDescriptionProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
41
+ export type ComposerCommandGroupProps<Item extends CommandItemData = CommandItemData> = {
42
+ /** Bucket the resolved items by this key (defines the app's grouping taxonomy). */
43
+ groupBy: (item: Item) => string;
44
+ /** Render one group: its key + that group's already-filtered items. */
45
+ children: (group: string, items: Item[]) => ReactNode;
46
+ };
47
+ export declare const ComposerCommandGroup: <Item extends CommandItemData = CommandItemData>({ groupBy, children: renderGroup, }: ComposerCommandGroupProps<Item>) => ReactNode;
48
+ export type ComposerCommandGroupLabelProps = PrimitiveProps<"div">;
49
+ export declare const ComposerCommandGroupLabel: ({ className, render, style, ...elementProps }: ComposerCommandGroupLabelProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,357 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ // Composer.Command family — resolution (sync/async items, fuzzy filter),
4
+ // keyboard/mouse selection, chip insertion, dismissal, and the nav/items
5
+ // context split (highlight changes don't re-render the items map, and items
6
+ // mutations don't re-render every row). All unstyled; state panels as
7
+ // data-state / data-highlighted.
8
+ import { createContext, Fragment, use, useCallback, useEffect, useMemo, useRef, useState, } from "react";
9
+ import { useRenderElement } from "../internal/render/useRenderElement.js";
10
+ import { Commands } from "./commands.js";
11
+ import { filterArrayItems, suggestionRemainder } from "./fuzzy.js";
12
+ import { useAsRef, useComposerInternals, useIsomorphicLayoutEffect } from "./internals.js";
13
+ import { useComposer, useComposerContextStore } from "./store.js";
14
+ const CommandListNavContext = createContext(null);
15
+ const CommandListItemsContext = createContext(null);
16
+ // ---------------------------------------------------------------------------
17
+ // Item resolution — turn the prefix's items config (array or async callback)
18
+ // into the current filtered list + loading/empty/ready state.
19
+ // ---------------------------------------------------------------------------
20
+ const useResolvedItems = (itemsProp, query, isActive) => {
21
+ const isCallback = typeof itemsProp === "function";
22
+ const [asyncState, setAsyncState] = useState(() => ({ items: [], loading: isCallback }));
23
+ useEffect(() => {
24
+ if (typeof itemsProp !== "function")
25
+ return;
26
+ if (!isActive)
27
+ return;
28
+ const controller = new AbortController();
29
+ let cancelled = false;
30
+ let result;
31
+ try {
32
+ result = itemsProp(query, { signal: controller.signal });
33
+ }
34
+ catch (error) {
35
+ console.warn("Composer.commands items callback threw:", error);
36
+ setAsyncState({ items: [], loading: false });
37
+ return () => {
38
+ cancelled = true;
39
+ controller.abort();
40
+ };
41
+ }
42
+ if (result instanceof Promise) {
43
+ setAsyncState((previous) => ({ ...previous, loading: true }));
44
+ result.then((resolved) => {
45
+ if (cancelled)
46
+ return;
47
+ setAsyncState({ items: resolved, loading: false });
48
+ }, (error) => {
49
+ if (cancelled)
50
+ return;
51
+ if (error?.name === "AbortError")
52
+ return;
53
+ console.warn("Composer.commands items callback rejected:", error);
54
+ setAsyncState((previous) => ({ ...previous, loading: false }));
55
+ });
56
+ }
57
+ else {
58
+ setAsyncState({ items: result, loading: false });
59
+ }
60
+ return () => {
61
+ cancelled = true;
62
+ controller.abort();
63
+ };
64
+ }, [itemsProp, query, isActive]);
65
+ if (Array.isArray(itemsProp)) {
66
+ const items = filterArrayItems(itemsProp, query);
67
+ return { items, state: items.length === 0 ? "empty" : "ready" };
68
+ }
69
+ if (asyncState.loading) {
70
+ // Locally filter the most-recent resolved set so typing feels instant
71
+ // while the new fetch is in flight. The server result replaces this once
72
+ // it lands.
73
+ return {
74
+ items: filterArrayItems(asyncState.items, query),
75
+ state: "loading",
76
+ };
77
+ }
78
+ return {
79
+ items: asyncState.items,
80
+ state: asyncState.items.length === 0 ? "empty" : "ready",
81
+ };
82
+ };
83
+ // ---------------------------------------------------------------------------
84
+ // Composer.Command — the orchestrator: resolves items for the active
85
+ // prefix, owns the highlight/selection/dismiss logic, and provides the two
86
+ // contexts. Renders null unless this prefix is the active one.
87
+ // ---------------------------------------------------------------------------
88
+ const EMPTY_ITEMS = [];
89
+ // Deterministic option-row id shared by the row itself and the editor's
90
+ // aria-activedescendant. Values sanitize to valid id tokens.
91
+ export const optionDomId = (listboxId, value) => `${listboxId}-option-${value.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
92
+ // The badge's hint chrome: non-editable so the caret can't enter it, no chip
93
+ // id so the reader/mappers treat it as zero-width presentation, aria-hidden
94
+ // so screen readers skip the ghost. Styled via its part attribute like any other part.
95
+ const createHintElement = (badge) => {
96
+ const hint = badge.ownerDocument.createElement("span");
97
+ hint.setAttribute("data-command-hint", "");
98
+ hint.setAttribute("contenteditable", "false");
99
+ hint.setAttribute("aria-hidden", "true");
100
+ badge.appendChild(hint);
101
+ return hint;
102
+ };
103
+ export const ComposerCommand = ({ prefix, className, render, style, children, ...elementProps }) => {
104
+ const store = useComposerContextStore();
105
+ const internals = useComposerInternals();
106
+ // Renders while this prefix is the active one AND through its close animation. `active`
107
+ // flips false the instant the trigger closes, but the store keeps `present` sticky until
108
+ // the Panel's exit animation finishes (finalizePanelClose). `wasActiveRef` marks that this
109
+ // prefix — not a sibling — is the one closing, so only it keeps rendering during the window.
110
+ const isActive = useComposer((composer) => composer.commands.active && composer.commands.trigger === prefix);
111
+ const present = useComposer((composer) => composer.commands.present);
112
+ const query = useComposer((composer) => composer.commands.query);
113
+ const wasActiveRef = useRef(false);
114
+ if (isActive)
115
+ wasActiveRef.current = true;
116
+ else if (!present)
117
+ wasActiveRef.current = false;
118
+ const isPresent = isActive || (present && wasActiveRef.current);
119
+ const config = internals.commands[prefix];
120
+ const itemsProp = config?.items ?? EMPTY_ITEMS;
121
+ const kind = config?.kind ?? "execute";
122
+ // Freeze the last resolved items while closing so the exiting panel shows what it had,
123
+ // not a re-filter/re-fetch against the now-empty query.
124
+ const resolved = useResolvedItems(itemsProp, query, isActive);
125
+ const lastResolvedRef = useRef(resolved);
126
+ if (isActive)
127
+ lastResolvedRef.current = resolved;
128
+ const { items, state } = isActive ? resolved : lastResolvedRef.current;
129
+ // The highlight index lives in the store (so the editor's keydown handler can
130
+ // move it). It's raw/unbounded; wrap it by the current item count here. The
131
+ // wrap runs over the enabled subset only, so disabled rows render but never
132
+ // take the highlight — arrow keys hop over them for free. An empty list has
133
+ // no highlighted row — the "No results" row stands in as the sole selectable.
134
+ const enabledItems = useMemo(() => items.filter((item) => !item.disabled), [items]);
135
+ const highlightIndex = useComposer((composer) => composer.commands.highlightIndex);
136
+ const activeIndex = enabledItems.length > 0
137
+ ? ((highlightIndex % enabledItems.length) + enabledItems.length) % enabledItems.length
138
+ : -1;
139
+ const highlightedItem = enabledItems[activeIndex] ?? null;
140
+ const effectiveHighlight = highlightedItem?.value ?? null;
141
+ // Badge hint — one real element (span[data-command-hint], appended
142
+ // inside the badge) carrying either the ghost-text completion of the
143
+ // highlighted item or the per-prefix empty-query placeholder. One slot, one
144
+ // value: the suggestion wins by a plain ?? chain, so exclusivity is code,
145
+ // not CSS specificity. The reader and position mappers treat non-editable,
146
+ // non-chip elements as zero-width presentation, so the hint never reaches
147
+ // the model. The badge is engine-owned DOM out of JSX reach — same access
148
+ // pattern as the popover's anchor query — so this is a true DOM-integration
149
+ // effect. No deps: badge identity changes on token rewraps, which always
150
+ // coincide with a re-render here (query/highlight subscribed).
151
+ const suggestion = isActive && highlightedItem && (config?.suggestion ?? true)
152
+ ? suggestionRemainder(query, highlightedItem.label)
153
+ : null;
154
+ const hintText = suggestion ?? (isActive && query === "" ? (config?.placeholder ?? "Type to filter") : null);
155
+ useIsomorphicLayoutEffect(() => {
156
+ const badge = store.editorRef.current?.getRootElement()?.querySelector("[data-command-badge]");
157
+ const existingHint = badge?.querySelector("[data-command-hint]") ?? null;
158
+ if (!badge || !hintText) {
159
+ existingHint?.remove();
160
+ return;
161
+ }
162
+ const hint = existingHint ?? createHintElement(badge);
163
+ if (hint.textContent !== hintText)
164
+ hint.textContent = hintText;
165
+ });
166
+ // Mirror the highlighted row's DOM id into the store — the editor renders
167
+ // it as aria-activedescendant. This is where the raw highlight index
168
+ // resolves against the actual item list, so the id can only be derived here.
169
+ // Only the ACTIVE command writes: with several Commands mounted, an inactive
170
+ // sibling writing null would ping-pong against the active one's id forever
171
+ // (max-update-depth). Clearing on close is setCommands' job.
172
+ useIsomorphicLayoutEffect(() => {
173
+ if (!isActive)
174
+ return;
175
+ store.setActiveOptionId(highlightedItem ? optionDomId(store.listboxId, highlightedItem.value) : null);
176
+ });
177
+ const selectByValue = useCallback((value) => {
178
+ const editor = store.editorRef.current;
179
+ if (!editor)
180
+ return;
181
+ const item = items.find((candidate) => candidate.value === value);
182
+ if (!item || item.disabled)
183
+ return;
184
+ if (kind === "insert") {
185
+ editor.insertChipAtTrigger({
186
+ prefix,
187
+ value: item.value,
188
+ label: item.label ?? item.value,
189
+ icon: item.icon,
190
+ });
191
+ }
192
+ else {
193
+ editor.deleteTrigger();
194
+ // Read the attachment actions lazily at selection time — their
195
+ // identities are store-stable, so subscribing would only re-render the
196
+ // list on unrelated attachment changes.
197
+ const { add, remove, openFileDialog } = store.getSnapshot().attachments;
198
+ const onSelectContext = {
199
+ editor: store.controller,
200
+ attachments: { add, remove, openFileDialog },
201
+ };
202
+ item.onSelect?.(onSelectContext);
203
+ }
204
+ editor.closeCommands();
205
+ }, [store, items, kind, prefix]);
206
+ const dismiss = useCallback(() => {
207
+ store.editorRef.current?.dismissCommands();
208
+ }, [store]);
209
+ // The editor's keydown handler invokes the current selection through
210
+ // commandSelectRef. Keep the latest values in a ref so the callback stays
211
+ // stable, and register it via the root's callback ref (commit-time) — not an
212
+ // effect, not a render-phase assignment.
213
+ const selectState = useAsRef({ effectiveHighlight, selectByValue, dismiss, state });
214
+ const runSelect = useCallback(() => {
215
+ const { effectiveHighlight, selectByValue, dismiss, state } = selectState.current;
216
+ // With a match, select it. When empty, the "No results" row is the sole
217
+ // option and selecting it dismisses — so Tab/Enter aren't dead (Linear-style).
218
+ if (effectiveHighlight)
219
+ selectByValue(effectiveHighlight);
220
+ else if (state === "empty")
221
+ dismiss();
222
+ }, []);
223
+ const registerSelect = useCallback((node) => {
224
+ store.commandSelectRef.current = node ? runSelect : null;
225
+ }, [store, runSelect]);
226
+ // Scroll the highlighted row into view as it becomes the highlight. Stable, so
227
+ // React calls it only on highlight change — no per-render scroll, no effect.
228
+ //
229
+ // container:"nearest" hard-scopes the scroll to the list on Chromium.
230
+ // Safari/Firefox silently ignore the member (unknown dictionary keys) and
231
+ // run an UNSCOPED block:"nearest" — scrollIntoView then scrolls every
232
+ // ancestor scrolling box, the window included, whenever the row isn't
233
+ // viewport-visible.
234
+ //
235
+ // KNOWN BUG (accepted 2026-07-10): on Safari, arrowing through the list
236
+ // while the popup opens can scroll the page behind it (reproduced; the
237
+ // positioner's viewport containment is not sufficient there). Accepted in
238
+ // exchange for native scroll-padding support and −24 lines; goes away when
239
+ // WebKit ships `container`. If it starts to hurt sooner, the scoped manual
240
+ // implementation lives in this function's git history (PR #39, first
241
+ // commit).
242
+ const scrollHighlightedIntoView = useCallback((node) => {
243
+ // Cast: lib.dom doesn't know the `container` member yet (Chromium-only).
244
+ node?.scrollIntoView({ block: "nearest", container: "nearest" });
245
+ }, []);
246
+ const navContext = useMemo(() => ({
247
+ highlightedValue: effectiveHighlight,
248
+ setHighlightedValue: (value) => {
249
+ const index = value === null ? -1 : enabledItems.findIndex((item) => item.value === value);
250
+ if (index >= 0)
251
+ store.setHighlight(index);
252
+ },
253
+ selectByValue,
254
+ dismiss,
255
+ scrollHighlightedIntoView,
256
+ }), [effectiveHighlight, enabledItems, selectByValue, dismiss, scrollHighlightedIntoView, store]);
257
+ const itemsContext = useMemo(() => ({ items, state }), [items, state]);
258
+ const element = useRenderElement("div", { className, render, style }, {
259
+ enabled: isPresent,
260
+ state: { loading: state === "loading", empty: state === "empty" },
261
+ ref: registerSelect,
262
+ props: [{ "data-composer-command-list": "", children }, elementProps],
263
+ });
264
+ if (!isPresent)
265
+ return null;
266
+ return (_jsx(CommandListItemsContext, { value: itemsContext, children: _jsx(CommandListNavContext, { value: navContext, children: element }) }));
267
+ };
268
+ // ---------------------------------------------------------------------------
269
+ // List hooks — the consumer-facing items hook and the internal nav accessor
270
+ // the slot primitives read.
271
+ // ---------------------------------------------------------------------------
272
+ export const useCommandListItems = () => {
273
+ const context = use(CommandListItemsContext);
274
+ if (!context) {
275
+ throw new Error("<Composer.CommandList> must be rendered inside <Composer.Command>.");
276
+ }
277
+ return context;
278
+ };
279
+ const useCommandListNav = (componentName) => {
280
+ const context = use(CommandListNavContext);
281
+ if (!context) {
282
+ throw new Error(`<Composer.${componentName}> must be rendered inside <Composer.Command>.`);
283
+ }
284
+ return context;
285
+ };
286
+ export const ComposerCommandList = ({ className, render, style, children: renderItem, ...elementProps }) => {
287
+ const { items, state } = useCommandListItems();
288
+ const store = useComposerContextStore();
289
+ return useRenderElement("div", { className, render, style }, {
290
+ props: [
291
+ {
292
+ // The combobox popup: the editor references this element via
293
+ // aria-controls and points aria-activedescendant at its rows.
294
+ role: "listbox",
295
+ id: store.listboxId,
296
+ "aria-label": "Suggestions",
297
+ "aria-busy": state === "loading" || undefined,
298
+ "data-composer-command-items": "",
299
+ children: items.map((item, index) => (_jsx(Fragment, { children: renderItem(item) }, item.value ?? `__cmd_${index}`))),
300
+ },
301
+ elementProps,
302
+ ],
303
+ });
304
+ };
305
+ export const ComposerCommandLoading = ({ children, className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, {
306
+ props: [{ "data-composer-command-loading": "", children }, elementProps],
307
+ });
308
+ export const ComposerCommandEmpty = ({ children, className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, {
309
+ props: [{ role: "status", "data-composer-command-empty": "", children }, elementProps],
310
+ });
311
+ export const ComposerCommandDismiss = ({ children, className, render, style, ...elementProps }) => {
312
+ const navContext = useCommandListNav("CommandDismiss");
313
+ return useRenderElement("button", { className, render, style }, {
314
+ props: [
315
+ {
316
+ tabIndex: -1,
317
+ "data-composer-command-dismiss": "",
318
+ onMouseDown: (event) => {
319
+ event.preventDefault();
320
+ navContext.dismiss();
321
+ },
322
+ children,
323
+ },
324
+ elementProps,
325
+ ],
326
+ });
327
+ };
328
+ export const ComposerCommandItem = ({ value, ...props }) => {
329
+ const navContext = useCommandListNav("CommandItem");
330
+ const itemsContext = use(CommandListItemsContext);
331
+ const store = useComposerContextStore();
332
+ const isHighlighted = navContext.highlightedValue === value;
333
+ // Disabled comes from the item data (CommandItemData.disabled) — the same
334
+ // source the nav wrap skips — so the row can never disagree with the
335
+ // keyboard behavior. Not a prop: two writers would let them drift.
336
+ const isDisabled = itemsContext?.items.some((item) => item.value === value && item.disabled) ?? false;
337
+ return (_jsx(Commands.Item, { ref: isHighlighted ? navContext.scrollHighlightedIntoView : undefined, id: optionDomId(store.listboxId, value), "aria-selected": isHighlighted, "data-composer-command-item": "", highlighted: isHighlighted, disabled: isDisabled, onMouseDown: (event) => {
338
+ // preventDefault even when disabled — the click must never steal
339
+ // focus from the editor.
340
+ event.preventDefault();
341
+ if (!isDisabled)
342
+ navContext.selectByValue(value);
343
+ }, onMouseEnter: isDisabled ? undefined : () => navContext.setHighlightedValue(value), ...props }));
344
+ };
345
+ export const ComposerCommandItemIcon = ({ className, render, style, ...elementProps }) => useRenderElement("span", { className, render, style }, { props: [{ "data-composer-command-item-icon": "" }, elementProps] });
346
+ export const ComposerCommandItemLabel = ({ className, render, style, ...elementProps }) => useRenderElement("span", { className, render, style }, { props: [{ "data-composer-command-item-label": "" }, elementProps] });
347
+ export const ComposerCommandItemDescription = ({ className, render, style, ...elementProps }) => useRenderElement("span", { className, render, style }, { props: [{ "data-composer-command-item-description": "" }, elementProps] });
348
+ // Grouping is a render concern: this reads the resolved items, buckets them by `groupBy`
349
+ // with `Map.groupBy` (which preserves first-appearance order — Object.groupBy would reorder
350
+ // integer-like keys), and renders the child once per group inside a `command-group`
351
+ // container. Nav order stays store-driven, so a group whose items all filter out just
352
+ // doesn't render. Annotate `groupBy`'s param to type the items (no inline JSX generic).
353
+ export const ComposerCommandGroup = ({ groupBy, children: renderGroup, }) => {
354
+ const { items } = useCommandListItems();
355
+ return (_jsx(_Fragment, { children: [...Map.groupBy(items, groupBy)].map(([group, groupItems]) => (_jsx(Commands.Group, { children: renderGroup(group, groupItems) }, group))) }));
356
+ };
357
+ export const ComposerCommandGroupLabel = ({ className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, { props: [{ "data-composer-command-group-label": "" }, elementProps] });
@@ -0,0 +1,28 @@
1
+ import type { PrimitiveProps } from "../internal/primitive-props.js";
2
+ type CommandsRootProps = PrimitiveProps<"div">;
3
+ type CommandsGroupProps = PrimitiveProps<"div">;
4
+ type CommandsGroupLabelProps = PrimitiveProps<"div">;
5
+ export type CommandsItemState = {
6
+ /** Present as data-highlighted while keyboard/hover highlighted. */
7
+ highlighted: boolean;
8
+ /** Present as data-disabled while the row is inert. */
9
+ disabled: boolean;
10
+ };
11
+ export type CommandsItemProps = PrimitiveProps<"button", CommandsItemState> & {
12
+ highlighted?: boolean;
13
+ disabled?: boolean;
14
+ };
15
+ type CommandsItemLabelProps = PrimitiveProps<"span">;
16
+ type CommandsItemDescriptionProps = PrimitiveProps<"span">;
17
+ type CommandsEmptyProps = PrimitiveProps<"div">;
18
+ type CommandsFooterProps = PrimitiveProps<"div">;
19
+ export declare const Commands: (({ className, render, style, ...elementProps }: CommandsRootProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>) & {
20
+ Group: ({ className, render, style, ...elementProps }: CommandsGroupProps) => import("react").JSX.Element;
21
+ GroupLabel: ({ className, render, style, ...elementProps }: CommandsGroupLabelProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
22
+ Item: ({ highlighted, disabled, className, render, style, ...elementProps }: CommandsItemProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
23
+ ItemLabel: ({ className, render, style, ...elementProps }: CommandsItemLabelProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
24
+ ItemDescription: ({ className, render, style, ...elementProps }: CommandsItemDescriptionProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
25
+ Empty: ({ children, className, render, style, ...elementProps }: CommandsEmptyProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
26
+ Footer: ({ className, render, style, ...elementProps }: CommandsFooterProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
27
+ };
28
+ export {};
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ // Command-list structural primitives used internally by the composer's command
4
+ // palette (see command-list.tsx). Pure markup with part-attribute hooks — selection
5
+ // and keyboard behavior live in the composer's command plumbing; styling belongs
6
+ // to the styled layer. Not a public export.
7
+ import { createContext, use, useId } from "react";
8
+ import { useRenderElement } from "../internal/render/useRenderElement.js";
9
+ const CommandsRoot = ({ className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, { props: [{ "data-command-list": "" }, elementProps] });
10
+ // Group → its label association: the group mints the id, the label stamps it.
11
+ // role="group" keeps listbox→option ownership intact through the wrapper div.
12
+ const GroupLabelIdContext = createContext("");
13
+ const CommandsGroup = ({ className, render, style, ...elementProps }) => {
14
+ const labelId = useId();
15
+ const element = useRenderElement("div", { className, render, style }, {
16
+ props: [
17
+ { role: "group", "aria-labelledby": labelId, "data-command-group": "" },
18
+ elementProps,
19
+ ],
20
+ });
21
+ return _jsx(GroupLabelIdContext, { value: labelId, children: element });
22
+ };
23
+ const CommandsGroupLabel = ({ className, render, style, ...elementProps }) => {
24
+ const labelId = use(GroupLabelIdContext);
25
+ return useRenderElement("div", { className, render, style }, { props: [{ id: labelId || undefined, "data-command-group-label": "" }, elementProps] });
26
+ };
27
+ const CommandsItem = ({ highlighted = false, disabled = false, className, render, style, ...elementProps }) => useRenderElement("button", { className, render, style }, {
28
+ state: { highlighted, disabled },
29
+ // Options, not buttons: keyboard selection stays in the editor
30
+ // (activedescendant pattern), so rows must never be tab stops or
31
+ // announce as "button". Disabled is aria-disabled, never the native
32
+ // attribute — a natively disabled button swallows the mousedown that
33
+ // keeps focus in the editor.
34
+ props: [
35
+ {
36
+ role: "option",
37
+ tabIndex: -1,
38
+ "aria-disabled": disabled || undefined,
39
+ "data-command-item": "",
40
+ },
41
+ elementProps,
42
+ ],
43
+ });
44
+ const CommandsItemLabel = ({ className, render, style, ...elementProps }) => useRenderElement("span", { className, render, style }, { props: [{ "data-command-item-label": "" }, elementProps] });
45
+ const CommandsItemDescription = ({ className, render, style, ...elementProps }) => useRenderElement("span", { className, render, style }, { props: [{ "data-command-item-description": "" }, elementProps] });
46
+ const CommandsEmpty = ({ children, className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, { props: [{ "data-command-empty": "", children }, elementProps] });
47
+ const CommandsFooter = ({ className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, { props: [{ "data-command-footer": "" }, elementProps] });
48
+ export const Commands = Object.assign(CommandsRoot, {
49
+ Group: CommandsGroup,
50
+ GroupLabel: CommandsGroupLabel,
51
+ Item: CommandsItem,
52
+ ItemLabel: CommandsItemLabel,
53
+ ItemDescription: CommandsItemDescription,
54
+ Empty: CommandsEmpty,
55
+ Footer: CommandsFooter,
56
+ });
@@ -0,0 +1,3 @@
1
+ import type { PrimitiveProps } from "../internal/primitive-props.js";
2
+ export type ComposerContainerProps = PrimitiveProps<"div">;
3
+ export declare const ComposerContainer: ({ className, render, style, ...elementProps }: ComposerContainerProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { useRenderElement } from "../internal/render/useRenderElement.js";
3
+ import { useComposerContextStore } from "./store.js";
4
+ export const ComposerContainer = ({ className, render, style, ...elementProps }) => {
5
+ const store = useComposerContextStore();
6
+ const handleMouseDown = (event) => {
7
+ const target = event.target;
8
+ // The editor and interactive controls own their mouse behavior. Calling
9
+ // preventDefault on a mousedown inside the editable area would cancel the
10
+ // browser's text-selection drag, so only hijack focus when the click lands
11
+ // on the surrounding chrome.
12
+ if (target.closest("button, a, input, [data-composer-editor]")) {
13
+ return;
14
+ }
15
+ event.preventDefault();
16
+ store.controller.ensureFocus();
17
+ };
18
+ return useRenderElement("div", { className, render, style }, {
19
+ // Register the element so Composer.Popover can observe it for repositioning.
20
+ ref: store.containerRef,
21
+ // No role/tabIndex: the chrome is a mouse-only focus passthrough, not a
22
+ // control — keyboard users tab straight to the editor, and an unnamed
23
+ // focusable "button" would only add a broken tab stop.
24
+ props: [
25
+ {
26
+ "data-composer-container": "",
27
+ onMouseDown: handleMouseDown,
28
+ },
29
+ elementProps,
30
+ ],
31
+ });
32
+ };
@@ -0,0 +1,3 @@
1
+ import { type ComposerEditorState } from "./store.js";
2
+ export type { ComposerEditorState };
3
+ export declare const useComposerController: () => ComposerEditorState;
@@ -0,0 +1,6 @@
1
+ "use client";
2
+ // Imperative composer access from inside the tree. From outside it, the
3
+ // Composer.createStore() handle carries the same surface directly:
4
+ // store.controller.focus() — no hook, no context.
5
+ import { useComposerContextStore } from "./store.js";
6
+ export const useComposerController = () => useComposerContextStore().controller;
@@ -0,0 +1,55 @@
1
+ import type { ChipData } from "../chip-markdown.js";
2
+ import { type SegmentDoc } from "./segments.js";
3
+ export type ReadableNode = {
4
+ nodeType: number;
5
+ nodeName: string;
6
+ /** Text node content (nodeType 3). */
7
+ data?: string;
8
+ childNodes: ArrayLike<ReadableNode>;
9
+ getAttribute?: (name: string) => string | null;
10
+ };
11
+ export declare const toReadable: (node: Node) => ReadableNode;
12
+ export declare const readDocumentFromDom: (root: ReadableNode, resolveChip: (id: string) => ChipData | null, makeId?: () => string) => {
13
+ doc: SegmentDoc;
14
+ dirty: boolean;
15
+ };
16
+ /** Logical position of (node, offset), or null when the point isn't inside root. */
17
+ export declare const logicalRangeFromDom: (root: ReadableNode, targetNode: ReadableNode, targetOffset: number) => number | null;
18
+ export type DomNodeSpec = {
19
+ kind: "text";
20
+ text: string;
21
+ } | {
22
+ kind: "br";
23
+ padding: boolean;
24
+ } | {
25
+ kind: "chip";
26
+ id: string;
27
+ };
28
+ export declare const documentToDomSpec: (doc: SegmentDoc) => DomNodeSpec[];
29
+ /**
30
+ * Render the canonical child list. Existing chip spans are reused by id —
31
+ * moving a node preserves its React portal; recreating it would remount the
32
+ * chip content. Returns the live id → element map for the portal layer.
33
+ */
34
+ export declare const renderDocumentToDom: (root: HTMLElement, doc: SegmentDoc) => Map<string, HTMLElement>;
35
+ /** DOM point for a logical position, preferring text-node points. */
36
+ export declare const domPointFromLogical: (root: HTMLElement, position: number) => {
37
+ node: Node;
38
+ offset: number;
39
+ };
40
+ export declare const readSelectionRange: (root: HTMLElement) => {
41
+ start: number;
42
+ end: number;
43
+ } | null;
44
+ export declare const writeCaretToDom: (root: HTMLElement, start: number, end?: number) => void;
45
+ export type BadgeToken = {
46
+ start: number;
47
+ end: number;
48
+ showPlaceholder: boolean;
49
+ };
50
+ /**
51
+ * Make the DOM match the token (or its absence). Returns true when the DOM
52
+ * changed — the caller must restore the caret afterwards.
53
+ */
54
+ export declare const syncBadge: (root: HTMLElement, token: BadgeToken | null) => boolean;
55
+ export declare const scrollCaretIntoView: (root: HTMLElement) => void;