@assistant-ui/react 0.12.23 → 0.12.25

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 +12 -12
  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
@@ -0,0 +1,129 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useEffect,
7
+ useId,
8
+ type ReactNode,
9
+ type FC,
10
+ } from "react";
11
+ import { useResource } from "@assistant-ui/tap/react";
12
+ import { useAui, useAuiState } from "@assistant-ui/store";
13
+ import type { Unstable_TriggerAdapter } from "@assistant-ui/core";
14
+ import {
15
+ TriggerPopoverResource,
16
+ type TriggerPopoverResourceOutput,
17
+ type OnSelectBehavior,
18
+ } from "./TriggerPopoverResource";
19
+ import {
20
+ useComposerInputPluginRegistryOptional,
21
+ ComposerInputPluginProvider,
22
+ } from "../ComposerInputPluginContext";
23
+
24
+ // =============================================================================
25
+ // Context
26
+ // =============================================================================
27
+
28
+ const TriggerPopoverContext =
29
+ createContext<TriggerPopoverResourceOutput | null>(null);
30
+
31
+ export const useTriggerPopoverContext = () => {
32
+ const ctx = useContext(TriggerPopoverContext);
33
+ if (!ctx)
34
+ throw new Error(
35
+ "useTriggerPopoverContext must be used within ComposerPrimitive.TriggerPopoverRoot",
36
+ );
37
+ return ctx;
38
+ };
39
+
40
+ export const useTriggerPopoverContextOptional = () => {
41
+ return useContext(TriggerPopoverContext);
42
+ };
43
+
44
+ // =============================================================================
45
+ // Root Component
46
+ // =============================================================================
47
+
48
+ export namespace ComposerPrimitiveTriggerPopoverRoot {
49
+ export type Props = {
50
+ children: ReactNode;
51
+ /** The adapter providing categories and items. */
52
+ adapter: Unstable_TriggerAdapter;
53
+ /** Character(s) that trigger the popover. @default "@" */
54
+ trigger?: string | undefined;
55
+ /** What happens when an item is selected. */
56
+ onSelect: OnSelectBehavior;
57
+ };
58
+ }
59
+
60
+ const TriggerPopoverRootInner: FC<
61
+ ComposerPrimitiveTriggerPopoverRoot.Props
62
+ > = ({ children, adapter, trigger: triggerChar = "@", onSelect }) => {
63
+ const aui = useAui();
64
+ const text = useAuiState((s) => s.composer.text);
65
+ const popoverId = useId();
66
+
67
+ const triggerPopover = useResource(
68
+ TriggerPopoverResource({
69
+ adapter,
70
+ text,
71
+ triggerChar,
72
+ onSelect,
73
+ aui,
74
+ popoverId,
75
+ }),
76
+ );
77
+
78
+ // Register as ComposerInput plugin
79
+ const pluginRegistry = useComposerInputPluginRegistryOptional();
80
+
81
+ useEffect(() => {
82
+ if (!pluginRegistry) return undefined;
83
+ return pluginRegistry.register(triggerPopover);
84
+ }, [pluginRegistry, triggerPopover]);
85
+
86
+ return (
87
+ <TriggerPopoverContext.Provider value={triggerPopover}>
88
+ {children}
89
+ </TriggerPopoverContext.Provider>
90
+ );
91
+ };
92
+
93
+ /**
94
+ * Provider that wraps the composer with trigger detection, keyboard navigation,
95
+ * and popover state. Supports any trigger character (`@`, `/`, `:`, etc.).
96
+ * Multiple trigger roots can coexist around the same input.
97
+ *
98
+ * @example
99
+ * ```tsx
100
+ * <ComposerPrimitive.Unstable_TriggerPopoverRoot
101
+ * trigger="/"
102
+ * adapter={slashAdapter}
103
+ * onSelect={{ type: "action", handler: (item) => console.log(item) }}
104
+ * >
105
+ * <ComposerPrimitive.Input />
106
+ * <ComposerPrimitive.Unstable_TriggerPopoverPopover>
107
+ * ...
108
+ * </ComposerPrimitive.Unstable_TriggerPopoverPopover>
109
+ * </ComposerPrimitive.Unstable_TriggerPopoverRoot>
110
+ * ```
111
+ */
112
+ export const ComposerPrimitiveTriggerPopoverRoot: FC<
113
+ ComposerPrimitiveTriggerPopoverRoot.Props
114
+ > = (props) => {
115
+ const existingRegistry = useComposerInputPluginRegistryOptional();
116
+
117
+ if (existingRegistry) {
118
+ return <TriggerPopoverRootInner {...props} />;
119
+ }
120
+
121
+ return (
122
+ <ComposerInputPluginProvider>
123
+ <TriggerPopoverRootInner {...props} />
124
+ </ComposerInputPluginProvider>
125
+ );
126
+ };
127
+
128
+ ComposerPrimitiveTriggerPopoverRoot.displayName =
129
+ "ComposerPrimitive.TriggerPopoverRoot";
@@ -9,66 +9,69 @@ 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_MentionItem } from "@assistant-ui/core";
12
+ import { useTriggerPopoverContext } from "./TriggerPopoverContext";
13
+ import type { Unstable_TriggerItem } from "@assistant-ui/core";
14
14
 
15
15
  // =============================================================================
16
- // MentionItems — Renders the list of items within a category
16
+ // TriggerPopoverItems — Renders the list of items within a category
17
17
  // =============================================================================
18
18
 
19
- export namespace ComposerPrimitiveMentionItems {
19
+ export namespace ComposerPrimitiveTriggerPopoverItems {
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 items 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: (items: readonly Unstable_MentionItem[]) => ReactNode;
25
+ children: (items: readonly Unstable_TriggerItem[]) => ReactNode;
32
26
  };
33
27
  }
34
28
 
35
- export const ComposerPrimitiveMentionItems = forwardRef<
36
- ComposerPrimitiveMentionItems.Element,
37
- ComposerPrimitiveMentionItems.Props
38
- >(({ children, ...props }, forwardedRef) => {
39
- const { items, activeCategoryId, isSearchMode } = useMentionContext();
29
+ /**
30
+ * Renders the list of items within a category or search results via a render function.
31
+ * Only renders when a category is active or search mode is on.
32
+ */
33
+ export const ComposerPrimitiveTriggerPopoverItems = forwardRef<
34
+ ComposerPrimitiveTriggerPopoverItems.Element,
35
+ ComposerPrimitiveTriggerPopoverItems.Props
36
+ >(({ children, "aria-label": ariaLabel, ...props }, forwardedRef) => {
37
+ const { items, activeCategoryId, isSearchMode } = useTriggerPopoverContext();
40
38
 
41
39
  if (!activeCategoryId && !isSearchMode) return null;
42
40
 
43
41
  return (
44
- <Primitive.div role="group" {...props} ref={forwardedRef}>
42
+ <Primitive.div
43
+ role="group"
44
+ aria-label={ariaLabel ?? "Items"}
45
+ {...props}
46
+ ref={forwardedRef}
47
+ >
45
48
  {children(items)}
46
49
  </Primitive.div>
47
50
  );
48
51
  });
49
52
 
50
- ComposerPrimitiveMentionItems.displayName = "ComposerPrimitive.MentionItems";
53
+ ComposerPrimitiveTriggerPopoverItems.displayName =
54
+ "ComposerPrimitive.TriggerPopoverItems";
51
55
 
52
56
  // =============================================================================
53
- // MentionItem — A single selectable mention item
57
+ // TriggerPopoverItem — A single selectable item
54
58
  // =============================================================================
55
59
 
56
- export namespace ComposerPrimitiveMentionItem {
60
+ export namespace ComposerPrimitiveTriggerPopoverItem {
57
61
  export type Element = ComponentRef<typeof Primitive.button>;
58
62
  export type Props = ComponentPropsWithoutRef<typeof Primitive.button> & {
59
- item: Unstable_MentionItem;
60
- /** Position in the items list. Used for keyboard highlight matching. Falls back to findIndex by id. */
63
+ item: Unstable_TriggerItem;
61
64
  index?: number | undefined;
62
65
  };
63
66
  }
64
67
 
65
68
  /**
66
- * A button that inserts the mention item into the composer.
69
+ * A button that selects a trigger item.
67
70
  * Automatically receives `data-highlighted` when keyboard-navigated.
68
71
  */
69
- export const ComposerPrimitiveMentionItem = forwardRef<
70
- ComposerPrimitiveMentionItem.Element,
71
- ComposerPrimitiveMentionItem.Props
72
+ export const ComposerPrimitiveTriggerPopoverItem = forwardRef<
73
+ ComposerPrimitiveTriggerPopoverItem.Element,
74
+ ComposerPrimitiveTriggerPopoverItem.Props
72
75
  >(({ item, index: indexProp, onClick, ...props }, forwardedRef) => {
73
76
  const {
74
77
  selectItem,
@@ -76,13 +79,13 @@ export const ComposerPrimitiveMentionItem = forwardRef<
76
79
  highlightedIndex,
77
80
  activeCategoryId,
78
81
  isSearchMode,
79
- } = useMentionContext();
82
+ popoverId,
83
+ } = useTriggerPopoverContext();
80
84
 
81
85
  const handleClick = useCallback(() => {
82
86
  selectItem(item);
83
87
  }, [selectItem, item]);
84
88
 
85
- // Use explicit index prop if provided, fall back to findIndex
86
89
  const itemIndex = indexProp ?? items.findIndex((i) => i.id === item.id);
87
90
  const isHighlighted =
88
91
  (isSearchMode || activeCategoryId !== null) &&
@@ -92,6 +95,7 @@ export const ComposerPrimitiveMentionItem = forwardRef<
92
95
  <Primitive.button
93
96
  type="button"
94
97
  role="option"
98
+ id={`${popoverId}-option-${item.id}`}
95
99
  aria-selected={isHighlighted}
96
100
  data-highlighted={isHighlighted ? "" : undefined}
97
101
  {...props}
@@ -101,4 +105,5 @@ export const ComposerPrimitiveMentionItem = forwardRef<
101
105
  );
102
106
  });
103
107
 
104
- ComposerPrimitiveMentionItem.displayName = "ComposerPrimitive.MentionItem";
108
+ ComposerPrimitiveTriggerPopoverItem.displayName =
109
+ "ComposerPrimitive.TriggerPopoverItem";
@@ -0,0 +1,51 @@
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 { useTriggerPopoverContext } from "./TriggerPopoverContext";
10
+
11
+ export namespace ComposerPrimitiveTriggerPopoverPopover {
12
+ export type Element = ComponentRef<typeof Primitive.div>;
13
+ export type Props = ComponentPropsWithoutRef<typeof Primitive.div>;
14
+ }
15
+
16
+ /**
17
+ * Renders a container for the trigger popover.
18
+ * Only renders when a trigger character is detected in the composer text.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <ComposerPrimitive.Unstable_TriggerPopoverRoot trigger="/" adapter={adapter} onSelect={onSelect}>
23
+ * <ComposerPrimitive.Input />
24
+ * <ComposerPrimitive.Unstable_TriggerPopoverPopover>
25
+ * <ComposerPrimitive.Unstable_TriggerPopoverCategories />
26
+ * </ComposerPrimitive.Unstable_TriggerPopoverPopover>
27
+ * </ComposerPrimitive.Unstable_TriggerPopoverRoot>
28
+ * ```
29
+ */
30
+ export const ComposerPrimitiveTriggerPopoverPopover = forwardRef<
31
+ ComposerPrimitiveTriggerPopoverPopover.Element,
32
+ ComposerPrimitiveTriggerPopoverPopover.Props
33
+ >(({ "aria-label": ariaLabel, ...props }, forwardedRef) => {
34
+ const { open, popoverId, highlightedItemId } = useTriggerPopoverContext();
35
+ if (!open) return null;
36
+
37
+ return (
38
+ <Primitive.div
39
+ role="listbox"
40
+ id={popoverId}
41
+ aria-label={ariaLabel ?? "Suggestions"}
42
+ aria-activedescendant={highlightedItemId}
43
+ data-state="open"
44
+ {...props}
45
+ ref={forwardedRef}
46
+ />
47
+ );
48
+ });
49
+
50
+ ComposerPrimitiveTriggerPopoverPopover.displayName =
51
+ "ComposerPrimitive.TriggerPopoverPopover";
@@ -7,43 +7,70 @@ import {
7
7
  tapRef,
8
8
  } from "@assistant-ui/tap";
9
9
  import type {
10
- Unstable_MentionAdapter,
11
- Unstable_MentionCategory,
12
- Unstable_MentionItem,
10
+ Unstable_TriggerAdapter,
11
+ Unstable_TriggerCategory,
12
+ Unstable_TriggerItem,
13
13
  Unstable_DirectiveFormatter,
14
14
  } from "@assistant-ui/core";
15
15
  import type { AssistantClient } from "@assistant-ui/store";
16
- import { detectMentionTrigger } from "./detectMentionTrigger";
16
+ import { detectTrigger } from "./detectTrigger";
17
+
18
+ function isTriggerItem(
19
+ x: Unstable_TriggerItem | Unstable_TriggerCategory,
20
+ ): x is Unstable_TriggerItem {
21
+ return "type" in x;
22
+ }
23
+
24
+ function matchesQuery(item: Unstable_TriggerItem, lower: string): boolean {
25
+ return (
26
+ item.id.toLowerCase().includes(lower) ||
27
+ item.label.toLowerCase().includes(lower) ||
28
+ (item.description?.toLowerCase().includes(lower) ?? false)
29
+ );
30
+ }
17
31
 
18
32
  // =============================================================================
19
33
  // Types
20
34
  // =============================================================================
21
35
 
22
- export type MentionKeyEvent = {
36
+ export type TriggerPopoverKeyEvent = {
23
37
  readonly key: string;
24
38
  readonly shiftKey: boolean;
25
39
  preventDefault(): void;
26
40
  };
27
41
 
28
- export type SelectItemOverride = (item: Unstable_MentionItem) => boolean;
42
+ export type SelectItemOverride = (item: Unstable_TriggerItem) => boolean;
29
43
 
30
- export type MentionResourceOutput = {
44
+ export type OnSelectBehavior =
45
+ | {
46
+ type: "insertDirective";
47
+ formatter: Unstable_DirectiveFormatter;
48
+ }
49
+ | {
50
+ type: "action";
51
+ handler: (item: Unstable_TriggerItem) => void;
52
+ };
53
+
54
+ export type TriggerPopoverResourceOutput = {
31
55
  // State
32
56
  readonly open: boolean;
33
57
  readonly query: string;
34
58
  readonly activeCategoryId: string | null;
35
- readonly categories: readonly Unstable_MentionCategory[];
36
- readonly items: readonly Unstable_MentionItem[];
59
+ readonly categories: readonly Unstable_TriggerCategory[];
60
+ readonly items: readonly Unstable_TriggerItem[];
37
61
  readonly highlightedIndex: number;
38
62
  readonly isSearchMode: boolean;
39
- readonly formatter: Unstable_DirectiveFormatter;
63
+ /** Stable ID prefix for generating accessible element IDs. */
64
+ readonly popoverId: string;
65
+ /** ID of the currently highlighted item (for aria-activedescendant). */
66
+ readonly highlightedItemId: string | undefined;
40
67
 
41
68
  // Actions
42
69
  selectCategory(categoryId: string): void;
43
70
  goBack(): void;
44
- selectItem(item: Unstable_MentionItem): void;
71
+ selectItem(item: Unstable_TriggerItem): void;
45
72
  close(): void;
46
- handleKeyDown(e: MentionKeyEvent): boolean;
73
+ handleKeyDown(e: TriggerPopoverKeyEvent): boolean;
47
74
 
48
75
  // Internal (for ComposerInput integration)
49
76
  setCursorPosition(pos: number): void;
@@ -54,20 +81,23 @@ export type MentionResourceOutput = {
54
81
  // Resource
55
82
  // =============================================================================
56
83
 
57
- export const MentionResource = resource(
84
+ export const TriggerPopoverResource = resource(
58
85
  ({
59
86
  adapter,
60
87
  text,
61
88
  triggerChar,
62
- formatter,
89
+ onSelect,
63
90
  aui,
91
+ popoverId,
64
92
  }: {
65
- adapter: Unstable_MentionAdapter | undefined;
93
+ adapter: Unstable_TriggerAdapter | undefined;
66
94
  text: string;
67
95
  triggerChar: string;
68
- formatter: Unstable_DirectiveFormatter;
96
+ onSelect: OnSelectBehavior;
69
97
  aui: AssistantClient;
70
- }): MentionResourceOutput => {
98
+ /** Stable ID for accessible element IDs (pass React's useId() from component layer). */
99
+ popoverId: string;
100
+ }): TriggerPopoverResourceOutput => {
71
101
  // -------------------------------------------------------------------------
72
102
  // Cursor tracking + trigger detection
73
103
  // -------------------------------------------------------------------------
@@ -76,7 +106,7 @@ export const MentionResource = resource(
76
106
 
77
107
  const trigger = tapMemo(() => {
78
108
  const pos = Math.min(cursorPosition, text.length);
79
- return detectMentionTrigger(text, triggerChar, pos);
109
+ return detectTrigger(text, triggerChar, pos);
80
110
  }, [cursorPosition, text, triggerChar]);
81
111
 
82
112
  const open = trigger !== null && adapter !== undefined;
@@ -95,7 +125,7 @@ export const MentionResource = resource(
95
125
  if (!open) setActiveCategoryId(null);
96
126
  }, [open]);
97
127
 
98
- const categories = tapMemo<readonly Unstable_MentionCategory[]>(() => {
128
+ const categories = tapMemo<readonly Unstable_TriggerCategory[]>(() => {
99
129
  if (!open || !adapter) return [];
100
130
  return adapter.categories();
101
131
  }, [open, adapter]);
@@ -106,33 +136,31 @@ export const MentionResource = resource(
106
136
  // Items + search
107
137
  // -------------------------------------------------------------------------
108
138
 
109
- const allItems = tapMemo<readonly Unstable_MentionItem[]>(() => {
139
+ const allItems = tapMemo<readonly Unstable_TriggerItem[]>(() => {
110
140
  if (!effectiveActiveCategoryId || !adapter) return [];
111
141
  return adapter.categoryItems(effectiveActiveCategoryId);
112
142
  }, [effectiveActiveCategoryId, adapter]);
113
143
 
114
144
  const searchResults = tapMemo<
115
- readonly Unstable_MentionItem[] | null
145
+ readonly Unstable_TriggerItem[] | null
116
146
  >(() => {
117
- if (!open || !adapter || !query || effectiveActiveCategoryId) return null;
147
+ if (!open || !adapter || effectiveActiveCategoryId) return null;
148
+ // If categories exist and query is empty, show categories first (not search)
149
+ if (!query && categories.length > 0) return null;
118
150
  if (adapter.search) return adapter.search(query);
119
151
 
120
- const cats = adapter.categories();
121
- const all: Unstable_MentionItem[] = [];
152
+ // Fallback: search all categories manually (reuse already-computed list)
153
+ const all: Unstable_TriggerItem[] = [];
122
154
  const lower = query.toLowerCase();
123
- for (const cat of cats) {
155
+ for (const cat of categories) {
124
156
  for (const item of adapter.categoryItems(cat.id)) {
125
- if (
126
- item.id.toLowerCase().includes(lower) ||
127
- item.label.toLowerCase().includes(lower) ||
128
- item.description?.toLowerCase().includes(lower)
129
- ) {
157
+ if (matchesQuery(item, lower)) {
130
158
  all.push(item);
131
159
  }
132
160
  }
133
161
  }
134
162
  return all;
135
- }, [open, adapter, query, effectiveActiveCategoryId]);
163
+ }, [open, adapter, query, effectiveActiveCategoryId, categories]);
136
164
 
137
165
  const isSearchMode = searchResults !== null;
138
166
 
@@ -153,12 +181,7 @@ export const MentionResource = resource(
153
181
  if (isSearchMode) return searchResults ?? [];
154
182
  if (!query) return allItems;
155
183
  const lower = query.toLowerCase();
156
- return allItems.filter(
157
- (item) =>
158
- item.id.toLowerCase().includes(lower) ||
159
- item.label.toLowerCase().includes(lower) ||
160
- item.description?.toLowerCase().includes(lower),
161
- );
184
+ return allItems.filter((item) => matchesQuery(item, lower));
162
185
  }, [allItems, query, isSearchMode, searchResults]);
163
186
 
164
187
  // -------------------------------------------------------------------------
@@ -186,7 +209,7 @@ export const MentionResource = resource(
186
209
  }, [navigableList]);
187
210
 
188
211
  // -------------------------------------------------------------------------
189
- // Lexical select-item override
212
+ // Select-item override (for Lexical integration)
190
213
  // -------------------------------------------------------------------------
191
214
 
192
215
  const selectItemOverrideRef = tapRef<SelectItemOverride | null>(null);
@@ -216,27 +239,42 @@ export const MentionResource = resource(
216
239
  setHighlightedIndex(0);
217
240
  });
218
241
 
219
- const selectItem = tapEffectEvent((item: Unstable_MentionItem) => {
242
+ const selectItem = tapEffectEvent((item: Unstable_TriggerItem) => {
220
243
  if (!trigger) return;
221
244
 
222
- // Try the Lexical override first
245
+ // Try the override first (e.g. Lexical MentionPlugin)
223
246
  if (selectItemOverrideRef.current?.(item)) {
224
247
  setActiveCategoryId(null);
225
248
  setHighlightedIndex(0);
226
249
  return;
227
250
  }
228
251
 
229
- // Default: text-based replacement (textarea path)
230
- const currentText = aui.composer().getState().text;
231
- const before = currentText.slice(0, trigger.offset);
232
- const after = currentText.slice(
233
- trigger.offset + triggerChar.length + trigger.query.length,
234
- );
235
- const directive = formatter.serialize(item);
236
- const newText =
237
- before + directive + (after.startsWith(" ") ? after : ` ${after}`);
252
+ // Behavior depends on the onSelect configuration
253
+ if (onSelect.type === "insertDirective") {
254
+ // Insert directive text (mention path)
255
+ const currentText = aui.composer().getState().text;
256
+ const before = currentText.slice(0, trigger.offset);
257
+ const after = currentText.slice(
258
+ trigger.offset + triggerChar.length + trigger.query.length,
259
+ );
260
+ const directive = onSelect.formatter.serialize(item);
261
+ const newText =
262
+ before + directive + (after.startsWith(" ") ? after : ` ${after}`);
263
+
264
+ aui.composer().setText(newText);
265
+ } else if (onSelect.type === "action") {
266
+ // Execute action + clear trigger text (slash command path)
267
+ const currentText = aui.composer().getState().text;
268
+ const before = currentText.slice(0, trigger.offset);
269
+ const after = currentText.slice(
270
+ trigger.offset + triggerChar.length + trigger.query.length,
271
+ );
272
+ const newText = before + after.trimStart();
273
+ aui.composer().setText(newText);
274
+
275
+ onSelect.handler(item);
276
+ }
238
277
 
239
- aui.composer().setText(newText);
240
278
  setActiveCategoryId(null);
241
279
  setHighlightedIndex(0);
242
280
  });
@@ -250,63 +288,72 @@ export const MentionResource = resource(
250
288
  }
251
289
  });
252
290
 
253
- const handleKeyDown = tapEffectEvent((e: MentionKeyEvent): boolean => {
254
- if (!open) return false;
255
-
256
- switch (e.key) {
257
- case "ArrowDown": {
258
- e.preventDefault();
259
- setHighlightedIndex((prev) => {
260
- const len = navigableList.length;
261
- if (len === 0) return 0;
262
- return prev < len - 1 ? prev + 1 : 0;
263
- });
264
- return true;
265
- }
266
- case "ArrowUp": {
267
- e.preventDefault();
268
- setHighlightedIndex((prev) => {
269
- const len = navigableList.length;
270
- if (len === 0) return 0;
271
- return prev > 0 ? prev - 1 : len - 1;
272
- });
273
- return true;
274
- }
275
- case "Enter": {
276
- if (e.shiftKey) return false;
277
- e.preventDefault();
278
- const item = navigableList[highlightedIndex];
279
- if (!item) return true;
280
-
281
- if (isSearchMode || effectiveActiveCategoryId) {
282
- selectItem(item as Unstable_MentionItem);
283
- } else {
284
- selectCategory((item as Unstable_MentionCategory).id);
291
+ const handleKeyDown = tapEffectEvent(
292
+ (e: TriggerPopoverKeyEvent): boolean => {
293
+ if (!open) return false;
294
+
295
+ switch (e.key) {
296
+ case "ArrowDown": {
297
+ e.preventDefault();
298
+ setHighlightedIndex((prev) => {
299
+ const len = navigableList.length;
300
+ if (len === 0) return 0;
301
+ return prev < len - 1 ? prev + 1 : 0;
302
+ });
303
+ return true;
285
304
  }
286
- return true;
287
- }
288
- case "Escape": {
289
- e.preventDefault();
290
- close();
291
- return true;
292
- }
293
- case "Backspace": {
294
- if (effectiveActiveCategoryId && query === "") {
305
+ case "ArrowUp": {
295
306
  e.preventDefault();
296
- goBack();
307
+ setHighlightedIndex((prev) => {
308
+ const len = navigableList.length;
309
+ if (len === 0) return 0;
310
+ return prev > 0 ? prev - 1 : len - 1;
311
+ });
297
312
  return true;
298
313
  }
299
- return false;
314
+ case "Enter": {
315
+ if (e.shiftKey) return false;
316
+ e.preventDefault();
317
+ const item = navigableList[highlightedIndex];
318
+ if (!item) return true;
319
+
320
+ if (isTriggerItem(item)) {
321
+ selectItem(item);
322
+ } else {
323
+ selectCategory(item.id);
324
+ }
325
+ return true;
326
+ }
327
+ case "Escape": {
328
+ e.preventDefault();
329
+ close();
330
+ return true;
331
+ }
332
+ case "Backspace": {
333
+ if (effectiveActiveCategoryId && query === "") {
334
+ e.preventDefault();
335
+ goBack();
336
+ return true;
337
+ }
338
+ return false;
339
+ }
340
+ default:
341
+ return false;
300
342
  }
301
- default:
302
- return false;
303
- }
304
- });
343
+ },
344
+ );
305
345
 
306
346
  // -------------------------------------------------------------------------
307
347
  // Output
308
348
  // -------------------------------------------------------------------------
309
349
 
350
+ // Compute highlighted item ID for aria-activedescendant
351
+ const highlightedEntry = navigableList[highlightedIndex];
352
+ const highlightedItemId =
353
+ open && highlightedEntry
354
+ ? `${popoverId}-option-${highlightedEntry.id}`
355
+ : undefined;
356
+
310
357
  return {
311
358
  open,
312
359
  query,
@@ -315,7 +362,8 @@ export const MentionResource = resource(
315
362
  items: filteredItems,
316
363
  highlightedIndex,
317
364
  isSearchMode,
318
- formatter,
365
+ popoverId,
366
+ highlightedItemId,
319
367
  selectCategory,
320
368
  goBack,
321
369
  selectItem,