@magicx-eng/ai-autocomplete-vanilla 0.7.0 → 0.9.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 +139 -2
- package/dist/index.d.mts +169 -8
- package/dist/index.d.ts +169 -8
- 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/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;
|
|
@@ -336,6 +419,12 @@ declare class AIAutocomplete {
|
|
|
336
419
|
private domRefs;
|
|
337
420
|
private dropdownRefs;
|
|
338
421
|
private timers;
|
|
422
|
+
/** One per instance — see {@link ConsumerBoundary}. Shared by the emitter, `subscribe()` and `optionOverrides`. */
|
|
423
|
+
private boundary;
|
|
424
|
+
/** Identity of the raw override record the wrapped copy below was built from. */
|
|
425
|
+
private rawOverrides;
|
|
426
|
+
private wrappedOverrides;
|
|
427
|
+
private subscriberCount;
|
|
339
428
|
private emitter;
|
|
340
429
|
private sessionId;
|
|
341
430
|
private readonly emitSubmit;
|
|
@@ -351,15 +440,54 @@ declare class AIAutocomplete {
|
|
|
351
440
|
setActivePill(index: number): void;
|
|
352
441
|
removeLastParam(): void;
|
|
353
442
|
/**
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
443
|
+
* Locate the chip whose rendered text covers `offset`.
|
|
444
|
+
*
|
|
445
|
+
* Walks the derived `segments` — the exact thing the editor renders — rather
|
|
446
|
+
* than re-deriving param positions here. That keeps chip hit-testing in step
|
|
447
|
+
* with `deriveSegments` by construction (including params whose text repeats
|
|
448
|
+
* earlier in the input, and identified params that had to dodge completed
|
|
449
|
+
* coverage) instead of hand-mirroring its walk in a third place.
|
|
450
|
+
*
|
|
451
|
+
* Both chip kinds are returned: completed and identified render as visually
|
|
452
|
+
* identical chips, so both must behave atomically under Backspace. Re-edit
|
|
453
|
+
* remains completed-only — see `startEditingParamAtCaret`.
|
|
454
|
+
*/
|
|
455
|
+
private findChipSpanAt;
|
|
456
|
+
/** Drop a located chip from whichever param array owns it. */
|
|
457
|
+
private withoutChip;
|
|
458
|
+
/**
|
|
459
|
+
* Backspace at the caret. A chip is atomic: the caret sits beside it, never
|
|
460
|
+
* within it, so the two positions mean different things.
|
|
461
|
+
*
|
|
462
|
+
* - Caret exactly at the chip's trailing edge (the position a Backspace over
|
|
463
|
+
* the following space leaves you in): delete the WHOLE chip, the way a
|
|
464
|
+
* chip-style token behaves. Collapses the space seam it leaves behind so
|
|
465
|
+
* the surrounding words don't end up double-spaced.
|
|
466
|
+
* - Caret strictly inside the chip (only reachable by clicking into it):
|
|
467
|
+
* drop the param so its text renders plain, and remove one grapheme — the
|
|
468
|
+
* user keeps the phrase they had and can edit it by hand.
|
|
469
|
+
*
|
|
470
|
+
* Applies to both chip kinds. Completed and identified params render
|
|
471
|
+
* identically, so they must delete identically; only the array the param is
|
|
472
|
+
* dropped from differs.
|
|
359
473
|
*
|
|
360
474
|
* Returns true when a param was reconciled (caller should `preventDefault`).
|
|
361
475
|
*/
|
|
362
476
|
removeParamAtCaret(offset: number): boolean;
|
|
477
|
+
/**
|
|
478
|
+
* ArrowLeft while the caret sits at a completed pill's trailing edge selects
|
|
479
|
+
* the pill rather than moving the caret into it: re-edit turns on, the pill
|
|
480
|
+
* renders highlighted, and the dropdown shows its cached options. The caret
|
|
481
|
+
* stays put at the trailing edge — it never enters the pill.
|
|
482
|
+
*
|
|
483
|
+
* Identified chips are excluded: they carry no cached options and are
|
|
484
|
+
* deliberately not re-editable (see `renderEditable`), even though Backspace
|
|
485
|
+
* treats them atomically like any other chip.
|
|
486
|
+
*
|
|
487
|
+
* Returns true when re-edit started (caller should `preventDefault` so the
|
|
488
|
+
* browser doesn't step the caret inside the `<strong>`).
|
|
489
|
+
*/
|
|
490
|
+
startEditingParamAtCaret(offset: number): boolean;
|
|
363
491
|
/**
|
|
364
492
|
* Set the editor caret at the given plain-text offset. Uses the core's own
|
|
365
493
|
* `domRefs.input` in "full" mode; falls back to the wrapper-provided
|
|
@@ -378,10 +506,25 @@ declare class AIAutocomplete {
|
|
|
378
506
|
handleCaretAfterInput(offset: number | null): void;
|
|
379
507
|
handleCaretMove(offset: number | null): void;
|
|
380
508
|
setActiveDropdownIndex(index: number): void;
|
|
509
|
+
/**
|
|
510
|
+
* Announce a product selection. The rendered cards call this on activation;
|
|
511
|
+
* headless consumers rendering their own strip call it themselves.
|
|
512
|
+
*
|
|
513
|
+
* Emitting is the entire behaviour — the SDK deliberately does not navigate
|
|
514
|
+
* to `product.url`, because only the integration knows whether a selection
|
|
515
|
+
* means "open the PDP", "add to cart" or "drop the title into the input".
|
|
516
|
+
*/
|
|
517
|
+
selectProduct(product: Product): void;
|
|
381
518
|
handleTextChange(value: string): void;
|
|
382
519
|
handleKeyDown(e: KeyboardEvent): void;
|
|
383
520
|
setFocused(focused: boolean): void;
|
|
384
|
-
/**
|
|
521
|
+
/**
|
|
522
|
+
* Subscribe to state changes. Listener receives the full (input + derived) shape.
|
|
523
|
+
*
|
|
524
|
+
* Consumer code, so it gets the same isolation as the `on*` events: a throw
|
|
525
|
+
* is contained and reported rather than unwinding into the `store.set` that
|
|
526
|
+
* triggered the notification. See {@link ConsumerBoundary}.
|
|
527
|
+
*/
|
|
385
528
|
subscribe(listener: (state: CoreState) => void): () => void;
|
|
386
529
|
getState(): CoreState;
|
|
387
530
|
get listboxId(): string;
|
|
@@ -399,6 +542,24 @@ declare class AIAutocomplete {
|
|
|
399
542
|
selectOption(option: SuggestionOption): void;
|
|
400
543
|
private startSelectionAnimationTimer;
|
|
401
544
|
private fireTelemetry;
|
|
545
|
+
/**
|
|
546
|
+
* `this.opts` with every `optionOverrides` entry wrapped in the instance's
|
|
547
|
+
* {@link ConsumerBoundary}.
|
|
548
|
+
*
|
|
549
|
+
* The derive layer calls these functions on the SDK's stack — from
|
|
550
|
+
* `getState()`, and from inside the store's notification drain — so an
|
|
551
|
+
* un-wrapped throw would unwind whatever internal operation triggered the
|
|
552
|
+
* derive and abort delivery of every queued notification with it, taking the
|
|
553
|
+
* instance down rather than just the override. Wrapped, a failed override
|
|
554
|
+
* answers `undefined` and each call site falls back to the server's options.
|
|
555
|
+
*
|
|
556
|
+
* Memoized on the raw record's identity so a swapped integration is
|
|
557
|
+
* re-wrapped while a stable one isn't re-wrapped on every derive. Note
|
|
558
|
+
* `update({ optionOverrides })` only becomes visible on the next store write
|
|
559
|
+
* — the derived layer memoizes on inputs identity, and `update` doesn't
|
|
560
|
+
* invalidate it for this key. Pre-existing, and unchanged by the wrapping.
|
|
561
|
+
*/
|
|
562
|
+
private deriveOpts;
|
|
402
563
|
private setupContainer;
|
|
403
564
|
private buildAndRenderFull;
|
|
404
565
|
private buildAndRenderDropdown;
|
|
@@ -603,4 +764,4 @@ declare function withSkippedParams(completed: CompletedParam[], skipped: Skipped
|
|
|
603
764
|
*/
|
|
604
765
|
declare function buildSubmitResult(text: string, completedParams: CompletedParamState[], skippedParams?: SkippedParamState[]): AutocompleteResult;
|
|
605
766
|
|
|
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 };
|
|
767
|
+
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 };
|