@magicx-eng/ai-autocomplete-vanilla 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -1
- package/dist/index.d.mts +94 -2
- package/dist/index.d.ts +94 -2
- package/dist/index.js +219 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +219 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,6 +87,13 @@ const ac = new AIAutocomplete(container, {
|
|
|
87
87
|
// Focus
|
|
88
88
|
autoFocus: true, // focus the input on mount (Tier 1 only)
|
|
89
89
|
|
|
90
|
+
// Product strip (opt-in). Omit it and nothing changes: no request, no markup.
|
|
91
|
+
products: {
|
|
92
|
+
fetch: (query, signal) => myPlatform.search(query, { signal }),
|
|
93
|
+
transform: (raw) => mapToProducts(raw),
|
|
94
|
+
limit: 8,
|
|
95
|
+
},
|
|
96
|
+
|
|
90
97
|
// Custom submit button (Tier 1 only)
|
|
91
98
|
// - undefined (default): renders the built-in arrow button
|
|
92
99
|
// - null: no submit button at all
|
|
@@ -100,6 +107,7 @@ const ac = new AIAutocomplete(container, {
|
|
|
100
107
|
onParamsChange: (params) => { ... },
|
|
101
108
|
onFocus: () => { ... },
|
|
102
109
|
onBlur: () => { ... },
|
|
110
|
+
onProductSelect: (product) => { ... }, // see "Product strip"
|
|
103
111
|
|
|
104
112
|
// Controlled mode
|
|
105
113
|
value: "initial text",
|
|
@@ -116,6 +124,7 @@ ac.reset(); // Clear everything, re-fetch, and start a new se
|
|
|
116
124
|
ac.destroy(); // Remove DOM, listeners, timers
|
|
117
125
|
ac.setMode("dark"); // Switch color mode
|
|
118
126
|
ac.update({ animations: false, optionsPosition: "above" });
|
|
127
|
+
ac.selectProduct(product); // Emit onProductSelect (Tier 3 / custom strips)
|
|
119
128
|
```
|
|
120
129
|
|
|
121
130
|
> **Tier 1 auto-resets** after both Enter key and built-in submit-button clicks — your `onSubmit` runs, then the SDK clears the input and starts a new session. You don't need to call `ac.reset()` yourself in Tier 1.
|
|
@@ -128,6 +137,109 @@ ac.update({ animations: false, optionsPosition: "above" });
|
|
|
128
137
|
|
|
129
138
|
> **Caret placement after a completion** — whenever a completed param is added (by any means — option click, exact-match typing, or re-edit), the caret lands right after the trailing space following the bold so typing can continue immediately. A space is inserted if one wasn't already there.
|
|
130
139
|
|
|
140
|
+
### Product strip
|
|
141
|
+
|
|
142
|
+
Opt in with `products` and the dropdown renders a horizontal row of product
|
|
143
|
+
cards below the options grid. Every platform (Shopify today, others later) has
|
|
144
|
+
its own search endpoint and its own response shape, so the SDK owns the UI and
|
|
145
|
+
the integration owns only the fetching and the mapping.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { AIAutocomplete, type Product } from "@magicx-eng/ai-autocomplete-vanilla";
|
|
149
|
+
|
|
150
|
+
new AIAutocomplete(container, {
|
|
151
|
+
apiConfig: { apiKey: "..." },
|
|
152
|
+
|
|
153
|
+
products: {
|
|
154
|
+
// You own the request entirely — auth headers, GraphQL body, locale
|
|
155
|
+
// prefixes, whatever the platform needs. Honour the signal: the SDK aborts
|
|
156
|
+
// it as soon as a newer query supersedes this one.
|
|
157
|
+
fetch: (query, signal) =>
|
|
158
|
+
fetch("/api/2024-10/graphql.json", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { "X-Shopify-Storefront-Access-Token": token },
|
|
161
|
+
body: JSON.stringify({ query: SEARCH_QUERY, variables: { q: query } }),
|
|
162
|
+
signal,
|
|
163
|
+
}).then((r) => r.json()),
|
|
164
|
+
|
|
165
|
+
// Pure mapping, kept out of `fetch` so you can unit-test it without a
|
|
166
|
+
// network. Fold it into `fetch` and return the array if you prefer.
|
|
167
|
+
transform: (raw) =>
|
|
168
|
+
raw.data.products.nodes.map((node) => ({
|
|
169
|
+
id: node.id,
|
|
170
|
+
title: node.title,
|
|
171
|
+
url: node.onlineStoreUrl,
|
|
172
|
+
imageUrl: node.featuredImage?.url ?? null,
|
|
173
|
+
price: formatMoney(node.priceRange.minVariantPrice), // you know the currency
|
|
174
|
+
vendor: node.vendor,
|
|
175
|
+
})),
|
|
176
|
+
|
|
177
|
+
limit: 8, // applied by the SDK, after transform
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// Selection emits — the SDK never navigates.
|
|
181
|
+
onProductSelect: (product) => {
|
|
182
|
+
window.location.assign(product.url); // …or add to cart, or fill the input
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`Product` is exactly six fields; only `id`, `title` and `url` are required:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
type Product = {
|
|
191
|
+
id: string;
|
|
192
|
+
title: string;
|
|
193
|
+
url: string;
|
|
194
|
+
imageUrl?: string | null; // null renders the placeholder tile
|
|
195
|
+
price?: string; // pre-formatted by you
|
|
196
|
+
vendor?: string;
|
|
197
|
+
};
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**What the SDK guarantees**
|
|
201
|
+
|
|
202
|
+
- **One fetch cadence.** The product search rides the same debounce, the same
|
|
203
|
+
`AbortController` and the same version guard as `/suggest` — there is no
|
|
204
|
+
second timer to drift out of step. A query that never triggers a suggestions
|
|
205
|
+
request (e.g. one still narrowing the current option list) doesn't trigger a
|
|
206
|
+
product search either.
|
|
207
|
+
- **Empty queries never reach you.** On mount and after `reset()` the strip
|
|
208
|
+
simply clears.
|
|
209
|
+
- **Out-of-order responses are dropped.** A slow response for an older query is
|
|
210
|
+
never rendered over a newer one.
|
|
211
|
+
- **Failure is silent.** A rejected `fetch` or a throwing `transform` clears the
|
|
212
|
+
strip, logs once, and leaves the suggestions half untouched — no `error`
|
|
213
|
+
state, no `onError`, no effect on `isLoading`.
|
|
214
|
+
- **The two halves are independent.** The panel opens if *either* has content,
|
|
215
|
+
so products keep it open when option filtering empties the list, and
|
|
216
|
+
suggestions render normally when there are no products. `dropdownTrigger`
|
|
217
|
+
(`manual` / `hidden`) still gates the panel as before — products don't
|
|
218
|
+
overrule an explicit opt-out of an auto-opening panel.
|
|
219
|
+
|
|
220
|
+
**Selection and links.** Cards are real `<a href>` elements, so cmd/ctrl-click,
|
|
221
|
+
middle-click and "copy link address" behave natively. A plain left click is
|
|
222
|
+
intercepted (`preventDefault`) and emits `onProductSelect` instead — the SDK
|
|
223
|
+
never navigates on your behalf.
|
|
224
|
+
|
|
225
|
+
`product.url` is used verbatim — the SDK trusts your `transform` output and
|
|
226
|
+
does not sanitise it, so validate the URL there if the platform response isn't
|
|
227
|
+
fully under your control. (Angular additionally runs its own `[href]`
|
|
228
|
+
sanitiser, so unrecognised schemes like `myapp://…` are rewritten to
|
|
229
|
+
`unsafe:…` in that package only.)
|
|
230
|
+
|
|
231
|
+
**Accessibility.** The strip is a `role="group"` labelled "Products"; each card
|
|
232
|
+
is a `role="option"` in the tab order, activated with Enter or Space, with a
|
|
233
|
+
visible focus ring. Tabbing into the strip does not close the panel. The row
|
|
234
|
+
scrolls horizontally by trackpad, wheel, touch and keyboard; the page never
|
|
235
|
+
scrolls sideways and the scrollbar chrome is hidden in all engines.
|
|
236
|
+
|
|
237
|
+
`update({ products })` swaps the integration (and clears the current cards);
|
|
238
|
+
`update({ products: undefined })` turns the strip off. Either way the strip
|
|
239
|
+
repopulates on the *next* `/suggest` request rather than immediately — the
|
|
240
|
+
product search rides that one scheduler, and changing the config doesn't itself
|
|
241
|
+
change the query.
|
|
242
|
+
|
|
131
243
|
#### Custom submit button
|
|
132
244
|
|
|
133
245
|
```ts
|
|
@@ -176,6 +288,8 @@ input.addEventListener("blur", () => ac.setFocused(false));
|
|
|
176
288
|
|
|
177
289
|
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.
|
|
178
290
|
|
|
291
|
+
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.
|
|
292
|
+
|
|
179
293
|
> 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.
|
|
180
294
|
|
|
181
295
|
---
|
|
@@ -244,6 +358,7 @@ unsub();
|
|
|
244
358
|
| `isLoading` | `boolean` | Fetch in progress. The Tier 1 / Tier 2 renderers gate the loading skeleton additionally on `!inSelectionAnimation` and `!editingParam`. |
|
|
245
359
|
| `inSelectionAnimation` | `boolean` | True for the 500 ms after a user-initiated option tap so the streak animation can finish before the dropdown switches to the loading skeleton. |
|
|
246
360
|
| `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. |
|
|
361
|
+
| `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`. |
|
|
247
362
|
| `isReady` | `boolean` | Server indicates query is complete |
|
|
248
363
|
| `error` | `Error \| null` | Last fetch error |
|
|
249
364
|
|
|
@@ -391,6 +506,21 @@ Override these on the container element. All built-in defaults use `:where()` (z
|
|
|
391
506
|
| `--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). |
|
|
392
507
|
| `--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), …)`). |
|
|
393
508
|
| `--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. |
|
|
509
|
+
| `--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. |
|
|
510
|
+
| `--aia-product-gap` | `8px` | `8px` | Gap between product cards. |
|
|
511
|
+
| `--aia-product-bg` | `transparent` | `transparent` | Product card background. |
|
|
512
|
+
| `--aia-product-bg-active` | `--aia-option-bg` | `--aia-option-bg` | Product card background on hover. |
|
|
513
|
+
| `--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. |
|
|
514
|
+
| `--aia-product-placeholder-color` | `--aia-option-color` | `--aia-option-color` | Glyph color of the no-image placeholder tile. |
|
|
515
|
+
| `--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. |
|
|
516
|
+
| `--aia-product-price-color` | `--aia-option-color-selected` | `--aia-option-color-selected` | Product price text. |
|
|
517
|
+
| `--aia-product-vendor-color` | `--aia-option-color` | `--aia-option-color` | Product vendor line. |
|
|
518
|
+
| `--aia-product-focus-ring` | `--aia-option-color-selected` | `--aia-option-color-selected` | Focus ring drawn on a keyboard-focused card. |
|
|
519
|
+
| `--aia-products-label-color` | `--aia-option-color` | `--aia-option-color` | "Products" section label. |
|
|
520
|
+
| `--aia-product-title-font-size` | `12px` | `12px` | Product title font size. |
|
|
521
|
+
| `--aia-product-price-font-size` | `11px` | `11px` | Product price font size. |
|
|
522
|
+
| `--aia-product-vendor-font-size` | `10px` | `10px` | Product vendor font size. |
|
|
523
|
+
| `--aia-products-label-font-size` | `11px` | `11px` | Section label font size. |
|
|
394
524
|
| `--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. |
|
|
395
525
|
|
|
396
526
|
### Per-mode Overrides
|
|
@@ -424,7 +554,11 @@ For styling beyond the CSS variables, target these stable `data-aia-*` attribute
|
|
|
424
554
|
| `[data-aia-pill]` | Each unfilled-suggestion pill |
|
|
425
555
|
| `[data-aia-pillbar]` | Pill bar container inside the dropdown |
|
|
426
556
|
| `[data-aia-option]` | Each suggestion option |
|
|
427
|
-
| `[data-aia-dropdown]` | The dropdown root (listbox) |
|
|
557
|
+
| `[data-aia-dropdown]` | The dropdown root (listbox). Carries `data-aia-has-products` while the product strip has cards. |
|
|
558
|
+
| `[data-aia-products]` | Product strip section (label + row) |
|
|
559
|
+
| `[data-aia-products-row]` | The horizontally scrolling row of cards |
|
|
560
|
+
| `[data-aia-product]` | Each product card |
|
|
561
|
+
| `[data-aia-product-placeholder]` | Media tile of a card whose product has no image |
|
|
428
562
|
|
|
429
563
|
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`).
|
|
430
564
|
|
package/dist/index.d.mts
CHANGED
|
@@ -147,6 +147,66 @@ interface AccessTokenResult {
|
|
|
147
147
|
}
|
|
148
148
|
type APIConfig = APIKeyConfig | AccessTokenConfig;
|
|
149
149
|
type OptionOverrides = Record<string, (query: string) => SuggestionOption[]>;
|
|
150
|
+
/**
|
|
151
|
+
* A single product card in the dropdown's product strip.
|
|
152
|
+
*
|
|
153
|
+
* Platform-agnostic on purpose: every integration (Shopify today, others
|
|
154
|
+
* later) maps its own search response onto exactly these fields, so the SDK
|
|
155
|
+
* renders one strip regardless of where the results came from. Only `id`,
|
|
156
|
+
* `title` and `url` are required — the card reflows when any of the rest is
|
|
157
|
+
* missing. Do NOT grow this shape casually; a new field is a new contract
|
|
158
|
+
* every integration has to satisfy.
|
|
159
|
+
*/
|
|
160
|
+
interface Product {
|
|
161
|
+
/** Stable identity — used as the render key, so it must be unique per result set. */
|
|
162
|
+
id: string;
|
|
163
|
+
title: string;
|
|
164
|
+
/** Destination for the card's `href`. Selection does not navigate; see `onProductSelect`. */
|
|
165
|
+
url: string;
|
|
166
|
+
/** Absent/null renders the placeholder tile — plenty of catalogues have no images. */
|
|
167
|
+
imageUrl?: string | null;
|
|
168
|
+
/** Pre-formatted by the integration, which is the side that knows the currency. */
|
|
169
|
+
price?: string;
|
|
170
|
+
vendor?: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Opt-in product-search wiring. Absent (the default) means the SDK never
|
|
174
|
+
* fetches and never renders a strip — behaviour is byte-for-byte what it was
|
|
175
|
+
* before products existed.
|
|
176
|
+
*/
|
|
177
|
+
interface ProductsConfig {
|
|
178
|
+
/**
|
|
179
|
+
* Runs the platform's product search. The integration owns the request
|
|
180
|
+
* entirely — auth headers, GraphQL body, locale prefixes, whatever the
|
|
181
|
+
* platform needs — and must honour `signal`: the SDK aborts it the moment a
|
|
182
|
+
* newer query supersedes this one.
|
|
183
|
+
*
|
|
184
|
+
* Called on the SDK's own fetch cadence (the same debounce that drives
|
|
185
|
+
* `/suggest`), never on a timer of its own, and never with an empty query.
|
|
186
|
+
*/
|
|
187
|
+
fetch: (query: string, signal: AbortSignal) => Promise<unknown>;
|
|
188
|
+
/**
|
|
189
|
+
* Maps the platform's raw payload onto `Product[]`. Kept separate from
|
|
190
|
+
* `fetch` so integrations can unit-test the mapping without a network; fold
|
|
191
|
+
* it into `fetch` and return the mapped array if you prefer.
|
|
192
|
+
*
|
|
193
|
+
* A throw here is treated exactly like a rejected fetch: the strip clears,
|
|
194
|
+
* the suggestions half is untouched.
|
|
195
|
+
*
|
|
196
|
+
* The output is trusted: `Product.url` goes straight onto the card's `href`
|
|
197
|
+
* with no sanitisation, and modifier / middle clicks are deliberately left
|
|
198
|
+
* to the browser. If the platform response is not fully under your control,
|
|
199
|
+
* validate the URL inside `transform`.
|
|
200
|
+
*
|
|
201
|
+
* One cross-framework caveat: Angular's `[href]` binding runs its own URL
|
|
202
|
+
* sanitiser, so a scheme it doesn't recognise (`myapp://…`) is rewritten to
|
|
203
|
+
* `unsafe:myapp://…` there while vanilla and React emit it verbatim. Stick
|
|
204
|
+
* to http/https if the same integration has to serve all three packages.
|
|
205
|
+
*/
|
|
206
|
+
transform: (raw: unknown) => Product[];
|
|
207
|
+
/** Optional cap the SDK applies after `transform`. */
|
|
208
|
+
limit?: number;
|
|
209
|
+
}
|
|
150
210
|
interface AutocompleteResult {
|
|
151
211
|
query: string;
|
|
152
212
|
raw_query: string;
|
|
@@ -188,6 +248,13 @@ interface CoreInputState {
|
|
|
188
248
|
snapshot: Suggestion[];
|
|
189
249
|
} | null;
|
|
190
250
|
suggestions: Suggestion[];
|
|
251
|
+
/**
|
|
252
|
+
* Results of the latest product search, already transformed and capped by
|
|
253
|
+
* `opts.products.limit`. Always empty when `opts.products` is unconfigured.
|
|
254
|
+
* Cleared on an empty query and on a failed fetch/transform, so the strip
|
|
255
|
+
* never shows results belonging to an older query.
|
|
256
|
+
*/
|
|
257
|
+
products: Product[];
|
|
191
258
|
activeDropdownIndex: number;
|
|
192
259
|
newParamId: string | null;
|
|
193
260
|
isLoading: boolean;
|
|
@@ -289,6 +356,13 @@ interface CoreOptions {
|
|
|
289
356
|
* arrow button. Clicks on the provided element bubble up and trigger submit.
|
|
290
357
|
*/
|
|
291
358
|
submitButton?: HTMLElement | null;
|
|
359
|
+
/**
|
|
360
|
+
* Opt-in product strip. When set, the dropdown renders a horizontal row of
|
|
361
|
+
* product cards below the options grid, fed by the integration's own search
|
|
362
|
+
* endpoint on the SDK's existing fetch cadence. Omit it and nothing about
|
|
363
|
+
* the dropdown changes — no request, no markup, no layout shift.
|
|
364
|
+
*/
|
|
365
|
+
products?: ProductsConfig;
|
|
292
366
|
onSubmit?: (result: AutocompleteResult) => void;
|
|
293
367
|
onError?: (error: Error) => void;
|
|
294
368
|
onChange?: (text: string) => void;
|
|
@@ -298,6 +372,13 @@ interface CoreOptions {
|
|
|
298
372
|
onFocus?: () => void;
|
|
299
373
|
/** Called when the input loses focus (or `setFocused(false)` is called). */
|
|
300
374
|
onBlur?: () => void;
|
|
375
|
+
/**
|
|
376
|
+
* Called when the user activates a product card. The SDK does NOT navigate —
|
|
377
|
+
* the integration decides what a selection means (open the PDP, add to cart,
|
|
378
|
+
* fill the input, …). Modifier / middle clicks are left to the browser so
|
|
379
|
+
* cmd-click and "copy link address" keep working, and don't fire this.
|
|
380
|
+
*/
|
|
381
|
+
onProductSelect?: (product: Product) => void;
|
|
301
382
|
value?: string;
|
|
302
383
|
completedParams?: CompletedParamState[];
|
|
303
384
|
/**
|
|
@@ -318,8 +399,9 @@ type AIAutocompleteEvents = {
|
|
|
318
399
|
stateChange: [state: CoreState];
|
|
319
400
|
focus: [];
|
|
320
401
|
blur: [];
|
|
402
|
+
productSelect: [product: Product];
|
|
321
403
|
};
|
|
322
|
-
type CoreUpdateOptions = Partial<Omit<CoreOptions, "onSubmit" | "onError" | "onChange" | "onParamsChange" | "onStateChange" | "onFocus" | "onBlur">>;
|
|
404
|
+
type CoreUpdateOptions = Partial<Omit<CoreOptions, "onSubmit" | "onError" | "onChange" | "onParamsChange" | "onStateChange" | "onFocus" | "onBlur" | "onProductSelect">>;
|
|
323
405
|
declare class AIAutocomplete {
|
|
324
406
|
private inputStore;
|
|
325
407
|
private store;
|
|
@@ -328,6 +410,7 @@ declare class AIAutocomplete {
|
|
|
328
410
|
private fetchController;
|
|
329
411
|
private keyboardController;
|
|
330
412
|
private pillsController;
|
|
413
|
+
private productsController;
|
|
331
414
|
private reEdit;
|
|
332
415
|
private modeController;
|
|
333
416
|
private container;
|
|
@@ -378,6 +461,15 @@ declare class AIAutocomplete {
|
|
|
378
461
|
handleCaretAfterInput(offset: number | null): void;
|
|
379
462
|
handleCaretMove(offset: number | null): void;
|
|
380
463
|
setActiveDropdownIndex(index: number): void;
|
|
464
|
+
/**
|
|
465
|
+
* Announce a product selection. The rendered cards call this on activation;
|
|
466
|
+
* headless consumers rendering their own strip call it themselves.
|
|
467
|
+
*
|
|
468
|
+
* Emitting is the entire behaviour — the SDK deliberately does not navigate
|
|
469
|
+
* to `product.url`, because only the integration knows whether a selection
|
|
470
|
+
* means "open the PDP", "add to cart" or "drop the title into the input".
|
|
471
|
+
*/
|
|
472
|
+
selectProduct(product: Product): void;
|
|
381
473
|
handleTextChange(value: string): void;
|
|
382
474
|
handleKeyDown(e: KeyboardEvent): void;
|
|
383
475
|
setFocused(focused: boolean): void;
|
|
@@ -603,4 +695,4 @@ declare function withSkippedParams(completed: CompletedParam[], skipped: Skipped
|
|
|
603
695
|
*/
|
|
604
696
|
declare function buildSubmitResult(text: string, completedParams: CompletedParamState[], skippedParams?: SkippedParamState[]): AutocompleteResult;
|
|
605
697
|
|
|
606
|
-
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 IdentifiedParam, type IdentifiedParamState, type InputItem, ModeController, type OptionOverrides, type RecentlySuggested, type RenderMode, SKIPPED_PARAM_TEXT, type Segment, type SkippedParamState, type Store, type Suggestion, type SuggestionOption, type TaskKind, buildAttributionUrl, buildQuery, buildSubmitResult, createStore, cursorIsAtEnd, extractPlainText, getCursorOffset, getFooterHint, plainTextLength, previousGraphemeBoundary, renderEditableContent, setCursorOffset, withSkippedParams };
|
|
698
|
+
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 IdentifiedParam, type IdentifiedParamState, type InputItem, ModeController, type OptionOverrides, type Product, type ProductsConfig, type RecentlySuggested, type RenderMode, SKIPPED_PARAM_TEXT, type Segment, type SkippedParamState, type Store, type Suggestion, type SuggestionOption, type TaskKind, buildAttributionUrl, buildQuery, buildSubmitResult, createStore, cursorIsAtEnd, extractPlainText, getCursorOffset, getFooterHint, plainTextLength, previousGraphemeBoundary, renderEditableContent, setCursorOffset, withSkippedParams };
|
package/dist/index.d.ts
CHANGED
|
@@ -147,6 +147,66 @@ interface AccessTokenResult {
|
|
|
147
147
|
}
|
|
148
148
|
type APIConfig = APIKeyConfig | AccessTokenConfig;
|
|
149
149
|
type OptionOverrides = Record<string, (query: string) => SuggestionOption[]>;
|
|
150
|
+
/**
|
|
151
|
+
* A single product card in the dropdown's product strip.
|
|
152
|
+
*
|
|
153
|
+
* Platform-agnostic on purpose: every integration (Shopify today, others
|
|
154
|
+
* later) maps its own search response onto exactly these fields, so the SDK
|
|
155
|
+
* renders one strip regardless of where the results came from. Only `id`,
|
|
156
|
+
* `title` and `url` are required — the card reflows when any of the rest is
|
|
157
|
+
* missing. Do NOT grow this shape casually; a new field is a new contract
|
|
158
|
+
* every integration has to satisfy.
|
|
159
|
+
*/
|
|
160
|
+
interface Product {
|
|
161
|
+
/** Stable identity — used as the render key, so it must be unique per result set. */
|
|
162
|
+
id: string;
|
|
163
|
+
title: string;
|
|
164
|
+
/** Destination for the card's `href`. Selection does not navigate; see `onProductSelect`. */
|
|
165
|
+
url: string;
|
|
166
|
+
/** Absent/null renders the placeholder tile — plenty of catalogues have no images. */
|
|
167
|
+
imageUrl?: string | null;
|
|
168
|
+
/** Pre-formatted by the integration, which is the side that knows the currency. */
|
|
169
|
+
price?: string;
|
|
170
|
+
vendor?: string;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Opt-in product-search wiring. Absent (the default) means the SDK never
|
|
174
|
+
* fetches and never renders a strip — behaviour is byte-for-byte what it was
|
|
175
|
+
* before products existed.
|
|
176
|
+
*/
|
|
177
|
+
interface ProductsConfig {
|
|
178
|
+
/**
|
|
179
|
+
* Runs the platform's product search. The integration owns the request
|
|
180
|
+
* entirely — auth headers, GraphQL body, locale prefixes, whatever the
|
|
181
|
+
* platform needs — and must honour `signal`: the SDK aborts it the moment a
|
|
182
|
+
* newer query supersedes this one.
|
|
183
|
+
*
|
|
184
|
+
* Called on the SDK's own fetch cadence (the same debounce that drives
|
|
185
|
+
* `/suggest`), never on a timer of its own, and never with an empty query.
|
|
186
|
+
*/
|
|
187
|
+
fetch: (query: string, signal: AbortSignal) => Promise<unknown>;
|
|
188
|
+
/**
|
|
189
|
+
* Maps the platform's raw payload onto `Product[]`. Kept separate from
|
|
190
|
+
* `fetch` so integrations can unit-test the mapping without a network; fold
|
|
191
|
+
* it into `fetch` and return the mapped array if you prefer.
|
|
192
|
+
*
|
|
193
|
+
* A throw here is treated exactly like a rejected fetch: the strip clears,
|
|
194
|
+
* the suggestions half is untouched.
|
|
195
|
+
*
|
|
196
|
+
* The output is trusted: `Product.url` goes straight onto the card's `href`
|
|
197
|
+
* with no sanitisation, and modifier / middle clicks are deliberately left
|
|
198
|
+
* to the browser. If the platform response is not fully under your control,
|
|
199
|
+
* validate the URL inside `transform`.
|
|
200
|
+
*
|
|
201
|
+
* One cross-framework caveat: Angular's `[href]` binding runs its own URL
|
|
202
|
+
* sanitiser, so a scheme it doesn't recognise (`myapp://…`) is rewritten to
|
|
203
|
+
* `unsafe:myapp://…` there while vanilla and React emit it verbatim. Stick
|
|
204
|
+
* to http/https if the same integration has to serve all three packages.
|
|
205
|
+
*/
|
|
206
|
+
transform: (raw: unknown) => Product[];
|
|
207
|
+
/** Optional cap the SDK applies after `transform`. */
|
|
208
|
+
limit?: number;
|
|
209
|
+
}
|
|
150
210
|
interface AutocompleteResult {
|
|
151
211
|
query: string;
|
|
152
212
|
raw_query: string;
|
|
@@ -188,6 +248,13 @@ interface CoreInputState {
|
|
|
188
248
|
snapshot: Suggestion[];
|
|
189
249
|
} | null;
|
|
190
250
|
suggestions: Suggestion[];
|
|
251
|
+
/**
|
|
252
|
+
* Results of the latest product search, already transformed and capped by
|
|
253
|
+
* `opts.products.limit`. Always empty when `opts.products` is unconfigured.
|
|
254
|
+
* Cleared on an empty query and on a failed fetch/transform, so the strip
|
|
255
|
+
* never shows results belonging to an older query.
|
|
256
|
+
*/
|
|
257
|
+
products: Product[];
|
|
191
258
|
activeDropdownIndex: number;
|
|
192
259
|
newParamId: string | null;
|
|
193
260
|
isLoading: boolean;
|
|
@@ -289,6 +356,13 @@ interface CoreOptions {
|
|
|
289
356
|
* arrow button. Clicks on the provided element bubble up and trigger submit.
|
|
290
357
|
*/
|
|
291
358
|
submitButton?: HTMLElement | null;
|
|
359
|
+
/**
|
|
360
|
+
* Opt-in product strip. When set, the dropdown renders a horizontal row of
|
|
361
|
+
* product cards below the options grid, fed by the integration's own search
|
|
362
|
+
* endpoint on the SDK's existing fetch cadence. Omit it and nothing about
|
|
363
|
+
* the dropdown changes — no request, no markup, no layout shift.
|
|
364
|
+
*/
|
|
365
|
+
products?: ProductsConfig;
|
|
292
366
|
onSubmit?: (result: AutocompleteResult) => void;
|
|
293
367
|
onError?: (error: Error) => void;
|
|
294
368
|
onChange?: (text: string) => void;
|
|
@@ -298,6 +372,13 @@ interface CoreOptions {
|
|
|
298
372
|
onFocus?: () => void;
|
|
299
373
|
/** Called when the input loses focus (or `setFocused(false)` is called). */
|
|
300
374
|
onBlur?: () => void;
|
|
375
|
+
/**
|
|
376
|
+
* Called when the user activates a product card. The SDK does NOT navigate —
|
|
377
|
+
* the integration decides what a selection means (open the PDP, add to cart,
|
|
378
|
+
* fill the input, …). Modifier / middle clicks are left to the browser so
|
|
379
|
+
* cmd-click and "copy link address" keep working, and don't fire this.
|
|
380
|
+
*/
|
|
381
|
+
onProductSelect?: (product: Product) => void;
|
|
301
382
|
value?: string;
|
|
302
383
|
completedParams?: CompletedParamState[];
|
|
303
384
|
/**
|
|
@@ -318,8 +399,9 @@ type AIAutocompleteEvents = {
|
|
|
318
399
|
stateChange: [state: CoreState];
|
|
319
400
|
focus: [];
|
|
320
401
|
blur: [];
|
|
402
|
+
productSelect: [product: Product];
|
|
321
403
|
};
|
|
322
|
-
type CoreUpdateOptions = Partial<Omit<CoreOptions, "onSubmit" | "onError" | "onChange" | "onParamsChange" | "onStateChange" | "onFocus" | "onBlur">>;
|
|
404
|
+
type CoreUpdateOptions = Partial<Omit<CoreOptions, "onSubmit" | "onError" | "onChange" | "onParamsChange" | "onStateChange" | "onFocus" | "onBlur" | "onProductSelect">>;
|
|
323
405
|
declare class AIAutocomplete {
|
|
324
406
|
private inputStore;
|
|
325
407
|
private store;
|
|
@@ -328,6 +410,7 @@ declare class AIAutocomplete {
|
|
|
328
410
|
private fetchController;
|
|
329
411
|
private keyboardController;
|
|
330
412
|
private pillsController;
|
|
413
|
+
private productsController;
|
|
331
414
|
private reEdit;
|
|
332
415
|
private modeController;
|
|
333
416
|
private container;
|
|
@@ -378,6 +461,15 @@ declare class AIAutocomplete {
|
|
|
378
461
|
handleCaretAfterInput(offset: number | null): void;
|
|
379
462
|
handleCaretMove(offset: number | null): void;
|
|
380
463
|
setActiveDropdownIndex(index: number): void;
|
|
464
|
+
/**
|
|
465
|
+
* Announce a product selection. The rendered cards call this on activation;
|
|
466
|
+
* headless consumers rendering their own strip call it themselves.
|
|
467
|
+
*
|
|
468
|
+
* Emitting is the entire behaviour — the SDK deliberately does not navigate
|
|
469
|
+
* to `product.url`, because only the integration knows whether a selection
|
|
470
|
+
* means "open the PDP", "add to cart" or "drop the title into the input".
|
|
471
|
+
*/
|
|
472
|
+
selectProduct(product: Product): void;
|
|
381
473
|
handleTextChange(value: string): void;
|
|
382
474
|
handleKeyDown(e: KeyboardEvent): void;
|
|
383
475
|
setFocused(focused: boolean): void;
|
|
@@ -603,4 +695,4 @@ declare function withSkippedParams(completed: CompletedParam[], skipped: Skipped
|
|
|
603
695
|
*/
|
|
604
696
|
declare function buildSubmitResult(text: string, completedParams: CompletedParamState[], skippedParams?: SkippedParamState[]): AutocompleteResult;
|
|
605
697
|
|
|
606
|
-
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 IdentifiedParam, type IdentifiedParamState, type InputItem, ModeController, type OptionOverrides, type RecentlySuggested, type RenderMode, SKIPPED_PARAM_TEXT, type Segment, type SkippedParamState, type Store, type Suggestion, type SuggestionOption, type TaskKind, buildAttributionUrl, buildQuery, buildSubmitResult, createStore, cursorIsAtEnd, extractPlainText, getCursorOffset, getFooterHint, plainTextLength, previousGraphemeBoundary, renderEditableContent, setCursorOffset, withSkippedParams };
|
|
698
|
+
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 IdentifiedParam, type IdentifiedParamState, type InputItem, ModeController, type OptionOverrides, type Product, type ProductsConfig, type RecentlySuggested, type RenderMode, SKIPPED_PARAM_TEXT, type Segment, type SkippedParamState, type Store, type Suggestion, type SuggestionOption, type TaskKind, buildAttributionUrl, buildQuery, buildSubmitResult, createStore, cursorIsAtEnd, extractPlainText, getCursorOffset, getFooterHint, plainTextLength, previousGraphemeBoundary, renderEditableContent, setCursorOffset, withSkippedParams };
|