@astryxdesign/core 0.7.3 → 0.7.4-canary.1a552f2

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 (143) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts +18 -6
  2. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheet.js +268 -58
  4. package/dist/BottomSheet/BottomSheetSwitcher.d.ts +41 -12
  5. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheetSwitcher.js +294 -165
  7. package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts +51 -1
  8. package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts.map +1 -1
  9. package/dist/BottomSheet/BottomSheetSwitcherContext.js +13 -1
  10. package/dist/Carousel/Carousel.d.ts.map +1 -1
  11. package/dist/Carousel/Carousel.js +7 -11
  12. package/dist/Chat/ChatMessageList.d.ts.map +1 -1
  13. package/dist/Chat/ChatMessageList.js +8 -1
  14. package/dist/Chat/useTriggerMenu.d.ts.map +1 -1
  15. package/dist/Chat/useTriggerMenu.js +46 -7
  16. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  17. package/dist/CheckboxInput/CheckboxInput.js +6 -0
  18. package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
  19. package/dist/CommandPalette/CommandPalette.js +6 -11
  20. package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
  21. package/dist/CommandPalette/CommandPaletteInput.js +7 -1
  22. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  23. package/dist/ComplexSelector/ComplexSelector.js +6 -1
  24. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  25. package/dist/ContextMenu/ContextMenu.js +12 -0
  26. package/dist/DateInput/DateInput.d.ts.map +1 -1
  27. package/dist/DateInput/DateInput.js +6 -1
  28. package/dist/DateInput/NativeDateField.d.ts.map +1 -1
  29. package/dist/DateInput/NativeDateField.js +6 -1
  30. package/dist/DateInput/TouchDateField.d.ts.map +1 -1
  31. package/dist/DateInput/TouchDateField.js +9 -1
  32. package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
  33. package/dist/DateRangeInput/DateRangeInput.js +6 -1
  34. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  35. package/dist/DateTimeInput/DateTimeInput.js +6 -1
  36. package/dist/DateTimeInput/TouchDateTimeField.d.ts.map +1 -1
  37. package/dist/DateTimeInput/TouchDateTimeField.js +9 -1
  38. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  39. package/dist/DropdownMenu/DropdownMenuSubMenu.js +7 -1
  40. package/dist/FileInput/FileInput.d.ts.map +1 -1
  41. package/dist/FileInput/FileInput.js +16 -2
  42. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  43. package/dist/MultiSelector/MultiSelector.js +6 -1
  44. package/dist/Selector/Selector.d.ts.map +1 -1
  45. package/dist/Selector/Selector.js +6 -1
  46. package/dist/TextArea/TextArea.d.ts.map +1 -1
  47. package/dist/TextArea/TextArea.js +6 -1
  48. package/dist/TextInput/TextInput.d.ts.map +1 -1
  49. package/dist/TextInput/TextInput.js +6 -1
  50. package/dist/Thumbnail/Thumbnail.d.ts.map +1 -1
  51. package/dist/Thumbnail/Thumbnail.js +9 -2
  52. package/dist/TimeInput/TimeInput.d.ts.map +1 -1
  53. package/dist/TimeInput/TimeInput.js +6 -1
  54. package/dist/TimeInput/TouchTimeField.d.ts.map +1 -1
  55. package/dist/TimeInput/TouchTimeField.js +9 -1
  56. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  57. package/dist/Tokenizer/Tokenizer.js +9 -1
  58. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  59. package/dist/Typeahead/BaseTypeahead.js +7 -1
  60. package/dist/Typeahead/Typeahead.d.ts.map +1 -1
  61. package/dist/Typeahead/Typeahead.js +9 -1
  62. package/dist/astryx.css +2 -0
  63. package/dist/hooks/useBusyAnnouncement.d.ts +22 -0
  64. package/dist/hooks/useBusyAnnouncement.d.ts.map +1 -0
  65. package/dist/hooks/useBusyAnnouncement.js +87 -0
  66. package/dist/hooks/useMenuPress.d.ts +2 -2
  67. package/dist/hooks/useMenuPress.d.ts.map +1 -1
  68. package/dist/hooks/useMenuPress.js +5 -1
  69. package/dist/hooks/useTypeahead.d.ts.map +1 -1
  70. package/dist/hooks/useTypeahead.js +7 -2
  71. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  72. package/dist/i18n/generated-locales/en.generated.js +1 -0
  73. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  74. package/dist/i18n/generated-locales/pseudo.generated.js +1 -0
  75. package/locales/en.json +6 -2
  76. package/locales/pseudo.json +3 -0
  77. package/package.json +4 -4
  78. package/src/BottomSheet/BottomSheet.doc.mjs +2 -2
  79. package/src/BottomSheet/BottomSheet.spec.md +3 -5
  80. package/src/BottomSheet/BottomSheet.tsx +260 -47
  81. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +29 -4
  82. package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +11 -10
  83. package/src/BottomSheet/BottomSheetSwitcher.spec.md +35 -41
  84. package/src/BottomSheet/BottomSheetSwitcher.test.tsx +1051 -49
  85. package/src/BottomSheet/BottomSheetSwitcher.tsx +396 -215
  86. package/src/BottomSheet/BottomSheetSwitcherContext.ts +57 -2
  87. package/src/BottomSheet/__tests__/BottomSheetNativeModal.a11y.browser.spec.ts +461 -0
  88. package/src/Carousel/Carousel.test.tsx +8 -12
  89. package/src/Carousel/Carousel.tsx +6 -13
  90. package/src/Chat/ChatComposerInput.test.tsx +167 -1
  91. package/src/Chat/ChatMessageList.test.tsx +6 -2
  92. package/src/Chat/ChatMessageList.tsx +7 -1
  93. package/src/Chat/__tests__/ChatMessageList.a11y.chromium.spec.ts +20 -12
  94. package/src/Chat/useTriggerMenu.tsx +55 -6
  95. package/src/CheckboxInput/CheckboxInput.tsx +6 -0
  96. package/src/CommandPalette/CommandPalette.test.tsx +4 -2
  97. package/src/CommandPalette/CommandPalette.tsx +6 -11
  98. package/src/CommandPalette/CommandPaletteInput.test.tsx +3 -1
  99. package/src/CommandPalette/CommandPaletteInput.tsx +10 -1
  100. package/src/ComplexSelector/ComplexSelector.tsx +5 -1
  101. package/src/ContextMenu/ContextMenu.test.tsx +34 -0
  102. package/src/ContextMenu/ContextMenu.tsx +13 -1
  103. package/src/DateInput/DateInput.tsx +5 -1
  104. package/src/DateInput/DateInputTouch.test.tsx +45 -0
  105. package/src/DateInput/NativeDateField.test.tsx +10 -2
  106. package/src/DateInput/NativeDateField.tsx +5 -1
  107. package/src/DateInput/TouchDateField.tsx +14 -2
  108. package/src/DateRangeInput/DateRangeInput.tsx +5 -1
  109. package/src/DateTimeInput/DateTimeInput.tsx +5 -1
  110. package/src/DateTimeInput/TouchDateTimeField.tsx +14 -2
  111. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +10 -1
  112. package/src/FileInput/FileInput.tsx +18 -2
  113. package/src/MultiSelector/MultiSelector.test.tsx +18 -4
  114. package/src/MultiSelector/MultiSelector.tsx +5 -1
  115. package/src/ProgressBar/ProgressBar.test.tsx +4 -21
  116. package/src/ProgressBar/__tests__/ProgressBar.a11y.chromium.spec.ts +114 -0
  117. package/src/ProgressBar/__tests__/ProgressBar.a11y.known-failures.ts +12 -0
  118. package/src/ProgressBar/__tests__/ProgressBar.a11y.renders.tsx +38 -0
  119. package/src/ProgressBar/__tests__/ProgressBar.a11y.states.ts +123 -0
  120. package/src/ProgressBar/__tests__/ProgressBar.a11y.test.tsx +107 -0
  121. package/src/Selector/Selector.test.tsx +22 -5
  122. package/src/Selector/Selector.tsx +5 -1
  123. package/src/TextArea/TextArea.test.tsx +9 -1
  124. package/src/TextArea/TextArea.tsx +5 -1
  125. package/src/TextInput/TextInput.tsx +5 -1
  126. package/src/Thumbnail/Thumbnail.test.tsx +6 -1
  127. package/src/Thumbnail/Thumbnail.tsx +12 -3
  128. package/src/TimeInput/TimeInput.tsx +5 -1
  129. package/src/TimeInput/TouchTimeField.tsx +12 -2
  130. package/src/Tokenizer/Tokenizer.test.tsx +17 -12
  131. package/src/Tokenizer/Tokenizer.tsx +10 -1
  132. package/src/Typeahead/BaseTypeahead.tsx +14 -1
  133. package/src/Typeahead/Typeahead.test.tsx +32 -41
  134. package/src/Typeahead/Typeahead.tsx +10 -1
  135. package/src/__tests__/BusyOwnerAnnouncement.test.tsx +792 -0
  136. package/src/hooks/__tests__/MenuPressShadowRoot.a11y.chromium.spec.ts +50 -0
  137. package/src/hooks/useBusyAnnouncement.ts +100 -0
  138. package/src/hooks/useMenuPress.test.tsx +27 -1
  139. package/src/hooks/useMenuPress.ts +12 -3
  140. package/src/hooks/useTypeahead.test.tsx +42 -1
  141. package/src/hooks/useTypeahead.ts +7 -2
  142. package/src/i18n/generated-locales/en.generated.ts +1 -0
  143. package/src/i18n/generated-locales/pseudo.generated.ts +1 -0
@@ -0,0 +1,50 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file MenuPressShadowRoot.a11y.chromium.spec.ts
5
+ * @input The `a11y/Menu press in a shadow root` story in a built Storybook and
6
+ * real mouse input.
7
+ * @output Real-engine proof that a menu rendered inside a shadow root acts on
8
+ * the row under a released pointer (module:DropdownMenu/useMenuPress FR1).
9
+ * @position Run with `pnpm test:a11y-contract`. Only a real engine resolves a
10
+ * point inside a shadow root to the shadow host at the document level.
11
+ */
12
+
13
+ import {expect, test} from '@playwright/test';
14
+ import {
15
+ DEFAULT_STORYBOOK_DIR,
16
+ serveStorybook,
17
+ type StaticServer,
18
+ } from '@astryxdesign/a11y-spec/storybook';
19
+
20
+ let storybook: StaticServer;
21
+
22
+ test.beforeAll(async () => {
23
+ storybook = await serveStorybook(
24
+ process.env.ASTRYX_STORYBOOK_DIR ?? DEFAULT_STORYBOOK_DIR,
25
+ );
26
+ });
27
+
28
+ test.afterAll(async () => {
29
+ await storybook?.close();
30
+ });
31
+
32
+ const STORY = 'a11y-menu-press-in-a-shadow-root--selector-in-shadow-root';
33
+
34
+ test('a mouse click on an option inside a shadow root selects it', async ({
35
+ page,
36
+ }) => {
37
+ await page.goto(`${storybook.origin}/iframe.html?id=${STORY}&viewMode=story`);
38
+ // Playwright's role locators pierce open shadow roots.
39
+ await page.getByRole('combobox', {name: 'Fruit'}).click();
40
+ const option = page.getByRole('option', {name: 'Banana'});
41
+ await expect(option).toBeVisible();
42
+ const box = await option.boundingBox();
43
+ if (box == null) {
44
+ throw new Error('the Banana option has no layout box');
45
+ }
46
+
47
+ await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
48
+
49
+ await expect(page.getByTestId('selected-value')).toHaveText('Banana');
50
+ });
@@ -0,0 +1,100 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file useBusyAnnouncement.ts
7
+ * @input Whether the owning control is busy, the words to announce, and a ref
8
+ * to an element inside the control
9
+ * @output Announces each busy start once, through the shared polite channel
10
+ * or a persistent region inside the open modal dialog holding the control
11
+ * @position Core a11y hook for controls that show a decorative busy Spinner
12
+ *
13
+ * The busy Spinner inside a control is decorative (hidden, no live role); the
14
+ * control announces its busy state instead (`spec:AST-086` FR8), once per busy
15
+ * start (FR6): rerenders and StrictMode replay do not repeat it, and settling
16
+ * re-arms it. While a native modal is open, the body-level channel is not
17
+ * exposed, so a control inside it announces through one persistent polite
18
+ * region in that dialog, inserted empty and filled a frame later (Platform
19
+ * support, FR3).
20
+ *
21
+ * SYNC: deliberately NOT exported from /packages/core/src/hooks/index.ts —
22
+ * this is a shared implementation detail of Astryx's busy controls.
23
+ */
24
+
25
+ import {useEffect, useRef, type RefObject} from 'react';
26
+ import {useAnnounce} from './useAnnounce';
27
+
28
+ const REGION_ATTR = 'data-astryx-modal-busy-region';
29
+ const VISUALLY_HIDDEN_CSS =
30
+ 'position:absolute;width:1px;height:1px;margin:-1px;padding:0;' +
31
+ 'overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
32
+ const CLEAR_DELAY_MS = 2000; // Matches useAnnounce.
33
+
34
+ // The pending write and clear of each in-dialog region; a new write cancels
35
+ // them so an earlier message can neither land late nor clear a later one.
36
+ const pendingByRegion = new WeakMap<
37
+ HTMLElement,
38
+ {frame: number; timer?: ReturnType<typeof setTimeout>}
39
+ >();
40
+
41
+ function announceInModal(dialog: HTMLDialogElement, message: string): void {
42
+ let region = dialog.querySelector<HTMLElement>(`:scope > [${REGION_ATTR}]`);
43
+ if (region == null) {
44
+ region = document.createElement('div');
45
+ region.setAttribute(REGION_ATTR, '');
46
+ region.setAttribute('role', 'status');
47
+ region.setAttribute('aria-live', 'polite');
48
+ region.setAttribute('aria-atomic', 'true');
49
+ region.style.cssText = VISUALLY_HIDDEN_CSS;
50
+ dialog.appendChild(region);
51
+ }
52
+ const target = region;
53
+ const previous = pendingByRegion.get(target);
54
+ if (previous != null) {
55
+ cancelAnimationFrame(previous.frame);
56
+ clearTimeout(previous.timer);
57
+ }
58
+ target.textContent = '';
59
+ const pending: {frame: number; timer?: ReturnType<typeof setTimeout>} = {
60
+ frame: 0,
61
+ };
62
+ pending.frame = requestAnimationFrame(() => {
63
+ target.textContent = message;
64
+ pending.timer = setTimeout(() => {
65
+ target.textContent = '';
66
+ pendingByRegion.delete(target);
67
+ }, CLEAR_DELAY_MS);
68
+ });
69
+ pendingByRegion.set(target, pending);
70
+ }
71
+
72
+ export function useBusyAnnouncement(
73
+ isBusy: boolean,
74
+ message: string,
75
+ elementRef: RefObject<Element | null>,
76
+ ): void {
77
+ const announce = useAnnounce();
78
+ const hasAnnouncedRef = useRef(false);
79
+ useEffect(() => {
80
+ if (!isBusy) {
81
+ hasAnnouncedRef.current = false;
82
+ return;
83
+ }
84
+ if (hasAnnouncedRef.current) {
85
+ return;
86
+ }
87
+ // Choose the route a frame later: a Dialog opening in the same commit
88
+ // calls showModal() in its own Effect, which runs after this one.
89
+ const frame = requestAnimationFrame(() => {
90
+ hasAnnouncedRef.current = true;
91
+ const dialog = elementRef.current?.closest('dialog');
92
+ if (dialog?.open && dialog.matches(':modal')) {
93
+ announceInModal(dialog, message);
94
+ } else {
95
+ announce(message, 'polite');
96
+ }
97
+ });
98
+ return () => cancelAnimationFrame(frame);
99
+ }, [announce, elementRef, isBusy, message]);
100
+ }
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
14
- import {act, fireEvent, render, screen} from '@testing-library/react';
14
+ import {act, fireEvent, render, screen, within} from '@testing-library/react';
15
15
  import {useRef, useState} from 'react';
16
16
  import {
17
17
  MENU_PRESS_AUTOSCROLL_ZONE_PX,
@@ -145,6 +145,32 @@ describe('useMenuPress — tracking inside the menu', () => {
145
145
  expect(onSelect).toHaveBeenCalledTimes(1);
146
146
  });
147
147
 
148
+ it('hit-tests in the shadow root that holds the menu, not the document', () => {
149
+ const host = document.createElement('div');
150
+ document.body.append(host);
151
+ const shadow = host.attachShadow({mode: 'open'});
152
+ const container = document.createElement('div');
153
+ shadow.append(container);
154
+ const onSelect = vi.fn();
155
+ render(<Harness onSelect={onSelect} />, {container});
156
+ const inShadow = (label: string) =>
157
+ within(container).getByRole('menuitem', {name: label});
158
+ // As in a browser: the document answers with the shadow host; only the
159
+ // shadow root knows the row under the point.
160
+ Object.assign(document, {elementFromPoint: () => host});
161
+ Object.assign(shadow, {elementFromPoint: () => inShadow('C')});
162
+ try {
163
+ const events = {...mouse(), composed: true, bubbles: true};
164
+ fireEvent.pointerDown(inShadow('A'), events);
165
+ fireEvent.pointerUp(inShadow('A'), events);
166
+ expect(onSelect).toHaveBeenCalledWith('C');
167
+ expect(onSelect).toHaveBeenCalledTimes(1);
168
+ } finally {
169
+ delete (document as {elementFromPoint?: unknown}).elementFromPoint;
170
+ host.remove();
171
+ }
172
+ });
173
+
148
174
  it('moves the highlight as focus with preventScroll', () => {
149
175
  render(<Harness />);
150
176
  const b = row('B');
@@ -209,8 +209,8 @@ export interface UseMenuPressOptions {
209
209
  getScroller?: () => HTMLElement | null;
210
210
  /**
211
211
  * Resolve the element under a viewport point. Defaults to
212
- * `document.elementFromPoint`, falling back to the event target where the
213
- * document lacks it (jsdom). A test seam.
212
+ * `elementFromPoint` on the menu's own document or shadow root, falling back
213
+ * to the event target where the document lacks it (jsdom). A test seam.
214
214
  */
215
215
  hitTest?: (x: number, y: number) => Element | null;
216
216
  /** @default 500 */
@@ -364,7 +364,16 @@ export function useMenuPress(options: UseMenuPressOptions): UseMenuPressReturn {
364
364
  if (hitTest != null) {
365
365
  element = hitTest(event.clientX, event.clientY);
366
366
  } else if (typeof doc.elementFromPoint === 'function') {
367
- element = doc.elementFromPoint(event.clientX, event.clientY);
367
+ // Hit-test in the menu's own tree: inside a shadow root the document
368
+ // answers with the shadow host, never the row under the pointer.
369
+ const tree = menu?.getRootNode();
370
+ const scope: Pick<Document, 'elementFromPoint'> =
371
+ tree != null &&
372
+ tree !== doc &&
373
+ typeof (tree as ShadowRoot).elementFromPoint === 'function'
374
+ ? (tree as ShadowRoot)
375
+ : doc;
376
+ element = scope.elementFromPoint(event.clientX, event.clientY);
368
377
  } else {
369
378
  element = event.target instanceof Element ? event.target : null;
370
379
  }
@@ -10,7 +10,8 @@
10
10
  */
11
11
 
12
12
  import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
13
- import {renderHook} from '@testing-library/react';
13
+ import {Activity, type ReactNode} from 'react';
14
+ import {render, renderHook} from '@testing-library/react';
14
15
  import {useTypeahead} from './useTypeahead';
15
16
 
16
17
  const LABELS = ['Apple', 'Apricot', 'Banana', 'Cherry'] as const;
@@ -86,6 +87,46 @@ describe('useTypeahead', () => {
86
87
  expect(onMatch).toHaveBeenLastCalledWith(2);
87
88
  });
88
89
 
90
+ it('clears the pending buffer reset when the component unmounts', () => {
91
+ const {result, unmount} = renderHook(() =>
92
+ useTypeahead({getItemLabels: () => LABELS, onMatch: vi.fn()}),
93
+ );
94
+ result.current.onKeyDown(key('a'));
95
+ expect(vi.getTimerCount()).toBe(1);
96
+ unmount();
97
+ expect(vi.getTimerCount()).toBe(0);
98
+ });
99
+
100
+ it('empties the buffer when an Activity boundary hides the collection', () => {
101
+ const latest: {api: ReturnType<typeof useTypeahead> | null} = {api: null};
102
+ function Probe(): ReactNode {
103
+ latest.api = useTypeahead({
104
+ getItemLabels: () => LABELS,
105
+ onMatch: vi.fn(),
106
+ });
107
+ return null;
108
+ }
109
+ const {rerender} = render(
110
+ <Activity mode="visible">
111
+ <Probe />
112
+ </Activity>,
113
+ );
114
+ latest.api?.onKeyDown(key('a'));
115
+ rerender(
116
+ <Activity mode="hidden">
117
+ <Probe />
118
+ </Activity>,
119
+ );
120
+ expect(vi.getTimerCount()).toBe(0);
121
+ rerender(
122
+ <Activity mode="visible">
123
+ <Probe />
124
+ </Activity>,
125
+ );
126
+ // The typed prefix went with the hide, so a bare Space is not typeahead.
127
+ expect(latest.api?.onKeyDown(key(' '))).toBe(false);
128
+ });
129
+
89
130
  it('skips disabled items', () => {
90
131
  const {onMatch, api} = setup({disabledIndices: [0]}); // Apple disabled
91
132
  api.onKeyDown(key('a'));
@@ -4,7 +4,7 @@
4
4
 
5
5
  /**
6
6
  * @file useTypeahead.ts
7
- * @input Uses React useCallback/useRef
7
+ * @input Uses React useCallback/useEffect/useRef
8
8
  * @output Exports useTypeahead hook for first-character (type-to-focus) search
9
9
  * @position Core hook; adds APG typeahead to menus, listboxes, and other
10
10
  * collections. Composes with useListFocus/useGridFocus (or any collection)
@@ -14,7 +14,7 @@
14
14
  * - /packages/core/src/hooks/index.ts
15
15
  */
16
16
 
17
- import {useCallback, useRef} from 'react';
17
+ import {useCallback, useEffect, useRef} from 'react';
18
18
 
19
19
  export interface UseTypeaheadOptions {
20
20
  /**
@@ -135,6 +135,11 @@ export function useTypeahead(options: UseTypeaheadOptions): UseTypeaheadReturn {
135
135
  }
136
136
  }, []);
137
137
 
138
+ // A collection that unmounts, or that an `<Activity>` boundary hides,
139
+ // mid-typing leaves no reset timer running and comes back with an empty
140
+ // buffer, as the reset would have left it.
141
+ useEffect(() => reset, [reset]);
142
+
138
143
  const scheduleReset = useCallback(() => {
139
144
  if (timeoutRef.current) {
140
145
  clearTimeout(timeoutRef.current);
@@ -340,6 +340,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
340
340
  '@astryx.chatToolCalls.status.complete': 'Complete',
341
341
  '@astryx.chatToolCalls.status.error': 'Failed',
342
342
  '@astryx.chatToolCalls.groupLabel': '{count} tool calls',
343
+ '@astryx.chatTriggerMenu.searching': 'Searching…',
343
344
  '@astryx.chatTriggerMenu.suggestions': 'Suggestions',
344
345
  '@astryx.citation.label': 'Citation {number}: {title}',
345
346
  '@astryx.codeBlock.copied': 'Copied',
@@ -347,6 +347,7 @@ const runtimeCatalog: Readonly<Record<string, string>> = {
347
347
  '@astryx.chatToolCalls.status.complete': '⟦Çóɱþłéţé⟧',
348
348
  '@astryx.chatToolCalls.status.error': '⟦Ƒàíłéð⟧',
349
349
  '@astryx.chatToolCalls.groupLabel': '⟦{count} ţóół çàłłš⟧',
350
+ '@astryx.chatTriggerMenu.searching': '⟦Šéàřçĥíñĝ…⟧',
350
351
  '@astryx.chatTriggerMenu.suggestions': '⟦Šúĝĝéšţíóñš⟧',
351
352
  '@astryx.citation.label': '⟦Çíţàţíóñ {number}: {title}⟧',
352
353
  '@astryx.codeBlock.copied': '⟦Çóþíéð⟧',