@magicx-eng/ai-autocomplete-react 0.7.0 → 0.8.0

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/README.md CHANGED
@@ -246,9 +246,110 @@ function App() {
246
246
  | `completedParams?` | `CompletedParamState[]` | — | Controlled completed params. |
247
247
  | `onChange?` | `(value: string) => void` | — | Called when text changes (controlled mode). |
248
248
  | `onParamsChange?` | `(params: CompletedParamState[]) => void` | — | Called when params change (controlled mode). |
249
+ | `products?` | `ProductsConfig` | — | Opt-in product strip — see [Product strip](#product-strip). Omit it and nothing about the dropdown changes. |
250
+ | `onProductSelect?` | `(product: Product) => void` | — | Called when a product card is activated. The SDK never navigates. |
249
251
  | `submitButton?` | `ReactNode` | — | Custom submit button. Pass any `ReactNode` to replace the default arrow button. Pass `null` to render no button. Clicks bubble up and trigger submit, so consumer-supplied buttons work without re-wiring `onClick`. |
250
252
  | `ref?` | `Ref<AIAutocompleteHandle>` | — | Imperative handle with `focus()`, `blur()`, `reset()`, and `setMode()`. |
251
253
 
254
+ #### Product strip
255
+
256
+ Opt in with `products` and the dropdown renders a horizontal row of product
257
+ cards below the options grid. Every platform (Shopify today, others later) has
258
+ its own search endpoint and its own response shape, so the SDK owns the UI and
259
+ the integration owns only the fetching and the mapping.
260
+
261
+ ```tsx
262
+ import { AIAutocomplete, type Product } from "@magicx-eng/ai-autocomplete-react";
263
+
264
+ const products = {
265
+ // You own the request entirely — auth headers, GraphQL body, locale
266
+ // prefixes. Honour the signal: the SDK aborts it as soon as a newer query
267
+ // supersedes this one.
268
+ fetch: (query: string, signal: AbortSignal) =>
269
+ fetch(`/api/products?q=${encodeURIComponent(query)}`, { signal }).then((r) => r.json()),
270
+
271
+ // Pure mapping, kept out of `fetch` so you can unit-test it without a
272
+ // network.
273
+ transform: (raw: unknown): Product[] =>
274
+ (raw as ApiResponse).items.map((item) => ({
275
+ id: item.id,
276
+ title: item.title,
277
+ url: item.url,
278
+ imageUrl: item.image?.src ?? null, // null renders the placeholder tile
279
+ price: formatMoney(item.price), // you know the currency
280
+ vendor: item.brand,
281
+ })),
282
+
283
+ limit: 8, // applied by the SDK, after transform
284
+ };
285
+
286
+ <AIAutocomplete
287
+ onSubmit={handleSubmit}
288
+ products={products}
289
+ // Selection emits — the SDK never navigates.
290
+ onProductSelect={(product) => router.push(product.url)}
291
+ />;
292
+ ```
293
+
294
+ `Product` is exactly six fields; only `id`, `title` and `url` are required:
295
+
296
+ ```ts
297
+ type Product = {
298
+ id: string;
299
+ title: string;
300
+ url: string;
301
+ imageUrl?: string | null;
302
+ price?: string;
303
+ vendor?: string;
304
+ };
305
+ ```
306
+
307
+ **What the SDK guarantees**
308
+
309
+ - **One fetch cadence.** The product search rides the same debounce, the same
310
+ `AbortController` and the same version guard as `/suggest` — there is no
311
+ second timer to drift out of step.
312
+ - **Empty queries never reach you.** On mount and after `reset()` the strip
313
+ clears.
314
+ - **Out-of-order responses are dropped.** A slow response for an older query is
315
+ never rendered over a newer one.
316
+ - **Failure is silent.** A rejected `fetch` or a throwing `transform` clears the
317
+ strip, logs once, and leaves the suggestions half untouched — no `error`
318
+ state, no `onError`, no effect on `isLoading`.
319
+ - **The two halves are independent.** The panel opens if *either* has content,
320
+ so products keep it open when option filtering empties the option list.
321
+ `dropdownTrigger` (`manual` / `hidden`) still gates the panel as before.
322
+ - **Inline configs are safe.** `products={{ fetch, transform }}` written inline
323
+ re-creates the object on every render; the hook forwards through a stable
324
+ proxy, so only turning the strip on or off reaches the core. Two deliberate
325
+ consequences: swapping one live config for another leaves the previous
326
+ integration's cards up until the next request replaces them (toggle
327
+ `products` off and back on to clear immediately), and turning the strip on
328
+ mid-session shows nothing until the next `/suggest` request fires — the
329
+ product search rides that one scheduler, and enabling the strip doesn't
330
+ itself change the query.
331
+
332
+ **Selection and links.** Cards are real `<a href>` elements, so cmd/ctrl-click,
333
+ middle-click and "copy link address" behave natively. A plain left click is
334
+ intercepted (`preventDefault`) and emits `onProductSelect` instead.
335
+
336
+ `product.url` is used verbatim — the SDK trusts your `transform` output and
337
+ does not sanitise it, so validate the URL there if the platform response isn't
338
+ fully under your control. (Angular additionally runs its own `[href]`
339
+ sanitiser, so unrecognised schemes like `myapp://…` are rewritten to
340
+ `unsafe:…` in that package only.)
341
+
342
+ **Accessibility.** The strip is a `role="group"` labelled "Products"; each card
343
+ is a `role="option"` in the tab order, activated with Enter or Space, with a
344
+ visible focus ring. Tabbing into the strip does not close the panel. The row
345
+ scrolls horizontally by trackpad, wheel, touch and keyboard; the page never
346
+ scrolls sideways and the scrollbar chrome is hidden in all engines.
347
+
348
+ Tier 2 gets the same thing: pass `products` / `onProductSelect` to
349
+ `useAIAutocomplete()` and the strip renders inside `<AIAutocompleteDropdown />`
350
+ via `dropdownProps`. Tier 3 consumers read `products` from the hook and call
351
+ `selectProduct(product)` from their own cards.
352
+
252
353
  #### Custom submit button
253
354
 
254
355
  ```tsx
@@ -329,6 +430,7 @@ The headless hook for Tier 2 and Tier 3. Accepts the same options as `<AIAutocom
329
430
  | `isDropdownOpen` | `boolean` | Whether the dropdown should be visible. Drive your own dropdown's visibility with this in Tier 3. |
330
431
  | `placeholderText` | `string` | Suggested placeholder text for the current step. |
331
432
  | `error` | `Error \| null` | Last fetch error. |
433
+ | `products` | `Product[]` | Results of the latest product search. Empty unless `products` is configured. Already spread into `dropdownProps` — read it directly only if you render your own strip. |
332
434
 
333
435
  **Actions**
334
436
 
@@ -338,6 +440,7 @@ The headless hook for Tier 2 and Tier 3. Accepts the same options as `<AIAutocom
338
440
  | `removeLastParam` | `() => void` | Remove the last completed param from state. The text stays in the input as plain text. |
339
441
  | `clearNewParamId` | `() => void` | Clear shimmer animation state. |
340
442
  | `reset` | `() => void` | Clear all state, re-fetch, and start a new session (rotates `session_id`). Call this after handling submit. |
443
+ | `selectProduct` | `(product: Product) => void` | Announce a product selection (fires `onProductSelect`). The built-in dropdown calls this for you; hand-rolled strips call it themselves. Never navigates. |
341
444
 
342
445
  **Input forwarding (custom inputs)** — call these instead of spreading `inputProps` when your input isn't a `<textarea>` (contentEditable / rich-text editors):
343
446
 
@@ -353,7 +456,7 @@ The headless hook for Tier 2 and Tier 3. Accepts the same options as `<AIAutocom
353
456
  | Field | Type | Description |
354
457
  |---|---|---|
355
458
  | `inputProps` | `object` | Spread onto a `<textarea>`. Includes `value`, `placeholder`, `onChange`, `onKeyDown`, and ARIA attributes. |
356
- | `dropdownProps` | `AIAutocompleteDropdownProps` | Spread onto `<AIAutocompleteDropdown />`. Carries the options, `activeIndex`, `onSelect` / `onHighlight`, pills, open state, and `optionsPosition` — read these directly to render your own dropdown in Tier 3. |
459
+ | `dropdownProps` | `AIAutocompleteDropdownProps` | Spread onto `<AIAutocompleteDropdown />`. Carries the options, `activeIndex`, `onSelect` / `onHighlight`, pills, open state, `optionsPosition`, and the product strip's `products` / `onProductSelect` / `onProductFocusChange` — read these directly to render your own dropdown in Tier 3. |
357
460
 
358
461
  ### `<AIAutocompleteDropdown />`
359
462
 
@@ -373,6 +476,9 @@ The dropdown component for Tier 2. Spread `dropdownProps` from the hook (and add
373
476
  | `pills?` | `Suggestion[]` | Pills to render inside the dropdown. |
374
477
  | `onPillClick?` | `(index: number) => void` | Called when a pill is clicked. |
375
478
  | `showPills?` | `boolean` | Whether to render pills. Default: `true`. |
479
+ | `products?` | `Product[]` | Product cards to render below the options grid. Empty (the default) renders no strip. |
480
+ | `onProductSelect?` | `(product: Product) => void` | Called when a card is activated. |
481
+ | `onProductFocusChange?` | `(focused: boolean) => void` | Called when focus enters/leaves the strip. Wired to `setFocused` by `dropdownProps` so a tabbed-to card doesn't close the panel. |
376
482
  | `isLoading?` | `boolean` | When `true`, the dropdown renders its pills + options as a skeleton: text masked, layout (count and widths) preserved, shimmer pulse animating. Falls back to a generic 3-bar placeholder when no pills/options are cached. |
377
483
 
378
484
  ### `AutocompleteResult`
@@ -431,6 +537,21 @@ Override on the container (via `className`). All defaults use `:where()` (zero s
431
537
  | `--aia-scrollbar-thumb` | `rgba(0, 0, 0, 0.3)` | `rgba(0, 0, 0, 0.3)` | Color of the option list's scrollbar thumb (Firefox + WebKit). |
432
538
  | `--aia-streak-rgb` | `99, 102, 241` | `255, 255, 255` | Comma-separated RGB triplet used to tint the option-selection streak animation (consumed via `rgba(var(--aia-streak-rgb), …)`). |
433
539
  | `--aia-streak-glass-bg` | `rgba(99, 102, 241, 0.1)` | `rgba(255, 255, 255, 0.1)` | Background fill for the streak's glass-pill effect. |
540
+ | `--aia-product-card-width` | `116px` | `116px` | Width of a product card in the strip. The media tile is square, so this also sets its height. |
541
+ | `--aia-product-gap` | `8px` | `8px` | Gap between product cards. |
542
+ | `--aia-product-bg` | `transparent` | `transparent` | Product card background. |
543
+ | `--aia-product-bg-active` | `--aia-option-bg` | `--aia-option-bg` | Product card background on hover. |
544
+ | `--aia-product-media-bg` | `--aia-skeleton-bg` | `--aia-skeleton-bg` | Fill behind the product image, and of the placeholder tile when a product has no image. |
545
+ | `--aia-product-placeholder-color` | `--aia-option-color` | `--aia-option-color` | Glyph color of the no-image placeholder tile. |
546
+ | `--aia-product-title-color` | `--aia-option-color-selected` | `--aia-option-color-selected` | Product title text. Follows the option colors by default, so theming the panel moves suggestions and products together. |
547
+ | `--aia-product-price-color` | `--aia-option-color-selected` | `--aia-option-color-selected` | Product price text. |
548
+ | `--aia-product-vendor-color` | `--aia-option-color` | `--aia-option-color` | Product vendor line. |
549
+ | `--aia-product-focus-ring` | `--aia-option-color-selected` | `--aia-option-color-selected` | Focus ring drawn on a keyboard-focused card. |
550
+ | `--aia-products-label-color` | `--aia-option-color` | `--aia-option-color` | "Products" section label. |
551
+ | `--aia-product-title-font-size` | `12px` | `12px` | Product title font size. |
552
+ | `--aia-product-price-font-size` | `11px` | `11px` | Product price font size. |
553
+ | `--aia-product-vendor-font-size` | `10px` | `10px` | Product vendor font size. |
554
+ | `--aia-products-label-font-size` | `11px` | `11px` | Section label font size. |
434
555
  | `--aia-skeleton-bg` | `rgba(189, 189, 189, 0.25)` | `#1a1b1d` | Fill color for the loading skeleton bars and the masked text in cached pills/options. |
435
556
 
436
557
  Legacy `--aia-color-*` variables are still supported as fallbacks.
@@ -459,7 +580,11 @@ For styling beyond the CSS variables, target these stable `data-aia-*` attribute
459
580
  | `[data-aia-pill]` | Each unfilled-suggestion pill |
460
581
  | `[data-aia-pillbar]` | Pill bar container inside the dropdown |
461
582
  | `[data-aia-option]` | Each suggestion option |
462
- | `[data-aia-dropdown]` | The dropdown root (listbox) |
583
+ | `[data-aia-dropdown]` | The dropdown root (listbox). Carries `data-aia-has-products` while the product strip has cards. |
584
+ | `[data-aia-products]` | Product strip section (label + row) |
585
+ | `[data-aia-products-row]` | The horizontally scrolling row of cards |
586
+ | `[data-aia-product]` | Each product card |
587
+ | `[data-aia-product-placeholder]` | Media tile of a card whose product has no image |
463
588
 
464
589
  Completed params render as inline `<strong>` elements inside the editor. Override their weight with `[data-aia-input] strong { font-weight: 700; }` (the built-in style uses `:where()` so any consumer selector wins without `!important`).
465
590
 
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { AutocompleteResult, OptionOverrides, APIConfig, AppearanceMode, CompletedParamState, Suggestion, SuggestionOption, SkippedParamState, Segment } from '@magicx-eng/ai-autocomplete-vanilla';
2
- export { APIConfig, APIKeyConfig, AccessTokenConfig, AccessTokenResult, AppearanceMode, AutocompleteResult, CompletedParam, CompletedParamState, OptionOverrides, Segment, SkippedParamState, Suggestion, SuggestionOption, TaskKind, buildSubmitResult, withSkippedParams } from '@magicx-eng/ai-autocomplete-vanilla';
1
+ import { AutocompleteResult, OptionOverrides, APIConfig, AppearanceMode, CompletedParamState, ProductsConfig, Product, Suggestion, SuggestionOption, SkippedParamState, Segment } from '@magicx-eng/ai-autocomplete-vanilla';
2
+ export { APIConfig, APIKeyConfig, AccessTokenConfig, AccessTokenResult, AppearanceMode, AutocompleteResult, CompletedParam, CompletedParamState, OptionOverrides, Product, ProductsConfig, Segment, SkippedParamState, Suggestion, SuggestionOption, TaskKind, buildSubmitResult, withSkippedParams } from '@magicx-eng/ai-autocomplete-vanilla';
3
3
  import * as react from 'react';
4
4
  import { ReactNode, KeyboardEvent, ChangeEvent } from 'react';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
@@ -42,6 +42,32 @@ interface AIAutocompleteProps {
42
42
  completedParams?: CompletedParamState[];
43
43
  onChange?: (value: string) => void;
44
44
  onParamsChange?: (params: CompletedParamState[]) => void;
45
+ /**
46
+ * Opt-in product strip. When set, the dropdown renders a horizontal row of
47
+ * product cards below the options grid, fed by your own search endpoint on
48
+ * the SDK's existing fetch cadence. Omit it and nothing about the dropdown
49
+ * changes — no request, no markup, no layout shift.
50
+ *
51
+ * The object may be re-created on every render: the hook forwards calls
52
+ * through a stable proxy, so only toggling the strip on or off reaches the
53
+ * core.
54
+ *
55
+ * Two consequences of that proxy, both deliberate:
56
+ * - Swapping one live config for another (A → B, both defined) can't be told
57
+ * apart from a re-render, so A's cards stay on screen until the next
58
+ * request replaces them. Toggle `products` off and back on to clear
59
+ * immediately.
60
+ * - Turning the strip on mid-session shows nothing until the next `/suggest`
61
+ * request fires, because the product search rides that one scheduler and
62
+ * enabling the strip doesn't itself change the query.
63
+ */
64
+ products?: ProductsConfig;
65
+ /**
66
+ * Called when the user activates a product card. The SDK does not navigate —
67
+ * you decide what a selection means. Modifier / middle clicks are left to the
68
+ * browser (so cmd-click still opens a tab) and don't fire this.
69
+ */
70
+ onProductSelect?: (product: Product) => void;
45
71
  /**
46
72
  * Custom submit button. Pass any ReactNode to replace the default button.
47
73
  * Pass `null` to render no button. Default (`undefined`) renders the built-in
@@ -75,6 +101,32 @@ interface UseAIAutocompleteOptions {
75
101
  onFocus?: () => void;
76
102
  /** Called when the input loses focus. */
77
103
  onBlur?: () => void;
104
+ /**
105
+ * Opt-in product strip. When set, the dropdown renders a horizontal row of
106
+ * product cards below the options grid, fed by your own search endpoint on
107
+ * the SDK's existing fetch cadence. Omit it and nothing about the dropdown
108
+ * changes — no request, no markup, no layout shift.
109
+ *
110
+ * The object may be re-created on every render: the hook forwards calls
111
+ * through a stable proxy, so only toggling the strip on or off reaches the
112
+ * core.
113
+ *
114
+ * Two consequences of that proxy, both deliberate:
115
+ * - Swapping one live config for another (A → B, both defined) can't be told
116
+ * apart from a re-render, so A's cards stay on screen until the next
117
+ * request replaces them. Toggle `products` off and back on to clear
118
+ * immediately.
119
+ * - Turning the strip on mid-session shows nothing until the next `/suggest`
120
+ * request fires, because the product search rides that one scheduler and
121
+ * enabling the strip doesn't itself change the query.
122
+ */
123
+ products?: ProductsConfig;
124
+ /**
125
+ * Called when the user activates a product card. The SDK does not navigate —
126
+ * you decide what a selection means. Modifier / middle clicks are left to the
127
+ * browser (so cmd-click still opens a tab) and don't fire this.
128
+ */
129
+ onProductSelect?: (product: Product) => void;
78
130
  value?: string;
79
131
  completedParams?: CompletedParamState[];
80
132
  onChange?: (value: string) => void;
@@ -120,6 +172,18 @@ interface UseAIAutocompleteReturn {
120
172
  placeholderText: string;
121
173
  listboxId: string;
122
174
  error: Error | null;
175
+ /**
176
+ * Results of the latest product search. Always empty unless `products` is
177
+ * configured. Already spread into `dropdownProps` — read it directly only if
178
+ * you render your own strip.
179
+ */
180
+ products: Product[];
181
+ /**
182
+ * Announce a product selection (fires `onProductSelect`). The built-in
183
+ * dropdown calls this for you; hand-rolled strips call it themselves. The SDK
184
+ * never navigates.
185
+ */
186
+ selectProduct: (product: Product) => void;
123
187
  /** Tier 1 helper: forward plain-text input to the core (autocapitalize handled). */
124
188
  handleTextChange: (value: string) => void;
125
189
  /** Tier 1 helper: forward keyboard events to the core. */
@@ -177,6 +241,17 @@ interface AIAutocompleteDropdownProps {
177
241
  isLoading?: boolean;
178
242
  /** When the input has no typed text, the footer hint reads "tab to select". Provided by `dropdownProps` from the hook. */
179
243
  isInputEmpty?: boolean;
244
+ /** Product cards to render below the options grid. Provided by `dropdownProps` from the hook. Empty (the default) renders no strip. */
245
+ products?: Product[];
246
+ /** Called when a product card is activated. Provided by `dropdownProps` from the hook. */
247
+ onProductSelect?: (product: Product) => void;
248
+ /**
249
+ * Called when focus moves into / out of the product strip. The panel closes
250
+ * on blur by default and the input blurs the instant a card takes focus, so
251
+ * without this a card is unreachable by Tab. Provided by `dropdownProps`
252
+ * from the hook (wired to `setFocused`).
253
+ */
254
+ onProductFocusChange?: (focused: boolean) => void;
180
255
  /**
181
256
  * Where the dropdown opens relative to the input. When `"above"`, the dropdown
182
257
  * positions itself above (and reverses its internal layout so options sit
@@ -200,8 +275,8 @@ interface AIAutocompleteDropdownProps {
200
275
 
201
276
  declare const AIAutocomplete: react.ForwardRefExoticComponent<AIAutocompleteProps & react.RefAttributes<AIAutocompleteHandle>>;
202
277
 
203
- declare function AIAutocompleteDropdown({ suggestions, activeIndex, onSelect, onHighlight, isOpen, id, className, pills, onPillClick, showPills, activeSelected, isLoading, isInputEmpty, optionsPosition, mode, }: AIAutocompleteDropdownProps): react_jsx_runtime.JSX.Element;
278
+ declare function AIAutocompleteDropdown({ suggestions, activeIndex, onSelect, onHighlight, isOpen, id, className, pills, onPillClick, showPills, activeSelected, isLoading, isInputEmpty, products, onProductSelect, onProductFocusChange, optionsPosition, mode, }: AIAutocompleteDropdownProps): react_jsx_runtime.JSX.Element;
204
279
 
205
- declare function useAIAutocomplete({ onSubmit, onError, optionOverrides, maskCompletedText, apiConfig, columns, dropdownTrigger, optionsPosition, closeDropdownOnBlur, showNonTappableOptions, onFocus, onBlur, value: controlledValue, completedParams: controlledParams, onChange: onChangeProp, onParamsChange, source, setCursor, }: UseAIAutocompleteOptions): UseAIAutocompleteReturn;
280
+ declare function useAIAutocomplete({ onSubmit, onError, optionOverrides, maskCompletedText, apiConfig, columns, dropdownTrigger, optionsPosition, closeDropdownOnBlur, showNonTappableOptions, onFocus, onBlur, value: controlledValue, completedParams: controlledParams, onChange: onChangeProp, onParamsChange, products, onProductSelect, source, setCursor, }: UseAIAutocompleteOptions): UseAIAutocompleteReturn;
206
281
 
207
282
  export { AIAutocomplete, AIAutocompleteDropdown, type AIAutocompleteDropdownProps, type AIAutocompleteHandle, type AIAutocompleteProps, type UseAIAutocompleteOptions, type UseAIAutocompleteReturn, useAIAutocomplete };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { AutocompleteResult, OptionOverrides, APIConfig, AppearanceMode, CompletedParamState, Suggestion, SuggestionOption, SkippedParamState, Segment } from '@magicx-eng/ai-autocomplete-vanilla';
2
- export { APIConfig, APIKeyConfig, AccessTokenConfig, AccessTokenResult, AppearanceMode, AutocompleteResult, CompletedParam, CompletedParamState, OptionOverrides, Segment, SkippedParamState, Suggestion, SuggestionOption, TaskKind, buildSubmitResult, withSkippedParams } from '@magicx-eng/ai-autocomplete-vanilla';
1
+ import { AutocompleteResult, OptionOverrides, APIConfig, AppearanceMode, CompletedParamState, ProductsConfig, Product, Suggestion, SuggestionOption, SkippedParamState, Segment } from '@magicx-eng/ai-autocomplete-vanilla';
2
+ export { APIConfig, APIKeyConfig, AccessTokenConfig, AccessTokenResult, AppearanceMode, AutocompleteResult, CompletedParam, CompletedParamState, OptionOverrides, Product, ProductsConfig, Segment, SkippedParamState, Suggestion, SuggestionOption, TaskKind, buildSubmitResult, withSkippedParams } from '@magicx-eng/ai-autocomplete-vanilla';
3
3
  import * as react from 'react';
4
4
  import { ReactNode, KeyboardEvent, ChangeEvent } from 'react';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
@@ -42,6 +42,32 @@ interface AIAutocompleteProps {
42
42
  completedParams?: CompletedParamState[];
43
43
  onChange?: (value: string) => void;
44
44
  onParamsChange?: (params: CompletedParamState[]) => void;
45
+ /**
46
+ * Opt-in product strip. When set, the dropdown renders a horizontal row of
47
+ * product cards below the options grid, fed by your own search endpoint on
48
+ * the SDK's existing fetch cadence. Omit it and nothing about the dropdown
49
+ * changes — no request, no markup, no layout shift.
50
+ *
51
+ * The object may be re-created on every render: the hook forwards calls
52
+ * through a stable proxy, so only toggling the strip on or off reaches the
53
+ * core.
54
+ *
55
+ * Two consequences of that proxy, both deliberate:
56
+ * - Swapping one live config for another (A → B, both defined) can't be told
57
+ * apart from a re-render, so A's cards stay on screen until the next
58
+ * request replaces them. Toggle `products` off and back on to clear
59
+ * immediately.
60
+ * - Turning the strip on mid-session shows nothing until the next `/suggest`
61
+ * request fires, because the product search rides that one scheduler and
62
+ * enabling the strip doesn't itself change the query.
63
+ */
64
+ products?: ProductsConfig;
65
+ /**
66
+ * Called when the user activates a product card. The SDK does not navigate —
67
+ * you decide what a selection means. Modifier / middle clicks are left to the
68
+ * browser (so cmd-click still opens a tab) and don't fire this.
69
+ */
70
+ onProductSelect?: (product: Product) => void;
45
71
  /**
46
72
  * Custom submit button. Pass any ReactNode to replace the default button.
47
73
  * Pass `null` to render no button. Default (`undefined`) renders the built-in
@@ -75,6 +101,32 @@ interface UseAIAutocompleteOptions {
75
101
  onFocus?: () => void;
76
102
  /** Called when the input loses focus. */
77
103
  onBlur?: () => void;
104
+ /**
105
+ * Opt-in product strip. When set, the dropdown renders a horizontal row of
106
+ * product cards below the options grid, fed by your own search endpoint on
107
+ * the SDK's existing fetch cadence. Omit it and nothing about the dropdown
108
+ * changes — no request, no markup, no layout shift.
109
+ *
110
+ * The object may be re-created on every render: the hook forwards calls
111
+ * through a stable proxy, so only toggling the strip on or off reaches the
112
+ * core.
113
+ *
114
+ * Two consequences of that proxy, both deliberate:
115
+ * - Swapping one live config for another (A → B, both defined) can't be told
116
+ * apart from a re-render, so A's cards stay on screen until the next
117
+ * request replaces them. Toggle `products` off and back on to clear
118
+ * immediately.
119
+ * - Turning the strip on mid-session shows nothing until the next `/suggest`
120
+ * request fires, because the product search rides that one scheduler and
121
+ * enabling the strip doesn't itself change the query.
122
+ */
123
+ products?: ProductsConfig;
124
+ /**
125
+ * Called when the user activates a product card. The SDK does not navigate —
126
+ * you decide what a selection means. Modifier / middle clicks are left to the
127
+ * browser (so cmd-click still opens a tab) and don't fire this.
128
+ */
129
+ onProductSelect?: (product: Product) => void;
78
130
  value?: string;
79
131
  completedParams?: CompletedParamState[];
80
132
  onChange?: (value: string) => void;
@@ -120,6 +172,18 @@ interface UseAIAutocompleteReturn {
120
172
  placeholderText: string;
121
173
  listboxId: string;
122
174
  error: Error | null;
175
+ /**
176
+ * Results of the latest product search. Always empty unless `products` is
177
+ * configured. Already spread into `dropdownProps` — read it directly only if
178
+ * you render your own strip.
179
+ */
180
+ products: Product[];
181
+ /**
182
+ * Announce a product selection (fires `onProductSelect`). The built-in
183
+ * dropdown calls this for you; hand-rolled strips call it themselves. The SDK
184
+ * never navigates.
185
+ */
186
+ selectProduct: (product: Product) => void;
123
187
  /** Tier 1 helper: forward plain-text input to the core (autocapitalize handled). */
124
188
  handleTextChange: (value: string) => void;
125
189
  /** Tier 1 helper: forward keyboard events to the core. */
@@ -177,6 +241,17 @@ interface AIAutocompleteDropdownProps {
177
241
  isLoading?: boolean;
178
242
  /** When the input has no typed text, the footer hint reads "tab to select". Provided by `dropdownProps` from the hook. */
179
243
  isInputEmpty?: boolean;
244
+ /** Product cards to render below the options grid. Provided by `dropdownProps` from the hook. Empty (the default) renders no strip. */
245
+ products?: Product[];
246
+ /** Called when a product card is activated. Provided by `dropdownProps` from the hook. */
247
+ onProductSelect?: (product: Product) => void;
248
+ /**
249
+ * Called when focus moves into / out of the product strip. The panel closes
250
+ * on blur by default and the input blurs the instant a card takes focus, so
251
+ * without this a card is unreachable by Tab. Provided by `dropdownProps`
252
+ * from the hook (wired to `setFocused`).
253
+ */
254
+ onProductFocusChange?: (focused: boolean) => void;
180
255
  /**
181
256
  * Where the dropdown opens relative to the input. When `"above"`, the dropdown
182
257
  * positions itself above (and reverses its internal layout so options sit
@@ -200,8 +275,8 @@ interface AIAutocompleteDropdownProps {
200
275
 
201
276
  declare const AIAutocomplete: react.ForwardRefExoticComponent<AIAutocompleteProps & react.RefAttributes<AIAutocompleteHandle>>;
202
277
 
203
- declare function AIAutocompleteDropdown({ suggestions, activeIndex, onSelect, onHighlight, isOpen, id, className, pills, onPillClick, showPills, activeSelected, isLoading, isInputEmpty, optionsPosition, mode, }: AIAutocompleteDropdownProps): react_jsx_runtime.JSX.Element;
278
+ declare function AIAutocompleteDropdown({ suggestions, activeIndex, onSelect, onHighlight, isOpen, id, className, pills, onPillClick, showPills, activeSelected, isLoading, isInputEmpty, products, onProductSelect, onProductFocusChange, optionsPosition, mode, }: AIAutocompleteDropdownProps): react_jsx_runtime.JSX.Element;
204
279
 
205
- declare function useAIAutocomplete({ onSubmit, onError, optionOverrides, maskCompletedText, apiConfig, columns, dropdownTrigger, optionsPosition, closeDropdownOnBlur, showNonTappableOptions, onFocus, onBlur, value: controlledValue, completedParams: controlledParams, onChange: onChangeProp, onParamsChange, source, setCursor, }: UseAIAutocompleteOptions): UseAIAutocompleteReturn;
280
+ declare function useAIAutocomplete({ onSubmit, onError, optionOverrides, maskCompletedText, apiConfig, columns, dropdownTrigger, optionsPosition, closeDropdownOnBlur, showNonTappableOptions, onFocus, onBlur, value: controlledValue, completedParams: controlledParams, onChange: onChangeProp, onParamsChange, products, onProductSelect, source, setCursor, }: UseAIAutocompleteOptions): UseAIAutocompleteReturn;
206
281
 
207
282
  export { AIAutocomplete, AIAutocompleteDropdown, type AIAutocompleteDropdownProps, type AIAutocompleteHandle, type AIAutocompleteProps, type UseAIAutocompleteOptions, type UseAIAutocompleteReturn, useAIAutocomplete };