@magicx-eng/ai-autocomplete-react 0.6.8 → 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
@@ -318,6 +419,7 @@ The headless hook for Tier 2 and Tier 3. Accepts the same options as `<AIAutocom
318
419
  | Field | Type | Description |
319
420
  |---|---|---|
320
421
  | `completedParams` | `CompletedParamState[]` | Filled parameters. |
422
+ | `skippedParams` | `SkippedParamState[]` | Suggestions the user dismissed with <kbd>→</kbd>. Nothing renders them — pass them to `buildSubmitResult` for a hand-rolled submit. |
321
423
  | `suggestionPills` | `Suggestion[]` | Unfilled suggestions (pills). First item is the active pill. |
322
424
  | `segments` | `Segment[]` | Input text split into typed text vs completed params — completed segments render as bold `<strong>` runs inside the editor. |
323
425
  | `newParamId` | `string \| null` | ID of the most recently added param (for shimmer animation). |
@@ -328,6 +430,7 @@ The headless hook for Tier 2 and Tier 3. Accepts the same options as `<AIAutocom
328
430
  | `isDropdownOpen` | `boolean` | Whether the dropdown should be visible. Drive your own dropdown's visibility with this in Tier 3. |
329
431
  | `placeholderText` | `string` | Suggested placeholder text for the current step. |
330
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. |
331
434
 
332
435
  **Actions**
333
436
 
@@ -337,6 +440,7 @@ The headless hook for Tier 2 and Tier 3. Accepts the same options as `<AIAutocom
337
440
  | `removeLastParam` | `() => void` | Remove the last completed param from state. The text stays in the input as plain text. |
338
441
  | `clearNewParamId` | `() => void` | Clear shimmer animation state. |
339
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. |
340
444
 
341
445
  **Input forwarding (custom inputs)** — call these instead of spreading `inputProps` when your input isn't a `<textarea>` (contentEditable / rich-text editors):
342
446
 
@@ -352,7 +456,7 @@ The headless hook for Tier 2 and Tier 3. Accepts the same options as `<AIAutocom
352
456
  | Field | Type | Description |
353
457
  |---|---|---|
354
458
  | `inputProps` | `object` | Spread onto a `<textarea>`. Includes `value`, `placeholder`, `onChange`, `onKeyDown`, and ARIA attributes. |
355
- | `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. |
356
460
 
357
461
  ### `<AIAutocompleteDropdown />`
358
462
 
@@ -372,6 +476,9 @@ The dropdown component for Tier 2. Spread `dropdownProps` from the hook (and add
372
476
  | `pills?` | `Suggestion[]` | Pills to render inside the dropdown. |
373
477
  | `onPillClick?` | `(index: number) => void` | Called when a pill is clicked. |
374
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. |
375
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. |
376
483
 
377
484
  ### `AutocompleteResult`
@@ -380,7 +487,19 @@ The dropdown component for Tier 2. Spread `dropdownProps` from the hook (and add
380
487
  |---|---|---|
381
488
  | `query` | `string` | Plain text as the user sees it. |
382
489
  | `raw_query` | `string` | Text with placeholder tokens (e.g. `"Create a {{TASK_1}}"`). |
383
- | `completed_params` | `CompletedParam[]` | Filled parameter values. |
490
+ | `completed_params` | `CompletedParam[]` | Filled parameter values, followed by any the user skipped (see below). |
491
+
492
+ #### Skipped parameters
493
+
494
+ Pressing <kbd>→</kbd> at the end of the input dismisses the active pill. The dismissal is reported to the server — and included in `completed_params` here — as an entry with no placeholder and the sentinel text `"skipped"`:
495
+
496
+ ```ts
497
+ { placeholder: "", type: "goal", text: "skipped", kind: null }
498
+ ```
499
+
500
+ Tier 2 consumers who build their own submit payload get the raw skips from the hook as `skippedParams`, and can fold them in the same way with the exported `buildSubmitResult(text, completedParams, skippedParams)`.
501
+
502
+ > **Reading `skippedParams` directly:** the array is append-only until `reset()`. The "drop a skip whose type got filled" rule is applied when the payload is built, not by pruning the array — so if the user skips `goal` and later fills one, the raw array still holds the `goal` entry. That's deliberate: the filter self-heals if they then delete that param's text, where pruning would discard the signal for good. `buildSubmitResult` applies the rule for you; to apply it elsewhere (say, a "you skipped X" badge), use the exported `withSkippedParams(completedParams, skippedParams)`.
384
503
 
385
504
  ---
386
505
 
@@ -418,6 +537,21 @@ Override on the container (via `className`). All defaults use `:where()` (zero s
418
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). |
419
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), …)`). |
420
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. |
421
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. |
422
556
 
423
557
  Legacy `--aia-color-*` variables are still supported as fallbacks.
@@ -446,7 +580,11 @@ For styling beyond the CSS variables, target these stable `data-aia-*` attribute
446
580
  | `[data-aia-pill]` | Each unfilled-suggestion pill |
447
581
  | `[data-aia-pillbar]` | Pill bar container inside the dropdown |
448
582
  | `[data-aia-option]` | Each suggestion option |
449
- | `[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 |
450
588
 
451
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`).
452
590
 
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
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';
1
3
  import * as react from 'react';
2
4
  import { ReactNode, KeyboardEvent, ChangeEvent } from 'react';
3
- import { AutocompleteResult, OptionOverrides, APIConfig, AppearanceMode, CompletedParamState, Suggestion, SuggestionOption, Segment } from '@magicx-eng/ai-autocomplete-vanilla';
4
- export { APIConfig, APIKeyConfig, AccessTokenConfig, AccessTokenResult, AppearanceMode, AutocompleteResult, CompletedParam, CompletedParamState, OptionOverrides, Segment, Suggestion, SuggestionOption, TaskKind } from '@magicx-eng/ai-autocomplete-vanilla';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
6
6
 
7
7
  interface AIAutocompleteHandle {
@@ -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;
@@ -90,6 +142,13 @@ interface UseAIAutocompleteOptions {
90
142
  }
91
143
  interface UseAIAutocompleteReturn {
92
144
  completedParams: CompletedParamState[];
145
+ /**
146
+ * Suggestions the user dismissed with the skip key (→). Not rendered
147
+ * anywhere — pass them to `buildSubmitResult` (or read them for your own
148
+ * telemetry) so a hand-rolled submit carries the same `text: "skipped"`
149
+ * entries the SDK's own requests do.
150
+ */
151
+ skippedParams: SkippedParamState[];
93
152
  suggestionPills: Suggestion[];
94
153
  setActivePill: (index: number) => void;
95
154
  removeLastParam: () => void;
@@ -113,6 +172,18 @@ interface UseAIAutocompleteReturn {
113
172
  placeholderText: string;
114
173
  listboxId: string;
115
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;
116
187
  /** Tier 1 helper: forward plain-text input to the core (autocapitalize handled). */
117
188
  handleTextChange: (value: string) => void;
118
189
  /** Tier 1 helper: forward keyboard events to the core. */
@@ -170,6 +241,17 @@ interface AIAutocompleteDropdownProps {
170
241
  isLoading?: boolean;
171
242
  /** When the input has no typed text, the footer hint reads "tab to select". Provided by `dropdownProps` from the hook. */
172
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;
173
255
  /**
174
256
  * Where the dropdown opens relative to the input. When `"above"`, the dropdown
175
257
  * positions itself above (and reverses its internal layout so options sit
@@ -193,8 +275,8 @@ interface AIAutocompleteDropdownProps {
193
275
 
194
276
  declare const AIAutocomplete: react.ForwardRefExoticComponent<AIAutocompleteProps & react.RefAttributes<AIAutocompleteHandle>>;
195
277
 
196
- 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;
197
279
 
198
- 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;
199
281
 
200
282
  export { AIAutocomplete, AIAutocompleteDropdown, type AIAutocompleteDropdownProps, type AIAutocompleteHandle, type AIAutocompleteProps, type UseAIAutocompleteOptions, type UseAIAutocompleteReturn, useAIAutocomplete };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
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';
1
3
  import * as react from 'react';
2
4
  import { ReactNode, KeyboardEvent, ChangeEvent } from 'react';
3
- import { AutocompleteResult, OptionOverrides, APIConfig, AppearanceMode, CompletedParamState, Suggestion, SuggestionOption, Segment } from '@magicx-eng/ai-autocomplete-vanilla';
4
- export { APIConfig, APIKeyConfig, AccessTokenConfig, AccessTokenResult, AppearanceMode, AutocompleteResult, CompletedParam, CompletedParamState, OptionOverrides, Segment, Suggestion, SuggestionOption, TaskKind } from '@magicx-eng/ai-autocomplete-vanilla';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
6
6
 
7
7
  interface AIAutocompleteHandle {
@@ -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;
@@ -90,6 +142,13 @@ interface UseAIAutocompleteOptions {
90
142
  }
91
143
  interface UseAIAutocompleteReturn {
92
144
  completedParams: CompletedParamState[];
145
+ /**
146
+ * Suggestions the user dismissed with the skip key (→). Not rendered
147
+ * anywhere — pass them to `buildSubmitResult` (or read them for your own
148
+ * telemetry) so a hand-rolled submit carries the same `text: "skipped"`
149
+ * entries the SDK's own requests do.
150
+ */
151
+ skippedParams: SkippedParamState[];
93
152
  suggestionPills: Suggestion[];
94
153
  setActivePill: (index: number) => void;
95
154
  removeLastParam: () => void;
@@ -113,6 +172,18 @@ interface UseAIAutocompleteReturn {
113
172
  placeholderText: string;
114
173
  listboxId: string;
115
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;
116
187
  /** Tier 1 helper: forward plain-text input to the core (autocapitalize handled). */
117
188
  handleTextChange: (value: string) => void;
118
189
  /** Tier 1 helper: forward keyboard events to the core. */
@@ -170,6 +241,17 @@ interface AIAutocompleteDropdownProps {
170
241
  isLoading?: boolean;
171
242
  /** When the input has no typed text, the footer hint reads "tab to select". Provided by `dropdownProps` from the hook. */
172
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;
173
255
  /**
174
256
  * Where the dropdown opens relative to the input. When `"above"`, the dropdown
175
257
  * positions itself above (and reverses its internal layout so options sit
@@ -193,8 +275,8 @@ interface AIAutocompleteDropdownProps {
193
275
 
194
276
  declare const AIAutocomplete: react.ForwardRefExoticComponent<AIAutocompleteProps & react.RefAttributes<AIAutocompleteHandle>>;
195
277
 
196
- 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;
197
279
 
198
- 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;
199
281
 
200
282
  export { AIAutocomplete, AIAutocompleteDropdown, type AIAutocompleteDropdownProps, type AIAutocompleteHandle, type AIAutocompleteProps, type UseAIAutocompleteOptions, type UseAIAutocompleteReturn, useAIAutocomplete };