@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.
- package/dist/client/ExternalThread.d.ts.map +1 -1
- package/dist/client/ExternalThread.js +1 -0
- package/dist/client/ExternalThread.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -0
- package/dist/internal.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +27 -12
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerInputPluginContext.d.ts +31 -0
- package/dist/primitives/composer/ComposerInputPluginContext.d.ts.map +1 -0
- package/dist/primitives/composer/ComposerInputPluginContext.js +32 -0
- package/dist/primitives/composer/ComposerInputPluginContext.js.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionContext.d.ts +4 -2
- package/dist/primitives/composer/mention/ComposerMentionContext.d.ts.map +1 -1
- package/dist/primitives/composer/mention/ComposerMentionContext.js +21 -13
- package/dist/primitives/composer/mention/ComposerMentionContext.js.map +1 -1
- package/dist/primitives/composer/mention/index.d.ts +4 -4
- package/dist/primitives/composer/mention/index.d.ts.map +1 -1
- package/dist/primitives/composer/mention/index.js +6 -4
- package/dist/primitives/composer/mention/index.js.map +1 -1
- package/dist/primitives/composer/slash-command/ComposerSlashCommandRoot.d.ts +36 -0
- package/dist/primitives/composer/slash-command/ComposerSlashCommandRoot.d.ts.map +1 -0
- package/dist/primitives/composer/slash-command/ComposerSlashCommandRoot.js +36 -0
- package/dist/primitives/composer/slash-command/ComposerSlashCommandRoot.js.map +1 -0
- package/dist/primitives/composer/slash-command/index.d.ts +2 -0
- package/dist/primitives/composer/slash-command/index.d.ts.map +1 -0
- package/dist/primitives/composer/slash-command/index.js +2 -0
- package/dist/primitives/composer/slash-command/index.js.map +1 -0
- package/dist/primitives/composer/{mention/ComposerMentionBack.d.ts → trigger/TriggerPopoverBack.d.ts} +3 -10
- package/dist/primitives/composer/trigger/TriggerPopoverBack.d.ts.map +1 -0
- package/dist/primitives/composer/trigger/TriggerPopoverBack.js +19 -0
- package/dist/primitives/composer/trigger/TriggerPopoverBack.js.map +1 -0
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts +38 -0
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts.map +1 -0
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +35 -0
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -0
- package/dist/primitives/composer/trigger/TriggerPopoverContext.d.ts +37 -0
- package/dist/primitives/composer/trigger/TriggerPopoverContext.d.ts.map +1 -0
- package/dist/primitives/composer/trigger/TriggerPopoverContext.js +70 -0
- package/dist/primitives/composer/trigger/TriggerPopoverContext.js.map +1 -0
- package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts +40 -0
- package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts.map +1 -0
- package/dist/primitives/composer/trigger/TriggerPopoverItems.js +35 -0
- package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -0
- package/dist/primitives/composer/trigger/TriggerPopoverPopover.d.ts +26 -0
- package/dist/primitives/composer/trigger/TriggerPopoverPopover.d.ts.map +1 -0
- package/dist/primitives/composer/trigger/TriggerPopoverPopover.js +28 -0
- package/dist/primitives/composer/trigger/TriggerPopoverPopover.js.map +1 -0
- package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +53 -0
- package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -0
- package/dist/primitives/composer/{mention/MentionResource.js → trigger/TriggerPopoverResource.js} +50 -25
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -0
- package/dist/primitives/composer/trigger/detectTrigger.d.ts +2 -0
- package/dist/primitives/composer/trigger/detectTrigger.d.ts.map +1 -0
- package/dist/primitives/composer/{mention/detectMentionTrigger.js → trigger/detectTrigger.js} +4 -4
- package/dist/primitives/composer/trigger/detectTrigger.js.map +1 -0
- package/dist/primitives/composer/trigger/index.d.ts +7 -0
- package/dist/primitives/composer/trigger/index.d.ts.map +1 -0
- package/dist/primitives/composer/trigger/index.js +6 -0
- package/dist/primitives/composer/trigger/index.js.map +1 -0
- package/dist/primitives/composer.d.ts +10 -0
- package/dist/primitives/composer.d.ts.map +1 -1
- package/dist/primitives/composer.js +14 -0
- package/dist/primitives/composer.js.map +1 -1
- package/dist/primitives/message/MessageRoot.d.ts +25 -3
- package/dist/primitives/message/MessageRoot.d.ts.map +1 -1
- package/dist/primitives/message/MessageRoot.js +2 -2
- package/dist/primitives/message/MessageRoot.js.map +1 -1
- package/dist/primitives/thread/ThreadViewportSlack.d.ts +2 -2
- package/dist/primitives/thread/ThreadViewportSlack.d.ts.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.d.ts +34 -0
- package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -0
- package/dist/unstable/useSlashCommandAdapter.js +50 -0
- package/dist/unstable/useSlashCommandAdapter.js.map +1 -0
- package/package.json +12 -12
- package/src/client/ExternalThread.ts +1 -0
- package/src/index.ts +14 -0
- package/src/internal.ts +3 -0
- package/src/primitives/composer/ComposerInput.tsx +25 -18
- package/src/primitives/composer/ComposerInputPluginContext.tsx +100 -0
- package/src/primitives/composer/mention/ComposerMentionContext.tsx +56 -22
- package/src/primitives/composer/mention/index.ts +11 -8
- package/src/primitives/composer/slash-command/ComposerSlashCommandRoot.tsx +76 -0
- package/src/primitives/composer/slash-command/index.ts +1 -0
- package/src/primitives/composer/trigger/TriggerPopoverBack.tsx +40 -0
- package/src/primitives/composer/{mention/ComposerMentionCategories.tsx → trigger/TriggerPopoverCategories.tsx} +33 -28
- package/src/primitives/composer/trigger/TriggerPopoverContext.tsx +129 -0
- package/src/primitives/composer/{mention/ComposerMentionItems.tsx → trigger/TriggerPopoverItems.tsx} +34 -29
- package/src/primitives/composer/trigger/TriggerPopoverPopover.tsx +51 -0
- package/src/primitives/composer/{mention/MentionResource.ts → trigger/TriggerPopoverResource.ts} +146 -98
- package/src/primitives/composer/{mention/detectMentionTrigger.test.ts → trigger/detectTrigger.test.ts} +15 -15
- package/src/primitives/composer/{mention/detectMentionTrigger.ts → trigger/detectTrigger.ts} +3 -3
- package/src/primitives/composer/trigger/index.ts +16 -0
- package/src/primitives/composer.ts +16 -0
- package/src/primitives/message/MessageRoot.tsx +18 -4
- package/src/primitives/thread/ThreadViewportSlack.tsx +2 -2
- package/src/tests/BaseComposerRuntimeCore.test.ts +33 -1
- package/src/unstable/useSlashCommandAdapter.ts +83 -0
- package/dist/primitives/composer/mention/ComposerMentionBack.d.ts.map +0 -1
- package/dist/primitives/composer/mention/ComposerMentionBack.js +0 -28
- package/dist/primitives/composer/mention/ComposerMentionBack.js.map +0 -1
- package/dist/primitives/composer/mention/ComposerMentionCategories.d.ts +0 -46
- package/dist/primitives/composer/mention/ComposerMentionCategories.d.ts.map +0 -1
- package/dist/primitives/composer/mention/ComposerMentionCategories.js +0 -32
- package/dist/primitives/composer/mention/ComposerMentionCategories.js.map +0 -1
- package/dist/primitives/composer/mention/ComposerMentionItems.d.ts +0 -50
- package/dist/primitives/composer/mention/ComposerMentionItems.d.ts.map +0 -1
- package/dist/primitives/composer/mention/ComposerMentionItems.js +0 -30
- package/dist/primitives/composer/mention/ComposerMentionItems.js.map +0 -1
- package/dist/primitives/composer/mention/ComposerMentionPopover.d.ts +0 -26
- package/dist/primitives/composer/mention/ComposerMentionPopover.d.ts.map +0 -1
- package/dist/primitives/composer/mention/ComposerMentionPopover.js +0 -28
- package/dist/primitives/composer/mention/ComposerMentionPopover.js.map +0 -1
- package/dist/primitives/composer/mention/MentionResource.d.ts +0 -39
- package/dist/primitives/composer/mention/MentionResource.d.ts.map +0 -1
- package/dist/primitives/composer/mention/MentionResource.js.map +0 -1
- package/dist/primitives/composer/mention/detectMentionTrigger.d.ts +0 -2
- package/dist/primitives/composer/mention/detectMentionTrigger.d.ts.map +0 -1
- package/dist/primitives/composer/mention/detectMentionTrigger.js.map +0 -1
- package/src/primitives/composer/mention/ComposerMentionBack.tsx +0 -55
- 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";
|
package/src/primitives/composer/{mention/ComposerMentionItems.tsx → trigger/TriggerPopoverItems.tsx}
RENAMED
|
@@ -9,66 +9,69 @@ import {
|
|
|
9
9
|
useCallback,
|
|
10
10
|
} from "react";
|
|
11
11
|
import { composeEventHandlers } from "@radix-ui/primitive";
|
|
12
|
-
import {
|
|
13
|
-
import type {
|
|
12
|
+
import { useTriggerPopoverContext } from "./TriggerPopoverContext";
|
|
13
|
+
import type { Unstable_TriggerItem } from "@assistant-ui/core";
|
|
14
14
|
|
|
15
15
|
// =============================================================================
|
|
16
|
-
//
|
|
16
|
+
// TriggerPopoverItems — Renders the list of items within a category
|
|
17
17
|
// =============================================================================
|
|
18
18
|
|
|
19
|
-
export namespace
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
53
|
+
ComposerPrimitiveTriggerPopoverItems.displayName =
|
|
54
|
+
"ComposerPrimitive.TriggerPopoverItems";
|
|
51
55
|
|
|
52
56
|
// =============================================================================
|
|
53
|
-
//
|
|
57
|
+
// TriggerPopoverItem — A single selectable item
|
|
54
58
|
// =============================================================================
|
|
55
59
|
|
|
56
|
-
export namespace
|
|
60
|
+
export namespace ComposerPrimitiveTriggerPopoverItem {
|
|
57
61
|
export type Element = ComponentRef<typeof Primitive.button>;
|
|
58
62
|
export type Props = ComponentPropsWithoutRef<typeof Primitive.button> & {
|
|
59
|
-
item:
|
|
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
|
|
69
|
+
* A button that selects a trigger item.
|
|
67
70
|
* Automatically receives `data-highlighted` when keyboard-navigated.
|
|
68
71
|
*/
|
|
69
|
-
export const
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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";
|
package/src/primitives/composer/{mention/MentionResource.ts → trigger/TriggerPopoverResource.ts}
RENAMED
|
@@ -7,43 +7,70 @@ import {
|
|
|
7
7
|
tapRef,
|
|
8
8
|
} from "@assistant-ui/tap";
|
|
9
9
|
import type {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 {
|
|
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
|
|
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:
|
|
42
|
+
export type SelectItemOverride = (item: Unstable_TriggerItem) => boolean;
|
|
29
43
|
|
|
30
|
-
export type
|
|
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
|
|
36
|
-
readonly items: readonly
|
|
59
|
+
readonly categories: readonly Unstable_TriggerCategory[];
|
|
60
|
+
readonly items: readonly Unstable_TriggerItem[];
|
|
37
61
|
readonly highlightedIndex: number;
|
|
38
62
|
readonly isSearchMode: boolean;
|
|
39
|
-
|
|
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:
|
|
71
|
+
selectItem(item: Unstable_TriggerItem): void;
|
|
45
72
|
close(): void;
|
|
46
|
-
handleKeyDown(e:
|
|
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
|
|
84
|
+
export const TriggerPopoverResource = resource(
|
|
58
85
|
({
|
|
59
86
|
adapter,
|
|
60
87
|
text,
|
|
61
88
|
triggerChar,
|
|
62
|
-
|
|
89
|
+
onSelect,
|
|
63
90
|
aui,
|
|
91
|
+
popoverId,
|
|
64
92
|
}: {
|
|
65
|
-
adapter:
|
|
93
|
+
adapter: Unstable_TriggerAdapter | undefined;
|
|
66
94
|
text: string;
|
|
67
95
|
triggerChar: string;
|
|
68
|
-
|
|
96
|
+
onSelect: OnSelectBehavior;
|
|
69
97
|
aui: AssistantClient;
|
|
70
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
145
|
+
readonly Unstable_TriggerItem[] | null
|
|
116
146
|
>(() => {
|
|
117
|
-
if (!open || !adapter ||
|
|
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
|
-
|
|
121
|
-
const all:
|
|
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
|
|
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
|
-
//
|
|
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:
|
|
242
|
+
const selectItem = tapEffectEvent((item: Unstable_TriggerItem) => {
|
|
220
243
|
if (!trigger) return;
|
|
221
244
|
|
|
222
|
-
// Try the
|
|
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
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
302
|
-
|
|
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
|
-
|
|
365
|
+
popoverId,
|
|
366
|
+
highlightedItemId,
|
|
319
367
|
selectCategory,
|
|
320
368
|
goBack,
|
|
321
369
|
selectItem,
|