@magicx-eng/ai-autocomplete-vanilla 0.16.1 → 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/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
  *
@@ -208,10 +243,33 @@ interface ProductsConfig {
208
243
  /** Optional cap the SDK applies after `transform`. */
209
244
  limit?: number;
210
245
  }
246
+ /**
247
+ * The structured query as the SDK currently understands it.
248
+ *
249
+ * Delivered to `onResult` after every successful suggestion round-trip and to
250
+ * `onSubmit` when the user submits. Both are built by `buildSubmitResult`, so
251
+ * a submit is just the last result the consumer already saw — plus whatever
252
+ * the user typed in between.
253
+ */
211
254
  interface AutocompleteResult {
255
+ /** Plain text as the user sees it, trimmed. */
212
256
  query: string;
257
+ /** `query` with each completed param replaced by its `{{PLACEHOLDER}}` token. */
213
258
  raw_query: string;
259
+ /**
260
+ * Params the user filled (in query order), followed by any they skipped
261
+ * (`text: "skipped"`, no placeholder). See `withSkippedParams`.
262
+ */
214
263
  completed_params: CompletedParam[];
264
+ /**
265
+ * Params the server identified in the user's own words — spans it matched to
266
+ * a parameter type without the user picking an option. Tentative: replaced
267
+ * wholesale by every response and dropped as soon as the text no longer
268
+ * matches. Never tokenized in `raw_query`.
269
+ */
270
+ identified_params: IdentifiedParam[];
271
+ /** Whether the server considers the query complete enough to act on. */
272
+ is_ready: boolean;
215
273
  }
216
274
 
217
275
  /** A calendar month on screen. `month` is 0-based, matching `Date#getMonth`. */
@@ -368,6 +426,20 @@ interface CoreInputState {
368
426
  * never shows results belonging to an older query.
369
427
  */
370
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;
371
443
  activeDropdownIndex: number;
372
444
  newParamId: string | null;
373
445
  isLoading: boolean;
@@ -393,7 +465,7 @@ interface CoreInputState {
393
465
  /** Current caret offset within the editor. Tracked via DOM `selectionchange`. */
394
466
  caretOffset: number | null;
395
467
  /**
396
- * 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
397
469
  * animation can finish before the dropdown switches to its loading skeleton.
398
470
  * Set by selectOption / ReEditManager.selectOption, cleared by a timer.
399
471
  */
@@ -467,6 +539,20 @@ interface CoreDerivedState {
467
539
  /** The month the datepicker is showing. Null unless `activeFormatType` is `"date"`. */
468
540
  dateView: DateMonthView | null;
469
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;
470
556
  isDropdownOpen: boolean;
471
557
  /**
472
558
  * Whether the active (leading) pill should render in its `selected` state
@@ -561,7 +647,22 @@ interface CoreOptions {
561
647
  * the dropdown changes — no request, no markup, no layout shift.
562
648
  */
563
649
  products?: ProductsConfig;
650
+ /** Called when the user submits (Enter, or the submit button in Tier 1). */
564
651
  onSubmit?: (result: AutocompleteResult) => void;
652
+ /**
653
+ * Called after every successful suggestion round-trip with the structured
654
+ * query as the SDK now understands it — the same `AutocompleteResult` shape
655
+ * `onSubmit` delivers, so a consumer can mirror the query as it is built
656
+ * (a live preview, a draft, analytics) without waiting for submit.
657
+ *
658
+ * Fires once per response that was applied, including the initial request
659
+ * on mount (an empty result) and after a response that promoted typed text
660
+ * into a completed param. Does not fire for a request that failed, was
661
+ * aborted, or was superseded by a newer one before it returned. Nothing
662
+ * fires for a keystroke that hasn't been sent yet — read `getResult()` for
663
+ * the current value on demand.
664
+ */
665
+ onResult?: (result: AutocompleteResult) => void;
565
666
  onError?: (error: Error) => void;
566
667
  onChange?: (text: string) => void;
567
668
  onParamsChange?: (params: CompletedParamState[]) => void;
@@ -591,6 +692,7 @@ interface CoreOptions {
591
692
 
592
693
  type AIAutocompleteEvents = {
593
694
  submit: [result: AutocompleteResult];
695
+ result: [result: AutocompleteResult];
594
696
  error: [error: Error];
595
697
  change: [text: string];
596
698
  paramsChange: [params: CompletedParamState[]];
@@ -599,7 +701,7 @@ type AIAutocompleteEvents = {
599
701
  blur: [];
600
702
  productSelect: [product: Product];
601
703
  };
602
- type CoreUpdateOptions = Partial<Omit<CoreOptions, "onSubmit" | "onError" | "onChange" | "onParamsChange" | "onStateChange" | "onFocus" | "onBlur" | "onProductSelect" | "styleRoot">>;
704
+ type CoreUpdateOptions = Partial<Omit<CoreOptions, "onSubmit" | "onResult" | "onError" | "onChange" | "onParamsChange" | "onStateChange" | "onFocus" | "onBlur" | "onProductSelect" | "styleRoot">>;
603
705
  declare class AIAutocomplete {
604
706
  private inputStore;
605
707
  private store;
@@ -609,6 +711,7 @@ declare class AIAutocomplete {
609
711
  private keyboardController;
610
712
  private pillsController;
611
713
  private productsController;
714
+ private optionSource;
612
715
  private reEdit;
613
716
  private modeController;
614
717
  private container;
@@ -617,11 +720,9 @@ declare class AIAutocomplete {
617
720
  private domRefs;
618
721
  private dropdownRefs;
619
722
  private timers;
620
- /** 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()`. */
621
724
  private boundary;
622
725
  /** Identity of the raw override record the wrapped copy below was built from. */
623
- private rawOverrides;
624
- private wrappedOverrides;
625
726
  private subscriberCount;
626
727
  private emitter;
627
728
  private sessionId;
@@ -782,6 +883,14 @@ declare class AIAutocomplete {
782
883
  */
783
884
  subscribe(listener: (state: CoreState) => void): () => void;
784
885
  getState(): CoreState;
886
+ /**
887
+ * The structured query as the SDK understands it right now — the same
888
+ * object `onResult` delivers after each response and `onSubmit` delivers
889
+ * on submit. Read it on demand from a place that has no event handy (a
890
+ * custom submit button, a "save draft" action).
891
+ */
892
+ getResult(): AutocompleteResult;
893
+ private buildResult;
785
894
  get listboxId(): string;
786
895
  get isReady(): boolean;
787
896
  /**
@@ -797,24 +906,6 @@ declare class AIAutocomplete {
797
906
  selectOption(option: SuggestionOption): void;
798
907
  private startSelectionAnimationTimer;
799
908
  private fireTelemetry;
800
- /**
801
- * `this.opts` with every `optionOverrides` entry wrapped in the instance's
802
- * {@link ConsumerBoundary}.
803
- *
804
- * The derive layer calls these functions on the SDK's stack — from
805
- * `getState()`, and from inside the store's notification drain — so an
806
- * un-wrapped throw would unwind whatever internal operation triggered the
807
- * derive and abort delivery of every queued notification with it, taking the
808
- * instance down rather than just the override. Wrapped, a failed override
809
- * answers `undefined` and each call site falls back to the server's options.
810
- *
811
- * Memoized on the raw record's identity so a swapped integration is
812
- * re-wrapped while a stable one isn't re-wrapped on every derive. Note
813
- * `update({ optionOverrides })` only becomes visible on the next store write
814
- * — the derived layer memoizes on inputs identity, and `update` doesn't
815
- * invalidate it for this key. Pre-existing, and unchanged by the wrapping.
816
- */
817
- private deriveOpts;
818
909
  private setupContainer;
819
910
  private buildAndRenderFull;
820
911
  private buildAndRenderDropdown;
@@ -1024,12 +1115,18 @@ declare function needsOptionsGridMeasurement(count: number, isMobile: boolean):
1024
1115
  *
1025
1116
  * Web, five-plus options: two columns of three visible rows — but only when
1026
1117
  * every option provably fits on one line. Rows fill row-major (even indices
1027
- * left, odd right), each column is as wide as its widest option, and the
1028
- * columns may be unequal: the template splits the width in proportion to the
1029
- * two column maxima, so whenever `left + right <= gridWidth`, each column gets
1030
- * at least what its widest row needs. When the pair doesn't fit or there are
1031
- * no usable measurements (SSR, hidden grid, loading skeletons) the layout
1032
- * 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.
1033
1130
  *
1034
1131
  * `rowWidths` are single-line pixel widths of the rendered rows (see
1035
1132
  * `measureOptionsGrid`); `gridWidth` is the grid's content width.
@@ -1189,10 +1286,17 @@ declare function renderEditableContent(args: RenderEditableArgs): void;
1189
1286
  * When the option list is taller than its scroll box, a small round button
1190
1287
  * with a down chevron sits at the bottom-centre of the list. It slides up into
1191
1288
  * view from behind the dropdown's lower edge (the footer, when the dropdown
1192
- * opens below the input) the moment there is more to scroll to, slides back
1193
- * down once the list is scrolled to its end, and scrolls the list a page on
1194
- * click. The reference (the RB2B support panel, 2026-08-19): a white 32 px
1195
- * 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.
1196
1300
  *
1197
1301
  * The DOM differs per package (vanilla builds the button here; React and
1198
1302
  * Angular render their own markup with the same classes and data attributes),
@@ -1215,6 +1319,16 @@ declare const SCROLL_ARROW_CLASS = "magicx-aia-scroll-arrow";
1215
1319
  declare const SCROLL_ARROW_ATTR = "data-aia-scroll-arrow";
1216
1320
  /** Present on the button while it is shown. The stylesheets slide it in on this. */
1217
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;
1218
1332
  /** Inline custom property: how far the button's bottom edge sits above the dropdown's padding edge. */
1219
1333
  declare const SCROLL_ARROW_BOTTOM_VAR = "--aia-scroll-arrow-bottom";
1220
1334
  declare const SCROLL_ARROW_LABEL = "Scroll down for more options";
@@ -1289,6 +1403,16 @@ declare function getFooterHint(optionHighlighted: boolean, isInputEmpty: boolean
1289
1403
  hint: string;
1290
1404
  };
1291
1405
 
1406
+ /**
1407
+ * Projects client-side identified-param state onto its wire shape.
1408
+ *
1409
+ * The one place the `{ type, value }` form is spelled out. Both the request
1410
+ * body (`api.ts`) and the `AutocompleteResult` handed to `onResult` /
1411
+ * `onSubmit` go through it, so the params the consumer receives are exactly
1412
+ * the ones the server was told about.
1413
+ */
1414
+ declare function toWireIdentifiedParams(params: IdentifiedParamState[]): IdentifiedParam[];
1415
+
1292
1416
  declare class ModeController {
1293
1417
  private container;
1294
1418
  private mode;
@@ -1342,14 +1466,25 @@ declare const SKIPPED_PARAM_TEXT = "skipped";
1342
1466
  declare function withSkippedParams(completed: CompletedParam[], skipped: SkippedParamState[]): CompletedParam[];
1343
1467
 
1344
1468
  /**
1345
- * Builds the `AutocompleteResult` handed to `onSubmit`: the placeholder-
1346
- * tokenized raw query plus the completed params, with skipped suggestions
1347
- * folded in (see {@link withSkippedParams}).
1469
+ * Fields of an {@link AutocompleteResult} that come from the latest server
1470
+ * response rather than from the input. Both default to "nothing yet".
1471
+ */
1472
+ interface SubmitResultExtras {
1473
+ /** Server-identified params (`state.identifiedParams`). Default: none. */
1474
+ identifiedParams?: IdentifiedParamState[];
1475
+ /** Server's "query is complete" verdict (`state.isReady`). Default: false. */
1476
+ isReady?: boolean;
1477
+ }
1478
+ /**
1479
+ * Builds the `AutocompleteResult` handed to `onSubmit` and `onResult`: the
1480
+ * placeholder-tokenized raw query plus the completed params, with skipped
1481
+ * suggestions folded in (see {@link withSkippedParams}), the server-identified
1482
+ * params in their wire shape, and the server's readiness verdict.
1348
1483
  *
1349
1484
  * Shared by every submit path — vanilla Enter / submit button, the React Tier 1
1350
- * component, the Angular Tier 1 component — so they can't drift on what a
1351
- * result contains.
1485
+ * component, the Angular Tier 1 component — and by the core's per-response
1486
+ * `result` event, so none of them can drift on what a result contains.
1352
1487
  */
1353
- declare function buildSubmitResult(text: string, completedParams: CompletedParamState[], skippedParams?: SkippedParamState[]): AutocompleteResult;
1488
+ declare function buildSubmitResult(text: string, completedParams: CompletedParamState[], skippedParams?: SkippedParamState[], extras?: SubmitResultExtras): AutocompleteResult;
1354
1489
 
1355
- 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 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, 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 };