@magicx-eng/ai-autocomplete-angular 0.5.8 → 0.7.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
@@ -306,6 +306,7 @@ Standalone-first apps don't need this — import the components/directive direct
306
306
  | `autoFocus` | `boolean` | `true` | Focus the input on mount. Set to `false` to leave focus to the consumer. |
307
307
  | `value` | `string` | — | Controlled text value. Pair with `(valueChange)` or use `[(value)]`. |
308
308
  | `completedParams` | `CompletedParamState[]` | — | Controlled completed params. Pair with `(completedParamsChange)` or use `[(completedParams)]`. |
309
+ | `products` | `ProductsConfig` | — | Opt-in product strip — see [Product strip](#product-strip). Bind a stable reference (a component field, not an inline object literal). Omit it and nothing about the dropdown changes. |
309
310
  | `submitButton` | `TemplateRef<unknown> \| null` | — | Custom submit button. Pass a `<ng-template>` 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. |
310
311
 
311
312
  | Output | Payload | Description |
@@ -316,6 +317,7 @@ Standalone-first apps don't need this — import the components/directive direct
316
317
  | `completedParamsChange` | `CompletedParamState[]` | Two-way binding partner for `[completedParams]`. |
317
318
  | `focused` | `void` | The input gained focus. |
318
319
  | `blurred` | `void` | The input lost focus. |
320
+ | `productSelect` | `Product` | A product card was activated. The SDK never navigates. |
319
321
 
320
322
  **Imperative methods** (via `@ViewChild`):
321
323
 
@@ -326,6 +328,112 @@ Standalone-first apps don't need this — import the components/directive direct
326
328
  | `reset()` | Clear all state, re-fetch, and start a new session (rotates `session_id`). |
327
329
  | `setMode(mode)` | Switch color mode at runtime. |
328
330
 
331
+ #### Product strip
332
+
333
+ Opt in with `[products]` and the dropdown renders a horizontal row of product
334
+ cards below the options grid. Every platform (Shopify today, others later) has
335
+ its own search endpoint and its own response shape, so the SDK owns the UI and
336
+ the integration owns only the fetching and the mapping.
337
+
338
+ ```ts
339
+ import { Component } from "@angular/core";
340
+ import type { Product, ProductsConfig } from "@magicx-eng/ai-autocomplete-angular";
341
+
342
+ @Component({
343
+ template: `<ai-autocomplete
344
+ [apiConfig]="apiConfig"
345
+ [products]="products"
346
+ (productSelect)="openProduct($event)"
347
+ (submitted)="onSubmit($event)"
348
+ ></ai-autocomplete>`,
349
+ })
350
+ export class SearchComponent {
351
+ // A component field, not an inline object literal: a new reference on each
352
+ // change-detection pass reads as a swapped integration and clears the
353
+ // current cards (the same rule `apiConfig` and `optionOverrides` follow).
354
+ products: ProductsConfig = {
355
+ // You own the request entirely — auth headers, GraphQL body, locale
356
+ // prefixes. Honour the signal: the SDK aborts it as soon as a newer query
357
+ // supersedes this one.
358
+ fetch: (query, signal) =>
359
+ fetch(`/api/products?q=${encodeURIComponent(query)}`, { signal }).then((r) => r.json()),
360
+
361
+ // Pure mapping, kept out of `fetch` so you can unit-test it without a
362
+ // network.
363
+ transform: (raw): Product[] =>
364
+ (raw as ApiResponse).items.map((item) => ({
365
+ id: item.id,
366
+ title: item.title,
367
+ url: item.url,
368
+ imageUrl: item.image?.src ?? null, // null renders the placeholder tile
369
+ price: formatMoney(item.price), // you know the currency
370
+ vendor: item.brand,
371
+ })),
372
+
373
+ limit: 8, // applied by the SDK, after transform
374
+ };
375
+
376
+ // Selection emits — the SDK never navigates.
377
+ openProduct(product: Product): void {
378
+ this.router.navigateByUrl(product.url);
379
+ }
380
+ }
381
+ ```
382
+
383
+ `Product` is exactly six fields; only `id`, `title` and `url` are required:
384
+
385
+ ```ts
386
+ type Product = {
387
+ id: string;
388
+ title: string;
389
+ url: string;
390
+ imageUrl?: string | null;
391
+ price?: string;
392
+ vendor?: string;
393
+ };
394
+ ```
395
+
396
+ **What the SDK guarantees**
397
+
398
+ - **One fetch cadence.** The product search rides the same debounce, the same
399
+ `AbortController` and the same version guard as `/suggest` — there is no
400
+ second timer to drift out of step.
401
+ - **Empty queries never reach you.** On mount and after `reset()` the strip
402
+ clears.
403
+ - **Out-of-order responses are dropped.** A slow response for an older query is
404
+ never rendered over a newer one.
405
+ - **Failure is silent.** A rejected `fetch` or a throwing `transform` clears the
406
+ strip, logs once, and leaves the suggestions half untouched — no `error`
407
+ state, no `errored` output, no effect on `isLoading$`.
408
+ - **The two halves are independent.** The panel opens if *either* has content,
409
+ so products keep it open when option filtering empties the option list.
410
+ `dropdownTrigger` (`manual` / `hidden`) still gates the panel as before.
411
+
412
+ **Selection and links.** Cards are real `<a href>` elements, so cmd/ctrl-click,
413
+ middle-click and "copy link address" behave natively. A plain left click is
414
+ intercepted (`preventDefault`) and emits `(productSelect)` instead.
415
+
416
+ `product.url` is used verbatim — the SDK trusts your `transform` output and
417
+ does not sanitise it, so validate the URL there if the platform response isn't
418
+ fully under your control. (Angular additionally runs its own `[href]`
419
+ sanitiser, so unrecognised schemes like `myapp://…` are rewritten to
420
+ `unsafe:…` in that package only.)
421
+
422
+ **Accessibility.** The strip is a `role="group"` labelled "Products"; each card
423
+ is a `role="option"` in the tab order, activated with Enter or Space, with a
424
+ visible focus ring. Tabbing into the strip does not close the panel. The row
425
+ scrolls horizontally by trackpad, wheel, touch and keyboard; the page never
426
+ scrolls sideways and the scrollbar chrome is hidden in all engines.
427
+
428
+ Swapping `[products]` for a different config (or `undefined`) clears the current
429
+ cards; the strip repopulates on the *next* `/suggest` request rather than
430
+ immediately, since the product search rides that one scheduler.
431
+
432
+ Tier 2 gets the same thing: pass `products` / `onProductSelect` to the
433
+ `AIAutocompleteController` and `<ai-autocomplete-dropdown>` renders the strip.
434
+ Tier 3 consumers subscribe to `products$` and call
435
+ `controller.selectProduct(product)` from their own cards.
436
+
329
437
  #### Custom submit button
330
438
 
331
439
  ```html
@@ -407,7 +515,7 @@ ac.destroy();
407
515
 
408
516
  #### Constructor options
409
517
 
410
- Accepts the same shape as `<ai-autocomplete>`'s inputs (`apiConfig`, `optionOverrides`, `columns`, `dropdownTrigger`, `optionsPosition`, `closeDropdownOnBlur`, `showNonTappableOptions`), plus event callbacks (`onSubmit`, `onError`, `onChange`, `onParamsChange`, `onFocus`, `onBlur`) and controlled-mode initial values (`value`, `completedParams`).
518
+ Accepts the same shape as `<ai-autocomplete>`'s inputs (`apiConfig`, `optionOverrides`, `columns`, `dropdownTrigger`, `optionsPosition`, `closeDropdownOnBlur`, `showNonTappableOptions`, `products`), plus event callbacks (`onSubmit`, `onError`, `onChange`, `onParamsChange`, `onFocus`, `onBlur`, `onProductSelect`) and controlled-mode initial values (`value`, `completedParams`).
411
519
 
412
520
  #### State observables
413
521
 
@@ -415,10 +523,12 @@ Accepts the same shape as `<ai-autocomplete>`'s inputs (`apiConfig`, `optionOver
415
523
  |---|---|---|
416
524
  | `text$` | `Observable<string>` | Editor's plain-text content. |
417
525
  | `completedParams$` | `Observable<CompletedParamState[]>` | Bold completed params currently in the editor. |
526
+ | `skippedParams$` | `Observable<SkippedParamState[]>` | Suggestions the user dismissed with <kbd>→</kbd>. Nothing renders them — pass them to `buildSubmitResult` for a hand-rolled submit. |
418
527
  | `suggestionPills$` | `Observable<Suggestion[]>` | Actionable suggestion pills (first item is the active pill). |
419
528
  | `segments$` | `Observable<Segment[]>` | Text + completed-param segments for rendering the editor body. |
420
529
  | `newParamId$` | `Observable<string \| null>` | ID of the most-recently-promoted param (drives shimmer animation). |
421
530
  | `suggestions$` | `Observable<Suggestion[]>` | All suggestions from the server (including non-actionable ones). |
531
+ | `products$` | `Observable<Product[]>` | Results of the latest product search. Always empty unless `products` is configured — see [Product strip](#product-strip). |
422
532
  | `activeIndex$` | `Observable<number>` | Currently highlighted dropdown option index. `-1` = none. |
423
533
  | `isLoading$` | `Observable<boolean>` | UI-visible loading flag (suppressed during selection animation and re-edit). |
424
534
  | `isReady$` | `Observable<boolean>` | Server-reported "query is complete" flag. |
@@ -440,6 +550,7 @@ Every observable is gated by `distinctUntilChanged` + `shareReplay({ bufferSize:
440
550
  |---|---|
441
551
  | `reset()` | Clear all state, rotate `session_id`, and re-fetch. Call after submit. |
442
552
  | `selectOption(option)` | Pick a dropdown option (or replace the re-edited param if in re-edit mode). |
553
+ | `selectProduct(product)` | Announce a product selection (fires `onProductSelect`). `<ai-autocomplete-dropdown>` calls this for you; hand-rolled strips call it themselves. Never navigates. |
443
554
  | `setActivePill(index)` | Promote the pill at `index` to active. |
444
555
  | `removeLastParam()` | Drop the last completed param's "completed" status. |
445
556
  | `clearNewParamId()` | Clear the shimmer-animation state. |
@@ -492,7 +603,7 @@ The dropdown component for Tier 2:
492
603
  | `mode` | `"light" \| "dark" \| "auto"` | — | Color mode for a **standalone** dropdown — self-scopes the SDK tokens so no `.magicx-aia` wrapper is needed. `"auto"` follows `prefers-color-scheme`. Leave unset when nested inside a `.magicx-aia` ancestor (e.g. Tier 1). |
493
604
  | `className` | `string` | — | Extra CSS class applied to the dropdown root. |
494
605
 
495
- Placement (`optionsPosition`) is read from the controller's options via `dropdown$` — set it once on the controller, not on the dropdown. All actions (option selection, hover, pill clicks) are forwarded to the controller — no outputs to wire.
606
+ Placement (`optionsPosition`) is read from the controller's options via `dropdown$` — set it once on the controller, not on the dropdown. The [product strip](#product-strip) comes from there too: configure `products` on the controller and the dropdown renders the cards. All actions (option selection, hover, pill clicks, product activation) are forwarded to the controller — no outputs to wire.
496
607
 
497
608
  ### `AutocompleteResult`
498
609
 
@@ -500,7 +611,19 @@ Placement (`optionsPosition`) is read from the controller's options via `dropdow
500
611
  |---|---|---|
501
612
  | `query` | `string` | Plain text as the user sees it. |
502
613
  | `raw_query` | `string` | Text with placeholder tokens (e.g. `"Create a {{TASK_1}}"`). |
503
- | `completed_params` | `CompletedParam[]` | Filled parameter values. |
614
+ | `completed_params` | `CompletedParam[]` | Filled parameter values, followed by any the user skipped (see below). |
615
+
616
+ #### Skipped parameters
617
+
618
+ 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"`:
619
+
620
+ ```ts
621
+ { placeholder: "", type: "goal", text: "skipped", kind: null }
622
+ ```
623
+
624
+ Tier 2 consumers who build their own submit payload get the raw skips from the controller (`skippedParams$`, or `getState().skippedParams`) and can fold them in the same way with the exported `buildSubmitResult(text, completedParams, skippedParams)`.
625
+
626
+ > **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)`.
504
627
 
505
628
  ---
506
629
 
@@ -531,6 +654,21 @@ Override on the host element (via `class`). All defaults use `:where()` (zero sp
531
654
  | `--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). |
532
655
  | `--aia-streak-rgb` | `99, 102, 241` | `255, 255, 255` | Comma-separated RGB triplet used to tint the option-selection streak animation. |
533
656
  | `--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. |
657
+ | `--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. |
658
+ | `--aia-product-gap` | `8px` | `8px` | Gap between product cards. |
659
+ | `--aia-product-bg` | `transparent` | `transparent` | Product card background. |
660
+ | `--aia-product-bg-active` | `--aia-option-bg` | `--aia-option-bg` | Product card background on hover. |
661
+ | `--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. |
662
+ | `--aia-product-placeholder-color` | `--aia-option-color` | `--aia-option-color` | Glyph color of the no-image placeholder tile. |
663
+ | `--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. |
664
+ | `--aia-product-price-color` | `--aia-option-color-selected` | `--aia-option-color-selected` | Product price text. |
665
+ | `--aia-product-vendor-color` | `--aia-option-color` | `--aia-option-color` | Product vendor line. |
666
+ | `--aia-product-focus-ring` | `--aia-option-color-selected` | `--aia-option-color-selected` | Focus ring drawn on a keyboard-focused card. |
667
+ | `--aia-products-label-color` | `--aia-option-color` | `--aia-option-color` | "Products" section label. |
668
+ | `--aia-product-title-font-size` | `12px` | `12px` | Product title font size. |
669
+ | `--aia-product-price-font-size` | `11px` | `11px` | Product price font size. |
670
+ | `--aia-product-vendor-font-size` | `10px` | `10px` | Product vendor font size. |
671
+ | `--aia-products-label-font-size` | `11px` | `11px` | Section label font size. |
534
672
  | `--aia-skeleton-bg` | `rgba(189, 189, 189, 0.25)` | `#1a1b1d` | Fill color for the loading skeleton bars and masked text in cached pills/options. |
535
673
  | `--aia-footer-chip-bg` | `--aia-surface` at 65% | `--aia-surface` at 65% | Fill behind the footer's keyboard hint and AI-Autocomplete badge. The option list scrolls under the footer, so this keeps both legible over the row passing behind them. `transparent` on the glass surface. |
536
674
 
@@ -558,7 +696,11 @@ For styling beyond the CSS variables, target these stable `data-aia-*` attribute
558
696
  | `[data-aia-pill]` | Each unfilled-suggestion pill |
559
697
  | `[data-aia-pillbar]` | Pill bar container inside the dropdown |
560
698
  | `[data-aia-option]` | Each suggestion option |
561
- | `[data-aia-dropdown]` | The dropdown root (listbox) |
699
+ | `[data-aia-dropdown]` | The dropdown root (listbox). Carries `data-aia-has-products` while the product strip has cards. |
700
+ | `[data-aia-products]` | Product strip section (label + row) |
701
+ | `[data-aia-products-row]` | The horizontally scrolling row of cards |
702
+ | `[data-aia-product]` | Each product card |
703
+ | `[data-aia-product-placeholder]` | Media tile of a card whose product has no image |
562
704
 
563
705
  Completed params render as inline `<strong>` elements inside the editor. Override their weight with `[data-aia-input] strong { font-weight: 700; }`.
564
706