@magicx-eng/ai-autocomplete-vanilla 0.17.0 → 0.18.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
@@ -15,7 +15,7 @@ A framework-agnostic vanilla JS/TypeScript library that provides a guided AI-pow
15
15
  - **Keyboard navigation** — arrow keys, enter to submit, tab to autocomplete, backspace to un-bold the last completed param
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.
18
- - **Option overrides** — inject or dynamically generate client-side options per suggestion type
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
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`
20
20
  - **Controlled & uncontrolled** — works out of the box or integrates with external state
21
21
  - **Accessible** — ARIA combobox 1.2 pattern with `role="listbox"`, `aria-activedescendant`
@@ -74,7 +74,7 @@ const ac = new AIAutocomplete(container, {
74
74
 
75
75
  // API
76
76
  apiConfig: { apiKey: "...", authScheme: "Bearer", endpoint: "https://api.ai-autocomplete.com/api/suggest" },
77
- optionOverrides: { account: (query) => [...] },
77
+ optionOverrides: { location: async (query, signal) => [...] }, // see "Option Overrides"
78
78
  columns: 2,
79
79
  maskCompletedText: false, // when true, omits completed params' text from API requests (PII masking)
80
80
  additionalContext: { tier: "gold" }, // optional user context, to personalize suggestions and options
@@ -453,6 +453,8 @@ unsub();
453
453
  | `isDropdownOpen` | `boolean` | Whether the dropdown should be visible |
454
454
  | `newParamId` | `string \| null` | ID of the most recently added param (for shimmer animation) |
455
455
  | `isLoading` | `boolean` | Fetch in progress. The Tier 1 / Tier 2 renderers gate the loading skeleton additionally on `!inSelectionAnimation` and `!editingParam`. |
456
+ | `optionQuery` | `string` | The phrase the active parameter's options are filtered by — what the user has typed for it, trimmed — or the replacement typed so far during a re-edit. This is what an [option override](#option-overrides) is asked with. |
457
+ | `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. |
456
458
  | `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. |
457
459
  | `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. |
458
460
  | `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`. |
@@ -620,7 +622,7 @@ Events: `submit`, `result`, `error`, `change`, `paramsChange`, `stateChange`, `f
620
622
 
621
623
  `result` carries an `AutocompleteResult` after every successful round-trip — see [Reading the query as it's built](#reading-the-query-as-its-built). Prefer it over `stateChange` when what you want is the structured query: `stateChange` fires on every internal update (focus, highlight, keystroke) and hands you raw state you would have to assemble yourself.
622
624
 
623
- **Errors in your callbacks are contained.** A listener that throws is logged to the console (once per event per instance) and the remaining listeners still run — the SDK's own state is unaffected, so a bug in one handler can't stall the widget. The same holds for `subscribe()` listeners and `optionOverrides` functions; an override that throws falls back to that suggestion's existing options rather than blanking the list. One exception is deliberate: if an `onSubmit` handler throws, Tier 1 skips its auto-reset so the user's typed query isn't cleared out from under a failed submit.
625
+ **Errors in your callbacks are contained.** A listener that throws is logged to the console (once per event per instance) and the remaining listeners still run — the SDK's own state is unaffected, so a bug in one handler can't stall the widget. The same holds for `subscribe()` listeners and `optionOverrides` functions; an override that throws or rejects is logged once per instance and treated as an empty answer, never as a fetch error. One exception is deliberate: if an `onSubmit` handler throws, Tier 1 skips its auto-reset so the user's typed query isn't cleared out from under a failed submit.
624
626
 
625
627
  Constructor callbacks (`onSubmit`, `onChange`, etc.) are registered once at construction as the initial listener for that event. Use `on()` for any additional or replacement listeners. **`update()` does not swap event listeners** — use `on()` for dynamic listener management.
626
628
 
@@ -806,24 +808,36 @@ const ac = new AIAutocomplete(container, {
806
808
 
807
809
  ## Option Overrides
808
810
 
811
+ Supply the options for a parameter yourself instead of taking the server's. Each entry is keyed by the suggestion `type` and is a function of the phrase the user has typed for that parameter:
812
+
809
813
  ```ts
810
814
  new AIAutocomplete(el, {
811
815
  optionOverrides: {
816
+ // A fixed list — a plain array is applied at once, no loading state.
812
817
  account: () => [
813
818
  { text: "Savings", is_tappable: true, kind: null },
814
819
  { text: "Checking", is_tappable: true, kind: null },
815
820
  ],
816
- value: (query) => {
817
- const digits = query.replace(/\D/g, "");
818
- if (!digits) return [{ text: "$100", is_tappable: true, kind: null }];
819
- return [{ text: `$${digits}`, is_tappable: true, kind: null }];
821
+ // A list that lives behind a request — return a promise. The dropdown
822
+ // shows its loading skeleton until it settles.
823
+ location: async (query, signal) => {
824
+ const res = await fetch(`/api/locations?q=${encodeURIComponent(query)}`, { signal });
825
+ const places = await res.json();
826
+ return places.map((p) => ({ text: p.name, is_tappable: true, kind: null }));
820
827
  },
821
828
  },
822
- onSubmit: handleSubmit,
823
829
  });
824
830
  ```
825
831
 
826
- ---
832
+ **When it's called.** Once the moment the parameter becomes active — a response suggests it, a skip or a selection moves it to the front, or the user taps a completed value of that type to change it — with whatever they have already typed for it (usually `""`, the request for the default list). Then again on the SDK's typing debounce with each new phrase, so you can run a search or page through a larger list. If a phrase is already covered by what you last returned, return that list again. The third argument is the `Suggestion` being answered, for the rare override that serves more than one type.
833
+
834
+ **What's shown.** Your answer, as-is: it is not filtered again by the phrase it was produced for, so a fuzzy or synonym match survives. Between two calls the previous answer is filtered locally by what the user types, so the list reacts to every keystroke. Typing the full text of an option in the answer completes the parameter, exactly as it does for a server option.
835
+
836
+ **The server's role.** The server's own options for an overridden type are never shown. While an override owns the active parameter the server is not asked for suggestions; it is asked again when the parameter is answered or skipped. Return an empty list for a typed phrase and the SDK falls back to the server for that phrase — the typed text goes out the way it does for a parameter with no matching options — and won't ask you the same phrase twice. An empty list for `""` leaves the parameter on screen with no options.
837
+
838
+ **Cancellation and errors.** Honour `signal`: it is aborted when a newer phrase supersedes the call, when the parameter stops being active, and on `destroy()`. A throw or a rejection is logged once per instance and treated as an empty answer, never as a fetch error.
839
+
840
+ Tier 3 exposes the pieces: `state.optionQuery` is the phrase, and `state.isSearchingOptions` is true while an answer is pending.
827
841
 
828
842
  ## License
829
843
 
package/dist/index.d.mts CHANGED
@@ -147,7 +147,42 @@ interface AccessTokenResult {
147
147
  expiresAt?: number;
148
148
  }
149
149
  type APIConfig = APIKeyConfig | AccessTokenConfig;
150
- type OptionOverrides = Record<string, (query: string) => SuggestionOption[]>;
150
+ /**
151
+ * Supplies the options for one suggestion type in place of the server's,
152
+ * which are never shown for an overridden type.
153
+ *
154
+ * Called
155
+ * - the moment a pill of this type becomes active (a response suggests it, a
156
+ * skip or a selection moves it to the front, a completed param of this type
157
+ * is tapped to re-edit), with whatever the user has already typed for it —
158
+ * usually `""`, the request for the default list;
159
+ * - again on the SDK's typing debounce with each new phrase, so a consumer
160
+ * whose options live behind a request can run a second search or page
161
+ * through a larger list. Return the same list early if the phrase is
162
+ * already covered by what was last returned.
163
+ *
164
+ * A plain array is applied synchronously — the right shape for a fixed or
165
+ * locally computed list. A promise shows the dropdown's loading state until it
166
+ * settles. Either way the answer is listed as-is, never filtered again by the
167
+ * phrase it was produced for, so a fuzzy or synonym match survives. Between
168
+ * two calls the previous answer is filtered locally by what the user types,
169
+ * for instant feedback.
170
+ *
171
+ * The server is not asked for suggestions while an override owns the active
172
+ * pill. It is asked again when the user answers or skips the pill, and as a
173
+ * fallback when the override returns an empty list for a non-empty phrase —
174
+ * the typed text then goes to the server the way it does for a pill with no
175
+ * matching options. An empty answer for `""` leaves the pill with no options,
176
+ * and the override is not re-asked for a phrase it has already answered empty.
177
+ *
178
+ * Honour `signal` in anything asynchronous: it is aborted when a newer phrase
179
+ * supersedes this call, when the pill stops being active, and on `destroy()`.
180
+ * A rejected promise or a throw is contained — logged once per instance and
181
+ * treated as an empty answer.
182
+ */
183
+ type OptionOverride = (query: string, signal: AbortSignal, suggestion: Suggestion) => Promise<SuggestionOption[]> | SuggestionOption[];
184
+ /** Per-suggestion-type option overrides, keyed by the suggestion's `type`. */
185
+ type OptionOverrides = Record<string, OptionOverride>;
151
186
  /**
152
187
  * A single product card in the dropdown's product strip.
153
188
  *
@@ -391,6 +426,20 @@ interface CoreInputState {
391
426
  * never shows results belonging to an older query.
392
427
  */
393
428
  products: Product[];
429
+ /**
430
+ * The consumer's {@link OptionSource} request for the pill on screen — the
431
+ * active suggestion, or the completed param being re-edited. `type` is the
432
+ * suggestion type it was asked for and `query` the phrase it was asked with;
433
+ * the answer itself is written into that suggestion's (or param's) `options`
434
+ * so every reader of those — filtering, exact-match promotion, the re-edit
435
+ * cache — sees it without knowing where it came from. Null when no source
436
+ * owns the pill on screen. Owned by `OptionSourceController`.
437
+ */
438
+ optionSearch: {
439
+ type: string;
440
+ query: string;
441
+ status: "loading" | "done";
442
+ } | null;
394
443
  activeDropdownIndex: number;
395
444
  newParamId: string | null;
396
445
  isLoading: boolean;
@@ -416,7 +465,7 @@ interface CoreInputState {
416
465
  /** Current caret offset within the editor. Tracked via DOM `selectionchange`. */
417
466
  caretOffset: number | null;
418
467
  /**
419
- * True for ~500ms after a user-initiated option selection so the press
468
+ * True for ~170ms after a user-initiated option selection so the press
420
469
  * animation can finish before the dropdown switches to its loading skeleton.
421
470
  * Set by selectOption / ReEditManager.selectOption, cleared by a timer.
422
471
  */
@@ -490,6 +539,20 @@ interface CoreDerivedState {
490
539
  /** The month the datepicker is showing. Null unless `activeFormatType` is `"date"`. */
491
540
  dateView: DateMonthView | null;
492
541
  placeholderText: string;
542
+ /**
543
+ * The phrase the active pill's options are filtered by — what the user has
544
+ * typed for it, trimmed — or, during a re-edit, the replacement typed so
545
+ * far. `""` when nothing has been typed for the pill yet. This is the query
546
+ * an {@link OptionSource} is asked with.
547
+ */
548
+ optionQuery: string;
549
+ /**
550
+ * True while the consumer's {@link OptionSource} for the pill on screen has
551
+ * been asked and hasn't answered yet. The built-in dropdowns show their
552
+ * loading skeleton for it exactly as they do for `isLoading`; a custom UI
553
+ * should treat it the same way. Never true for a pill without a source.
554
+ */
555
+ isSearchingOptions: boolean;
493
556
  isDropdownOpen: boolean;
494
557
  /**
495
558
  * Whether the active (leading) pill should render in its `selected` state
@@ -648,6 +711,7 @@ declare class AIAutocomplete {
648
711
  private keyboardController;
649
712
  private pillsController;
650
713
  private productsController;
714
+ private optionSource;
651
715
  private reEdit;
652
716
  private modeController;
653
717
  private container;
@@ -656,11 +720,9 @@ declare class AIAutocomplete {
656
720
  private domRefs;
657
721
  private dropdownRefs;
658
722
  private timers;
659
- /** One per instance — see {@link ConsumerBoundary}. Shared by the emitter, `subscribe()` and `optionOverrides`. */
723
+ /** One per instance — see {@link ConsumerBoundary}. Shared by the emitter and `subscribe()`. */
660
724
  private boundary;
661
725
  /** Identity of the raw override record the wrapped copy below was built from. */
662
- private rawOverrides;
663
- private wrappedOverrides;
664
726
  private subscriberCount;
665
727
  private emitter;
666
728
  private sessionId;
@@ -844,24 +906,6 @@ declare class AIAutocomplete {
844
906
  selectOption(option: SuggestionOption): void;
845
907
  private startSelectionAnimationTimer;
846
908
  private fireTelemetry;
847
- /**
848
- * `this.opts` with every `optionOverrides` entry wrapped in the instance's
849
- * {@link ConsumerBoundary}.
850
- *
851
- * The derive layer calls these functions on the SDK's stack — from
852
- * `getState()`, and from inside the store's notification drain — so an
853
- * un-wrapped throw would unwind whatever internal operation triggered the
854
- * derive and abort delivery of every queued notification with it, taking the
855
- * instance down rather than just the override. Wrapped, a failed override
856
- * answers `undefined` and each call site falls back to the server's options.
857
- *
858
- * Memoized on the raw record's identity so a swapped integration is
859
- * re-wrapped while a stable one isn't re-wrapped on every derive. Note
860
- * `update({ optionOverrides })` only becomes visible on the next store write
861
- * — the derived layer memoizes on inputs identity, and `update` doesn't
862
- * invalidate it for this key. Pre-existing, and unchanged by the wrapping.
863
- */
864
- private deriveOpts;
865
909
  private setupContainer;
866
910
  private buildAndRenderFull;
867
911
  private buildAndRenderDropdown;
@@ -1071,12 +1115,18 @@ declare function needsOptionsGridMeasurement(count: number, isMobile: boolean):
1071
1115
  *
1072
1116
  * Web, five-plus options: two columns of three visible rows — but only when
1073
1117
  * every option provably fits on one line. Rows fill row-major (even indices
1074
- * left, odd right), each column is as wide as its widest option, and the
1075
- * columns may be unequal: the template splits the width in proportion to the
1076
- * two column maxima, so whenever `left + right <= gridWidth`, each column gets
1077
- * at least what its widest row needs. When the pair doesn't fit or there are
1078
- * no usable measurements (SSR, hidden grid, loading skeletons) the layout
1079
- * stays one scrollable column.
1118
+ * left, odd right). The left track is exactly as wide as its widest option
1119
+ * (a fixed pixel width from the measurement) and the right track takes the
1120
+ * rest of the grid which is at least its own widest option, because
1121
+ * `left + right <= gridWidth` is the fit check. So the second column starts
1122
+ * right where the first column's longest text ends, and short options such as
1123
+ * sizes sit beside each other. (Splitting the width in proportion to the two
1124
+ * maxima, as this used to, handed ALL the slack out in that ratio too: one
1125
+ * long option in the left column and "34"-length options on the right gave
1126
+ * the left column ~85% of the box and pushed the right column to the far
1127
+ * edge, 2026-09-03.) When the pair doesn't fit — or there are no usable
1128
+ * measurements (SSR, hidden grid, loading skeletons) — the layout stays one
1129
+ * scrollable column.
1080
1130
  *
1081
1131
  * `rowWidths` are single-line pixel widths of the rendered rows (see
1082
1132
  * `measureOptionsGrid`); `gridWidth` is the grid's content width.
@@ -1236,10 +1286,17 @@ declare function renderEditableContent(args: RenderEditableArgs): void;
1236
1286
  * When the option list is taller than its scroll box, a small round button
1237
1287
  * with a down chevron sits at the bottom-centre of the list. It slides up into
1238
1288
  * view from behind the dropdown's lower edge (the footer, when the dropdown
1239
- * opens below the input) the moment there is more to scroll to, slides back
1240
- * down once the list is scrolled to its end, and scrolls the list a page on
1241
- * click. The reference (the RB2B support panel, 2026-08-19): a white 32 px
1242
- * disc that rises from behind the composer over ~150–180 ms, no fade.
1289
+ * opens below the input) the moment there is more to scroll to, dissolves in
1290
+ * place once the list is scrolled to its end, and scrolls the list a page on
1291
+ * click. The entrance follows the reference (the RB2B support panel,
1292
+ * 2026-08-19): a white 32 px disc that rises from behind the composer over
1293
+ * ~150–180 ms, no fade. The exit deliberately does not mirror it — sliding
1294
+ * back down read as the disc "falling" into the footer (2026-09-03), so
1295
+ * instead it fades out where it stands, with a slight shrink, over
1296
+ * SCROLL_ARROW_LEAVE_MS. The stylesheets key the two moves off two attributes:
1297
+ * `data-aia-visible` (rise, stay) and `data-aia-leaving` (dissolve), which
1298
+ * this controller holds for the fade's length and then clears, so the disc
1299
+ * re-parks below the edge — invisibly — ready to rise again.
1243
1300
  *
1244
1301
  * The DOM differs per package (vanilla builds the button here; React and
1245
1302
  * Angular render their own markup with the same classes and data attributes),
@@ -1262,6 +1319,16 @@ declare const SCROLL_ARROW_CLASS = "magicx-aia-scroll-arrow";
1262
1319
  declare const SCROLL_ARROW_ATTR = "data-aia-scroll-arrow";
1263
1320
  /** Present on the button while it is shown. The stylesheets slide it in on this. */
1264
1321
  declare const SCROLL_ARROW_VISIBLE_ATTR = "data-aia-visible";
1322
+ /**
1323
+ * Present on the button while it fades out. The stylesheets dissolve it in
1324
+ * place on this; the controller clears it after SCROLL_ARROW_LEAVE_MS.
1325
+ */
1326
+ declare const SCROLL_ARROW_LEAVING_ATTR = "data-aia-leaving";
1327
+ /**
1328
+ * Length of the fade-out. Mirrors the `[data-aia-leaving]` transition in each
1329
+ * package's stylesheet — change both together.
1330
+ */
1331
+ declare const SCROLL_ARROW_LEAVE_MS = 240;
1265
1332
  /** Inline custom property: how far the button's bottom edge sits above the dropdown's padding edge. */
1266
1333
  declare const SCROLL_ARROW_BOTTOM_VAR = "--aia-scroll-arrow-bottom";
1267
1334
  declare const SCROLL_ARROW_LABEL = "Scroll down for more options";
@@ -1420,4 +1487,4 @@ interface SubmitResultExtras {
1420
1487
  */
1421
1488
  declare function buildSubmitResult(text: string, completedParams: CompletedParamState[], skippedParams?: SkippedParamState[], extras?: SubmitResultExtras): AutocompleteResult;
1422
1489
 
1423
- 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 DateMonthView, 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 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, type RecentlySuggested, type RenderMode, SCROLL_ARROW_ATTR, SCROLL_ARROW_BOTTOM_VAR, SCROLL_ARROW_CLASS, SCROLL_ARROW_LABEL, 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, extractPlainText, formatDate, getCursorOffset, getFooterHint, identifiedParamLabel, isOptionsGridMobileViewport, isoDate, measureOptionsGrid, monthLabel, needsOptionsGridMeasurement, optionEnterDelayMs, optionsEntranceDurationMs, optionsGridTemplateColumns, parseDate, parseLooseDate, plainTextLength, planOptionsGrid, previousGraphemeBoundary, renderEditableContent, resolveFormatType, resolveIdentifiedDate, scrollCaretIntoView, selectedIsoFromText, setCursorOffset, toWireIdentifiedParams, withSkippedParams };
1490
+ 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 DateMonthView, 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, type RecentlySuggested, type RenderMode, SCROLL_ARROW_ATTR, SCROLL_ARROW_BOTTOM_VAR, SCROLL_ARROW_CLASS, SCROLL_ARROW_LABEL, SCROLL_ARROW_LEAVE_MS, SCROLL_ARROW_LEAVING_ATTR, 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, extractPlainText, formatDate, getCursorOffset, getFooterHint, identifiedParamLabel, isOptionsGridMobileViewport, isoDate, measureOptionsGrid, monthLabel, needsOptionsGridMeasurement, optionEnterDelayMs, optionsEntranceDurationMs, optionsGridTemplateColumns, parseDate, parseLooseDate, plainTextLength, planOptionsGrid, previousGraphemeBoundary, renderEditableContent, resolveFormatType, resolveIdentifiedDate, scrollCaretIntoView, selectedIsoFromText, setCursorOffset, toWireIdentifiedParams, withSkippedParams };
package/dist/index.d.ts CHANGED
@@ -147,7 +147,42 @@ interface AccessTokenResult {
147
147
  expiresAt?: number;
148
148
  }
149
149
  type APIConfig = APIKeyConfig | AccessTokenConfig;
150
- type OptionOverrides = Record<string, (query: string) => SuggestionOption[]>;
150
+ /**
151
+ * Supplies the options for one suggestion type in place of the server's,
152
+ * which are never shown for an overridden type.
153
+ *
154
+ * Called
155
+ * - the moment a pill of this type becomes active (a response suggests it, a
156
+ * skip or a selection moves it to the front, a completed param of this type
157
+ * is tapped to re-edit), with whatever the user has already typed for it —
158
+ * usually `""`, the request for the default list;
159
+ * - again on the SDK's typing debounce with each new phrase, so a consumer
160
+ * whose options live behind a request can run a second search or page
161
+ * through a larger list. Return the same list early if the phrase is
162
+ * already covered by what was last returned.
163
+ *
164
+ * A plain array is applied synchronously — the right shape for a fixed or
165
+ * locally computed list. A promise shows the dropdown's loading state until it
166
+ * settles. Either way the answer is listed as-is, never filtered again by the
167
+ * phrase it was produced for, so a fuzzy or synonym match survives. Between
168
+ * two calls the previous answer is filtered locally by what the user types,
169
+ * for instant feedback.
170
+ *
171
+ * The server is not asked for suggestions while an override owns the active
172
+ * pill. It is asked again when the user answers or skips the pill, and as a
173
+ * fallback when the override returns an empty list for a non-empty phrase —
174
+ * the typed text then goes to the server the way it does for a pill with no
175
+ * matching options. An empty answer for `""` leaves the pill with no options,
176
+ * and the override is not re-asked for a phrase it has already answered empty.
177
+ *
178
+ * Honour `signal` in anything asynchronous: it is aborted when a newer phrase
179
+ * supersedes this call, when the pill stops being active, and on `destroy()`.
180
+ * A rejected promise or a throw is contained — logged once per instance and
181
+ * treated as an empty answer.
182
+ */
183
+ type OptionOverride = (query: string, signal: AbortSignal, suggestion: Suggestion) => Promise<SuggestionOption[]> | SuggestionOption[];
184
+ /** Per-suggestion-type option overrides, keyed by the suggestion's `type`. */
185
+ type OptionOverrides = Record<string, OptionOverride>;
151
186
  /**
152
187
  * A single product card in the dropdown's product strip.
153
188
  *
@@ -391,6 +426,20 @@ interface CoreInputState {
391
426
  * never shows results belonging to an older query.
392
427
  */
393
428
  products: Product[];
429
+ /**
430
+ * The consumer's {@link OptionSource} request for the pill on screen — the
431
+ * active suggestion, or the completed param being re-edited. `type` is the
432
+ * suggestion type it was asked for and `query` the phrase it was asked with;
433
+ * the answer itself is written into that suggestion's (or param's) `options`
434
+ * so every reader of those — filtering, exact-match promotion, the re-edit
435
+ * cache — sees it without knowing where it came from. Null when no source
436
+ * owns the pill on screen. Owned by `OptionSourceController`.
437
+ */
438
+ optionSearch: {
439
+ type: string;
440
+ query: string;
441
+ status: "loading" | "done";
442
+ } | null;
394
443
  activeDropdownIndex: number;
395
444
  newParamId: string | null;
396
445
  isLoading: boolean;
@@ -416,7 +465,7 @@ interface CoreInputState {
416
465
  /** Current caret offset within the editor. Tracked via DOM `selectionchange`. */
417
466
  caretOffset: number | null;
418
467
  /**
419
- * True for ~500ms after a user-initiated option selection so the press
468
+ * True for ~170ms after a user-initiated option selection so the press
420
469
  * animation can finish before the dropdown switches to its loading skeleton.
421
470
  * Set by selectOption / ReEditManager.selectOption, cleared by a timer.
422
471
  */
@@ -490,6 +539,20 @@ interface CoreDerivedState {
490
539
  /** The month the datepicker is showing. Null unless `activeFormatType` is `"date"`. */
491
540
  dateView: DateMonthView | null;
492
541
  placeholderText: string;
542
+ /**
543
+ * The phrase the active pill's options are filtered by — what the user has
544
+ * typed for it, trimmed — or, during a re-edit, the replacement typed so
545
+ * far. `""` when nothing has been typed for the pill yet. This is the query
546
+ * an {@link OptionSource} is asked with.
547
+ */
548
+ optionQuery: string;
549
+ /**
550
+ * True while the consumer's {@link OptionSource} for the pill on screen has
551
+ * been asked and hasn't answered yet. The built-in dropdowns show their
552
+ * loading skeleton for it exactly as they do for `isLoading`; a custom UI
553
+ * should treat it the same way. Never true for a pill without a source.
554
+ */
555
+ isSearchingOptions: boolean;
493
556
  isDropdownOpen: boolean;
494
557
  /**
495
558
  * Whether the active (leading) pill should render in its `selected` state
@@ -648,6 +711,7 @@ declare class AIAutocomplete {
648
711
  private keyboardController;
649
712
  private pillsController;
650
713
  private productsController;
714
+ private optionSource;
651
715
  private reEdit;
652
716
  private modeController;
653
717
  private container;
@@ -656,11 +720,9 @@ declare class AIAutocomplete {
656
720
  private domRefs;
657
721
  private dropdownRefs;
658
722
  private timers;
659
- /** One per instance — see {@link ConsumerBoundary}. Shared by the emitter, `subscribe()` and `optionOverrides`. */
723
+ /** One per instance — see {@link ConsumerBoundary}. Shared by the emitter and `subscribe()`. */
660
724
  private boundary;
661
725
  /** Identity of the raw override record the wrapped copy below was built from. */
662
- private rawOverrides;
663
- private wrappedOverrides;
664
726
  private subscriberCount;
665
727
  private emitter;
666
728
  private sessionId;
@@ -844,24 +906,6 @@ declare class AIAutocomplete {
844
906
  selectOption(option: SuggestionOption): void;
845
907
  private startSelectionAnimationTimer;
846
908
  private fireTelemetry;
847
- /**
848
- * `this.opts` with every `optionOverrides` entry wrapped in the instance's
849
- * {@link ConsumerBoundary}.
850
- *
851
- * The derive layer calls these functions on the SDK's stack — from
852
- * `getState()`, and from inside the store's notification drain — so an
853
- * un-wrapped throw would unwind whatever internal operation triggered the
854
- * derive and abort delivery of every queued notification with it, taking the
855
- * instance down rather than just the override. Wrapped, a failed override
856
- * answers `undefined` and each call site falls back to the server's options.
857
- *
858
- * Memoized on the raw record's identity so a swapped integration is
859
- * re-wrapped while a stable one isn't re-wrapped on every derive. Note
860
- * `update({ optionOverrides })` only becomes visible on the next store write
861
- * — the derived layer memoizes on inputs identity, and `update` doesn't
862
- * invalidate it for this key. Pre-existing, and unchanged by the wrapping.
863
- */
864
- private deriveOpts;
865
909
  private setupContainer;
866
910
  private buildAndRenderFull;
867
911
  private buildAndRenderDropdown;
@@ -1071,12 +1115,18 @@ declare function needsOptionsGridMeasurement(count: number, isMobile: boolean):
1071
1115
  *
1072
1116
  * Web, five-plus options: two columns of three visible rows — but only when
1073
1117
  * every option provably fits on one line. Rows fill row-major (even indices
1074
- * left, odd right), each column is as wide as its widest option, and the
1075
- * columns may be unequal: the template splits the width in proportion to the
1076
- * two column maxima, so whenever `left + right <= gridWidth`, each column gets
1077
- * at least what its widest row needs. When the pair doesn't fit or there are
1078
- * no usable measurements (SSR, hidden grid, loading skeletons) the layout
1079
- * stays one scrollable column.
1118
+ * left, odd right). The left track is exactly as wide as its widest option
1119
+ * (a fixed pixel width from the measurement) and the right track takes the
1120
+ * rest of the grid which is at least its own widest option, because
1121
+ * `left + right <= gridWidth` is the fit check. So the second column starts
1122
+ * right where the first column's longest text ends, and short options such as
1123
+ * sizes sit beside each other. (Splitting the width in proportion to the two
1124
+ * maxima, as this used to, handed ALL the slack out in that ratio too: one
1125
+ * long option in the left column and "34"-length options on the right gave
1126
+ * the left column ~85% of the box and pushed the right column to the far
1127
+ * edge, 2026-09-03.) When the pair doesn't fit — or there are no usable
1128
+ * measurements (SSR, hidden grid, loading skeletons) — the layout stays one
1129
+ * scrollable column.
1080
1130
  *
1081
1131
  * `rowWidths` are single-line pixel widths of the rendered rows (see
1082
1132
  * `measureOptionsGrid`); `gridWidth` is the grid's content width.
@@ -1236,10 +1286,17 @@ declare function renderEditableContent(args: RenderEditableArgs): void;
1236
1286
  * When the option list is taller than its scroll box, a small round button
1237
1287
  * with a down chevron sits at the bottom-centre of the list. It slides up into
1238
1288
  * view from behind the dropdown's lower edge (the footer, when the dropdown
1239
- * opens below the input) the moment there is more to scroll to, slides back
1240
- * down once the list is scrolled to its end, and scrolls the list a page on
1241
- * click. The reference (the RB2B support panel, 2026-08-19): a white 32 px
1242
- * disc that rises from behind the composer over ~150–180 ms, no fade.
1289
+ * opens below the input) the moment there is more to scroll to, dissolves in
1290
+ * place once the list is scrolled to its end, and scrolls the list a page on
1291
+ * click. The entrance follows the reference (the RB2B support panel,
1292
+ * 2026-08-19): a white 32 px disc that rises from behind the composer over
1293
+ * ~150–180 ms, no fade. The exit deliberately does not mirror it — sliding
1294
+ * back down read as the disc "falling" into the footer (2026-09-03), so
1295
+ * instead it fades out where it stands, with a slight shrink, over
1296
+ * SCROLL_ARROW_LEAVE_MS. The stylesheets key the two moves off two attributes:
1297
+ * `data-aia-visible` (rise, stay) and `data-aia-leaving` (dissolve), which
1298
+ * this controller holds for the fade's length and then clears, so the disc
1299
+ * re-parks below the edge — invisibly — ready to rise again.
1243
1300
  *
1244
1301
  * The DOM differs per package (vanilla builds the button here; React and
1245
1302
  * Angular render their own markup with the same classes and data attributes),
@@ -1262,6 +1319,16 @@ declare const SCROLL_ARROW_CLASS = "magicx-aia-scroll-arrow";
1262
1319
  declare const SCROLL_ARROW_ATTR = "data-aia-scroll-arrow";
1263
1320
  /** Present on the button while it is shown. The stylesheets slide it in on this. */
1264
1321
  declare const SCROLL_ARROW_VISIBLE_ATTR = "data-aia-visible";
1322
+ /**
1323
+ * Present on the button while it fades out. The stylesheets dissolve it in
1324
+ * place on this; the controller clears it after SCROLL_ARROW_LEAVE_MS.
1325
+ */
1326
+ declare const SCROLL_ARROW_LEAVING_ATTR = "data-aia-leaving";
1327
+ /**
1328
+ * Length of the fade-out. Mirrors the `[data-aia-leaving]` transition in each
1329
+ * package's stylesheet — change both together.
1330
+ */
1331
+ declare const SCROLL_ARROW_LEAVE_MS = 240;
1265
1332
  /** Inline custom property: how far the button's bottom edge sits above the dropdown's padding edge. */
1266
1333
  declare const SCROLL_ARROW_BOTTOM_VAR = "--aia-scroll-arrow-bottom";
1267
1334
  declare const SCROLL_ARROW_LABEL = "Scroll down for more options";
@@ -1420,4 +1487,4 @@ interface SubmitResultExtras {
1420
1487
  */
1421
1488
  declare function buildSubmitResult(text: string, completedParams: CompletedParamState[], skippedParams?: SkippedParamState[], extras?: SubmitResultExtras): AutocompleteResult;
1422
1489
 
1423
- 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 DateMonthView, 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 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, type RecentlySuggested, type RenderMode, SCROLL_ARROW_ATTR, SCROLL_ARROW_BOTTOM_VAR, SCROLL_ARROW_CLASS, SCROLL_ARROW_LABEL, 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, extractPlainText, formatDate, getCursorOffset, getFooterHint, identifiedParamLabel, isOptionsGridMobileViewport, isoDate, measureOptionsGrid, monthLabel, needsOptionsGridMeasurement, optionEnterDelayMs, optionsEntranceDurationMs, optionsGridTemplateColumns, parseDate, parseLooseDate, plainTextLength, planOptionsGrid, previousGraphemeBoundary, renderEditableContent, resolveFormatType, resolveIdentifiedDate, scrollCaretIntoView, selectedIsoFromText, setCursorOffset, toWireIdentifiedParams, withSkippedParams };
1490
+ 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 DateMonthView, 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, type RecentlySuggested, type RenderMode, SCROLL_ARROW_ATTR, SCROLL_ARROW_BOTTOM_VAR, SCROLL_ARROW_CLASS, SCROLL_ARROW_LABEL, SCROLL_ARROW_LEAVE_MS, SCROLL_ARROW_LEAVING_ATTR, 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, extractPlainText, formatDate, getCursorOffset, getFooterHint, identifiedParamLabel, isOptionsGridMobileViewport, isoDate, measureOptionsGrid, monthLabel, needsOptionsGridMeasurement, optionEnterDelayMs, optionsEntranceDurationMs, optionsGridTemplateColumns, parseDate, parseLooseDate, plainTextLength, planOptionsGrid, previousGraphemeBoundary, renderEditableContent, resolveFormatType, resolveIdentifiedDate, scrollCaretIntoView, selectedIsoFromText, setCursorOffset, toWireIdentifiedParams, withSkippedParams };