@assistant-ui/react 0.14.15 → 0.14.18
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 +4 -3
- package/dist/client/ExternalThread.d.ts.map +1 -1
- package/dist/client/ExternalThread.js +46 -21
- package/dist/client/ExternalThread.js.map +1 -1
- package/dist/client/InMemoryThreadList.d.ts +1 -1
- package/dist/client/InMemoryThreadList.d.ts.map +1 -1
- package/dist/client/InMemoryThreadList.js +7 -5
- package/dist/client/InMemoryThreadList.js.map +1 -1
- package/dist/client/SingleThreadList.d.ts +1 -6
- package/dist/client/SingleThreadList.d.ts.map +1 -1
- package/dist/client/SingleThreadList.js +6 -4
- package/dist/client/SingleThreadList.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/mcp-apps/McpAppRenderer.d.ts +2 -10
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +3 -2
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/McpAppsRemoteHost.d.ts +1 -8
- package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppsRemoteHost.js +3 -2
- package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +3 -3
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +2 -10
- package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js +3 -2
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts +2 -6
- package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerDetectionResource.js +3 -2
- package/dist/primitives/composer/trigger/triggerDetectionResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts +2 -17
- package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js +3 -2
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts +2 -10
- package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.js +3 -2
- package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts +2 -10
- package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerSelectionResource.js +3 -2
- package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.d.ts +5 -2
- package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
- package/dist/primitives/reasoning/useScrollLock.js +11 -2
- package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +5 -0
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/unstable/useComposerInputHistory.d.ts +30 -0
- package/dist/unstable/useComposerInputHistory.d.ts.map +1 -0
- package/dist/unstable/useComposerInputHistory.js +117 -0
- package/dist/unstable/useComposerInputHistory.js.map +1 -0
- package/dist/utils/smooth/useSmooth.d.ts +40 -2
- package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
- package/dist/utils/smooth/useSmooth.js +48 -9
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/package.json +31 -24
- package/src/client/ExternalThread.ts +70 -27
- package/src/client/InMemoryThreadList.ts +11 -7
- package/src/client/SingleThreadList.ts +29 -27
- package/src/index.ts +8 -0
- package/src/mcp-apps/McpAppRenderer.tsx +5 -3
- package/src/mcp-apps/McpAppsRemoteHost.ts +5 -3
- package/src/primitives/composer/ComposerInput.test.tsx +1 -1
- package/src/primitives/composer/ComposerInput.tsx +3 -3
- package/src/primitives/composer/trigger/TriggerPopoverResource.ts +5 -3
- package/src/primitives/composer/trigger/triggerDetectionResource.ts +21 -21
- package/src/primitives/composer/trigger/triggerKeyboardResource.test.ts +5 -4
- package/src/primitives/composer/trigger/triggerKeyboardResource.ts +99 -101
- package/src/primitives/composer/trigger/triggerNavigationResource.ts +92 -98
- package/src/primitives/composer/trigger/triggerSelectionResource.ts +76 -76
- package/src/primitives/messagePart/MessagePartText.tsx +3 -2
- package/src/primitives/reasoning/useScrollLock.ts +25 -2
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +8 -0
- package/src/tests/external-thread-branches.test.tsx +160 -0
- package/src/tests/shouldContinue.test.ts +33 -0
- package/src/unstable/useComposerInputHistory.test.tsx +201 -0
- package/src/unstable/useComposerInputHistory.ts +160 -0
- package/src/utils/smooth/useSmooth.test.tsx +95 -0
- package/src/utils/smooth/useSmooth.ts +82 -10
package/src/index.ts
CHANGED
|
@@ -141,6 +141,7 @@ export {
|
|
|
141
141
|
type MessageQueueDriver,
|
|
142
142
|
type MessageQueueController,
|
|
143
143
|
type ExternalThreadQueueAdapter,
|
|
144
|
+
type ExternalThreadBranchAdapter,
|
|
144
145
|
} from "@assistant-ui/core";
|
|
145
146
|
export { useExternalStoreRuntime } from "./legacy-runtime/runtime-cores/external-store/useExternalStoreRuntime";
|
|
146
147
|
export { useExternalStoreSharedOptions } from "@assistant-ui/core/react";
|
|
@@ -288,6 +289,7 @@ export { useThreadViewportAutoScroll } from "./primitives/thread/useThreadViewpo
|
|
|
288
289
|
export { useScrollLock } from "./primitives/reasoning/useScrollLock";
|
|
289
290
|
export { useMessageQuote } from "./hooks/useMessageQuote";
|
|
290
291
|
export { useMessageTiming } from "./hooks/useMessageTiming";
|
|
292
|
+
export { useSmooth, type SmoothOptions } from "./utils/smooth/useSmooth";
|
|
291
293
|
|
|
292
294
|
// Re-export core types from @assistant-ui/core
|
|
293
295
|
export type {
|
|
@@ -421,6 +423,12 @@ export type {
|
|
|
421
423
|
} from "@assistant-ui/core";
|
|
422
424
|
export { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
|
|
423
425
|
|
|
426
|
+
// Unstable - composer input history (terminal-style ArrowUp/ArrowDown recall)
|
|
427
|
+
export {
|
|
428
|
+
unstable_useComposerInputHistory,
|
|
429
|
+
type Unstable_ComposerInputHistory,
|
|
430
|
+
} from "./unstable/useComposerInputHistory";
|
|
431
|
+
|
|
424
432
|
export type { Assistant } from "./augmentations";
|
|
425
433
|
|
|
426
434
|
// --- mcp-apps ---
|
|
@@ -195,9 +195,9 @@ function InlineRenderer({
|
|
|
195
195
|
* renderer loads that resource from the configured host and displays it in a
|
|
196
196
|
* sandboxed frame.
|
|
197
197
|
*/
|
|
198
|
-
|
|
198
|
+
const useMcpAppRenderer = (
|
|
199
199
|
options: McpAppRendererOptions,
|
|
200
|
-
): { readonly render: ToolCallMessagePartComponent } {
|
|
200
|
+
): { readonly render: ToolCallMessagePartComponent } => {
|
|
201
201
|
const host = useResource(options.host);
|
|
202
202
|
|
|
203
203
|
const optionsRef = useRef<McpAppRendererOptions>(options);
|
|
@@ -219,4 +219,6 @@ export const McpAppRenderer = resource(function McpAppRenderer(
|
|
|
219
219
|
}, []);
|
|
220
220
|
|
|
221
221
|
return { render };
|
|
222
|
-
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const McpAppRenderer = resource(useMcpAppRenderer);
|
|
@@ -34,9 +34,9 @@ async function postToHost(
|
|
|
34
34
|
* params }`, using the method names expected by the assistant-ui MCP Apps
|
|
35
35
|
* guide.
|
|
36
36
|
*/
|
|
37
|
-
|
|
37
|
+
const useMcpAppsRemoteHost = (
|
|
38
38
|
options: McpAppsRemoteHostOptions,
|
|
39
|
-
): McpAppsHost {
|
|
39
|
+
): McpAppsHost => {
|
|
40
40
|
const optionsRef = useRef(options);
|
|
41
41
|
optionsRef.current = options;
|
|
42
42
|
|
|
@@ -57,4 +57,6 @@ export const McpAppsRemoteHost = resource(function McpAppsRemoteHost(
|
|
|
57
57
|
}),
|
|
58
58
|
[],
|
|
59
59
|
);
|
|
60
|
-
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const McpAppsRemoteHost = resource(useMcpAppsRemoteHost);
|
|
@@ -22,7 +22,7 @@ import { useEscapeKeydown } from "@radix-ui/react-use-escape-keydown";
|
|
|
22
22
|
import { useOnScrollToBottom } from "../../utils/hooks/useOnScrollToBottom";
|
|
23
23
|
import { useMediaQuery } from "../../utils/hooks/useMediaQuery";
|
|
24
24
|
import { useAuiState, useAui } from "@assistant-ui/store";
|
|
25
|
-
import {
|
|
25
|
+
import { flushTapSync } from "@assistant-ui/tap";
|
|
26
26
|
import { useComposerInputPluginRegistryOptional } from "./ComposerInputPluginContext";
|
|
27
27
|
import { useTriggerPopoverActiveAriaOptional } from "./trigger/TriggerPopoverRootContext";
|
|
28
28
|
|
|
@@ -350,7 +350,7 @@ export const ComposerPrimitiveInput = forwardRef<
|
|
|
350
350
|
}
|
|
351
351
|
const isComposing = nativeIsComposing || compositionRef.current;
|
|
352
352
|
// keep controlled value in sync mid-IME so react does not reset the textarea to a stale value
|
|
353
|
-
|
|
353
|
+
flushTapSync(() => {
|
|
354
354
|
aui.composer().setText(e.target.value);
|
|
355
355
|
});
|
|
356
356
|
if (isComposing) return;
|
|
@@ -377,7 +377,7 @@ export const ComposerPrimitiveInput = forwardRef<
|
|
|
377
377
|
compositionRef.current = false;
|
|
378
378
|
if (!aui.composer().getState().isEditing) return;
|
|
379
379
|
const target = e.target as HTMLTextAreaElement;
|
|
380
|
-
|
|
380
|
+
flushTapSync(() => {
|
|
381
381
|
aui.composer().setText(target.value);
|
|
382
382
|
});
|
|
383
383
|
const pos = target.selectionStart ?? target.value.length;
|
|
@@ -51,7 +51,7 @@ export type TriggerPopoverResourceOutput = {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
/** Composes detection, navigation, keyboard, and selection sub-resources. */
|
|
54
|
-
|
|
54
|
+
const useTriggerPopoverResource = ({
|
|
55
55
|
adapter,
|
|
56
56
|
text,
|
|
57
57
|
triggerChar,
|
|
@@ -66,7 +66,7 @@ export const TriggerPopoverResource = resource(function TriggerPopoverResource({
|
|
|
66
66
|
aui: AssistantClient;
|
|
67
67
|
/** Stable ID for accessible element IDs (pass React's useId() from component layer). */
|
|
68
68
|
popoverId: string;
|
|
69
|
-
}): TriggerPopoverResourceOutput {
|
|
69
|
+
}): TriggerPopoverResourceOutput => {
|
|
70
70
|
const detection = useResource(
|
|
71
71
|
TriggerDetectionResource({ text, triggerChar }),
|
|
72
72
|
);
|
|
@@ -133,4 +133,6 @@ export const TriggerPopoverResource = resource(function TriggerPopoverResource({
|
|
|
133
133
|
setCursorPosition: detection.setCursorPosition,
|
|
134
134
|
registerSelectItemOverride: selection.registerSelectItemOverride,
|
|
135
135
|
};
|
|
136
|
-
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const TriggerPopoverResource = resource(useTriggerPopoverResource);
|
|
@@ -18,27 +18,27 @@ export type TriggerDetectionResourceOutput = {
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
/** Tracks cursor position and derives the active trigger + query from composer text. */
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const [cursorPosition, setCursorPosition] = useState(text.length);
|
|
21
|
+
const useTriggerDetectionResource = ({
|
|
22
|
+
text,
|
|
23
|
+
triggerChar,
|
|
24
|
+
}: {
|
|
25
|
+
text: string;
|
|
26
|
+
triggerChar: string;
|
|
27
|
+
}): TriggerDetectionResourceOutput => {
|
|
28
|
+
const [cursorPosition, setCursorPosition] = useState(text.length);
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
const trigger = useMemo(() => {
|
|
31
|
+
const pos = Math.min(cursorPosition, text.length);
|
|
32
|
+
return detectTrigger(text, triggerChar, pos);
|
|
33
|
+
}, [cursorPosition, text, triggerChar]);
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
const query = trigger?.query ?? "";
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
return {
|
|
38
|
+
trigger,
|
|
39
|
+
query,
|
|
40
|
+
setCursorPosition,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const TriggerDetectionResource = resource(useTriggerDetectionResource);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { createTapRoot, useResource } from "@assistant-ui/tap";
|
|
3
3
|
import type {
|
|
4
4
|
Unstable_TriggerCategory,
|
|
5
5
|
Unstable_TriggerItem,
|
|
@@ -41,9 +41,10 @@ const render = (
|
|
|
41
41
|
close: vi.fn(),
|
|
42
42
|
...overrides,
|
|
43
43
|
};
|
|
44
|
-
const root =
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const root = createTapRoot(function Root() {
|
|
45
|
+
return useResource(TriggerKeyboardResource(props));
|
|
46
|
+
});
|
|
47
|
+
return { sub: root, props };
|
|
47
48
|
};
|
|
48
49
|
|
|
49
50
|
describe("TriggerKeyboardResource", () => {
|
|
@@ -34,113 +34,111 @@ export type TriggerKeyboardResourceOutput = {
|
|
|
34
34
|
* Owns keyboard-driven highlight state for the popover. Delegates selection,
|
|
35
35
|
* category drill-in, back, and close to the callbacks supplied by the parent.
|
|
36
36
|
*/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
37
|
+
const useTriggerKeyboardResource = ({
|
|
38
|
+
navigableList,
|
|
39
|
+
isSearchMode,
|
|
40
|
+
activeCategoryId,
|
|
41
|
+
query,
|
|
42
|
+
popoverId,
|
|
43
|
+
open,
|
|
44
|
+
selectItem,
|
|
45
|
+
selectCategory,
|
|
46
|
+
goBack,
|
|
47
|
+
close,
|
|
48
|
+
}: {
|
|
49
|
+
navigableList: readonly (Unstable_TriggerCategory | Unstable_TriggerItem)[];
|
|
50
|
+
isSearchMode: boolean;
|
|
51
|
+
activeCategoryId: string | null;
|
|
52
|
+
query: string;
|
|
53
|
+
popoverId: string;
|
|
54
|
+
open: boolean;
|
|
55
|
+
selectItem: (item: Unstable_TriggerItem) => void;
|
|
56
|
+
selectCategory: (categoryId: string) => void;
|
|
57
|
+
goBack: () => void;
|
|
58
|
+
close: () => void;
|
|
59
|
+
}): TriggerKeyboardResourceOutput => {
|
|
60
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
setHighlightedIndex(0);
|
|
64
|
+
}, [navigableList]);
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
setHighlightedIndex(0);
|
|
68
|
+
}, [isSearchMode, activeCategoryId]);
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
const highlightIndex = useEffectEvent((index: number) => {
|
|
71
|
+
if (index < 0 || index >= navigableList.length) return;
|
|
72
|
+
if (index === highlightedIndex) return;
|
|
73
|
+
setHighlightedIndex(index);
|
|
74
|
+
});
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (!open) return false;
|
|
76
|
+
const handleKeyDown = useEffectEvent((e: TriggerPopoverKeyEvent): boolean => {
|
|
77
|
+
if (!open) return false;
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
79
|
+
switch (e.key) {
|
|
80
|
+
case "ArrowDown": {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
setHighlightedIndex((prev) => {
|
|
83
|
+
const len = navigableList.length;
|
|
84
|
+
if (len === 0) return 0;
|
|
85
|
+
return prev < len - 1 ? prev + 1 : 0;
|
|
86
|
+
});
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
case "ArrowUp": {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
setHighlightedIndex((prev) => {
|
|
92
|
+
const len = navigableList.length;
|
|
93
|
+
if (len === 0) return 0;
|
|
94
|
+
return prev > 0 ? prev - 1 : len - 1;
|
|
95
|
+
});
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
case "Enter":
|
|
99
|
+
case "Tab": {
|
|
100
|
+
if (e.shiftKey) return false;
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
const item = navigableList[highlightedIndex];
|
|
103
|
+
if (!item) return true;
|
|
106
104
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
case "Escape": {
|
|
115
|
-
e.preventDefault();
|
|
116
|
-
close();
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
case "Backspace": {
|
|
120
|
-
if (activeCategoryId && query === "") {
|
|
121
|
-
e.preventDefault();
|
|
122
|
-
goBack();
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
default:
|
|
128
|
-
return false;
|
|
105
|
+
if (isTriggerItem(item)) {
|
|
106
|
+
selectItem(item);
|
|
107
|
+
} else {
|
|
108
|
+
selectCategory(item.id);
|
|
129
109
|
}
|
|
130
|
-
|
|
131
|
-
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
case "Escape": {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
close();
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
case "Backspace": {
|
|
118
|
+
if (activeCategoryId && query === "") {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
goBack();
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
default:
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const highlightedEntry = navigableList[highlightedIndex];
|
|
131
|
+
const highlightedItemId =
|
|
132
|
+
open && highlightedEntry
|
|
133
|
+
? `${popoverId}-option-${highlightedEntry.id}`
|
|
134
|
+
: undefined;
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
return {
|
|
137
|
+
highlightedIndex,
|
|
138
|
+
highlightedItemId,
|
|
139
|
+
highlightIndex,
|
|
140
|
+
handleKeyDown,
|
|
141
|
+
};
|
|
142
|
+
};
|
|
138
143
|
|
|
139
|
-
|
|
140
|
-
highlightedIndex,
|
|
141
|
-
highlightedItemId,
|
|
142
|
-
highlightIndex,
|
|
143
|
-
handleKeyDown,
|
|
144
|
-
};
|
|
145
|
-
},
|
|
146
|
-
);
|
|
144
|
+
export const TriggerKeyboardResource = resource(useTriggerKeyboardResource);
|
|
@@ -38,103 +38,97 @@ export type TriggerNavigationResourceOutput = {
|
|
|
38
38
|
* Computes categories, items, search results, and navigation state from the
|
|
39
39
|
* adapter + current query. Pure derivation — no side effects on the composer.
|
|
40
40
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const lower = query.toLowerCase();
|
|
82
|
-
for (const cat of categories) {
|
|
83
|
-
for (const item of adapter.categoryItems(cat.id)) {
|
|
84
|
-
if (matchesQuery(item, lower)) {
|
|
85
|
-
all.push(item);
|
|
86
|
-
}
|
|
41
|
+
const useTriggerNavigationResource = ({
|
|
42
|
+
adapter,
|
|
43
|
+
query,
|
|
44
|
+
open,
|
|
45
|
+
}: {
|
|
46
|
+
adapter: Unstable_TriggerAdapter | undefined;
|
|
47
|
+
query: string;
|
|
48
|
+
open: boolean;
|
|
49
|
+
}): TriggerNavigationResourceOutput => {
|
|
50
|
+
const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!open) setActiveCategoryId(null);
|
|
54
|
+
}, [open]);
|
|
55
|
+
|
|
56
|
+
const categories = useMemo<readonly Unstable_TriggerCategory[]>(() => {
|
|
57
|
+
if (!open || !adapter) return [];
|
|
58
|
+
return adapter.categories();
|
|
59
|
+
}, [open, adapter]);
|
|
60
|
+
|
|
61
|
+
const effectiveActiveCategoryId = open ? activeCategoryId : null;
|
|
62
|
+
|
|
63
|
+
const allItems = useMemo<readonly Unstable_TriggerItem[]>(() => {
|
|
64
|
+
if (!effectiveActiveCategoryId || !adapter) return [];
|
|
65
|
+
return adapter.categoryItems(effectiveActiveCategoryId);
|
|
66
|
+
}, [effectiveActiveCategoryId, adapter]);
|
|
67
|
+
|
|
68
|
+
const searchResults = useMemo<readonly Unstable_TriggerItem[] | null>(() => {
|
|
69
|
+
if (!open || !adapter || effectiveActiveCategoryId) return null;
|
|
70
|
+
// If categories exist and query is empty, show categories first (not search)
|
|
71
|
+
if (!query && categories.length > 0) return null;
|
|
72
|
+
if (adapter.search) return adapter.search(query);
|
|
73
|
+
|
|
74
|
+
// fallback: no adapter.search
|
|
75
|
+
const all: Unstable_TriggerItem[] = [];
|
|
76
|
+
const lower = query.toLowerCase();
|
|
77
|
+
for (const cat of categories) {
|
|
78
|
+
for (const item of adapter.categoryItems(cat.id)) {
|
|
79
|
+
if (matchesQuery(item, lower)) {
|
|
80
|
+
all.push(item);
|
|
87
81
|
}
|
|
88
82
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
);
|
|
83
|
+
}
|
|
84
|
+
return all;
|
|
85
|
+
}, [open, adapter, query, effectiveActiveCategoryId, categories]);
|
|
86
|
+
|
|
87
|
+
const isSearchMode = searchResults !== null;
|
|
88
|
+
|
|
89
|
+
const filteredCategories = useMemo(() => {
|
|
90
|
+
if (isSearchMode) return [];
|
|
91
|
+
if (!query) return categories;
|
|
92
|
+
const lower = query.toLowerCase();
|
|
93
|
+
return categories.filter((cat) => cat.label.toLowerCase().includes(lower));
|
|
94
|
+
}, [categories, query, isSearchMode]);
|
|
95
|
+
|
|
96
|
+
const filteredItems = useMemo(() => {
|
|
97
|
+
if (isSearchMode) return searchResults ?? [];
|
|
98
|
+
if (!query) return allItems;
|
|
99
|
+
const lower = query.toLowerCase();
|
|
100
|
+
return allItems.filter((item) => matchesQuery(item, lower));
|
|
101
|
+
}, [allItems, query, isSearchMode, searchResults]);
|
|
102
|
+
|
|
103
|
+
const navigableList = useMemo(() => {
|
|
104
|
+
if (isSearchMode) return searchResults ?? [];
|
|
105
|
+
if (effectiveActiveCategoryId) return filteredItems;
|
|
106
|
+
return filteredCategories;
|
|
107
|
+
}, [
|
|
108
|
+
isSearchMode,
|
|
109
|
+
searchResults,
|
|
110
|
+
effectiveActiveCategoryId,
|
|
111
|
+
filteredItems,
|
|
112
|
+
filteredCategories,
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const selectCategory = useEffectEvent((categoryId: string) => {
|
|
116
|
+
setActiveCategoryId(categoryId);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const goBack = useEffectEvent(() => {
|
|
120
|
+
setActiveCategoryId(null);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
categories: filteredCategories,
|
|
125
|
+
items: filteredItems,
|
|
126
|
+
isSearchMode,
|
|
127
|
+
activeCategoryId: effectiveActiveCategoryId,
|
|
128
|
+
navigableList,
|
|
129
|
+
selectCategory,
|
|
130
|
+
goBack,
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const TriggerNavigationResource = resource(useTriggerNavigationResource);
|