@assistant-ui/react 0.12.23 → 0.12.24

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 (127) hide show
  1. package/dist/client/ExternalThread.d.ts.map +1 -1
  2. package/dist/client/ExternalThread.js +1 -0
  3. package/dist/client/ExternalThread.js.map +1 -1
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +4 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/internal.d.ts +1 -0
  9. package/dist/internal.d.ts.map +1 -1
  10. package/dist/internal.js +2 -0
  11. package/dist/internal.js.map +1 -1
  12. package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
  13. package/dist/primitives/composer/ComposerInput.js +27 -12
  14. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  15. package/dist/primitives/composer/ComposerInputPluginContext.d.ts +31 -0
  16. package/dist/primitives/composer/ComposerInputPluginContext.d.ts.map +1 -0
  17. package/dist/primitives/composer/ComposerInputPluginContext.js +32 -0
  18. package/dist/primitives/composer/ComposerInputPluginContext.js.map +1 -0
  19. package/dist/primitives/composer/mention/ComposerMentionContext.d.ts +4 -2
  20. package/dist/primitives/composer/mention/ComposerMentionContext.d.ts.map +1 -1
  21. package/dist/primitives/composer/mention/ComposerMentionContext.js +21 -13
  22. package/dist/primitives/composer/mention/ComposerMentionContext.js.map +1 -1
  23. package/dist/primitives/composer/mention/index.d.ts +4 -4
  24. package/dist/primitives/composer/mention/index.d.ts.map +1 -1
  25. package/dist/primitives/composer/mention/index.js +6 -4
  26. package/dist/primitives/composer/mention/index.js.map +1 -1
  27. package/dist/primitives/composer/slash-command/ComposerSlashCommandRoot.d.ts +36 -0
  28. package/dist/primitives/composer/slash-command/ComposerSlashCommandRoot.d.ts.map +1 -0
  29. package/dist/primitives/composer/slash-command/ComposerSlashCommandRoot.js +36 -0
  30. package/dist/primitives/composer/slash-command/ComposerSlashCommandRoot.js.map +1 -0
  31. package/dist/primitives/composer/slash-command/index.d.ts +2 -0
  32. package/dist/primitives/composer/slash-command/index.d.ts.map +1 -0
  33. package/dist/primitives/composer/slash-command/index.js +2 -0
  34. package/dist/primitives/composer/slash-command/index.js.map +1 -0
  35. package/dist/primitives/composer/{mention/ComposerMentionBack.d.ts → trigger/TriggerPopoverBack.d.ts} +3 -10
  36. package/dist/primitives/composer/trigger/TriggerPopoverBack.d.ts.map +1 -0
  37. package/dist/primitives/composer/trigger/TriggerPopoverBack.js +19 -0
  38. package/dist/primitives/composer/trigger/TriggerPopoverBack.js.map +1 -0
  39. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts +38 -0
  40. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts.map +1 -0
  41. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +35 -0
  42. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -0
  43. package/dist/primitives/composer/trigger/TriggerPopoverContext.d.ts +37 -0
  44. package/dist/primitives/composer/trigger/TriggerPopoverContext.d.ts.map +1 -0
  45. package/dist/primitives/composer/trigger/TriggerPopoverContext.js +70 -0
  46. package/dist/primitives/composer/trigger/TriggerPopoverContext.js.map +1 -0
  47. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts +40 -0
  48. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts.map +1 -0
  49. package/dist/primitives/composer/trigger/TriggerPopoverItems.js +35 -0
  50. package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -0
  51. package/dist/primitives/composer/trigger/TriggerPopoverPopover.d.ts +26 -0
  52. package/dist/primitives/composer/trigger/TriggerPopoverPopover.d.ts.map +1 -0
  53. package/dist/primitives/composer/trigger/TriggerPopoverPopover.js +28 -0
  54. package/dist/primitives/composer/trigger/TriggerPopoverPopover.js.map +1 -0
  55. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +53 -0
  56. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -0
  57. package/dist/primitives/composer/{mention/MentionResource.js → trigger/TriggerPopoverResource.js} +50 -25
  58. package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -0
  59. package/dist/primitives/composer/trigger/detectTrigger.d.ts +2 -0
  60. package/dist/primitives/composer/trigger/detectTrigger.d.ts.map +1 -0
  61. package/dist/primitives/composer/{mention/detectMentionTrigger.js → trigger/detectTrigger.js} +4 -4
  62. package/dist/primitives/composer/trigger/detectTrigger.js.map +1 -0
  63. package/dist/primitives/composer/trigger/index.d.ts +7 -0
  64. package/dist/primitives/composer/trigger/index.d.ts.map +1 -0
  65. package/dist/primitives/composer/trigger/index.js +6 -0
  66. package/dist/primitives/composer/trigger/index.js.map +1 -0
  67. package/dist/primitives/composer.d.ts +10 -0
  68. package/dist/primitives/composer.d.ts.map +1 -1
  69. package/dist/primitives/composer.js +14 -0
  70. package/dist/primitives/composer.js.map +1 -1
  71. package/dist/primitives/message/MessageRoot.d.ts +25 -3
  72. package/dist/primitives/message/MessageRoot.d.ts.map +1 -1
  73. package/dist/primitives/message/MessageRoot.js +2 -2
  74. package/dist/primitives/message/MessageRoot.js.map +1 -1
  75. package/dist/primitives/thread/ThreadViewportSlack.d.ts +2 -2
  76. package/dist/primitives/thread/ThreadViewportSlack.d.ts.map +1 -1
  77. package/dist/unstable/useSlashCommandAdapter.d.ts +34 -0
  78. package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -0
  79. package/dist/unstable/useSlashCommandAdapter.js +50 -0
  80. package/dist/unstable/useSlashCommandAdapter.js.map +1 -0
  81. package/package.json +6 -6
  82. package/src/client/ExternalThread.ts +1 -0
  83. package/src/index.ts +14 -0
  84. package/src/internal.ts +3 -0
  85. package/src/primitives/composer/ComposerInput.tsx +25 -18
  86. package/src/primitives/composer/ComposerInputPluginContext.tsx +100 -0
  87. package/src/primitives/composer/mention/ComposerMentionContext.tsx +56 -22
  88. package/src/primitives/composer/mention/index.ts +11 -8
  89. package/src/primitives/composer/slash-command/ComposerSlashCommandRoot.tsx +76 -0
  90. package/src/primitives/composer/slash-command/index.ts +1 -0
  91. package/src/primitives/composer/trigger/TriggerPopoverBack.tsx +40 -0
  92. package/src/primitives/composer/{mention/ComposerMentionCategories.tsx → trigger/TriggerPopoverCategories.tsx} +33 -28
  93. package/src/primitives/composer/trigger/TriggerPopoverContext.tsx +129 -0
  94. package/src/primitives/composer/{mention/ComposerMentionItems.tsx → trigger/TriggerPopoverItems.tsx} +34 -29
  95. package/src/primitives/composer/trigger/TriggerPopoverPopover.tsx +51 -0
  96. package/src/primitives/composer/{mention/MentionResource.ts → trigger/TriggerPopoverResource.ts} +146 -98
  97. package/src/primitives/composer/{mention/detectMentionTrigger.test.ts → trigger/detectTrigger.test.ts} +15 -15
  98. package/src/primitives/composer/{mention/detectMentionTrigger.ts → trigger/detectTrigger.ts} +3 -3
  99. package/src/primitives/composer/trigger/index.ts +16 -0
  100. package/src/primitives/composer.ts +16 -0
  101. package/src/primitives/message/MessageRoot.tsx +18 -4
  102. package/src/primitives/thread/ThreadViewportSlack.tsx +2 -2
  103. package/src/tests/BaseComposerRuntimeCore.test.ts +33 -1
  104. package/src/unstable/useSlashCommandAdapter.ts +83 -0
  105. package/dist/primitives/composer/mention/ComposerMentionBack.d.ts.map +0 -1
  106. package/dist/primitives/composer/mention/ComposerMentionBack.js +0 -28
  107. package/dist/primitives/composer/mention/ComposerMentionBack.js.map +0 -1
  108. package/dist/primitives/composer/mention/ComposerMentionCategories.d.ts +0 -46
  109. package/dist/primitives/composer/mention/ComposerMentionCategories.d.ts.map +0 -1
  110. package/dist/primitives/composer/mention/ComposerMentionCategories.js +0 -32
  111. package/dist/primitives/composer/mention/ComposerMentionCategories.js.map +0 -1
  112. package/dist/primitives/composer/mention/ComposerMentionItems.d.ts +0 -50
  113. package/dist/primitives/composer/mention/ComposerMentionItems.d.ts.map +0 -1
  114. package/dist/primitives/composer/mention/ComposerMentionItems.js +0 -30
  115. package/dist/primitives/composer/mention/ComposerMentionItems.js.map +0 -1
  116. package/dist/primitives/composer/mention/ComposerMentionPopover.d.ts +0 -26
  117. package/dist/primitives/composer/mention/ComposerMentionPopover.d.ts.map +0 -1
  118. package/dist/primitives/composer/mention/ComposerMentionPopover.js +0 -28
  119. package/dist/primitives/composer/mention/ComposerMentionPopover.js.map +0 -1
  120. package/dist/primitives/composer/mention/MentionResource.d.ts +0 -39
  121. package/dist/primitives/composer/mention/MentionResource.d.ts.map +0 -1
  122. package/dist/primitives/composer/mention/MentionResource.js.map +0 -1
  123. package/dist/primitives/composer/mention/detectMentionTrigger.d.ts +0 -2
  124. package/dist/primitives/composer/mention/detectMentionTrigger.d.ts.map +0 -1
  125. package/dist/primitives/composer/mention/detectMentionTrigger.js.map +0 -1
  126. package/src/primitives/composer/mention/ComposerMentionBack.tsx +0 -55
  127. package/src/primitives/composer/mention/ComposerMentionPopover.tsx +0 -52
@@ -22,10 +22,7 @@ import { useEscapeKeydown } from "@radix-ui/react-use-escape-keydown";
22
22
  import { useOnScrollToBottom } from "../../utils/hooks/useOnScrollToBottom";
23
23
  import { useAuiState, useAui } from "@assistant-ui/store";
24
24
  import { flushResourcesSync } from "@assistant-ui/tap";
25
- import {
26
- useMentionContextOptional,
27
- useMentionInternalContext,
28
- } from "./mention/ComposerMentionContext";
25
+ import { useComposerInputPluginRegistryOptional } from "./ComposerInputPluginContext";
29
26
 
30
27
  export namespace ComposerPrimitiveInput {
31
28
  export type Element = HTMLTextAreaElement;
@@ -144,8 +141,7 @@ export const ComposerPrimitiveInput = forwardRef<
144
141
  forwardedRef,
145
142
  ) => {
146
143
  const aui = useAui();
147
- const mentionContext = useMentionContextOptional();
148
- const mentionInternalContext = useMentionInternalContext();
144
+ const pluginRegistry = useComposerInputPluginRegistryOptional();
149
145
 
150
146
  const effectiveSubmitMode =
151
147
  submitMode ?? (submitOnEnter === false ? "none" : "enter");
@@ -166,10 +162,11 @@ export const ComposerPrimitiveInput = forwardRef<
166
162
  // Only handle ESC if it originated from within this input
167
163
  if (!textareaRef.current?.contains(e.target as Node)) return;
168
164
 
169
- // Let mention popover handle Escape first
170
- if (mentionContext?.open) {
171
- mentionContext.handleKeyDown(e);
172
- return;
165
+ // Let registered plugins (mention, slash command, etc.) handle Escape first
166
+ if (pluginRegistry) {
167
+ for (const plugin of pluginRegistry.getPlugins()) {
168
+ if (plugin.handleKeyDown(e)) return;
169
+ }
173
170
  }
174
171
 
175
172
  if (!cancelOnEscape) return;
@@ -187,8 +184,12 @@ export const ComposerPrimitiveInput = forwardRef<
187
184
  // ignore IME composition events
188
185
  if (e.nativeEvent.isComposing) return;
189
186
 
190
- // Let the mention popover handle keyboard events first
191
- if (mentionContext?.handleKeyDown(e)) return;
187
+ // Let registered plugins (mention, slash command, etc.) handle keyboard events first
188
+ if (pluginRegistry) {
189
+ for (const plugin of pluginRegistry.getPlugins()) {
190
+ if (plugin.handleKeyDown(e)) return;
191
+ }
192
+ }
192
193
 
193
194
  if (e.key === "Enter") {
194
195
  const threadState = aui.thread().getState();
@@ -297,9 +298,12 @@ export const ComposerPrimitiveInput = forwardRef<
297
298
  flushResourcesSync(() => {
298
299
  aui.composer().setText(e.target.value);
299
300
  });
300
- mentionInternalContext?.setCursorPosition(
301
- e.target.selectionStart ?? e.target.value.length,
302
- );
301
+ const pos = e.target.selectionStart ?? e.target.value.length;
302
+ if (pluginRegistry) {
303
+ for (const plugin of pluginRegistry.getPlugins()) {
304
+ plugin.setCursorPosition(pos);
305
+ }
306
+ }
303
307
  },
304
308
  ),
305
309
  onKeyDown: composeEventHandlers(onKeyDown, handleKeyPress),
@@ -307,9 +311,12 @@ export const ComposerPrimitiveInput = forwardRef<
307
311
  onSelect,
308
312
  (e: React.SyntheticEvent<HTMLTextAreaElement>) => {
309
313
  const target = e.target as HTMLTextAreaElement;
310
- mentionInternalContext?.setCursorPosition(
311
- target.selectionStart ?? target.value.length,
312
- );
314
+ const pos = target.selectionStart ?? target.value.length;
315
+ if (pluginRegistry) {
316
+ for (const plugin of pluginRegistry.getPlugins()) {
317
+ plugin.setCursorPosition(pos);
318
+ }
319
+ }
313
320
  },
314
321
  ),
315
322
  onPaste: composeEventHandlers(onPaste, handlePaste),
@@ -0,0 +1,100 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useRef,
7
+ useCallback,
8
+ useMemo,
9
+ type ReactNode,
10
+ type FC,
11
+ } from "react";
12
+
13
+ // =============================================================================
14
+ // Plugin interface — any trigger system (mention, slash command, emoji, etc.)
15
+ // registers one of these with the input.
16
+ // =============================================================================
17
+
18
+ /**
19
+ * A plugin that intercepts keyboard events and cursor changes in the composer
20
+ * input. Used by trigger roots (MentionRoot, SlashCommandRoot, etc.) to handle
21
+ * popover navigation without ComposerInput knowing about specific triggers.
22
+ */
23
+ export type ComposerInputPlugin = {
24
+ /** Handle a key event. Return true if consumed (stops propagation to other plugins and default behavior). */
25
+ handleKeyDown(e: {
26
+ readonly key: string;
27
+ readonly shiftKey: boolean;
28
+ readonly ctrlKey?: boolean;
29
+ readonly metaKey?: boolean;
30
+ readonly nativeEvent?: { isComposing?: boolean };
31
+ preventDefault(): void;
32
+ }): boolean;
33
+
34
+ /** Called on every cursor position change (selection change / text change). */
35
+ setCursorPosition(pos: number): void;
36
+ };
37
+
38
+ // =============================================================================
39
+ // Registry — mutable, ref-based. No re-renders on register/unregister because
40
+ // plugins are read imperatively at event time.
41
+ // =============================================================================
42
+
43
+ export type ComposerInputPluginRegistry = {
44
+ register(plugin: ComposerInputPlugin): () => void;
45
+ getPlugins(): readonly ComposerInputPlugin[];
46
+ };
47
+
48
+ const ComposerInputPluginRegistryContext =
49
+ createContext<ComposerInputPluginRegistry | null>(null);
50
+
51
+ export const useComposerInputPluginRegistry =
52
+ (): ComposerInputPluginRegistry => {
53
+ const ctx = useContext(ComposerInputPluginRegistryContext);
54
+ if (!ctx)
55
+ throw new Error(
56
+ "useComposerInputPluginRegistry must be used within a ComposerInputPluginProvider",
57
+ );
58
+ return ctx;
59
+ };
60
+
61
+ export const useComposerInputPluginRegistryOptional =
62
+ (): ComposerInputPluginRegistry | null => {
63
+ return useContext(ComposerInputPluginRegistryContext);
64
+ };
65
+
66
+ // =============================================================================
67
+ // Provider
68
+ // =============================================================================
69
+
70
+ export const ComposerInputPluginProvider: FC<{ children: ReactNode }> = ({
71
+ children,
72
+ }) => {
73
+ const pluginsRef = useRef<Set<ComposerInputPlugin>>(new Set());
74
+ const snapshotRef = useRef<readonly ComposerInputPlugin[]>([]);
75
+
76
+ const register = useCallback((plugin: ComposerInputPlugin) => {
77
+ pluginsRef.current.add(plugin);
78
+ snapshotRef.current = Array.from(pluginsRef.current);
79
+ return () => {
80
+ pluginsRef.current.delete(plugin);
81
+ snapshotRef.current = Array.from(pluginsRef.current);
82
+ };
83
+ }, []);
84
+
85
+ const getPlugins = useCallback(
86
+ (): readonly ComposerInputPlugin[] => snapshotRef.current,
87
+ [],
88
+ );
89
+
90
+ const registry = useMemo<ComposerInputPluginRegistry>(
91
+ () => ({ register, getPlugins }),
92
+ [register, getPlugins],
93
+ );
94
+
95
+ return (
96
+ <ComposerInputPluginRegistryContext.Provider value={registry}>
97
+ {children}
98
+ </ComposerInputPluginRegistryContext.Provider>
99
+ );
100
+ };
@@ -10,21 +10,25 @@ import {
10
10
  type ReactNode,
11
11
  type FC,
12
12
  } from "react";
13
- import { useResource } from "@assistant-ui/tap/react";
14
- import { useAui, useAuiState } from "@assistant-ui/store";
13
+ import { useAui } from "@assistant-ui/store";
15
14
  import type {
16
15
  Unstable_MentionAdapter,
17
16
  Unstable_DirectiveFormatter,
18
17
  } from "@assistant-ui/core";
19
18
  import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
20
- import {
21
- MentionResource,
22
- type MentionResourceOutput,
23
- type SelectItemOverride,
24
- } from "./MentionResource";
19
+ import { ComposerPrimitiveTriggerPopoverRoot } from "../trigger/TriggerPopoverContext";
20
+ import type {
21
+ TriggerPopoverResourceOutput,
22
+ SelectItemOverride,
23
+ OnSelectBehavior,
24
+ } from "../trigger/TriggerPopoverResource";
25
+
26
+ type MentionResourceOutput = TriggerPopoverResourceOutput & {
27
+ readonly formatter: Unstable_DirectiveFormatter;
28
+ };
25
29
 
26
30
  // =============================================================================
27
- // Context — public (popover components read state + actions from here)
31
+ // Context — public (provides formatter on top of TriggerPopoverContext)
28
32
  // =============================================================================
29
33
 
30
34
  const MentionContext = createContext<MentionResourceOutput | null>(null);
@@ -43,11 +47,10 @@ export const useMentionContextOptional = () => {
43
47
  };
44
48
 
45
49
  // =============================================================================
46
- // Internal context — ComposerInput MentionRoot communication
50
+ // Internal context — only registerSelectItemOverride for Lexical integration
47
51
  // =============================================================================
48
52
 
49
53
  type MentionInternalContextValue = {
50
- setCursorPosition(pos: number): void;
51
54
  registerSelectItemOverride(fn: SelectItemOverride): () => void;
52
55
  };
53
56
 
@@ -59,7 +62,7 @@ export const useMentionInternalContext = () => {
59
62
  };
60
63
 
61
64
  // =============================================================================
62
- // Provider Component
65
+ // Provider Component — delegates to TriggerPopoverRoot internally
63
66
  // =============================================================================
64
67
 
65
68
  export namespace ComposerPrimitiveMentionRoot {
@@ -82,7 +85,6 @@ export const ComposerPrimitiveMentionRoot: FC<
82
85
  formatter: formatterProp,
83
86
  }) => {
84
87
  const aui = useAui();
85
- const text = useAuiState((s) => s.composer.text);
86
88
  const formatter = formatterProp ?? unstable_defaultDirectiveFormatter;
87
89
 
88
90
  // ---------------------------------------------------------------------------
@@ -110,32 +112,64 @@ export const ComposerPrimitiveMentionRoot: FC<
110
112
  const adapter = adapterProp ?? runtimeAdapter;
111
113
 
112
114
  // ---------------------------------------------------------------------------
113
- // Mention resource (all state + logic managed via tap primitives)
115
+ // onSelect behavior for mentions: insert directive text
114
116
  // ---------------------------------------------------------------------------
115
117
 
116
- const mention = useResource(
117
- MentionResource({ adapter, text, triggerChar, formatter, aui }),
118
+ const onSelect = useMemo<OnSelectBehavior>(
119
+ () => ({ type: "insertDirective", formatter }),
120
+ [formatter],
118
121
  );
119
122
 
120
123
  // ---------------------------------------------------------------------------
121
- // Internal context (stable methods come from tapEffectEvent)
124
+ // MentionContext provides formatter + delegates state to TriggerPopoverContext
125
+ // We use useAuiState to read trigger popover state via the inner context.
126
+ // For backward compat, MentionContext wraps TriggerPopoverContext output.
122
127
  // ---------------------------------------------------------------------------
123
128
 
129
+ return (
130
+ <ComposerPrimitiveTriggerPopoverRoot
131
+ adapter={adapter}
132
+ trigger={triggerChar}
133
+ onSelect={onSelect}
134
+ >
135
+ <MentionContextBridge formatter={formatter}>
136
+ {children}
137
+ </MentionContextBridge>
138
+ </ComposerPrimitiveTriggerPopoverRoot>
139
+ );
140
+ };
141
+
142
+ ComposerPrimitiveMentionRoot.displayName = "ComposerPrimitive.MentionRoot";
143
+
144
+ // =============================================================================
145
+ // Bridge — reads TriggerPopoverContext, wraps it as MentionContext
146
+ // =============================================================================
147
+
148
+ import { useTriggerPopoverContext } from "../trigger/TriggerPopoverContext";
149
+
150
+ const MentionContextBridge: FC<{
151
+ formatter: Unstable_DirectiveFormatter;
152
+ children: ReactNode;
153
+ }> = ({ formatter, children }) => {
154
+ const triggerCtx = useTriggerPopoverContext();
155
+
156
+ const mentionValue = useMemo<MentionResourceOutput>(
157
+ () => ({ ...triggerCtx, formatter }),
158
+ [triggerCtx, formatter],
159
+ );
160
+
124
161
  const internalContextValue = useMemo<MentionInternalContextValue>(
125
162
  () => ({
126
- setCursorPosition: mention.setCursorPosition,
127
- registerSelectItemOverride: mention.registerSelectItemOverride,
163
+ registerSelectItemOverride: triggerCtx.registerSelectItemOverride,
128
164
  }),
129
- [mention.setCursorPosition, mention.registerSelectItemOverride],
165
+ [triggerCtx.registerSelectItemOverride],
130
166
  );
131
167
 
132
168
  return (
133
- <MentionContext.Provider value={mention}>
169
+ <MentionContext.Provider value={mentionValue}>
134
170
  <MentionInternalContext.Provider value={internalContextValue}>
135
171
  {children}
136
172
  </MentionInternalContext.Provider>
137
173
  </MentionContext.Provider>
138
174
  );
139
175
  };
140
-
141
- ComposerPrimitiveMentionRoot.displayName = "ComposerPrimitive.MentionRoot";
@@ -4,13 +4,16 @@ export {
4
4
  useMentionContextOptional,
5
5
  useMentionInternalContext,
6
6
  } from "./ComposerMentionContext";
7
- export { ComposerPrimitiveMentionPopover } from "./ComposerMentionPopover";
7
+
8
+ // UI primitives — re-exported from the shared trigger popover implementation.
9
+ // MentionRoot internally renders TriggerPopoverRoot, so these work within it.
10
+ export { ComposerPrimitiveTriggerPopoverPopover as ComposerPrimitiveMentionPopover } from "../trigger/TriggerPopoverPopover";
8
11
  export {
9
- ComposerPrimitiveMentionCategories,
10
- ComposerPrimitiveMentionCategoryItem,
11
- } from "./ComposerMentionCategories";
12
+ ComposerPrimitiveTriggerPopoverCategories as ComposerPrimitiveMentionCategories,
13
+ ComposerPrimitiveTriggerPopoverCategoryItem as ComposerPrimitiveMentionCategoryItem,
14
+ } from "../trigger/TriggerPopoverCategories";
12
15
  export {
13
- ComposerPrimitiveMentionItems,
14
- ComposerPrimitiveMentionItem,
15
- } from "./ComposerMentionItems";
16
- export { ComposerPrimitiveMentionBack } from "./ComposerMentionBack";
16
+ ComposerPrimitiveTriggerPopoverItems as ComposerPrimitiveMentionItems,
17
+ ComposerPrimitiveTriggerPopoverItem as ComposerPrimitiveMentionItem,
18
+ } from "../trigger/TriggerPopoverItems";
19
+ export { ComposerPrimitiveTriggerPopoverBack as ComposerPrimitiveMentionBack } from "../trigger/TriggerPopoverBack";
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ import { type ReactNode, type FC, useCallback, useMemo } from "react";
4
+ import type {
5
+ Unstable_SlashCommandAdapter,
6
+ Unstable_SlashCommandItem,
7
+ } from "@assistant-ui/core";
8
+ import { ComposerPrimitiveTriggerPopoverRoot } from "../trigger/TriggerPopoverContext";
9
+ import type { OnSelectBehavior } from "../trigger/TriggerPopoverResource";
10
+
11
+ // =============================================================================
12
+ // SlashCommandRoot — convenience wrapper around TriggerPopoverRoot
13
+ // =============================================================================
14
+
15
+ export namespace ComposerPrimitiveSlashCommandRoot {
16
+ export type Props = {
17
+ children: ReactNode;
18
+ /** The adapter providing slash command categories and items. */
19
+ adapter: Unstable_SlashCommandAdapter;
20
+ /** Character(s) that trigger the popover. @default "/" */
21
+ trigger?: string | undefined;
22
+ /** Callback when a slash command is selected. */
23
+ onSelect?: ((item: Unstable_SlashCommandItem) => void) | undefined;
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Convenience wrapper around `TriggerPopoverRoot` pre-configured for `/` slash commands.
29
+ * When a user selects a command, the `/command` text is removed from the composer
30
+ * and the item's `execute` callback (if any) and `onSelect` prop are called.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * <ComposerPrimitive.Unstable_SlashCommandRoot adapter={slashAdapter}>
35
+ * <ComposerPrimitive.Input />
36
+ * <ComposerPrimitive.Unstable_TriggerPopoverPopover>
37
+ * <ComposerPrimitive.Unstable_TriggerPopoverItems>
38
+ * {(items) => items.map(item => (
39
+ * <ComposerPrimitive.Unstable_TriggerPopoverItem key={item.id} item={item}>
40
+ * {item.label}
41
+ * </ComposerPrimitive.Unstable_TriggerPopoverItem>
42
+ * ))}
43
+ * </ComposerPrimitive.Unstable_TriggerPopoverItems>
44
+ * </ComposerPrimitive.Unstable_TriggerPopoverPopover>
45
+ * </ComposerPrimitive.Unstable_SlashCommandRoot>
46
+ * ```
47
+ */
48
+ export const ComposerPrimitiveSlashCommandRoot: FC<
49
+ ComposerPrimitiveSlashCommandRoot.Props
50
+ > = ({ children, adapter, trigger = "/", onSelect: onSelectProp }) => {
51
+ const handler = useCallback(
52
+ (item: Unstable_SlashCommandItem) => {
53
+ item.execute?.();
54
+ onSelectProp?.(item);
55
+ },
56
+ [onSelectProp],
57
+ );
58
+
59
+ const onSelect = useMemo<OnSelectBehavior>(
60
+ () => ({ type: "action", handler }),
61
+ [handler],
62
+ );
63
+
64
+ return (
65
+ <ComposerPrimitiveTriggerPopoverRoot
66
+ adapter={adapter}
67
+ trigger={trigger}
68
+ onSelect={onSelect}
69
+ >
70
+ {children}
71
+ </ComposerPrimitiveTriggerPopoverRoot>
72
+ );
73
+ };
74
+
75
+ ComposerPrimitiveSlashCommandRoot.displayName =
76
+ "ComposerPrimitive.SlashCommandRoot";
@@ -0,0 +1 @@
1
+ export { ComposerPrimitiveSlashCommandRoot } from "./ComposerSlashCommandRoot";
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { Primitive } from "../../../utils/Primitive";
4
+ import {
5
+ type ComponentRef,
6
+ type ComponentPropsWithoutRef,
7
+ forwardRef,
8
+ } from "react";
9
+ import { composeEventHandlers } from "@radix-ui/primitive";
10
+ import { useTriggerPopoverContext } from "./TriggerPopoverContext";
11
+
12
+ export namespace ComposerPrimitiveTriggerPopoverBack {
13
+ export type Element = ComponentRef<typeof Primitive.button>;
14
+ export type Props = ComponentPropsWithoutRef<typeof Primitive.button>;
15
+ }
16
+
17
+ /**
18
+ * A button that navigates back from category items to the category list.
19
+ * Only renders when a category is active (drill-down view).
20
+ */
21
+ export const ComposerPrimitiveTriggerPopoverBack = forwardRef<
22
+ ComposerPrimitiveTriggerPopoverBack.Element,
23
+ ComposerPrimitiveTriggerPopoverBack.Props
24
+ >(({ onClick, ...props }, forwardedRef) => {
25
+ const { activeCategoryId, isSearchMode, goBack } = useTriggerPopoverContext();
26
+
27
+ if (!activeCategoryId || isSearchMode) return null;
28
+
29
+ return (
30
+ <Primitive.button
31
+ type="button"
32
+ {...props}
33
+ ref={forwardedRef}
34
+ onClick={composeEventHandlers(onClick, goBack)}
35
+ />
36
+ );
37
+ });
38
+
39
+ ComposerPrimitiveTriggerPopoverBack.displayName =
40
+ "ComposerPrimitive.TriggerPopoverBack";
@@ -9,52 +9,56 @@ import {
9
9
  useCallback,
10
10
  } from "react";
11
11
  import { composeEventHandlers } from "@radix-ui/primitive";
12
- import { useMentionContext } from "./ComposerMentionContext";
13
- import type { Unstable_MentionCategory } from "@assistant-ui/core";
12
+ import { useTriggerPopoverContext } from "./TriggerPopoverContext";
13
+ import type { Unstable_TriggerCategory } from "@assistant-ui/core";
14
14
 
15
15
  // =============================================================================
16
- // MentionCategories — Renders the list of categories
16
+ // TriggerPopoverCategories — Renders the list of categories
17
17
  // =============================================================================
18
18
 
19
- export namespace ComposerPrimitiveMentionCategories {
19
+ export namespace ComposerPrimitiveTriggerPopoverCategories {
20
20
  export type Element = ComponentRef<typeof Primitive.div>;
21
21
  export type Props = Omit<
22
22
  ComponentPropsWithoutRef<typeof Primitive.div>,
23
23
  "children"
24
24
  > & {
25
- /**
26
- * Render function that receives the filtered categories and returns
27
- * the UI. A render-function pattern is used here (instead of a
28
- * `components` prop) to give consumers full control over list layout,
29
- * ordering, grouping, and empty states.
30
- */
31
- children: (categories: readonly Unstable_MentionCategory[]) => ReactNode;
25
+ children: (categories: readonly Unstable_TriggerCategory[]) => ReactNode;
32
26
  };
33
27
  }
34
28
 
35
- export const ComposerPrimitiveMentionCategories = forwardRef<
36
- ComposerPrimitiveMentionCategories.Element,
37
- ComposerPrimitiveMentionCategories.Props
38
- >(({ children, ...props }, forwardedRef) => {
39
- const { categories, activeCategoryId, isSearchMode } = useMentionContext();
29
+ /**
30
+ * Renders the top-level category list via a render function.
31
+ * Only renders when no category is active and search mode is off.
32
+ */
33
+ export const ComposerPrimitiveTriggerPopoverCategories = forwardRef<
34
+ ComposerPrimitiveTriggerPopoverCategories.Element,
35
+ ComposerPrimitiveTriggerPopoverCategories.Props
36
+ >(({ children, "aria-label": ariaLabel, ...props }, forwardedRef) => {
37
+ const { categories, activeCategoryId, isSearchMode } =
38
+ useTriggerPopoverContext();
40
39
 
41
40
  if (activeCategoryId || isSearchMode) return null;
42
41
 
43
42
  return (
44
- <Primitive.div role="group" {...props} ref={forwardedRef}>
43
+ <Primitive.div
44
+ role="group"
45
+ aria-label={ariaLabel ?? "Categories"}
46
+ {...props}
47
+ ref={forwardedRef}
48
+ >
45
49
  {children(categories)}
46
50
  </Primitive.div>
47
51
  );
48
52
  });
49
53
 
50
- ComposerPrimitiveMentionCategories.displayName =
51
- "ComposerPrimitive.MentionCategories";
54
+ ComposerPrimitiveTriggerPopoverCategories.displayName =
55
+ "ComposerPrimitive.TriggerPopoverCategories";
52
56
 
53
57
  // =============================================================================
54
- // MentionCategoryItem — A single category row (clickable to drill-down)
58
+ // TriggerPopoverCategoryItem — A single category row
55
59
  // =============================================================================
56
60
 
57
- export namespace ComposerPrimitiveMentionCategoryItem {
61
+ export namespace ComposerPrimitiveTriggerPopoverCategoryItem {
58
62
  export type Element = ComponentRef<typeof Primitive.button>;
59
63
  export type Props = ComponentPropsWithoutRef<typeof Primitive.button> & {
60
64
  categoryId: string;
@@ -65,9 +69,9 @@ export namespace ComposerPrimitiveMentionCategoryItem {
65
69
  * A button that selects a category and triggers drill-down navigation.
66
70
  * Automatically receives `data-highlighted` when keyboard-navigated.
67
71
  */
68
- export const ComposerPrimitiveMentionCategoryItem = forwardRef<
69
- ComposerPrimitiveMentionCategoryItem.Element,
70
- ComposerPrimitiveMentionCategoryItem.Props
72
+ export const ComposerPrimitiveTriggerPopoverCategoryItem = forwardRef<
73
+ ComposerPrimitiveTriggerPopoverCategoryItem.Element,
74
+ ComposerPrimitiveTriggerPopoverCategoryItem.Props
71
75
  >(({ categoryId, onClick, ...props }, forwardedRef) => {
72
76
  const {
73
77
  selectCategory,
@@ -75,13 +79,13 @@ export const ComposerPrimitiveMentionCategoryItem = forwardRef<
75
79
  highlightedIndex,
76
80
  activeCategoryId,
77
81
  isSearchMode,
78
- } = useMentionContext();
82
+ popoverId,
83
+ } = useTriggerPopoverContext();
79
84
 
80
85
  const handleClick = useCallback(() => {
81
86
  selectCategory(categoryId);
82
87
  }, [selectCategory, categoryId]);
83
88
 
84
- // Derive highlighted state from context — no manual wiring needed
85
89
  const isHighlighted =
86
90
  !activeCategoryId &&
87
91
  !isSearchMode &&
@@ -91,6 +95,7 @@ export const ComposerPrimitiveMentionCategoryItem = forwardRef<
91
95
  <Primitive.button
92
96
  type="button"
93
97
  role="option"
98
+ id={`${popoverId}-option-${categoryId}`}
94
99
  aria-selected={isHighlighted}
95
100
  data-highlighted={isHighlighted ? "" : undefined}
96
101
  {...props}
@@ -100,5 +105,5 @@ export const ComposerPrimitiveMentionCategoryItem = forwardRef<
100
105
  );
101
106
  });
102
107
 
103
- ComposerPrimitiveMentionCategoryItem.displayName =
104
- "ComposerPrimitive.MentionCategoryItem";
108
+ ComposerPrimitiveTriggerPopoverCategoryItem.displayName =
109
+ "ComposerPrimitive.TriggerPopoverCategoryItem";