@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,102 @@
1
+ "use client";
2
+ // Component-wiring shared by the composer parts: the internals context
3
+ // (command registry, editor-update reporting) and the small hooks the parts
4
+ // build on. Not consumer API.
5
+ import { createContext, use, useCallback, useEffect, useLayoutEffect, useRef, } from "react";
6
+ import { createDragHandlers } from "./attachments-machine.js";
7
+ // ---------------------------------------------------------------------------
8
+ // Generic hooks — small, composer-agnostic utilities the parts build on.
9
+ // ---------------------------------------------------------------------------
10
+ // SSR-safe layout effect — same shape as cmdk's. useLayoutEffect runs before
11
+ // paint; useEffect is a no-op fallback when window is undefined (SSR pass).
12
+ export const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
13
+ // Mirror a prop into a ref so closures always see the latest value without
14
+ // having to add the prop to dep arrays. Layout effect ensures `.current` is
15
+ // updated before any sibling layout effect or sync user event observes it.
16
+ export const useAsRef = (value) => {
17
+ const ref = useRef(value);
18
+ useIsomorphicLayoutEffect(() => {
19
+ ref.current = value;
20
+ });
21
+ return ref;
22
+ };
23
+ export const ComposerInternalsContext = createContext(null);
24
+ export const useComposerInternals = () => {
25
+ const context = use(ComposerInternalsContext);
26
+ if (!context) {
27
+ throw new Error("useComposerInternals must be called inside a <Composer> subtree.");
28
+ }
29
+ return context;
30
+ };
31
+ // ---------------------------------------------------------------------------
32
+ // Feature hooks — the composer-specific wiring: command registry, drag-and-drop
33
+ // file intake, and the controlled/uncontrolled snapshot bridge.
34
+ // ---------------------------------------------------------------------------
35
+ export const useCommandRegistry = (commands) => {
36
+ const registryRef = useAsRef(commands);
37
+ const getRegisteredPrefixes = useCallback(() => {
38
+ const result = [];
39
+ for (const [prefix, entry] of Object.entries(registryRef.current)) {
40
+ result.push({ prefix, triggerRule: entry.trigger });
41
+ }
42
+ return result;
43
+ }, []);
44
+ return { getRegisteredPrefixes };
45
+ };
46
+ export const useDragDropFiles = ({ rootRef, globalDropRef, onFiles, setDragging, }) => {
47
+ const onFilesRef = useAsRef(onFiles);
48
+ useEffect(() => {
49
+ const isInScope = (event) => globalDropRef.current || (rootRef.current?.contains(event.target) ?? false);
50
+ const handlers = createDragHandlers({
51
+ isInScope,
52
+ onFiles: (files) => onFilesRef.current(files),
53
+ setDragging,
54
+ });
55
+ document.addEventListener("dragover", handlers.onDragOver);
56
+ document.addEventListener("dragenter", handlers.onDragEnter);
57
+ document.addEventListener("dragleave", handlers.onDragLeave);
58
+ document.addEventListener("drop", handlers.onDrop);
59
+ return () => {
60
+ document.removeEventListener("dragover", handlers.onDragOver);
61
+ document.removeEventListener("dragenter", handlers.onDragEnter);
62
+ document.removeEventListener("dragleave", handlers.onDragLeave);
63
+ document.removeEventListener("drop", handlers.onDrop);
64
+ };
65
+ }, [rootRef, globalDropRef, setDragging]);
66
+ };
67
+ export const useComposerSnapshot = ({ editorRef, defaultValue, value, onValueChange, }) => {
68
+ const isControlled = value !== undefined;
69
+ const lastAppliedRef = useRef(null);
70
+ const initializedRef = useRef(false);
71
+ const onValueChangeRef = useAsRef(onValueChange);
72
+ useEffect(() => {
73
+ const editor = editorRef.current;
74
+ if (!editor || initializedRef.current)
75
+ return;
76
+ if (defaultValue) {
77
+ editor.applySnapshot(defaultValue);
78
+ lastAppliedRef.current = defaultValue;
79
+ }
80
+ initializedRef.current = true;
81
+ }, [editorRef, defaultValue]);
82
+ useEffect(() => {
83
+ if (!isControlled)
84
+ return;
85
+ const editor = editorRef.current;
86
+ if (!editor || !value)
87
+ return;
88
+ if (lastAppliedRef.current === value)
89
+ return;
90
+ editor.applySnapshot(value);
91
+ lastAppliedRef.current = value;
92
+ }, [editorRef, isControlled, value]);
93
+ const reportEditorUpdate = useCallback(() => {
94
+ const editor = editorRef.current;
95
+ if (!editor || !onValueChangeRef.current)
96
+ return;
97
+ const snapshot = editor.getSnapshot();
98
+ lastAppliedRef.current = snapshot;
99
+ onValueChangeRef.current(snapshot);
100
+ }, [editorRef]);
101
+ return { reportEditorUpdate };
102
+ };
@@ -0,0 +1,61 @@
1
+ export type EditorKeyAction = {
2
+ type: "command-select";
3
+ } | {
4
+ type: "command-close";
5
+ } | {
6
+ type: "command-navigate";
7
+ direction: 1 | -1;
8
+ } | {
9
+ type: "command-caret";
10
+ direction: 1 | -1;
11
+ } | {
12
+ type: "ask-user-arrow";
13
+ direction: 1 | -1;
14
+ } | {
15
+ type: "ask-user-dismiss";
16
+ } | {
17
+ type: "remove-last-attachment";
18
+ } | {
19
+ type: "submit-form";
20
+ } | {
21
+ type: "soft-break";
22
+ };
23
+ /** Which Enter chord sends the message; the other inserts a soft break. */
24
+ export type ComposerSubmitOn = "enter" | "shift-enter";
25
+ export type EditorKeyContext = {
26
+ isCommandListOpen: boolean;
27
+ hasActiveAskUser: boolean;
28
+ isEditorEmpty: boolean;
29
+ hasAttachments: boolean;
30
+ /** Defaults to "enter" (Enter sends, Shift+Enter breaks). */
31
+ submitOn?: ComposerSubmitOn;
32
+ };
33
+ export declare const interpretEditorKey: (event: {
34
+ key: string;
35
+ shiftKey: boolean;
36
+ }, context: EditorKeyContext) => EditorKeyAction | null;
37
+ export type AskUserKeyAction = {
38
+ type: "dismiss-step";
39
+ } | {
40
+ type: "navigate-options";
41
+ direction: 1 | -1;
42
+ } | {
43
+ type: "select-option";
44
+ } | {
45
+ type: "go-back";
46
+ } | {
47
+ type: "go-next";
48
+ } | {
49
+ type: "insert-character";
50
+ character: string;
51
+ };
52
+ export type AskUserKeyEvent = {
53
+ key: string;
54
+ ctrlKey: boolean;
55
+ metaKey: boolean;
56
+ altKey: boolean;
57
+ defaultPrevented: boolean;
58
+ };
59
+ export declare const interpretAskUserKey: (event: AskUserKeyEvent, context: {
60
+ hasHighlight: boolean;
61
+ }) => AskUserKeyAction | null;
@@ -0,0 +1,75 @@
1
+ // Keyboard interpreters — translate a raw key event (reduced to plain data)
2
+ // plus surrounding state into a high-level action. No React or DOM access:
3
+ // the component layer decides what a key means here and keeps the how
4
+ // (preventDefault, focus, dispatch) at the call site.
5
+ export const interpretEditorKey = (event, context) => {
6
+ const { key, shiftKey } = event;
7
+ const { isCommandListOpen, hasActiveAskUser } = context;
8
+ switch (true) {
9
+ case isCommandListOpen && key === "Tab":
10
+ return { type: "command-select" };
11
+ case isCommandListOpen && key === "Escape":
12
+ return { type: "command-close" };
13
+ case isCommandListOpen && key === "ArrowUp":
14
+ return { type: "command-navigate", direction: -1 };
15
+ case isCommandListOpen && key === "ArrowDown":
16
+ return { type: "command-navigate", direction: 1 };
17
+ case isCommandListOpen && key === "ArrowLeft":
18
+ return { type: "command-caret", direction: -1 };
19
+ case isCommandListOpen && key === "ArrowRight":
20
+ return { type: "command-caret", direction: 1 };
21
+ case isCommandListOpen && key === "Enter" && !shiftKey:
22
+ return { type: "command-select" };
23
+ case hasActiveAskUser && key === "ArrowUp":
24
+ return { type: "ask-user-arrow", direction: -1 };
25
+ case hasActiveAskUser && key === "ArrowDown":
26
+ return { type: "ask-user-arrow", direction: 1 };
27
+ // Question-mode keys now attach to the options container (roving focus),
28
+ // so an editor-focused Escape must dismiss from here.
29
+ case hasActiveAskUser && key === "Escape":
30
+ return { type: "ask-user-dismiss" };
31
+ case key === "Backspace" && context.isEditorEmpty && context.hasAttachments:
32
+ return { type: "remove-last-attachment" };
33
+ // Send / soft break — the chord mapping swaps on submitOn. Sending stays
34
+ // suppressed while the command list is open (its own Enter branch above
35
+ // selects; the surviving chord falls through to a soft break here, so
36
+ // neither mapping can submit mid-popup).
37
+ case key === "Enter": {
38
+ const sendChordPressed = (context.submitOn ?? "enter") === "enter" ? !shiftKey : shiftKey;
39
+ return sendChordPressed && !isCommandListOpen
40
+ ? { type: "submit-form" }
41
+ : { type: "soft-break" };
42
+ }
43
+ default:
44
+ return null;
45
+ }
46
+ };
47
+ export const interpretAskUserKey = (event, context) => {
48
+ const { key } = event;
49
+ switch (true) {
50
+ case event.defaultPrevented:
51
+ return null;
52
+ case key === "Escape":
53
+ return { type: "dismiss-step" };
54
+ case !context.hasHighlight:
55
+ return null;
56
+ case key === "ArrowUp":
57
+ return { type: "navigate-options", direction: -1 };
58
+ case key === "ArrowDown":
59
+ return { type: "navigate-options", direction: 1 };
60
+ case key === "Enter":
61
+ return { type: "select-option" };
62
+ // APG radio/checkbox: Space toggles the focused option. Wins over the
63
+ // printable branch — an answer can't meaningfully start with a space.
64
+ case key === " ":
65
+ return { type: "select-option" };
66
+ case key === "ArrowLeft":
67
+ return { type: "go-back" };
68
+ case key === "ArrowRight":
69
+ return { type: "go-next" };
70
+ default: {
71
+ const isPrintable = key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
72
+ return isPrintable ? { type: "insert-character", character: key } : null;
73
+ }
74
+ }
75
+ };
@@ -0,0 +1,28 @@
1
+ import { type ReactNode, type RefObject } from "react";
2
+ import type { PrimitiveProps } from "../internal/primitive-props.js";
3
+ import { type AnchorAlign, type AnchorSide } from "../internal/render/positioning.js";
4
+ import { type TransitionStatus } from "../internal/render/transition.js";
5
+ import { type ComposerState } from "./store.js";
6
+ export type ComposerPanelState = {
7
+ open: boolean;
8
+ transitionStatus: TransitionStatus;
9
+ };
10
+ export type ComposerPanelChildren = ReactNode | ((composer: ComposerState) => ReactNode);
11
+ export type ComposerPanelProps = Omit<PrimitiveProps<"div", ComposerPanelState>, "children"> & {
12
+ children?: ComposerPanelChildren;
13
+ /**
14
+ * Positioned, portaled overlay (default `true`, anchored to the composer Container) or an
15
+ * in-flow block (`false`). Pass a ref/element to anchor elsewhere. Match the anchor's width
16
+ * with `width: var(--anchor-width)` in your styling.
17
+ */
18
+ anchor?: boolean | Element | RefObject<Element | null>;
19
+ /** Overlay preferred side; flips to the opposite on collision. Default "top". */
20
+ side?: AnchorSide;
21
+ /** Overlay alignment along the side. Default "center". */
22
+ align?: AnchorAlign;
23
+ /** Overlay gap between the anchor and the panel. Default 0. */
24
+ sideOffset?: number;
25
+ /** Hold `side`/`align` without collision repositioning (drops flip + shift). Default false. */
26
+ pin?: boolean;
27
+ };
28
+ export declare const ComposerPanel: ({ className, render, style, children, anchor, side, align, sideOffset, pin, ...elementProps }: ComposerPanelProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | null;
@@ -0,0 +1,80 @@
1
+ "use client";
2
+ // Composer.Panel — a surface region for the command list / ask-user / steps. `children`
3
+ // is either normal nodes or a callback receiving the whole composer state, so you can pick
4
+ // what to show by priority: `{(c) => c.commands.active ? <CommandList/> : …}`.
5
+ //
6
+ // Two layouts:
7
+ // • positioned overlay (default, `anchor`) — collision-aware (flip/shift/size) + portaled to
8
+ // the body via useAnchorPositioning; `anchor` (`true`) anchors to the composer Container, a
9
+ // ref/element anchors elsewhere. The positioner publishes --anchor-width /
10
+ // --anchor-available-height for the styling layer (e.g. width: var(--anchor-width)); it
11
+ // doesn't set the panel's own width. `pin` opts out of flip/shift (holds side/align).
12
+ // • in-flow block (`anchor={false}`) — grows the composer; contained by the styling layer's
13
+ // max-height. No positioning, no portal.
14
+ //
15
+ // Base UI-style open/close lifecycle either way: `open` tracks whether the resolved content is
16
+ // non-empty (flips false the instant the source closes); the host stays mounted through its
17
+ // close animation (keeps rendering the last content, exposes data-ending-style) and unmounts
18
+ // only once animations finish (useOpenChangeComplete → store.finalizePanelClose clears the
19
+ // native slices' sticky `present`). Seam: data-open / data-closed / data-starting-style /
20
+ // data-ending-style (+ data-side/data-align when positioned) — animate with CSS or Motion.
21
+ import { Children, useCallback, useRef } from "react";
22
+ import { createPortal } from "react-dom";
23
+ import { useAnchorPositioning, } from "../internal/render/positioning.js";
24
+ import { useOpenTransition } from "../internal/render/transition.js";
25
+ import { useRenderElement } from "../internal/render/useRenderElement.js";
26
+ import { openStateMapping, transitionStatusMapping } from "../internal/state-mappings.js";
27
+ import { useComposer, useComposerContextStore } from "./store.js";
28
+ export const ComposerPanel = ({ className, render, style, children, anchor = true, side = "top", align = "center", sideOffset = 0, pin = false, ...elementProps }) => {
29
+ const store = useComposerContextStore();
30
+ const composer = useComposer((c) => c);
31
+ const content = typeof children === "function" ? children(composer) : children;
32
+ // Logical open — from the raw, source-gated content, so it goes false immediately on close.
33
+ const open = Children.toArray(content).length > 0;
34
+ const ref = useRef(null);
35
+ // Freeze the last non-empty content so the panel keeps rendering it while it animates closed.
36
+ // React reconciles by type+position, so the content fiber (e.g. the command list) isn't
37
+ // remounted — and it survives store-driven nulling because the native slices' sticky
38
+ // `present` flag keeps its own gate open until finalizePanelClose() below.
39
+ const lastContentRef = useRef(content);
40
+ if (open)
41
+ lastContentRef.current = content;
42
+ const rendered = open ? content : lastContentRef.current;
43
+ // Stay mounted through the exit; once the close animation finishes, unmount and clear
44
+ // `present` (via onClosed) so the frozen content can drop.
45
+ const { mounted, transitionStatus } = useOpenTransition(open, ref, {
46
+ onClosed: store.finalizePanelClose,
47
+ });
48
+ // Overlay mode: position against the anchor with collision handling + portal. `anchor` omitted
49
+ // (or false) → in-flow block, no positioning.
50
+ const positioned = anchor != null && anchor !== false;
51
+ const getAnchor = useCallback(() => {
52
+ if (anchor === true)
53
+ return store.containerRef.current;
54
+ if (anchor instanceof Element)
55
+ return anchor;
56
+ if (anchor && typeof anchor === "object" && "current" in anchor)
57
+ return anchor.current;
58
+ return null;
59
+ }, [anchor, store]);
60
+ useAnchorPositioning(getAnchor, ref, {
61
+ enabled: positioned && mounted,
62
+ side,
63
+ align,
64
+ sideOffset,
65
+ pin,
66
+ });
67
+ const element = useRenderElement("div", { className, render, style }, {
68
+ enabled: mounted,
69
+ state: { open, transitionStatus },
70
+ stateAttributesMapping: { ...openStateMapping, ...transitionStatusMapping },
71
+ ref,
72
+ props: [{ "data-composer-panel": "", children: rendered }, elementProps],
73
+ });
74
+ if (positioned) {
75
+ if (typeof document === "undefined")
76
+ return null;
77
+ return createPortal(element, document.body);
78
+ }
79
+ return element;
80
+ };
@@ -0,0 +1,10 @@
1
+ import type { ReactNode } from "react";
2
+ import type { PrimitiveProps } from "../internal/primitive-props.js";
3
+ export type ComposerPlaceholderProps = Omit<PrimitiveProps<"div">, "children"> & ({
4
+ placeholder: string;
5
+ children?: never;
6
+ } | {
7
+ placeholder?: never;
8
+ children: ReactNode;
9
+ });
10
+ export declare const ComposerPlaceholder: ({ placeholder, children, className, render, style, ...elementProps }: ComposerPlaceholderProps) => import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ import { useRenderElement } from "../internal/render/useRenderElement.js";
3
+ export const ComposerPlaceholder = ({ placeholder, children, className, render, style, ...elementProps }) => useRenderElement("div", { className, render, style }, {
4
+ props: [
5
+ { "data-composer-placeholder-text": "", children: placeholder ?? children },
6
+ elementProps,
7
+ ],
8
+ });
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from "react";
2
+ import type { PrimitiveProps } from "../internal/primitive-props.js";
3
+ import { type ComposerState } from "./store.js";
4
+ export type ComposerPopoverState = {
5
+ open: boolean;
6
+ };
7
+ export type ComposerPopoverProps = Omit<PrimitiveProps<"div", ComposerPopoverState>, "children"> & {
8
+ children?: ReactNode | ((composer: ComposerState) => ReactNode);
9
+ /** Hold the placement without collision repositioning (drops flip + shift). Default false. */
10
+ pin?: boolean;
11
+ };
12
+ export declare const ComposerPopover: ({ children, className, render, style, pin, ...elementProps }: ComposerPopoverProps) => import("react").ReactPortal | null;
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ // Composer.Popover — the floating alternative to a Composer.Panel for the
3
+ // command list. Where a Panel renders in-flow (growing the composer), Popover
4
+ // lifts the same CommandList children into a portal above the field, so it
5
+ // overlays instead of pushing layout. Like Panel, it's content-driven: the
6
+ // consumer gates its children on `commands.active`, so an empty popover (all
7
+ // gated out) stays closed. Positioned against the active command badge with
8
+ // collision handling (flip / shift / size) via useAnchorPositioning, portaled to
9
+ // the body. Exposes data-open/data-closed for the open/close animation plus
10
+ // data-side/data-align (from the positioner) so the transition origin can follow
11
+ // a flip, and a --anchor-available-height var so the list caps height + scrolls.
12
+ import { Children, useCallback, useRef } from "react";
13
+ import { createPortal } from "react-dom";
14
+ import { useAnchorPositioning } from "../internal/render/positioning.js";
15
+ import { useRenderElement } from "../internal/render/useRenderElement.js";
16
+ import { openStateMapping } from "../internal/state-mappings.js";
17
+ import { useComposer, useComposerContextStore } from "./store.js";
18
+ export const ComposerPopover = ({ children, className, render, style, pin = false, ...elementProps }) => {
19
+ const store = useComposerContextStore();
20
+ const composer = useComposer((c) => c);
21
+ // `children` may be a callback receiving composer state (to gate on a native's
22
+ // active flag) or plain nodes. Open while the resolved content is non-empty —
23
+ // React can't read a child's rendered null off the prop, so count is the test.
24
+ const content = typeof children === "function" ? children(composer) : children;
25
+ const open = Children.toArray(content).length > 0;
26
+ const elementRef = useRef(null);
27
+ // Anchor to the active command badge, opening upward and left-aligned, with collision
28
+ // handling (flip below when there's no room above, shift into view, size to fit), tracked
29
+ // across scroll/resize/container growth. A getter (not a captured element) so the
30
+ // re-rendered badge decoration is always the current one.
31
+ const getBadge = useCallback(() => store.editorRef.current?.getRootElement()?.querySelector("[data-command-badge]") ?? null, [store]);
32
+ useAnchorPositioning(getBadge, elementRef, {
33
+ enabled: open,
34
+ side: "top",
35
+ align: "start",
36
+ sideOffset: 8,
37
+ pin,
38
+ });
39
+ const element = useRenderElement("div", { className, render, style }, {
40
+ ref: elementRef,
41
+ state: { open },
42
+ stateAttributesMapping: openStateMapping,
43
+ props: [
44
+ {
45
+ "data-composer-popover": "",
46
+ // position/left/top are written imperatively by useAnchorPositioning; the styled
47
+ // layer only supplies box/animation styling, not positioning.
48
+ // Keep the editor focused when clicking the popover chrome so it
49
+ // doesn't blur-dismiss; command items run their own selection on
50
+ // mousedown. The list never takes focus.
51
+ onMouseDown: (event) => event.preventDefault(),
52
+ children: content,
53
+ },
54
+ elementProps,
55
+ ],
56
+ });
57
+ // Stays mounted (positioning only runs while open, so a closed popover just
58
+ // holds its last position). Open/closed is exposed as data-open/data-closed
59
+ // for the styled layer to animate against — same as the other parts.
60
+ if (typeof document === "undefined")
61
+ return null;
62
+ return createPortal(element, document.body);
63
+ };
@@ -0,0 +1,23 @@
1
+ import type { TriggerRule } from "./types.js";
2
+ export type RegisteredPrefix = {
3
+ prefix: string;
4
+ triggerRule: TriggerRule;
5
+ };
6
+ export type ActiveTokenState = {
7
+ isOpen: boolean;
8
+ trigger: string | null;
9
+ query: string;
10
+ triggerStartPosition: number;
11
+ triggerEndPosition: number;
12
+ dismissedAt: number | null;
13
+ };
14
+ export declare const CLOSED_COMMAND_STATE: ActiveTokenState;
15
+ export declare const detectActivePrefix: (args: {
16
+ registered: RegisteredPrefix[];
17
+ blockStart: number;
18
+ blockEnd: number;
19
+ cursorPosition: number;
20
+ textBeforeCursor: string;
21
+ textAfterCursor: string;
22
+ fullDocText: string;
23
+ }) => ActiveTokenState;
@@ -0,0 +1,58 @@
1
+ // Prefix detection — the engine-agnostic kernel of the command system: the
2
+ // active-token state shape and the pure scan that derives it from the text
3
+ // around the caret. Sticky range tracking and dismissal memory live one layer
4
+ // up, in trigger-tracker.ts.
5
+ export const CLOSED_COMMAND_STATE = {
6
+ isOpen: false,
7
+ trigger: null,
8
+ query: "",
9
+ triggerStartPosition: 0,
10
+ triggerEndPosition: 0,
11
+ dismissedAt: null,
12
+ };
13
+ // ---------------------------------------------------------------------------
14
+ // Prefix detection — scan the block around the caret for a registered trigger
15
+ // and derive the active-token state (pure; engine integrations call it).
16
+ // ---------------------------------------------------------------------------
17
+ export const detectActivePrefix = (args) => {
18
+ const { registered, blockStart, blockEnd, cursorPosition, textBeforeCursor, textAfterCursor, fullDocText, } = args;
19
+ for (const entry of registered) {
20
+ if (entry.triggerRule === "doc-start") {
21
+ if (fullDocText.startsWith(entry.prefix)) {
22
+ return {
23
+ isOpen: true,
24
+ trigger: entry.prefix,
25
+ query: fullDocText.slice(entry.prefix.length),
26
+ triggerStartPosition: blockStart,
27
+ triggerEndPosition: blockEnd,
28
+ dismissedAt: null,
29
+ };
30
+ }
31
+ continue;
32
+ }
33
+ // The token is the contiguous non-whitespace run the caret sits inside,
34
+ // taken from both sides of the caret so it stays whole as the caret moves
35
+ // within it. Text chars map 1:1 to positions and an atomic chip can never
36
+ // sit inside a run, so the run length is the position delta on each side.
37
+ const leftRun = textBeforeCursor.match(/\S*$/)?.[0] ?? "";
38
+ const rightRun = textAfterCursor.match(/^\S*/)?.[0] ?? "";
39
+ const runText = leftRun + rightRun;
40
+ if (!runText.startsWith(entry.prefix))
41
+ continue;
42
+ // The prefix must sit at a word boundary: line start or after whitespace.
43
+ const charBeforeRun = textBeforeCursor
44
+ .slice(0, textBeforeCursor.length - leftRun.length)
45
+ .at(-1);
46
+ if (charBeforeRun !== undefined && !/\s/.test(charBeforeRun))
47
+ continue;
48
+ return {
49
+ isOpen: true,
50
+ trigger: entry.prefix,
51
+ query: runText.slice(entry.prefix.length),
52
+ triggerStartPosition: cursorPosition - leftRun.length,
53
+ triggerEndPosition: cursorPosition + rightRun.length,
54
+ dismissedAt: null,
55
+ };
56
+ }
57
+ return CLOSED_COMMAND_STATE;
58
+ };
@@ -0,0 +1,21 @@
1
+ import type { PrimitiveProps } from "../internal/primitive-props.js";
2
+ import { type ComposerStore } from "./store.js";
3
+ import type { AskUserQuestion, ComposerCommandsMap, ComposerSnapshot, ComposerSubmitData } from "./types.js";
4
+ export type ComposerRootState = {
5
+ /** Present as data-submitting while a submission is in flight. */
6
+ submitting: boolean;
7
+ /** Present as data-dragging while files are dragged over the drop scope. */
8
+ dragging: boolean;
9
+ };
10
+ export type ComposerRootProps = Omit<PrimitiveProps<"form", ComposerRootState>, "onSubmit"> & {
11
+ onSubmit?: (data: ComposerSubmitData) => void | Promise<void>;
12
+ isSubmitting?: boolean;
13
+ commands?: ComposerCommandsMap;
14
+ questions?: AskUserQuestion[];
15
+ defaultValue?: ComposerSnapshot;
16
+ value?: ComposerSnapshot;
17
+ onValueChange?: (snapshot: ComposerSnapshot) => void;
18
+ /** Explicit Composer.createStore() handle; defaults to a per-mount instance. */
19
+ store?: ComposerStore;
20
+ };
21
+ export declare const ComposerRoot: ({ onSubmit, isSubmitting, commands, questions, defaultValue, value, onValueChange, store: storeProp, className, render, style, ...elementProps }: ComposerRootProps) => import("react").JSX.Element;
@@ -0,0 +1,107 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ // Composer.Root — the form element that owns the submit flow, prop→store
4
+ // bridges, drag-drop scope, and the per-instance store resolution: an explicit
5
+ // Composer.createStore() handle via the store prop, or an instance created for
6
+ // this mount. Every bare <Composer> is fully isolated.
7
+ import { useEffect, useId, useMemo, useRef, useSyncExternalStore } from "react";
8
+ import { useRefWithInit } from "../internal/render/useRefWithInit.js";
9
+ import { useRenderElement } from "../internal/render/useRenderElement.js";
10
+ import { ComposerInternalsContext, useAsRef, useCommandRegistry, useComposerSnapshot, useDragDropFiles, } from "./internals.js";
11
+ import { ComposerStoreContext, createComposerStore } from "./store.js";
12
+ const EMPTY_COMMANDS = {};
13
+ export const ComposerRoot = ({ onSubmit, isSubmitting = false, commands = EMPTY_COMMANDS, questions, defaultValue, value, onValueChange, store: storeProp, className, render, style, ...elementProps }) => {
14
+ // SSR-stable per-mount id for the command listbox (aria-controls target +
15
+ // option-id root). Assigned onto the store in init below — the factory
16
+ // can't mint it because explicit handles are created outside React.
17
+ const listboxId = useId();
18
+ // Resolved once at mount (lazy-init ref, not reactive state): an explicit
19
+ // handle, or an instance this mount creates and owns. Swapping the store
20
+ // prop after mount is not supported.
21
+ const { store, ownsStore } = useRefWithInit(() => {
22
+ const resolved = storeProp ?? createComposerStore();
23
+ resolved.listboxId = listboxId;
24
+ return { store: resolved, ownsStore: !storeProp };
25
+ }).current;
26
+ const formRef = useRef(null);
27
+ const onSubmitRef = useAsRef(onSubmit);
28
+ // Register this mount on the store: answers submit through this mount's
29
+ // onSubmit. Only a store this mount created gets reset on unmount — an
30
+ // explicit handle's state belongs to its owner and survives remounts.
31
+ useEffect(() => {
32
+ store.submitAnswersRef.current = (answers) => onSubmitRef.current?.({ kind: "answers", answers });
33
+ return () => {
34
+ store.submitAnswersRef.current = null;
35
+ if (ownsStore)
36
+ store.reset();
37
+ };
38
+ }, [store, ownsStore]);
39
+ // Prop → store bridges. Actions and refs on the snapshot are identity-stable,
40
+ // so reading them here without subscribing is safe.
41
+ const { add: addAttachments, globalDropRef } = store.getSnapshot().attachments;
42
+ // Subscribed on the resolved store (not through useComposer — a `store` prop
43
+ // may differ from the nearest context) to surface drag state as data-dragging.
44
+ const getIsDragging = () => store.getSnapshot().attachments.isDragging;
45
+ const isDragging = useSyncExternalStore(store.subscribe, getIsDragging, getIsDragging);
46
+ useEffect(() => {
47
+ store.setIsSubmitting(isSubmitting);
48
+ }, [store, isSubmitting]);
49
+ // Sync the questions prop into the store and arm ask-user mode (editor blur
50
+ // + document-level keyboard handling) while questions are active.
51
+ useEffect(() => {
52
+ store.setQuestions(questions ?? null);
53
+ if (!questions?.length)
54
+ return;
55
+ return store.activateAskUser();
56
+ }, [store, questions]);
57
+ const { getRegisteredPrefixes } = useCommandRegistry(commands);
58
+ useDragDropFiles({
59
+ rootRef: formRef,
60
+ globalDropRef,
61
+ onFiles: addAttachments,
62
+ setDragging: store.setDragging,
63
+ });
64
+ const { reportEditorUpdate } = useComposerSnapshot({
65
+ editorRef: store.editorRef,
66
+ defaultValue,
67
+ value,
68
+ onValueChange,
69
+ });
70
+ const handleFormSubmit = async (event) => {
71
+ event.preventDefault();
72
+ const { askUser, attachments } = store.getSnapshot();
73
+ if (askUser.questions?.length) {
74
+ askUser.continueStep(store.controller.getText());
75
+ return;
76
+ }
77
+ if (isSubmitting)
78
+ return;
79
+ const serialized = store.controller.serialize();
80
+ const trimmedText = serialized.text.trim();
81
+ if (!trimmedText && !attachments.items.length)
82
+ return;
83
+ // Emit the generic attachment descriptors; the consumer's onSubmit adapts
84
+ // them to its wire format (e.g. FilePart via prepareAttachmentsForSend).
85
+ // Text may be empty on an attachments-only submit — any placeholder copy
86
+ // is the consumer's.
87
+ const files = attachments.items;
88
+ store.resetAttachments();
89
+ store.controller.clear();
90
+ await onSubmitRef.current?.({
91
+ kind: "message",
92
+ text: trimmedText,
93
+ files,
94
+ });
95
+ };
96
+ const internalsValue = useMemo(() => ({
97
+ commands,
98
+ getRegisteredPrefixes,
99
+ reportEditorUpdate,
100
+ }), [commands, getRegisteredPrefixes, reportEditorUpdate]);
101
+ const formElement = useRenderElement("form", { className, render, style }, {
102
+ state: { submitting: isSubmitting, dragging: isDragging },
103
+ ref: formRef,
104
+ props: [{ "data-composer-root": "", onSubmit: handleFormSubmit }, elementProps],
105
+ });
106
+ return (_jsx(ComposerStoreContext, { value: store, children: _jsx(ComposerInternalsContext, { value: internalsValue, children: formElement }) }));
107
+ };