@magicx-eng/ai-autocomplete-vanilla 0.20.2 → 0.22.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
@@ -16,7 +16,8 @@ A framework-agnostic vanilla JS/TypeScript library that provides a guided AI-pow
16
16
  - **Client-side filtering** — instant substring filtering on every keystroke
17
17
  - **Datepicker** — date parameters are answered with a calendar instead of an option list. Click a day or navigate with the arrow keys; the date is committed the way it would be written — `Tuesday` for a date inside the next week, `March 23` for one later this year, `March 23 2027` for another year. Tapping a committed date re-opens the calendar on the month that text names now — for a weekday name, that is the next such day, not the one originally picked. A parameter whose options are written as spans (`September 11 - October 13`) gets the same calendar answered in two clicks, committed as one range.
18
18
  - **Option overrides** — supply the options for a parameter yourself: a fixed list, a computed one, or one fetched from your own search endpoint as the user types
19
- - **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`
19
+ - **Product strip** — the dropdown renders a horizontal row of product cards below the options, filled by default with the items the `/suggest` response reports as matching the query; plug in your own product search instead with `products`, or turn the strip off with `showProducts: false`
20
+ - **Catalog report** — every response says which of the query's constraints the catalog applied, which it had to drop, and how many items are left, on `customFields` / `custom_fields`
20
21
  - **Controlled & uncontrolled** — works out of the box or integrates with external state
21
22
  - **Accessible** — ARIA combobox 1.2 pattern with `role="listbox"`, `aria-activedescendant`
22
23
  - **IME-safe** — composition events are buffered so input text is committed once, after composition ends
@@ -99,7 +100,10 @@ const ac = new AIAutocomplete(container, {
99
100
  // this is only for sending the styles somewhere else. See "Shadow DOM".
100
101
  styleRoot: undefined,
101
102
 
102
- // Product strip (opt-in). Omit it and nothing changes: no request, no markup.
103
+ // Product strip. Shown by default from the items the /suggest response
104
+ // reports; false renders no strip.
105
+ showProducts: true,
106
+ // Custom source for the strip. Omit it and the server's items are shown.
103
107
  products: {
104
108
  fetch: (query, signal) => myPlatform.search(query, { signal }),
105
109
  transform: (raw) => mapToProducts(raw),
@@ -153,10 +157,80 @@ ac.getResult(); // The AutocompleteResult as it stands right now
153
157
 
154
158
  ### Product strip
155
159
 
156
- Opt in with `products` and the dropdown renders a horizontal row of product
157
- cards below the options grid. Every platform (Shopify today, others later) has
158
- its own search endpoint and its own response shape, so the SDK owns the UI and
159
- the integration owns only the fetching and the mapping.
160
+ The dropdown renders a horizontal row of product cards below the options grid
161
+ whenever it has products to show. By default those are the items the
162
+ `/suggest` response itself reports as matching the query no configuration,
163
+ no extra request: the cards land in the same state write as the suggestions,
164
+ so what the strip shows and what the pills offer always describe the same
165
+ narrowing.
166
+
167
+ ```ts
168
+ import { AIAutocomplete } from "@magicx-eng/ai-autocomplete-vanilla";
169
+
170
+ new AIAutocomplete(container, {
171
+ apiConfig: { apiKey: "..." },
172
+ // Selection emits — the SDK never navigates.
173
+ onProductSelect: (product) => {
174
+ if (product.url) window.location.assign(product.url); // …or add to cart, or fill the input
175
+ },
176
+ });
177
+ ```
178
+
179
+ Each server item becomes a `Product` with `id` (the item's stable id), `title`,
180
+ `price` (the item's lowest price, verbatim as the catalog spells it — the
181
+ currency is yours to add), `vendor`, `imageUrl`, and a **relative** `url` of
182
+ `/products/{handle}` — the right link for a widget running on the storefront
183
+ itself. An item the catalog has no handle for gets no `url`: its card is still
184
+ activatable and still emits `onProductSelect`, it just has nothing for
185
+ cmd-click to open. `productFromMatchedItem` / `productsFromCustomFields` are
186
+ exported for a hand-rolled strip that wants the same mapping.
187
+
188
+ Set `showProducts: false` to render no strip at all. The catalog report
189
+ (below) stays readable either way.
190
+
191
+ #### The catalog report (`customFields`)
192
+
193
+ Every response that read the catalog also says what it did with the query, and
194
+ the SDK exposes that as `customFields` on state (and as `custom_fields` on
195
+ `AutocompleteResult`, so `onResult` / `getResult()` carry it too):
196
+
197
+ ```ts
198
+ ac.subscribe((state) => {
199
+ const report = state.customFields;
200
+ if (!report) return; // no catalog was read for this response
201
+ count.textContent = `${report.items.total} results`;
202
+ if (report.dropped_filters.length > 0) {
203
+ // e.g. { param: "Color", value: "Chartreuse", op: "eq" } — the shopper asked
204
+ // for a colour no item carries, so the results ignore it. Say so.
205
+ note.textContent = `No match for ${report.dropped_filters.map((f) => f.value).join(", ")}`;
206
+ }
207
+ });
208
+ ```
209
+
210
+ - `applied_filters` / `dropped_filters` — the constraints the server read out
211
+ of the query as `{ param, value, op }` (`op` is one of `eq`, `ne`, `lt`,
212
+ `lte`, `gt`, `gte`), split into the ones the catalog answered and the ones it
213
+ could not (a parameter it does not have, a bound that is not a number, a
214
+ value no item carries). The items answer the applied set, so the dropped
215
+ list is what tells "87 black t-shirts" from "no black item, so here are
216
+ t-shirts". Both are always arrays.
217
+ - `items.total` — how many items the applied filters left standing. It also
218
+ moves with the query's own words, so it drops as a typed word starts
219
+ matching a title. `items.matched` is the list the strip renders, capped by
220
+ the server; it is empty (with a non-zero `total`) when nothing narrowed the
221
+ catalog, where listing arbitrary items as matches would mislead.
222
+ - `null` means the response carried no report — no catalog was read for it
223
+ (a product with no catalog, the starting state, a response with no
224
+ option-bearing parameter to offer). That is "no news", not an empty
225
+ catalog. The strip clears on such a response rather than keeping an older
226
+ query's items.
227
+
228
+ #### Your own product search (`products`)
229
+
230
+ To source the cards from your own search instead — a platform with its own
231
+ endpoint and response shape — set `products`. The SDK owns the UI; you own
232
+ only the fetching and the mapping. The server's items are then not rendered
233
+ (they stay readable on `customFields`).
160
234
 
161
235
  ```ts
162
236
  import { AIAutocomplete, type Product } from "@magicx-eng/ai-autocomplete-vanilla";
@@ -191,20 +265,19 @@ new AIAutocomplete(container, {
191
265
  limit: 8, // applied by the SDK, after transform
192
266
  },
193
267
 
194
- // Selection emits — the SDK never navigates.
195
268
  onProductSelect: (product) => {
196
- window.location.assign(product.url); // …or add to cart, or fill the input
269
+ if (product.url) window.location.assign(product.url);
197
270
  },
198
271
  });
199
272
  ```
200
273
 
201
- `Product` is exactly six fields; only `id`, `title` and `url` are required:
274
+ `Product` is exactly six fields; only `id` and `title` are required:
202
275
 
203
276
  ```ts
204
277
  type Product = {
205
278
  id: string;
206
279
  title: string;
207
- url: string;
280
+ url?: string; // absent = a card with no link (still activatable)
208
281
  imageUrl?: string | null; // null renders the placeholder tile
209
282
  price?: string; // pre-formatted by you
210
283
  vendor?: string;
@@ -238,7 +311,7 @@ never navigates on your behalf.
238
311
 
239
312
  `product.url` is used verbatim — the SDK trusts your `transform` output and
240
313
  does not sanitise it, so validate the URL there if the platform response isn't
241
- fully under your control. (Angular additionally runs its own `[href]`
314
+ fully under your control. (Angular additionally runs its own `href`
242
315
  sanitiser, so unrecognised schemes like `myapp://…` are rewritten to
243
316
  `unsafe:…` in that package only.)
244
317
 
@@ -248,11 +321,11 @@ visible focus ring. Tabbing into the strip does not close the panel. The row
248
321
  scrolls horizontally by trackpad, wheel, touch and keyboard; the page never
249
322
  scrolls sideways and the scrollbar chrome is hidden in all engines.
250
323
 
251
- `update({ products })` swaps the integration (and clears the current cards);
252
- `update({ products: undefined })` turns the strip off. Either way the strip
253
- repopulates on the *next* `/suggest` request rather than immediately the
254
- product search rides that one scheduler, and changing the config doesn't itself
255
- change the query.
324
+ `update({ products })` swaps the integration and clears the current cards; the
325
+ strip repopulates on the *next* `/suggest` request, since the product search
326
+ rides that one scheduler and changing the config doesn't itself change the
327
+ query. `update({ products: undefined })` hands the strip back to the server's
328
+ items at once, and `update({ showProducts })` empties or restores it at once.
256
329
 
257
330
  #### Custom submit button
258
331
 
@@ -400,7 +473,7 @@ input.addEventListener("blur", () => ac.setFocused(false));
400
473
 
401
474
  Pills always render inside the dropdown in this mode. The library creates the dropdown DOM inside your container — you just provide the element and wire up your input's events.
402
475
 
403
- The [product strip](#product-strip) works here too: pass `products` / `onProductSelect` exactly as in Tier 1 and the strip renders inside the dropdown the library owns.
476
+ The [product strip](#product-strip) works here too: it renders inside the dropdown the library owns, from the server's items by default, and `products` / `showProducts` / `onProductSelect` behave exactly as in Tier 1.
404
477
 
405
478
  > Focus/blur wiring is required when `dropdownTrigger` is `"auto"` (the default) — the dropdown only opens while the input is focused. Without `setFocused()`, the dropdown will never open.
406
479
 
@@ -482,7 +555,8 @@ unsub();
482
555
  | `isSearchingOptions` | `boolean` | True while an [option override](#option-overrides) for the parameter on screen has been asked and hasn't answered. Render the same skeleton you render for `isLoading`; the built-in dropdowns do. |
483
556
  | `inSelectionAnimation` | `boolean` | True for the 500 ms after a user-initiated option tap so the press animation can finish before the dropdown switches to the loading skeleton. |
484
557
  | `editingParam` | `CompletedParamState \| null` | When non-null, the user is re-editing a bold completed param; cached options remain visible and the loading skeleton is suppressed. |
485
- | `products` | `Product[]` | Results of the latest product search (empty unless `products` is configured). See [Product strip](#product-strip) — Tier 3 consumers render their own cards and call `selectProduct(product)` to emit `onProductSelect`. |
558
+ | `products` | `Product[]` | What the product strip shows: the items the latest response reported as matching the query, or the results of a custom `products` search. See [Product strip](#product-strip) — Tier 3 consumers render their own cards and call `selectProduct(product)` to emit `onProductSelect`. |
559
+ | `customFields` | `CustomFieldsReport \| null` | The catalog report the latest response carried — applied and dropped filters, and the matching items' count and list — or `null` when it carried none. See [The catalog report](#the-catalog-report-customfields). |
486
560
  | `isReady` | `boolean` | Server indicates query is complete |
487
561
  | `error` | `Error \| null` | Last fetch error |
488
562
 
@@ -591,6 +665,7 @@ The structured query as the SDK currently understands it. The same object is pas
591
665
  | `completed_params` | `CompletedParam[]` | Filled parameter values in query order, followed by any the user skipped (see below). |
592
666
  | `identified_params` | `IdentifiedParam[]` | Parameters the server recognised in the user's own words, as `{ type, value }` — e.g. `{ type: "due_date", value: "Friday" }` when the user typed "by Friday" instead of picking a date. They are not tokenized in `raw_query`, and they are tentative: each response replaces the set, and an entry drops out as soon as its text is edited away. |
593
667
  | `is_ready` | `boolean` | Whether the server considers the query complete enough to act on. |
668
+ | `custom_fields?` | `CustomFieldsReport` | What catalog grounding did for the latest applied response, when it read a catalog at all — see [Product strip](#product-strip). `applied_filters` and `dropped_filters` list the query's constraints as `{ param, value, op }`; `items.total` is how many items the applied ones left standing and `items.matched` the ones the strip renders. Absent (no key) when the response carried no report. |
594
669
 
595
670
  #### Reading the query as it's built
596
671
 
@@ -717,7 +792,7 @@ Override these on the container element. All built-in defaults use `:where()` (z
717
792
  | `--aia-scroll-arrow-border` | `--aia-dropdown-border` | `--aia-dropdown-border` | Hairline around the arrow. |
718
793
  | `--aia-scroll-arrow-shadow` | `0 2px 8px rgba(0,0,0,0.12)` | `0 2px 8px rgba(0,0,0,0.12)` | Elevation of the arrow. |
719
794
  | `--aia-placeholder-fade` | `120ms` | `120ms` | Fade-out of the outgoing placeholder phrase when the starting-state placeholder changes (the incoming one types itself in). |
720
- | `--aia-product-placeholder-color` | `--aia-option-color` | `--aia-option-color` | Glyph color of the no-image placeholder tile. |
795
+ | `--aia-product-placeholder-image` | storefront pictogram | storefront pictogram | The image drawn on the placeholder tile when a product has no `imageUrl`: a two-tone storefront pictogram, inlined as a data URI. Set it to any `url(...)` to swap the pictogram; it is drawn centered and contained inside the tile, over `--aia-product-media-bg`. |
721
796
  | `--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. |
722
797
  | `--aia-product-price-color` | `--aia-option-color-selected` | `--aia-option-color-selected` | Product price text. |
723
798
  | `--aia-product-vendor-color` | `--aia-option-color` | `--aia-option-color` | Product vendor line. |
package/dist/index.d.mts CHANGED
@@ -74,12 +74,93 @@ interface AutocompleteResponse {
74
74
  input: InputItem[];
75
75
  suggestions: Suggestion[];
76
76
  is_ready?: boolean;
77
+ /**
78
+ * What catalog grounding did for this request. Absent whenever no
79
+ * catalog was read — see {@link CustomFieldsReport}.
80
+ */
81
+ custom_fields?: CustomFieldsReport;
77
82
  };
78
83
  meta: {
79
84
  request_id: string;
80
85
  request_at: string;
81
86
  };
82
87
  }
88
+ /** Comparison an {@link ExpressedFilter} applies to its value. */
89
+ type FilterOp = "eq" | "ne" | "lt" | "lte" | "gt" | "gte";
90
+ /**
91
+ * One constraint the server read out of the query — a parameter, a value and
92
+ * how the two compare ("price", "100", "lt").
93
+ */
94
+ interface ExpressedFilter {
95
+ param: string;
96
+ value: string;
97
+ op: FilterOp;
98
+ }
99
+ /**
100
+ * One catalog item the query narrowed down to, in the shape needed to render
101
+ * it. Only `source_id` and `title` are always present.
102
+ */
103
+ interface MatchedItem {
104
+ /** Stable identity of the item within the catalog. */
105
+ source_id: string;
106
+ /** The item's id in the platform it was imported from, when it has one. */
107
+ origin_id?: string;
108
+ title: string;
109
+ /**
110
+ * The item's lowest price, verbatim as the catalog spells it ("131.00").
111
+ * Not formatted for display: the currency is the storefront's to add.
112
+ */
113
+ price?: string;
114
+ vendor?: string;
115
+ /**
116
+ * The platform's URL slug for the item, not a URL. The SDK builds a relative
117
+ * `/products/{handle}` link from it for the product strip, which is right for
118
+ * a widget running on the storefront itself.
119
+ */
120
+ handle?: string;
121
+ image_url?: string;
122
+ }
123
+ /** The items the applied filters left standing. */
124
+ interface MatchedItems {
125
+ /**
126
+ * How many items the query narrowed the catalog to — `matched.length` at
127
+ * most, and larger when the list was capped. Also counts what the query's own
128
+ * words matched, so it drops as a typed word starts matching a title.
129
+ */
130
+ total: number;
131
+ /**
132
+ * The items themselves, capped by the server. Empty with a non-zero `total`
133
+ * when nothing narrowed the catalog: naming arbitrary items as matches would
134
+ * be worse than naming none.
135
+ */
136
+ matched: MatchedItem[];
137
+ }
138
+ /**
139
+ * What catalog grounding did for a request, as opposed to what it decided to
140
+ * ask next.
141
+ *
142
+ * Absent from a response whenever no catalog was read: a product with no
143
+ * catalog, a starting-state response, or a request whose suggestions offer no
144
+ * option-bearing parameter. Treat absence as "no news", not as an empty
145
+ * catalog.
146
+ *
147
+ * `applied_filters` and `dropped_filters` are what make `items` interpretable:
148
+ * the items answer the applied set, so without the dropped one "87 black
149
+ * t-shirts" and "no black item, so here are t-shirts" look identical. Both
150
+ * lists are always present, so a consumer reads `[]` rather than guarding a
151
+ * null.
152
+ */
153
+ interface CustomFieldsReport {
154
+ /** Constraints the catalog read actually ran under. */
155
+ applied_filters: ExpressedFilter[];
156
+ /**
157
+ * Constraints the query expressed that the catalog could not answer — a
158
+ * parameter it does not have, a bound that is not a number, or a value no
159
+ * item carries. `items` were narrowed without them.
160
+ */
161
+ dropped_filters: ExpressedFilter[];
162
+ items: MatchedItems;
163
+ }
83
164
  interface CompletedParamState extends CompletedParam {
84
165
  id: string;
85
166
  text: string;
@@ -205,19 +286,24 @@ type OptionOverrides = Record<string, OptionOverride>;
205
286
  /**
206
287
  * A single product card in the dropdown's product strip.
207
288
  *
208
- * Platform-agnostic on purpose: every integration (Shopify today, others
209
- * later) maps its own search response onto exactly these fields, so the SDK
210
- * renders one strip regardless of where the results came from. Only `id`,
211
- * `title` and `url` are required — the card reflows when any of the rest is
212
- * missing. Do NOT grow this shape casually; a new field is a new contract
213
- * every integration has to satisfy.
289
+ * Platform-agnostic on purpose: the server's matched items (the default
290
+ * source) and every custom integration (Shopify search today, others later)
291
+ * map onto exactly these fields, so the SDK renders one strip regardless of
292
+ * where the results came from. Only `id` and `title` are required — the card
293
+ * reflows when any of the rest is missing. Do NOT grow this shape casually; a
294
+ * new field is a new contract every source has to satisfy.
214
295
  */
215
296
  interface Product {
216
297
  /** Stable identity — used as the render key, so it must be unique per result set. */
217
298
  id: string;
218
299
  title: string;
219
- /** Destination for the card's `href`. Selection does not navigate; see `onProductSelect`. */
220
- url: string;
300
+ /**
301
+ * Destination for the card's `href`. Absent, the card has no link — it is
302
+ * still activatable and still emits `onProductSelect`, but cmd-click and
303
+ * "copy link address" have nothing to open. Selection never navigates; see
304
+ * `onProductSelect`.
305
+ */
306
+ url?: string;
221
307
  /** Absent/null renders the placeholder tile — plenty of catalogues have no images. */
222
308
  imageUrl?: string | null;
223
309
  /** Pre-formatted by the integration, which is the side that knows the currency. */
@@ -225,9 +311,13 @@ interface Product {
225
311
  vendor?: string;
226
312
  }
227
313
  /**
228
- * Opt-in product-search wiring. Absent (the default) means the SDK never
229
- * fetches and never renders a strip behaviour is byte-for-byte what it was
230
- * before products existed.
314
+ * Custom product-search wiring for the strip. Absent (the default), the strip
315
+ * shows the items the `/suggest` response itself reports as matching the query
316
+ * (`custom_fields.items`), with no extra request. Set it to source the cards
317
+ * from your own search instead: the server's items are then never rendered
318
+ * (they stay readable on `customFields`), and the SDK fetches through this
319
+ * config on its own cadence. To render no strip at all, set `showProducts`
320
+ * to false.
231
321
  */
232
322
  interface ProductsConfig {
233
323
  /**
@@ -289,6 +379,12 @@ interface AutocompleteResult {
289
379
  identified_params: IdentifiedParam[];
290
380
  /** Whether the server considers the query complete enough to act on. */
291
381
  is_ready: boolean;
382
+ /**
383
+ * What catalog grounding did for the latest applied response — the filters
384
+ * it applied and dropped, and the items they left standing. Present only
385
+ * when that response carried the block; see {@link CustomFieldsReport}.
386
+ */
387
+ custom_fields?: CustomFieldsReport;
292
388
  }
293
389
 
294
390
  /** A calendar month on screen. `month` is 0-based, matching `Date#getMonth`. */
@@ -458,12 +554,26 @@ interface CoreInputState {
458
554
  } | null;
459
555
  suggestions: Suggestion[];
460
556
  /**
461
- * Results of the latest product search, already transformed and capped by
462
- * `opts.products.limit`. Always empty when `opts.products` is unconfigured.
463
- * Cleared on an empty query and on a failed fetch/transform, so the strip
464
- * never shows results belonging to an older query.
557
+ * What the product strip shows. By default the items the latest `/suggest`
558
+ * response reported as matching the query (`custom_fields.items.matched`,
559
+ * mapped onto `Product`), replaced on every response and empty when the
560
+ * response carried none — so the strip never shows items belonging to an
561
+ * older query. With `opts.products` configured, the results of the latest
562
+ * custom search instead, already transformed and capped by its `limit`,
563
+ * cleared on an empty query and on a failed fetch/transform. Always empty
564
+ * while `opts.showProducts` is false.
465
565
  */
466
566
  products: Product[];
567
+ /**
568
+ * The catalog grounding report the latest applied response carried, or null
569
+ * when it carried none. Replaced wholesale per response, so it always
570
+ * describes the suggestions on screen: which of the query's constraints the
571
+ * catalog applied, which it had to drop, and how many items (and which) the
572
+ * applied ones left standing. Null is "no news", not an empty catalog — a
573
+ * response reads no catalog when it has no option-bearing parameter to
574
+ * offer. See {@link CustomFieldsReport}.
575
+ */
576
+ customFields: CustomFieldsReport | null;
467
577
  /**
468
578
  * The consumer's {@link OptionSource} request for the pill on screen — the
469
579
  * active suggestion, or the completed param being re-edited. `type` is the
@@ -724,10 +834,23 @@ interface CoreOptions {
724
834
  */
725
835
  submitButton?: HTMLElement | null;
726
836
  /**
727
- * Opt-in product strip. When set, the dropdown renders a horizontal row of
728
- * product cards below the options grid, fed by the integration's own search
729
- * endpoint on the SDK's existing fetch cadence. Omit it and nothing about
730
- * the dropdown changes no request, no markup, no layout shift.
837
+ * When true (default), the dropdown renders a horizontal row of product
838
+ * cards below the options grid whenever it has products to show: by default
839
+ * the items the `/suggest` response reports as matching the query, or the
840
+ * results of a custom `products` search. Set to false to render no strip and
841
+ * run no custom search; `customFields` stays readable either way.
842
+ *
843
+ * Turning it back on restores the server's items at once — they are already
844
+ * in the store — while a custom `products` config repopulates on the next
845
+ * request, since its search rides the suggest cadence.
846
+ */
847
+ showProducts?: boolean;
848
+ /**
849
+ * Custom source for the product strip. Omit it and the strip shows the items
850
+ * the `/suggest` response itself reports (`custom_fields.items`), with no
851
+ * extra request. Set it and the dropdown is fed by your own search endpoint
852
+ * on the SDK's existing fetch cadence instead; the server's items are then
853
+ * not rendered.
731
854
  */
732
855
  products?: ProductsConfig;
733
856
  /** Called when the user submits (Enter, or the submit button in Tier 1). */
@@ -1669,6 +1792,31 @@ declare function getFooterHint(optionHighlighted: boolean, isInputEmpty: boolean
1669
1792
  */
1670
1793
  declare function toWireIdentifiedParams(params: IdentifiedParamState[]): IdentifiedParam[];
1671
1794
 
1795
+ /**
1796
+ * Where the product strip links a server-matched item to. The server ships a
1797
+ * bare `handle` on purpose: the only shop domain it holds is the internal one,
1798
+ * and an absolute link built from it would send a shopper off the storefront
1799
+ * the widget is running on. A relative path resolves against the page the
1800
+ * widget is on, which for a storefront embed is the shop itself.
1801
+ */
1802
+ declare const PRODUCT_PATH_PREFIX = "/products/";
1803
+ /**
1804
+ * Maps one item the `/suggest` response reported onto the strip's `Product`
1805
+ * shape. Pure, so a Tier 3 consumer rendering its own strip from
1806
+ * `customFields` gets the same cards the built-in one draws.
1807
+ *
1808
+ * An item without a `handle` yields a card without a `url`: still activatable,
1809
+ * still emitting `onProductSelect`, just nothing for cmd-click to open.
1810
+ */
1811
+ declare function productFromMatchedItem(item: MatchedItem): Product;
1812
+ /**
1813
+ * The strip's contents for a response's catalog report. `null` — no block on
1814
+ * the response — is an empty strip rather than "keep the previous one": the
1815
+ * previous items answered a previous query, and a strip must never show
1816
+ * results belonging to an older one.
1817
+ */
1818
+ declare function productsFromCustomFields(report: CustomFieldsReport | null | undefined): Product[];
1819
+
1672
1820
  declare class ModeController {
1673
1821
  private container;
1674
1822
  private mode;
@@ -1730,6 +1878,13 @@ interface SubmitResultExtras {
1730
1878
  identifiedParams?: IdentifiedParamState[];
1731
1879
  /** Server's "query is complete" verdict (`state.isReady`). Default: false. */
1732
1880
  isReady?: boolean;
1881
+ /**
1882
+ * The catalog grounding report the latest response carried
1883
+ * (`state.customFields`). Default: none, and the result then has no
1884
+ * `custom_fields` key at all — mirroring the wire, where the block is absent
1885
+ * rather than null when no catalog was read.
1886
+ */
1887
+ customFields?: CustomFieldsReport | null;
1733
1888
  }
1734
1889
  /**
1735
1890
  * Builds the `AutocompleteResult` handed to `onSubmit` and `onResult`: the
@@ -1743,4 +1898,4 @@ interface SubmitResultExtras {
1743
1898
  */
1744
1899
  declare function buildSubmitResult(text: string, completedParams: CompletedParamState[], skippedParams?: SkippedParamState[], extras?: SubmitResultExtras): AutocompleteResult;
1745
1900
 
1746
- export { AIAutocomplete, type APIConfig, type APIKeyConfig, ATTRIBUTION_URL, type AccessTokenConfig, type AccessTokenResult, type AppearanceMode, type AutocompleteRequest, type AutocompleteResponse, type AutocompleteResult, type CompletedParam, type CompletedParamState, type CoreOptions, type CoreState, DATE_RANGE_META_END, DATE_RANGE_META_START, type DateMonthView, type DateRange, type DateSelection, type FormatType, type IdentifiedParam, type IdentifiedParamState, type InputItem, type LooseDateOptions, ModeController, OPTIONS_GRID_MOBILE_QUERY, OPTION_ENTER_DELAY_VAR, OPTION_ENTER_FADE_MS, OPTION_ENTER_RISE_MS, OPTION_ENTER_RISE_PX, OPTION_ENTER_STAGGER_MS, type OptionOverride, type OptionOverrides, type OptionsGridLayout, type OptionsGridPlan, PLACEHOLDER_FADE_OUT_MS, PLACEHOLDER_LEAVING_ATTR, PLACEHOLDER_SWAP_GAP_MS, PLACEHOLDER_TYPE_MS, PLACEHOLDER_WORD_PAUSE_MS, type Product, type ProductsConfig, RANGE_SEPARATOR, type RecentlySuggested, type RenderMode, SCROLL_ARROW_ATTR, SCROLL_ARROW_BOTTOM_VAR, SCROLL_ARROW_CLASS, SCROLL_ARROW_FADE_MS, SCROLL_ARROW_LABEL, SCROLL_ARROW_SCROLL_IDLE_MS, SCROLL_ARROW_VISIBLE_ATTR, SKIPPED_PARAM_TEXT, type ScrollArrowArgs, type ScrollArrowController, type Segment, type SkippedParamState, type Store, type SubmitResultExtras, type Suggestion, type SuggestionOption, type TaskKind, WEEKDAY_LABELS, addMonths, attachScrollArrow, buildAttributionUrl, buildDateOptions, buildQuery, buildSubmitResult, cellDay, cellIso, computeOptionsGridLayout, createStore, cursorIsAtEnd, dateCellMarks, dateSelectionFor, extractPlainText, formatAbsoluteDate, formatDate, formatDateRange, getCursorOffset, getFooterHint, identifiedParamLabel, isCalendarFormat, isOptionsGridMobileViewport, isoDate, measureOptionsGrid, monthLabel, needsOptionsGridMeasurement, optionEnterDelayMs, optionLabel, optionsEntranceDurationMs, optionsGridTemplateColumns, parseDate, parseLooseDate, parseLooseDateRange, plainTextLength, planOptionsGrid, previousGraphemeBoundary, renderEditableContent, resolveFormatType, resolveIdentifiedDate, sanitizeOptionIconSvg, scrollCaretIntoView, selectedIsoFromText, selectedRangeFor, setCursorOffset, toWireIdentifiedParams, visibleDateRange, withSkippedParams };
1901
+ export { AIAutocomplete, type APIConfig, type APIKeyConfig, ATTRIBUTION_URL, type AccessTokenConfig, type AccessTokenResult, type AppearanceMode, type AutocompleteRequest, type AutocompleteResponse, type AutocompleteResult, type CompletedParam, type CompletedParamState, type CoreOptions, type CoreState, type CustomFieldsReport, DATE_RANGE_META_END, DATE_RANGE_META_START, type DateMonthView, type DateRange, type DateSelection, type ExpressedFilter, type FilterOp, type FormatType, type IdentifiedParam, type IdentifiedParamState, type InputItem, type LooseDateOptions, type MatchedItem, type MatchedItems, ModeController, OPTIONS_GRID_MOBILE_QUERY, OPTION_ENTER_DELAY_VAR, OPTION_ENTER_FADE_MS, OPTION_ENTER_RISE_MS, OPTION_ENTER_RISE_PX, OPTION_ENTER_STAGGER_MS, type OptionOverride, type OptionOverrides, type OptionsGridLayout, type OptionsGridPlan, PLACEHOLDER_FADE_OUT_MS, PLACEHOLDER_LEAVING_ATTR, PLACEHOLDER_SWAP_GAP_MS, PLACEHOLDER_TYPE_MS, PLACEHOLDER_WORD_PAUSE_MS, PRODUCT_PATH_PREFIX, type Product, type ProductsConfig, RANGE_SEPARATOR, type RecentlySuggested, type RenderMode, SCROLL_ARROW_ATTR, SCROLL_ARROW_BOTTOM_VAR, SCROLL_ARROW_CLASS, SCROLL_ARROW_FADE_MS, SCROLL_ARROW_LABEL, SCROLL_ARROW_SCROLL_IDLE_MS, SCROLL_ARROW_VISIBLE_ATTR, SKIPPED_PARAM_TEXT, type ScrollArrowArgs, type ScrollArrowController, type Segment, type SkippedParamState, type Store, type SubmitResultExtras, type Suggestion, type SuggestionOption, type TaskKind, WEEKDAY_LABELS, addMonths, attachScrollArrow, buildAttributionUrl, buildDateOptions, buildQuery, buildSubmitResult, cellDay, cellIso, computeOptionsGridLayout, createStore, cursorIsAtEnd, dateCellMarks, dateSelectionFor, extractPlainText, formatAbsoluteDate, formatDate, formatDateRange, getCursorOffset, getFooterHint, identifiedParamLabel, isCalendarFormat, isOptionsGridMobileViewport, isoDate, measureOptionsGrid, monthLabel, needsOptionsGridMeasurement, optionEnterDelayMs, optionLabel, optionsEntranceDurationMs, optionsGridTemplateColumns, parseDate, parseLooseDate, parseLooseDateRange, plainTextLength, planOptionsGrid, previousGraphemeBoundary, productFromMatchedItem, productsFromCustomFields, renderEditableContent, resolveFormatType, resolveIdentifiedDate, sanitizeOptionIconSvg, scrollCaretIntoView, selectedIsoFromText, selectedRangeFor, setCursorOffset, toWireIdentifiedParams, visibleDateRange, withSkippedParams };