@magicx-eng/ai-autocomplete-vanilla 0.28.1 → 0.29.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
@@ -109,6 +109,8 @@ const ac = new AIAutocomplete(container, {
109
109
  // How the strip lays its cards out: a scrolling row, or "list" — the same
110
110
  // cards stacked as full-width rows.
111
111
  productsLayout: "row",
112
+ // Missing/blank URL or load error: "image" (default) or the title initial.
113
+ productImageFallback: "image",
112
114
  // Custom source for the strip. Omit it and the server's items are shown.
113
115
  products: {
114
116
  fetch: (query, signal) => myPlatform.search(query, { signal }),
@@ -138,6 +140,15 @@ const ac = new AIAutocomplete(container, {
138
140
  });
139
141
  ```
140
142
 
143
+ Product display options apply to both `renderMode: "full"` and
144
+ `renderMode: "dropdown"`, at construction and through `update()`:
145
+
146
+ | Option | Type | Default | Description |
147
+ |---|---|---|---|
148
+ | `showProducts?` | `boolean` | `true` | Show product cards whenever there are products to display. |
149
+ | `productsLayout?` | `"row" \| "list"` | `"row"` | Lay out cards as a scrolling row or a vertical list. |
150
+ | `productImageFallback?` | `"image" \| "initial"` | `"image"` | Fallback for a missing or blank product image URL, or an image load error. `"image"` shows the placeholder pictogram; `"initial"` shows the title initial. A working image still takes priority. See [Product strip](#product-strip). |
151
+
141
152
  ### Methods
142
153
 
143
154
  ```ts
@@ -204,6 +215,44 @@ default); `--aia-product-list-media-size` (44px) sizes the thumbnail and
204
215
  is the horizontal strip. `update({ productsLayout })` re-lays the strip out at
205
216
  once.
206
217
 
218
+ **Product image fallback.** A product's image takes priority when its
219
+ `imageUrl` loads successfully. A missing or blank URL, or an image load error,
220
+ uses `productImageFallback`: `"image"` (the default) shows the existing
221
+ placeholder pictogram; `"initial"` shows the first character of the trimmed,
222
+ uppercased title. This is a Unicode grapheme (so a combined character or emoji
223
+ stays together), with a Unicode code point fallback in older browsers. An
224
+ empty or whitespace-only title shows `?`. The fallback works in both row and
225
+ list layouts. Initial tiles are decorative (`aria-hidden="true"`); the card's
226
+ visible title supplies its text.
227
+
228
+ Set the option at construction or change it through `update()`. Both
229
+ rendered tiers (`"full"` and `"dropdown"`) apply it to the visible cards:
230
+
231
+ ```ts
232
+ const ac = new AIAutocomplete(container, {
233
+ apiConfig: { apiKey: "..." },
234
+ productImageFallback: "initial",
235
+ });
236
+
237
+ // Switch existing fallback tiles back to the placeholder pictogram.
238
+ ac.update({ productImageFallback: "image" });
239
+ ```
240
+
241
+ Style the initial tile itself with `[data-aia-product-initial]`. For
242
+ example, wrap the widget in an element with `id="catalog-search"` and add this
243
+ rule to your stylesheet. The existing `--aia-product-media-bg` variable sets
244
+ the background; `color`, `font-size` and `border-radius` style the initial
245
+ without additional SDK options.
246
+
247
+ ```css
248
+ #catalog-search [data-aia-product-initial] {
249
+ --aia-product-media-bg: #eef2ff;
250
+ color: #4338ca;
251
+ font-size: 22px;
252
+ border-radius: 12px;
253
+ }
254
+ ```
255
+
207
256
  #### The catalog report (`customFields`)
208
257
 
209
258
  Every response that read the catalog also says what it did with the query, and
@@ -294,7 +343,7 @@ type Product = {
294
343
  id: string;
295
344
  title: string;
296
345
  url?: string; // absent = a card with no link (still activatable)
297
- imageUrl?: string | null; // null renders the placeholder tile
346
+ imageUrl?: string | null; // null uses the configured productImageFallback
298
347
  price?: string; // pre-formatted by you
299
348
  vendor?: string;
300
349
  };
@@ -489,7 +538,7 @@ input.addEventListener("blur", () => ac.setFocused(false));
489
538
 
490
539
  Pills always render inside the dropdown in this mode. The library creates the dropdown DOM inside your container — you just provide the element and wire up your input's events.
491
540
 
492
- The [product strip](#product-strip) works here too: it renders inside the dropdown the library owns, from the server's items by default, and `products` / `showProducts` / `onProductSelect` behave exactly as in Tier 1.
541
+ The [product strip](#product-strip) works here too: it renders inside the dropdown the library owns, from the server's items by default, and `products` / `showProducts` / `productImageFallback` / `onProductSelect` behave exactly as in Tier 1.
493
542
 
494
543
  > Focus/blur wiring is required when `dropdownTrigger` is `"auto"` (the default) — the dropdown only opens while the input is focused. Without `setFocused()`, the dropdown will never open.
495
544
 
@@ -549,7 +598,7 @@ unsub();
549
598
 
550
599
  > **Option icons.** An option may carry an icon: `icon_svg` is inline SVG markup and `icon` its name. The built-in dropdown draws the SVG before the option's text, in the text color, and hides it while the row shows its loading skeleton. When rendering options yourself, pass `icon_svg` through the exported `sanitizeOptionIconSvg` before inserting it — it reduces the markup to plain vector drawing and returns `null` for anything else — and use `optionLabel(option)` for the text — it is the option's `text` alone; `icon` is the icon's name and is never shown as text, even when `icon_svg` is missing or fails to draw. A picked option's `icon` / `icon_svg` are copied onto its completed parameter, and the chip in the input draws the glyph before its text. Both surfaces can be switched off: `showOptionIcons: false` renders option rows as text only and `showChipIcons: false` renders chips as text only; either way the icon element is omitted rather than hidden.
551
600
 
552
- > **Option pictures and counts.** An option may also carry `image_url` — a picture of the best-matching catalog item offering it — and `item_count`, how many items offer it (each is present only when the server includes it; when it includes counts, the options also arrive ordered by them, most items first; a starting state carries pictures but no counts). The built-in dropdown draws the picture as a square thumbnail before the option's text (in place of its `icon_svg`, so a row never leads with two visuals) and the count as a muted label at the row's trailing edge (`"12 items"`); a skeleton row keeps the tile's placeholder fill and hides the count. The unit word after the number is a token: set `--aia-option-count-unit: "sneakers"` to name what is being counted, `--aia-option-count-unit-one: "sneaker"` for a count of 1 (it falls back to the plural token), or `--aia-option-count-unit: ""` for the number alone. When rendering options yourself, pass `image_url` through the exported `optionImageSrc` before using it as an `<img src>` — it returns the URL only when it is `http(s)` and `null` otherwise — and `formatOptionCount(option.item_count)` gives the number the built-in rows print (`null` when there is no count); `optionCountLabel(count, unit?)` appends a unit for a dropdown that prints the word itself (`"item"` / `"items"` by default; one word, a `{ one, other }` pair, or `""` for none). Both surfaces can be switched off: `showOptionImages: false` renders rows without pictures (an option's icon then draws instead) and `showOptionCounts: false` hides the counts. A picked option's `image_url` is copied onto its completed parameter too, and the chip in the input draws it as a small rounded tile before its text, in place of the icon; `showChipImages: false` renders chips without pictures (the icon then draws instead). The tile is sized by `--aia-option-image-size` (40px), rounded by `--aia-option-image-radius` (12px), spaced by `--aia-option-image-gap` (12px) and filled by `--aia-option-image-bg` while its picture loads; the count reads `--aia-option-count-font-size` (12px), `--aia-option-count-color` and `--aia-option-count-opacity` (0.6); the chip's tile reads `--aia-chip-image-size` (1.25em of the chip's font), `--aia-chip-image-radius` (4px) and `--aia-chip-image-bg`.
601
+ > **Option pictures and counts.** An option may also carry `image_url` — a picture of the best-matching catalog item offering it — and `item_count`, how many items offer it (each is present only when the server includes it; when it includes counts, the options also arrive ordered by them, most items first; a starting state carries pictures but no counts). The built-in dropdown draws the picture as a square thumbnail before the option's text (in place of its `icon_svg`, so a row never leads with two visuals) and the count as a muted label at the row's trailing edge (`"12 items"`); a skeleton row keeps the tile's placeholder fill and hides the count. The unit word after the number is a token: set `--aia-option-count-unit: "sneakers"` to name what is being counted, `--aia-option-count-unit-one: "sneaker"` for a count of 1 (it falls back to the plural token), or `--aia-option-count-unit: ""` for the number alone. When rendering options yourself, pass `image_url` through the exported `optionImageSrc` before using it as an `<img src>` — it returns the URL only when it is `http(s)` and `null` otherwise — and `formatOptionCount(option.item_count)` gives the number the built-in rows print (`null` when there is no count); `optionCountLabel(count, unit?)` appends a unit for a dropdown that prints the word itself (`"item"` / `"items"` by default; one word, a `{ one, other }` pair, or `""` for none). Both surfaces can be switched off: `showOptionImages: false` renders rows without pictures (an option's icon then draws instead) and `showOptionCounts: false` hides the counts. A picked option's `image_url` is copied onto its completed parameter too, and the chip in the input draws it as a small rounded tile before its text, in place of the icon; `showChipImages: false` renders chips without pictures (the icon then draws instead). The tile is sized by `--aia-option-image-size` (40px), rounded by `--aia-option-image-radius` (12px), spaced by `--aia-option-image-gap` (12px) and filled by `--aia-option-image-bg` (default: `--aia-skeleton-bg`) while its picture loads; the count reads `--aia-option-count-font-size` (12px), `--aia-option-count-color` and `--aia-option-count-opacity` (0.6); the chip's tile reads `--aia-chip-image-size` (1.25em of the chip's font), `--aia-chip-image-radius` (4px) and `--aia-chip-image-bg` (default: `--aia-skeleton-bg`).
553
602
 
554
603
  ### State shape (`CoreState`)
555
604
 
@@ -809,10 +858,10 @@ Override these on the container element. The variables are declared with `:where
809
858
  | `--aia-product-gap` | `8px` | `8px` | Gap between product cards. |
810
859
  | `--aia-product-bg` | `transparent` | `transparent` | Product card background. |
811
860
  | `--aia-product-bg-active` | `--aia-option-bg` | `--aia-option-bg` | Product card background on hover. |
812
- | `--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. |
861
+ | `--aia-product-media-bg` | `--aia-skeleton-bg` | `--aia-skeleton-bg` | Fill behind product images and both fallback tiles (placeholder pictogram or title initial), used when an image URL is missing, blank or fails to load. |
813
862
  | `--aia-scroll-arrow-color` | `--aia-option-color` | `--aia-option-color` | Chevron color of the arrow (`--aia-scroll-arrow-color-hover` on hover). |
814
863
  | `--aia-placeholder-fade` | `120ms` | `120ms` | Fade-out of the outgoing placeholder phrase when the starting-state placeholder changes (the incoming one types itself in). |
815
- | `--aia-product-placeholder-image` | storefront pictogram | storefront pictogram | The image drawn on the placeholder tile when a product has no `imageUrl`: a two-tone storefront pictogram, inlined as a data URI. Set it to any `url(...)` to swap the pictogram; it is drawn centered and contained inside the tile, over `--aia-product-media-bg`. |
864
+ | `--aia-product-placeholder-image` | storefront pictogram | storefront pictogram | The pictogram used by `productImageFallback: "image"` (the default) when an image URL is missing, blank or fails to load. Defaults to a two-tone storefront inlined as a data URI. Set any `url(...)` to swap it; it is centered and contained over `--aia-product-media-bg`. Does not affect `"initial"` tiles. |
816
865
  | `--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. |
817
866
  | `--aia-product-price-color` | `--aia-option-color-selected` | `--aia-option-color-selected` | Product price text. |
818
867
  | `--aia-product-vendor-color` | `--aia-option-color` | `--aia-option-color` | Product vendor line. |
@@ -831,7 +880,7 @@ Override these on the container element. The variables are declared with `:where
831
880
  | `--aia-date-today-ring` | `--aia-option-color` | `--aia-option-color` | Outline drawn around today's date. |
832
881
  | `--aia-date-selected-bg` | white at 12% | white at 12% | Fill behind the date a re-edited parameter already holds. |
833
882
  | `--aia-date-range-bg` | white at 6% | white at 6% | Band across the days between the two ends of a picked date range. The ends themselves use `--aia-date-selected-bg`. |
834
- | `--aia-skeleton-bg` | `rgba(189, 189, 189, 0.25)` | `#1a1b1d` | Fill color for the loading skeleton bars and the masked text in cached pills/options. |
883
+ | `--aia-skeleton-bg` | `rgba(189, 189, 189, 0.083)` | `#1a1b1d` | Fill color for the loading skeleton bars and the masked text in cached pills/options, and of the option and chip image tiles while their picture loads (unless `--aia-option-image-bg` / `--aia-chip-image-bg` are set). |
835
884
 
836
885
  ### Per-mode Overrides
837
886
 
@@ -877,7 +926,8 @@ For styling beyond the CSS variables, target these stable `data-aia-*` attribute
877
926
  | `[data-aia-products]` | Product strip section (label + row) |
878
927
  | `[data-aia-products-row]` | The horizontally scrolling row of cards |
879
928
  | `[data-aia-product]` | Each product card |
880
- | `[data-aia-product-placeholder]` | Media tile of a card whose product has no image |
929
+ | `[data-aia-product-placeholder]` | Media tile showing the placeholder pictogram (`productImageFallback: "image"`) after a missing/blank URL or load error. The existing hook is unchanged. |
930
+ | `[data-aia-product-initial]` | The media tile itself when it shows the title initial (`productImageFallback: "initial"`). Decorative (`aria-hidden="true"`); target it to style the background, color, font size and border radius. |
881
931
 
882
932
  Completed params render as inline `<strong>` elements inside the editor. Override their weight with `[data-aia-input] strong { font-weight: 700; }` — a selector that names the editor wins without `!important`, while a bare `strong { … }` page rule is kept out by the isolation reset.
883
933
 
package/dist/index.d.mts CHANGED
@@ -358,6 +358,8 @@ interface Product {
358
358
  * (courses, listings, documents) rather than products on a shelf.
359
359
  */
360
360
  type ProductsLayout = "row" | "list";
361
+ /** What a product tile shows when its image is missing or fails to load. */
362
+ type ProductImageFallback = "image" | "initial";
361
363
  /**
362
364
  * Custom product-search wiring for the strip. Absent (the default), the strip
363
365
  * shows the items the `/suggest` response itself reports as matching the query
@@ -666,6 +668,27 @@ interface CoreInputState {
666
668
  * Set by selectOption / ReEditManager.selectOption, cleared by a timer.
667
669
  */
668
670
  inSelectionAnimation: boolean;
671
+ /**
672
+ * The options that were on screen when the user answered, held while the
673
+ * answer's request is in flight so the loading skeleton draws *those* rows —
674
+ * same count, same widths, same columns — instead of the next cached pill's.
675
+ * Display-only: the active suggestion is unaffected, so nothing can select
676
+ * from it. Null outside that window, and for a calendar answer (day cells
677
+ * have no skeleton form).
678
+ */
679
+ skeletonOptions: SuggestionOption[] | null;
680
+ /**
681
+ * The question `skeletonOptions` were answering — the dropdown's active
682
+ * pill at pick time (or the pill being re-edited). See `dropdownSuggestion`.
683
+ */
684
+ skeletonSuggestion: Suggestion | null;
685
+ /**
686
+ * Whether the answer's request has started since `skeletonOptions` was set.
687
+ * Lets the derive layer tell "the request settled" (fade the skeleton out)
688
+ * from "the request hasn't started yet" without a follow-up write — see
689
+ * `skeletonExiting`.
690
+ */
691
+ skeletonRequested: boolean;
669
692
  /**
670
693
  * Which month the datepicker is showing, when one is showing at all.
671
694
  *
@@ -790,6 +813,36 @@ interface CoreDerivedState {
790
813
  * should treat it the same way. Never true for a pill without a source.
791
814
  */
792
815
  isSearchingOptions: boolean;
816
+ /**
817
+ * Whether the dropdown draws its loading skeleton: a request in flight
818
+ * (outside re-edit and the post-answer press window), a consumer option
819
+ * source still answering, or the skeleton's fade-out. Every built-in
820
+ * dropdown reads this rather than `isLoading`.
821
+ */
822
+ dropdownLoading: boolean;
823
+ /**
824
+ * True for the short fade-out after the answer's request settles, while the
825
+ * skeleton is still drawn from `skeletonOptions` and the dropdown carries
826
+ * `data-aia-skeleton-exiting`. Derived, so it switches on in the same state
827
+ * update that ends the loading — a renderer never sees a frame between the
828
+ * two.
829
+ */
830
+ skeletonExiting: boolean;
831
+ /**
832
+ * The options the dropdown renders: `skeletonOptions` while that skeleton
833
+ * is drawn, otherwise `filteredOptions`. Rows are only interactive when
834
+ * `dropdownLoading` is false, and then this *is* `filteredOptions`.
835
+ */
836
+ dropdownOptions: SuggestionOption[];
837
+ /**
838
+ * The question the dropdown's rows belong to while it draws the mirrored
839
+ * skeleton (`skeletonSuggestion`), else null — use the active pill. Renderers
840
+ * key rows by their question, so holding it steady keeps the skeleton rows
841
+ * mounted from the pick through the fade-out; letting it follow the active
842
+ * pill remounts them as the response lands, which replays their entrance
843
+ * and flashes the old option text.
844
+ */
845
+ dropdownSuggestion: Suggestion | null;
793
846
  isDropdownOpen: boolean;
794
847
  /**
795
848
  * Whether the active (leading) pill should render in its `selected` state
@@ -910,6 +963,12 @@ interface CoreOptions {
910
963
  * the strip out at once.
911
964
  */
912
965
  productsLayout?: ProductsLayout;
966
+ /**
967
+ * Fallback for a missing or failed product image. "image" (default) uses
968
+ * the placeholder image; "initial" uses the title's first character.
969
+ * Style initial tiles with `[data-aia-product-initial]` in CSS.
970
+ */
971
+ productImageFallback?: ProductImageFallback;
913
972
  /**
914
973
  * Custom source for the product strip. Omit it and the strip shows the items
915
974
  * the `/suggest` response itself reports (`custom_fields.items`), with no
@@ -1192,6 +1251,15 @@ declare class AIAutocomplete {
1192
1251
  /** Batched render subscriber — coalesces multiple store.set calls into one DOM update. */
1193
1252
  private subscribeBatchedRender;
1194
1253
  /** Auto-clear newParamId after shimmer animation. */
1254
+ /**
1255
+ * Tracks the answer's request for the skeleton mirror. When the request
1256
+ * starts, `skeletonRequested` records it (the skeleton is already showing,
1257
+ * so the extra write changes nothing on screen). When it settles, the
1258
+ * derived `skeletonExiting` is already true in that same state — this only
1259
+ * schedules the release after SKELETON_FADE_MS, or releases at once when the
1260
+ * response beat the press window and no skeleton was ever drawn.
1261
+ */
1262
+ private subscribeSkeletonMirror;
1195
1263
  private subscribeNewParamTimer;
1196
1264
  private handleChange;
1197
1265
  /**
@@ -1570,6 +1638,19 @@ declare function measureOptionsGrid(grid: HTMLElement): {
1570
1638
  declare function optionsGridTemplateColumns(cols: number): string;
1571
1639
  /** Whether the current viewport is phone-width. `false` when there's no DOM. */
1572
1640
  declare function isOptionsGridMobileViewport(): boolean;
1641
+ /**
1642
+ * Whether two option lists draw the same rows (same texts, same order). A
1643
+ * grid that measured `measured` keeps that plan while `next` shows as a
1644
+ * loading skeleton: after an answer the skeleton mirrors the rows just on
1645
+ * screen (`CoreInputState.skeletonOptions`), so their measured columns still
1646
+ * describe it — re-planning would drop a two-column grid to the one-column
1647
+ * baseline under the user's eyes.
1648
+ */
1649
+ declare function sameOptionRows(measured: readonly {
1650
+ text: string;
1651
+ }[] | null, next: readonly {
1652
+ text: string;
1653
+ }[]): boolean;
1573
1654
 
1574
1655
  /**
1575
1656
  * Plain-text caret utilities for contentEditable elements.
@@ -2043,6 +2124,9 @@ declare class ModeController {
2043
2124
  */
2044
2125
  declare function identifiedParamLabel(type: string): string;
2045
2126
 
2127
+ /** Uppercase first character for a product tile; an empty title displays "?". */
2128
+ declare function productInitial(title: string): string;
2129
+
2046
2130
  /**
2047
2131
  * Sentinel `text` marking a `completed_params` entry the user skipped (→)
2048
2132
  * rather than filled. Sent regardless of `maskCompletedText` — it's a fixed
@@ -2093,4 +2177,4 @@ interface SubmitResultExtras {
2093
2177
  */
2094
2178
  declare function buildSubmitResult(text: string, completedParams: CompletedParamState[], skippedParams?: SkippedParamState[], extras?: SubmitResultExtras): AutocompleteResult;
2095
2179
 
2096
- 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 CustomFieldsReport, DATE_RANGE_META_END, DATE_RANGE_META_START, DEFAULT_OPTION_COUNT_UNIT, type DateMonthView, type DateRange, type DateSelection, type ExpressedFilter, type FilterOp, type FormatType, type IdentifiedParam, type IdentifiedParamState, type InputItem, type LooseDateOptions, type MatchedItem, type MatchedItems, 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, PRODUCT_PATH_PREFIX, PRODUCT_ROW_PX_PER_LINE, type Product, type ProductRowMetrics, type ProductRowWheel, type ProductsConfig, type ProductsLayout, RANGE_SEPARATOR, type RecentlySuggested, type RenderMode, SCROLL_ARROW_ATTR, SCROLL_ARROW_BOTTOM_VAR, SCROLL_ARROW_CLASS, SCROLL_ARROW_FADE_MS, SCROLL_ARROW_LABEL, SCROLL_ARROW_SCROLL_IDLE_MS, 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, attachProductRowWheel, attachScrollArrow, buildAttributionUrl, buildDateOptions, buildQuery, buildSubmitResult, cellDay, cellIso, computeOptionsGridLayout, createStore, cursorIsAtEnd, dateCellMarks, dateSelectionFor, extractPlainText, formatAbsoluteDate, formatDate, formatDateRange, formatOptionCount, getCursorOffset, getFooterHint, identifiedParamLabel, isCalendarFormat, isOptionsGridMobileViewport, isoDate, measureOptionsGrid, monthLabel, needsOptionsGridMeasurement, optionCountLabel, optionEnterDelayMs, optionHasImage, optionImageSrc, optionLabel, optionsEntranceDurationMs, optionsGridTemplateColumns, parseDate, parseLooseDate, parseLooseDateRange, plainTextLength, planOptionsGrid, previousGraphemeBoundary, productFromMatchedItem, productRowWheelTarget, productsFromCustomFields, renderEditableContent, resolveFormatType, resolveIdentifiedDate, sanitizeOptionIconSvg, scrollCaretIntoView, selectedIsoFromText, selectedRangeFor, setCursorOffset, toWireIdentifiedParams, visibleDateRange, withSkippedParams };
2180
+ 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 CustomFieldsReport, DATE_RANGE_META_END, DATE_RANGE_META_START, DEFAULT_OPTION_COUNT_UNIT, type DateMonthView, type DateRange, type DateSelection, type ExpressedFilter, type FilterOp, type FormatType, type IdentifiedParam, type IdentifiedParamState, type InputItem, type LooseDateOptions, type MatchedItem, type MatchedItems, 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, PRODUCT_PATH_PREFIX, PRODUCT_ROW_PX_PER_LINE, type Product, type ProductImageFallback, type ProductRowMetrics, type ProductRowWheel, type ProductsConfig, type ProductsLayout, RANGE_SEPARATOR, type RecentlySuggested, type RenderMode, SCROLL_ARROW_ATTR, SCROLL_ARROW_BOTTOM_VAR, SCROLL_ARROW_CLASS, SCROLL_ARROW_FADE_MS, SCROLL_ARROW_LABEL, SCROLL_ARROW_SCROLL_IDLE_MS, 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, attachProductRowWheel, attachScrollArrow, buildAttributionUrl, buildDateOptions, buildQuery, buildSubmitResult, cellDay, cellIso, computeOptionsGridLayout, createStore, cursorIsAtEnd, dateCellMarks, dateSelectionFor, extractPlainText, formatAbsoluteDate, formatDate, formatDateRange, formatOptionCount, getCursorOffset, getFooterHint, identifiedParamLabel, isCalendarFormat, isOptionsGridMobileViewport, isoDate, measureOptionsGrid, monthLabel, needsOptionsGridMeasurement, optionCountLabel, optionEnterDelayMs, optionHasImage, optionImageSrc, optionLabel, optionsEntranceDurationMs, optionsGridTemplateColumns, parseDate, parseLooseDate, parseLooseDateRange, plainTextLength, planOptionsGrid, previousGraphemeBoundary, productFromMatchedItem, productInitial, productRowWheelTarget, productsFromCustomFields, renderEditableContent, resolveFormatType, resolveIdentifiedDate, sameOptionRows, sanitizeOptionIconSvg, scrollCaretIntoView, selectedIsoFromText, selectedRangeFor, setCursorOffset, toWireIdentifiedParams, visibleDateRange, withSkippedParams };
package/dist/index.d.ts CHANGED
@@ -358,6 +358,8 @@ interface Product {
358
358
  * (courses, listings, documents) rather than products on a shelf.
359
359
  */
360
360
  type ProductsLayout = "row" | "list";
361
+ /** What a product tile shows when its image is missing or fails to load. */
362
+ type ProductImageFallback = "image" | "initial";
361
363
  /**
362
364
  * Custom product-search wiring for the strip. Absent (the default), the strip
363
365
  * shows the items the `/suggest` response itself reports as matching the query
@@ -666,6 +668,27 @@ interface CoreInputState {
666
668
  * Set by selectOption / ReEditManager.selectOption, cleared by a timer.
667
669
  */
668
670
  inSelectionAnimation: boolean;
671
+ /**
672
+ * The options that were on screen when the user answered, held while the
673
+ * answer's request is in flight so the loading skeleton draws *those* rows —
674
+ * same count, same widths, same columns — instead of the next cached pill's.
675
+ * Display-only: the active suggestion is unaffected, so nothing can select
676
+ * from it. Null outside that window, and for a calendar answer (day cells
677
+ * have no skeleton form).
678
+ */
679
+ skeletonOptions: SuggestionOption[] | null;
680
+ /**
681
+ * The question `skeletonOptions` were answering — the dropdown's active
682
+ * pill at pick time (or the pill being re-edited). See `dropdownSuggestion`.
683
+ */
684
+ skeletonSuggestion: Suggestion | null;
685
+ /**
686
+ * Whether the answer's request has started since `skeletonOptions` was set.
687
+ * Lets the derive layer tell "the request settled" (fade the skeleton out)
688
+ * from "the request hasn't started yet" without a follow-up write — see
689
+ * `skeletonExiting`.
690
+ */
691
+ skeletonRequested: boolean;
669
692
  /**
670
693
  * Which month the datepicker is showing, when one is showing at all.
671
694
  *
@@ -790,6 +813,36 @@ interface CoreDerivedState {
790
813
  * should treat it the same way. Never true for a pill without a source.
791
814
  */
792
815
  isSearchingOptions: boolean;
816
+ /**
817
+ * Whether the dropdown draws its loading skeleton: a request in flight
818
+ * (outside re-edit and the post-answer press window), a consumer option
819
+ * source still answering, or the skeleton's fade-out. Every built-in
820
+ * dropdown reads this rather than `isLoading`.
821
+ */
822
+ dropdownLoading: boolean;
823
+ /**
824
+ * True for the short fade-out after the answer's request settles, while the
825
+ * skeleton is still drawn from `skeletonOptions` and the dropdown carries
826
+ * `data-aia-skeleton-exiting`. Derived, so it switches on in the same state
827
+ * update that ends the loading — a renderer never sees a frame between the
828
+ * two.
829
+ */
830
+ skeletonExiting: boolean;
831
+ /**
832
+ * The options the dropdown renders: `skeletonOptions` while that skeleton
833
+ * is drawn, otherwise `filteredOptions`. Rows are only interactive when
834
+ * `dropdownLoading` is false, and then this *is* `filteredOptions`.
835
+ */
836
+ dropdownOptions: SuggestionOption[];
837
+ /**
838
+ * The question the dropdown's rows belong to while it draws the mirrored
839
+ * skeleton (`skeletonSuggestion`), else null — use the active pill. Renderers
840
+ * key rows by their question, so holding it steady keeps the skeleton rows
841
+ * mounted from the pick through the fade-out; letting it follow the active
842
+ * pill remounts them as the response lands, which replays their entrance
843
+ * and flashes the old option text.
844
+ */
845
+ dropdownSuggestion: Suggestion | null;
793
846
  isDropdownOpen: boolean;
794
847
  /**
795
848
  * Whether the active (leading) pill should render in its `selected` state
@@ -910,6 +963,12 @@ interface CoreOptions {
910
963
  * the strip out at once.
911
964
  */
912
965
  productsLayout?: ProductsLayout;
966
+ /**
967
+ * Fallback for a missing or failed product image. "image" (default) uses
968
+ * the placeholder image; "initial" uses the title's first character.
969
+ * Style initial tiles with `[data-aia-product-initial]` in CSS.
970
+ */
971
+ productImageFallback?: ProductImageFallback;
913
972
  /**
914
973
  * Custom source for the product strip. Omit it and the strip shows the items
915
974
  * the `/suggest` response itself reports (`custom_fields.items`), with no
@@ -1192,6 +1251,15 @@ declare class AIAutocomplete {
1192
1251
  /** Batched render subscriber — coalesces multiple store.set calls into one DOM update. */
1193
1252
  private subscribeBatchedRender;
1194
1253
  /** Auto-clear newParamId after shimmer animation. */
1254
+ /**
1255
+ * Tracks the answer's request for the skeleton mirror. When the request
1256
+ * starts, `skeletonRequested` records it (the skeleton is already showing,
1257
+ * so the extra write changes nothing on screen). When it settles, the
1258
+ * derived `skeletonExiting` is already true in that same state — this only
1259
+ * schedules the release after SKELETON_FADE_MS, or releases at once when the
1260
+ * response beat the press window and no skeleton was ever drawn.
1261
+ */
1262
+ private subscribeSkeletonMirror;
1195
1263
  private subscribeNewParamTimer;
1196
1264
  private handleChange;
1197
1265
  /**
@@ -1570,6 +1638,19 @@ declare function measureOptionsGrid(grid: HTMLElement): {
1570
1638
  declare function optionsGridTemplateColumns(cols: number): string;
1571
1639
  /** Whether the current viewport is phone-width. `false` when there's no DOM. */
1572
1640
  declare function isOptionsGridMobileViewport(): boolean;
1641
+ /**
1642
+ * Whether two option lists draw the same rows (same texts, same order). A
1643
+ * grid that measured `measured` keeps that plan while `next` shows as a
1644
+ * loading skeleton: after an answer the skeleton mirrors the rows just on
1645
+ * screen (`CoreInputState.skeletonOptions`), so their measured columns still
1646
+ * describe it — re-planning would drop a two-column grid to the one-column
1647
+ * baseline under the user's eyes.
1648
+ */
1649
+ declare function sameOptionRows(measured: readonly {
1650
+ text: string;
1651
+ }[] | null, next: readonly {
1652
+ text: string;
1653
+ }[]): boolean;
1573
1654
 
1574
1655
  /**
1575
1656
  * Plain-text caret utilities for contentEditable elements.
@@ -2043,6 +2124,9 @@ declare class ModeController {
2043
2124
  */
2044
2125
  declare function identifiedParamLabel(type: string): string;
2045
2126
 
2127
+ /** Uppercase first character for a product tile; an empty title displays "?". */
2128
+ declare function productInitial(title: string): string;
2129
+
2046
2130
  /**
2047
2131
  * Sentinel `text` marking a `completed_params` entry the user skipped (→)
2048
2132
  * rather than filled. Sent regardless of `maskCompletedText` — it's a fixed
@@ -2093,4 +2177,4 @@ interface SubmitResultExtras {
2093
2177
  */
2094
2178
  declare function buildSubmitResult(text: string, completedParams: CompletedParamState[], skippedParams?: SkippedParamState[], extras?: SubmitResultExtras): AutocompleteResult;
2095
2179
 
2096
- 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 CustomFieldsReport, DATE_RANGE_META_END, DATE_RANGE_META_START, DEFAULT_OPTION_COUNT_UNIT, type DateMonthView, type DateRange, type DateSelection, type ExpressedFilter, type FilterOp, type FormatType, type IdentifiedParam, type IdentifiedParamState, type InputItem, type LooseDateOptions, type MatchedItem, type MatchedItems, 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, PRODUCT_PATH_PREFIX, PRODUCT_ROW_PX_PER_LINE, type Product, type ProductRowMetrics, type ProductRowWheel, type ProductsConfig, type ProductsLayout, RANGE_SEPARATOR, type RecentlySuggested, type RenderMode, SCROLL_ARROW_ATTR, SCROLL_ARROW_BOTTOM_VAR, SCROLL_ARROW_CLASS, SCROLL_ARROW_FADE_MS, SCROLL_ARROW_LABEL, SCROLL_ARROW_SCROLL_IDLE_MS, 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, attachProductRowWheel, attachScrollArrow, buildAttributionUrl, buildDateOptions, buildQuery, buildSubmitResult, cellDay, cellIso, computeOptionsGridLayout, createStore, cursorIsAtEnd, dateCellMarks, dateSelectionFor, extractPlainText, formatAbsoluteDate, formatDate, formatDateRange, formatOptionCount, getCursorOffset, getFooterHint, identifiedParamLabel, isCalendarFormat, isOptionsGridMobileViewport, isoDate, measureOptionsGrid, monthLabel, needsOptionsGridMeasurement, optionCountLabel, optionEnterDelayMs, optionHasImage, optionImageSrc, optionLabel, optionsEntranceDurationMs, optionsGridTemplateColumns, parseDate, parseLooseDate, parseLooseDateRange, plainTextLength, planOptionsGrid, previousGraphemeBoundary, productFromMatchedItem, productRowWheelTarget, productsFromCustomFields, renderEditableContent, resolveFormatType, resolveIdentifiedDate, sanitizeOptionIconSvg, scrollCaretIntoView, selectedIsoFromText, selectedRangeFor, setCursorOffset, toWireIdentifiedParams, visibleDateRange, withSkippedParams };
2180
+ 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 CustomFieldsReport, DATE_RANGE_META_END, DATE_RANGE_META_START, DEFAULT_OPTION_COUNT_UNIT, type DateMonthView, type DateRange, type DateSelection, type ExpressedFilter, type FilterOp, type FormatType, type IdentifiedParam, type IdentifiedParamState, type InputItem, type LooseDateOptions, type MatchedItem, type MatchedItems, 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, PRODUCT_PATH_PREFIX, PRODUCT_ROW_PX_PER_LINE, type Product, type ProductImageFallback, type ProductRowMetrics, type ProductRowWheel, type ProductsConfig, type ProductsLayout, RANGE_SEPARATOR, type RecentlySuggested, type RenderMode, SCROLL_ARROW_ATTR, SCROLL_ARROW_BOTTOM_VAR, SCROLL_ARROW_CLASS, SCROLL_ARROW_FADE_MS, SCROLL_ARROW_LABEL, SCROLL_ARROW_SCROLL_IDLE_MS, 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, attachProductRowWheel, attachScrollArrow, buildAttributionUrl, buildDateOptions, buildQuery, buildSubmitResult, cellDay, cellIso, computeOptionsGridLayout, createStore, cursorIsAtEnd, dateCellMarks, dateSelectionFor, extractPlainText, formatAbsoluteDate, formatDate, formatDateRange, formatOptionCount, getCursorOffset, getFooterHint, identifiedParamLabel, isCalendarFormat, isOptionsGridMobileViewport, isoDate, measureOptionsGrid, monthLabel, needsOptionsGridMeasurement, optionCountLabel, optionEnterDelayMs, optionHasImage, optionImageSrc, optionLabel, optionsEntranceDurationMs, optionsGridTemplateColumns, parseDate, parseLooseDate, parseLooseDateRange, plainTextLength, planOptionsGrid, previousGraphemeBoundary, productFromMatchedItem, productInitial, productRowWheelTarget, productsFromCustomFields, renderEditableContent, resolveFormatType, resolveIdentifiedDate, sameOptionRows, sanitizeOptionIconSvg, scrollCaretIntoView, selectedIsoFromText, selectedRangeFor, setCursorOffset, toWireIdentifiedParams, visibleDateRange, withSkippedParams };