@assistant-ui/react 0.14.16 → 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.
Files changed (84) hide show
  1. package/dist/client/ExternalThread.d.ts +4 -3
  2. package/dist/client/ExternalThread.d.ts.map +1 -1
  3. package/dist/client/ExternalThread.js +46 -21
  4. package/dist/client/ExternalThread.js.map +1 -1
  5. package/dist/client/InMemoryThreadList.d.ts +1 -1
  6. package/dist/client/InMemoryThreadList.d.ts.map +1 -1
  7. package/dist/client/InMemoryThreadList.js +7 -5
  8. package/dist/client/InMemoryThreadList.js.map +1 -1
  9. package/dist/client/SingleThreadList.d.ts +1 -6
  10. package/dist/client/SingleThreadList.d.ts.map +1 -1
  11. package/dist/client/SingleThreadList.js +6 -4
  12. package/dist/client/SingleThreadList.js.map +1 -1
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.js +3 -1
  15. package/dist/mcp-apps/McpAppRenderer.d.ts +2 -10
  16. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  17. package/dist/mcp-apps/McpAppRenderer.js +3 -2
  18. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  19. package/dist/mcp-apps/McpAppsRemoteHost.d.ts +1 -8
  20. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
  21. package/dist/mcp-apps/McpAppsRemoteHost.js +3 -2
  22. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
  23. package/dist/primitives/composer/ComposerInput.js +3 -3
  24. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  25. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts +2 -10
  26. package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
  27. package/dist/primitives/composer/trigger/TriggerPopoverResource.js +3 -2
  28. package/dist/primitives/composer/trigger/TriggerPopoverResource.js.map +1 -1
  29. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts +2 -6
  30. package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
  31. package/dist/primitives/composer/trigger/triggerDetectionResource.js +3 -2
  32. package/dist/primitives/composer/trigger/triggerDetectionResource.js.map +1 -1
  33. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts +2 -17
  34. package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
  35. package/dist/primitives/composer/trigger/triggerKeyboardResource.js +3 -2
  36. package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
  37. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts +2 -10
  38. package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
  39. package/dist/primitives/composer/trigger/triggerNavigationResource.js +3 -2
  40. package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
  41. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts +2 -10
  42. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  43. package/dist/primitives/composer/trigger/triggerSelectionResource.js +3 -2
  44. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  45. package/dist/primitives/messagePart/MessagePartText.d.ts +5 -2
  46. package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
  47. package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
  48. package/dist/primitives/reasoning/useScrollLock.js +11 -2
  49. package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
  50. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  51. package/dist/primitives/thread/useThreadViewportAutoScroll.js +5 -0
  52. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  53. package/dist/unstable/useComposerInputHistory.d.ts +30 -0
  54. package/dist/unstable/useComposerInputHistory.d.ts.map +1 -0
  55. package/dist/unstable/useComposerInputHistory.js +117 -0
  56. package/dist/unstable/useComposerInputHistory.js.map +1 -0
  57. package/dist/utils/smooth/useSmooth.d.ts +40 -2
  58. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  59. package/dist/utils/smooth/useSmooth.js +48 -9
  60. package/dist/utils/smooth/useSmooth.js.map +1 -1
  61. package/package.json +4 -4
  62. package/src/client/ExternalThread.ts +70 -27
  63. package/src/client/InMemoryThreadList.ts +11 -7
  64. package/src/client/SingleThreadList.ts +29 -27
  65. package/src/index.ts +8 -0
  66. package/src/mcp-apps/McpAppRenderer.tsx +5 -3
  67. package/src/mcp-apps/McpAppsRemoteHost.ts +5 -3
  68. package/src/primitives/composer/ComposerInput.test.tsx +1 -1
  69. package/src/primitives/composer/ComposerInput.tsx +3 -3
  70. package/src/primitives/composer/trigger/TriggerPopoverResource.ts +5 -3
  71. package/src/primitives/composer/trigger/triggerDetectionResource.ts +21 -21
  72. package/src/primitives/composer/trigger/triggerKeyboardResource.test.ts +5 -4
  73. package/src/primitives/composer/trigger/triggerKeyboardResource.ts +99 -101
  74. package/src/primitives/composer/trigger/triggerNavigationResource.ts +92 -98
  75. package/src/primitives/composer/trigger/triggerSelectionResource.ts +76 -76
  76. package/src/primitives/messagePart/MessagePartText.tsx +3 -2
  77. package/src/primitives/reasoning/useScrollLock.ts +25 -2
  78. package/src/primitives/thread/useThreadViewportAutoScroll.ts +8 -0
  79. package/src/tests/external-thread-branches.test.tsx +160 -0
  80. package/src/tests/shouldContinue.test.ts +33 -0
  81. package/src/unstable/useComposerInputHistory.test.tsx +201 -0
  82. package/src/unstable/useComposerInputHistory.ts +160 -0
  83. package/src/utils/smooth/useSmooth.test.tsx +95 -0
  84. package/src/utils/smooth/useSmooth.ts +82 -10
@@ -61,7 +61,7 @@ vi.mock("@assistant-ui/store", () => {
61
61
  });
62
62
 
63
63
  vi.mock("@assistant-ui/tap", () => ({
64
- flushResourcesSync: (fn: () => void) => fn(),
64
+ flushTapSync: (fn: () => void) => fn(),
65
65
  }));
66
66
 
67
67
  vi.mock("./ComposerInputPluginContext", () => ({
@@ -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 { flushResourcesSync } from "@assistant-ui/tap";
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
- flushResourcesSync(() => {
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
- flushResourcesSync(() => {
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
- export const TriggerPopoverResource = resource(function TriggerPopoverResource({
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
- export const TriggerDetectionResource = resource(
22
- function TriggerDetectionResource({
23
- text,
24
- triggerChar,
25
- }: {
26
- text: string;
27
- triggerChar: string;
28
- }): TriggerDetectionResourceOutput {
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
- const trigger = useMemo(() => {
32
- const pos = Math.min(cursorPosition, text.length);
33
- return detectTrigger(text, triggerChar, pos);
34
- }, [cursorPosition, text, triggerChar]);
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
- const query = trigger?.query ?? "";
35
+ const query = trigger?.query ?? "";
37
36
 
38
- return {
39
- trigger,
40
- query,
41
- setCursorPosition,
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 { createResourceRoot } from "@assistant-ui/tap";
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 = createResourceRoot();
45
- const sub = root.render(TriggerKeyboardResource(props));
46
- return { sub, props };
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
- export const TriggerKeyboardResource = resource(
38
- function TriggerKeyboardResource({
39
- navigableList,
40
- isSearchMode,
41
- activeCategoryId,
42
- query,
43
- popoverId,
44
- open,
45
- selectItem,
46
- selectCategory,
47
- goBack,
48
- close,
49
- }: {
50
- navigableList: readonly (Unstable_TriggerCategory | Unstable_TriggerItem)[];
51
- isSearchMode: boolean;
52
- activeCategoryId: string | null;
53
- query: string;
54
- popoverId: string;
55
- open: boolean;
56
- selectItem: (item: Unstable_TriggerItem) => void;
57
- selectCategory: (categoryId: string) => void;
58
- goBack: () => void;
59
- close: () => void;
60
- }): TriggerKeyboardResourceOutput {
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
- useEffect(() => {
64
- setHighlightedIndex(0);
65
- }, [navigableList]);
62
+ useEffect(() => {
63
+ setHighlightedIndex(0);
64
+ }, [navigableList]);
66
65
 
67
- useEffect(() => {
68
- setHighlightedIndex(0);
69
- }, [isSearchMode, activeCategoryId]);
66
+ useEffect(() => {
67
+ setHighlightedIndex(0);
68
+ }, [isSearchMode, activeCategoryId]);
70
69
 
71
- const highlightIndex = useEffectEvent((index: number) => {
72
- if (index < 0 || index >= navigableList.length) return;
73
- if (index === highlightedIndex) return;
74
- setHighlightedIndex(index);
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
- const handleKeyDown = useEffectEvent(
78
- (e: TriggerPopoverKeyEvent): boolean => {
79
- if (!open) return false;
76
+ const handleKeyDown = useEffectEvent((e: TriggerPopoverKeyEvent): boolean => {
77
+ if (!open) return false;
80
78
 
81
- switch (e.key) {
82
- case "ArrowDown": {
83
- e.preventDefault();
84
- setHighlightedIndex((prev) => {
85
- const len = navigableList.length;
86
- if (len === 0) return 0;
87
- return prev < len - 1 ? prev + 1 : 0;
88
- });
89
- return true;
90
- }
91
- case "ArrowUp": {
92
- e.preventDefault();
93
- setHighlightedIndex((prev) => {
94
- const len = navigableList.length;
95
- if (len === 0) return 0;
96
- return prev > 0 ? prev - 1 : len - 1;
97
- });
98
- return true;
99
- }
100
- case "Enter":
101
- case "Tab": {
102
- if (e.shiftKey) return false;
103
- e.preventDefault();
104
- const item = navigableList[highlightedIndex];
105
- if (!item) return true;
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
- if (isTriggerItem(item)) {
108
- selectItem(item);
109
- } else {
110
- selectCategory(item.id);
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
- const highlightedEntry = navigableList[highlightedIndex];
134
- const highlightedItemId =
135
- open && highlightedEntry
136
- ? `${popoverId}-option-${highlightedEntry.id}`
137
- : undefined;
136
+ return {
137
+ highlightedIndex,
138
+ highlightedItemId,
139
+ highlightIndex,
140
+ handleKeyDown,
141
+ };
142
+ };
138
143
 
139
- return {
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
- export const TriggerNavigationResource = resource(
42
- function TriggerNavigationResource({
43
- adapter,
44
- query,
45
- open,
46
- }: {
47
- adapter: Unstable_TriggerAdapter | undefined;
48
- query: string;
49
- open: boolean;
50
- }): TriggerNavigationResourceOutput {
51
- const [activeCategoryId, setActiveCategoryId] = useState<string | null>(
52
- null,
53
- );
54
-
55
- useEffect(() => {
56
- if (!open) setActiveCategoryId(null);
57
- }, [open]);
58
-
59
- const categories = useMemo<readonly Unstable_TriggerCategory[]>(() => {
60
- if (!open || !adapter) return [];
61
- return adapter.categories();
62
- }, [open, adapter]);
63
-
64
- const effectiveActiveCategoryId = open ? activeCategoryId : null;
65
-
66
- const allItems = useMemo<readonly Unstable_TriggerItem[]>(() => {
67
- if (!effectiveActiveCategoryId || !adapter) return [];
68
- return adapter.categoryItems(effectiveActiveCategoryId);
69
- }, [effectiveActiveCategoryId, adapter]);
70
-
71
- const searchResults = useMemo<
72
- readonly Unstable_TriggerItem[] | null
73
- >(() => {
74
- if (!open || !adapter || effectiveActiveCategoryId) return null;
75
- // If categories exist and query is empty, show categories first (not search)
76
- if (!query && categories.length > 0) return null;
77
- if (adapter.search) return adapter.search(query);
78
-
79
- // fallback: no adapter.search
80
- const all: Unstable_TriggerItem[] = [];
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
- return all;
90
- }, [open, adapter, query, effectiveActiveCategoryId, categories]);
91
-
92
- const isSearchMode = searchResults !== null;
93
-
94
- const filteredCategories = useMemo(() => {
95
- if (isSearchMode) return [];
96
- if (!query) return categories;
97
- const lower = query.toLowerCase();
98
- return categories.filter((cat) =>
99
- cat.label.toLowerCase().includes(lower),
100
- );
101
- }, [categories, query, isSearchMode]);
102
-
103
- const filteredItems = useMemo(() => {
104
- if (isSearchMode) return searchResults ?? [];
105
- if (!query) return allItems;
106
- const lower = query.toLowerCase();
107
- return allItems.filter((item) => matchesQuery(item, lower));
108
- }, [allItems, query, isSearchMode, searchResults]);
109
-
110
- const navigableList = useMemo(() => {
111
- if (isSearchMode) return searchResults ?? [];
112
- if (effectiveActiveCategoryId) return filteredItems;
113
- return filteredCategories;
114
- }, [
115
- isSearchMode,
116
- searchResults,
117
- effectiveActiveCategoryId,
118
- filteredItems,
119
- filteredCategories,
120
- ]);
121
-
122
- const selectCategory = useEffectEvent((categoryId: string) => {
123
- setActiveCategoryId(categoryId);
124
- });
125
-
126
- const goBack = useEffectEvent(() => {
127
- setActiveCategoryId(null);
128
- });
129
-
130
- return {
131
- categories: filteredCategories,
132
- items: filteredItems,
133
- isSearchMode,
134
- activeCategoryId: effectiveActiveCategoryId,
135
- navigableList,
136
- selectCategory,
137
- goBack,
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);