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

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
@@ -17,6 +17,7 @@ A React/TypeScript SDK that provides a guided AI-powered autocomplete experience
17
17
  - **IME-safe** — composition events are buffered so input text is committed once, after composition ends
18
18
  - **Client-side filtering** — instant substring filtering on every keystroke
19
19
  - **Option overrides** — inject or dynamically generate client-side options per suggestion type
20
+ - **Product strip (opt-in)** — plug in any platform's product search and the dropdown renders a horizontal row of product cards below the options; the SDK owns the UI, your integration owns only `fetch` and `transform`
20
21
  - **Controlled & uncontrolled** — works out of the box or integrates with external state
21
22
  - **Ref forwarding** — imperative `focus()`, `blur()`, `reset()`, and `setMode()` via ref
22
23
  - **Accessible** — ARIA combobox 1.2 pattern with `role="listbox"`, `aria-activedescendant`
@@ -246,9 +247,110 @@ function App() {
246
247
  | `completedParams?` | `CompletedParamState[]` | — | Controlled completed params. |
247
248
  | `onChange?` | `(value: string) => void` | — | Called when text changes (controlled mode). |
248
249
  | `onParamsChange?` | `(params: CompletedParamState[]) => void` | — | Called when params change (controlled mode). |
250
+ | `products?` | `ProductsConfig` | — | Opt-in product strip — see [Product strip](#product-strip). Omit it and nothing about the dropdown changes. |
251
+ | `onProductSelect?` | `(product: Product) => void` | — | Called when a product card is activated. The SDK never navigates. |
249
252
  | `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
253
  | `ref?` | `Ref<AIAutocompleteHandle>` | — | Imperative handle with `focus()`, `blur()`, `reset()`, and `setMode()`. |
251
254
 
255
+ #### Product strip
256
+
257
+ Opt in with `products` and the dropdown renders a horizontal row of product
258
+ cards below the options grid. Every platform (Shopify today, others later) has
259
+ its own search endpoint and its own response shape, so the SDK owns the UI and
260
+ the integration owns only the fetching and the mapping.
261
+
262
+ ```tsx
263
+ import { AIAutocomplete, type Product } from "@magicx-eng/ai-autocomplete-react";
264
+
265
+ const products = {
266
+ // You own the request entirely — auth headers, GraphQL body, locale
267
+ // prefixes. Honour the signal: the SDK aborts it as soon as a newer query
268
+ // supersedes this one.
269
+ fetch: (query: string, signal: AbortSignal) =>
270
+ fetch(`/api/products?q=${encodeURIComponent(query)}`, { signal }).then((r) => r.json()),
271
+
272
+ // Pure mapping, kept out of `fetch` so you can unit-test it without a
273
+ // network.
274
+ transform: (raw: unknown): Product[] =>
275
+ (raw as ApiResponse).items.map((item) => ({
276
+ id: item.id,
277
+ title: item.title,
278
+ url: item.url,
279
+ imageUrl: item.image?.src ?? null, // null renders the placeholder tile
280
+ price: formatMoney(item.price), // you know the currency
281
+ vendor: item.brand,
282
+ })),
283
+
284
+ limit: 8, // applied by the SDK, after transform
285
+ };
286
+
287
+ <AIAutocomplete
288
+ onSubmit={handleSubmit}
289
+ products={products}
290
+ // Selection emits — the SDK never navigates.
291
+ onProductSelect={(product) => router.push(product.url)}
292
+ />;
293
+ ```
294
+
295
+ `Product` is exactly six fields; only `id`, `title` and `url` are required:
296
+
297
+ ```ts
298
+ type Product = {
299
+ id: string;
300
+ title: string;
301
+ url: string;
302
+ imageUrl?: string | null;
303
+ price?: string;
304
+ vendor?: string;
305
+ };
306
+ ```
307
+
308
+ **What the SDK guarantees**
309
+
310
+ - **One fetch cadence.** The product search rides the same debounce, the same
311
+ `AbortController` and the same version guard as `/suggest` — there is no
312
+ second timer to drift out of step.
313
+ - **Empty queries never reach you.** On mount and after `reset()` the strip
314
+ clears.
315
+ - **Out-of-order responses are dropped.** A slow response for an older query is
316
+ never rendered over a newer one.
317
+ - **Failure is silent.** A rejected `fetch` or a throwing `transform` clears the
318
+ strip, logs once, and leaves the suggestions half untouched — no `error`
319
+ state, no `onError`, no effect on `isLoading`.
320
+ - **The two halves are independent.** The panel opens if *either* has content,
321
+ so products keep it open when option filtering empties the option list.
322
+ `dropdownTrigger` (`manual` / `hidden`) still gates the panel as before.
323
+ - **Inline configs are safe.** `products={{ fetch, transform }}` written inline
324
+ re-creates the object on every render; the hook forwards through a stable
325
+ proxy, so only turning the strip on or off reaches the core. Two deliberate
326
+ consequences: swapping one live config for another leaves the previous
327
+ integration's cards up until the next request replaces them (toggle
328
+ `products` off and back on to clear immediately), and turning the strip on
329
+ mid-session shows nothing until the next `/suggest` request fires — the
330
+ product search rides that one scheduler, and enabling the strip doesn't
331
+ itself change the query.
332
+
333
+ **Selection and links.** Cards are real `<a href>` elements, so cmd/ctrl-click,
334
+ middle-click and "copy link address" behave natively. A plain left click is
335
+ intercepted (`preventDefault`) and emits `onProductSelect` instead.
336
+
337
+ `product.url` is used verbatim — the SDK trusts your `transform` output and
338
+ does not sanitise it, so validate the URL there if the platform response isn't
339
+ fully under your control. (Angular additionally runs its own `[href]`
340
+ sanitiser, so unrecognised schemes like `myapp://…` are rewritten to
341
+ `unsafe:…` in that package only.)
342
+
343
+ **Accessibility.** The strip is a `role="group"` labelled "Products"; each card
344
+ is a `role="option"` in the tab order, activated with Enter or Space, with a
345
+ visible focus ring. Tabbing into the strip does not close the panel. The row
346
+ scrolls horizontally by trackpad, wheel, touch and keyboard; the page never
347
+ scrolls sideways and the scrollbar chrome is hidden in all engines.
348
+
349
+ Tier 2 gets the same thing: pass `products` / `onProductSelect` to
350
+ `useAIAutocomplete()` and the strip renders inside `<AIAutocompleteDropdown />`
351
+ via `dropdownProps`. Tier 3 consumers read `products` from the hook and call
352
+ `selectProduct(product)` from their own cards.
353
+
252
354
  #### Custom submit button
253
355
 
254
356
  ```tsx
@@ -329,6 +431,7 @@ The headless hook for Tier 2 and Tier 3. Accepts the same options as `<AIAutocom
329
431
  | `isDropdownOpen` | `boolean` | Whether the dropdown should be visible. Drive your own dropdown's visibility with this in Tier 3. |
330
432
  | `placeholderText` | `string` | Suggested placeholder text for the current step. |
331
433
  | `error` | `Error \| null` | Last fetch error. |
434
+ | `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
435
 
333
436
  **Actions**
334
437
 
@@ -338,6 +441,7 @@ The headless hook for Tier 2 and Tier 3. Accepts the same options as `<AIAutocom
338
441
  | `removeLastParam` | `() => void` | Remove the last completed param from state. The text stays in the input as plain text. |
339
442
  | `clearNewParamId` | `() => void` | Clear shimmer animation state. |
340
443
  | `reset` | `() => void` | Clear all state, re-fetch, and start a new session (rotates `session_id`). Call this after handling submit. |
444
+ | `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
445
 
342
446
  **Input forwarding (custom inputs)** — call these instead of spreading `inputProps` when your input isn't a `<textarea>` (contentEditable / rich-text editors):
343
447
 
@@ -353,7 +457,7 @@ The headless hook for Tier 2 and Tier 3. Accepts the same options as `<AIAutocom
353
457
  | Field | Type | Description |
354
458
  |---|---|---|
355
459
  | `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. |
460
+ | `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
461
 
358
462
  ### `<AIAutocompleteDropdown />`
359
463
 
@@ -373,6 +477,9 @@ The dropdown component for Tier 2. Spread `dropdownProps` from the hook (and add
373
477
  | `pills?` | `Suggestion[]` | Pills to render inside the dropdown. |
374
478
  | `onPillClick?` | `(index: number) => void` | Called when a pill is clicked. |
375
479
  | `showPills?` | `boolean` | Whether to render pills. Default: `true`. |
480
+ | `products?` | `Product[]` | Product cards to render below the options grid. Empty (the default) renders no strip. |
481
+ | `onProductSelect?` | `(product: Product) => void` | Called when a card is activated. |
482
+ | `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
483
  | `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
484
 
378
485
  ### `AutocompleteResult`
@@ -431,6 +538,21 @@ Override on the container (via `className`). All defaults use `:where()` (zero s
431
538
  | `--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
539
  | `--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
540
  | `--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. |
541
+ | `--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. |
542
+ | `--aia-product-gap` | `8px` | `8px` | Gap between product cards. |
543
+ | `--aia-product-bg` | `transparent` | `transparent` | Product card background. |
544
+ | `--aia-product-bg-active` | `--aia-option-bg` | `--aia-option-bg` | Product card background on hover. |
545
+ | `--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. |
546
+ | `--aia-product-placeholder-color` | `--aia-option-color` | `--aia-option-color` | Glyph color of the no-image placeholder tile. |
547
+ | `--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. |
548
+ | `--aia-product-price-color` | `--aia-option-color-selected` | `--aia-option-color-selected` | Product price text. |
549
+ | `--aia-product-vendor-color` | `--aia-option-color` | `--aia-option-color` | Product vendor line. |
550
+ | `--aia-product-focus-ring` | `--aia-option-color-selected` | `--aia-option-color-selected` | Focus ring drawn on a keyboard-focused card. |
551
+ | `--aia-products-label-color` | `--aia-option-color` | `--aia-option-color` | "Products" section label. |
552
+ | `--aia-product-title-font-size` | `12px` | `12px` | Product title font size. |
553
+ | `--aia-product-price-font-size` | `11px` | `11px` | Product price font size. |
554
+ | `--aia-product-vendor-font-size` | `10px` | `10px` | Product vendor font size. |
555
+ | `--aia-products-label-font-size` | `11px` | `11px` | Section label font size. |
434
556
  | `--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
557
 
436
558
  Legacy `--aia-color-*` variables are still supported as fallbacks.
@@ -459,7 +581,11 @@ For styling beyond the CSS variables, target these stable `data-aia-*` attribute
459
581
  | `[data-aia-pill]` | Each unfilled-suggestion pill |
460
582
  | `[data-aia-pillbar]` | Pill bar container inside the dropdown |
461
583
  | `[data-aia-option]` | Each suggestion option |
462
- | `[data-aia-dropdown]` | The dropdown root (listbox) |
584
+ | `[data-aia-dropdown]` | The dropdown root (listbox). Carries `data-aia-has-products` while the product strip has cards. |
585
+ | `[data-aia-products]` | Product strip section (label + row) |
586
+ | `[data-aia-products-row]` | The horizontally scrolling row of cards |
587
+ | `[data-aia-product]` | Each product card |
588
+ | `[data-aia-product-placeholder]` | Media tile of a card whose product has no image |
463
589
 
464
590
  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
591
 
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 };