@luigisbox/luma 0.0.0-stage → 0.2.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.
Files changed (145) hide show
  1. package/LICENSE.md +90 -0
  2. package/README.md +615 -2
  3. package/a11y/audit.mjs +735 -0
  4. package/a11y/axe.mjs +73 -0
  5. package/a11y/report.mjs +347 -0
  6. package/a11y/run.mjs +256 -0
  7. package/a11y/wcag.mjs +233 -0
  8. package/bin/config.mjs +60 -0
  9. package/bin/luma.mjs +99 -0
  10. package/bin/paths.mjs +11 -0
  11. package/bin/tools.mjs +43 -0
  12. package/build/baseline.d.mts +8 -0
  13. package/build/baseline.mjs +167 -0
  14. package/build/tooling.mjs +102 -0
  15. package/build/vite.mjs +507 -0
  16. package/chrome/block.mjs +8 -0
  17. package/chrome/run.mjs +150 -0
  18. package/docs/ACCESSIBILITY.md +501 -0
  19. package/docs/INTEGRATION-PERFORMANCE.md +213 -0
  20. package/docs/PARITY.md +946 -0
  21. package/footprint/model.mjs +184 -0
  22. package/footprint/run.mjs +331 -0
  23. package/package.json +58 -4
  24. package/parity/alignment.mjs +109 -0
  25. package/parity/archive.mjs +223 -0
  26. package/parity/args.mjs +137 -0
  27. package/parity/behaviour.mjs +379 -0
  28. package/parity/compare.mjs +985 -0
  29. package/parity/engine.mjs +848 -0
  30. package/parity/hints.mjs +132 -0
  31. package/parity/hold.mjs +54 -0
  32. package/parity/init.mjs +703 -0
  33. package/parity/map.mjs +640 -0
  34. package/parity/owner.mjs +170 -0
  35. package/parity/pairing.mjs +110 -0
  36. package/parity/pixels.mjs +33 -0
  37. package/parity/probe.mjs +1239 -0
  38. package/parity/report.mjs +236 -0
  39. package/parity/run.mjs +267 -0
  40. package/snippet/early-hold.js +64 -0
  41. package/snippet/early-hold.min.js +1 -0
  42. package/src/analytics/chain.ts +137 -0
  43. package/src/analytics/collect.ts +64 -0
  44. package/src/analytics/datalayer.ts +67 -0
  45. package/src/analytics/from-ga4.ts +68 -0
  46. package/src/analytics/from-luma.ts +21 -0
  47. package/src/analytics/index.ts +24 -0
  48. package/src/analytics/outbox.ts +118 -0
  49. package/src/analytics/page.ts +46 -0
  50. package/src/analytics/pageview.ts +82 -0
  51. package/src/analytics/ref.ts +19 -0
  52. package/src/analytics/setup.ts +41 -0
  53. package/src/analytics/start.ts +53 -0
  54. package/src/analytics/triggers.ts +239 -0
  55. package/src/analytics/user.ts +12 -0
  56. package/src/analytics/wire.ts +67 -0
  57. package/src/api/ai.ts +98 -0
  58. package/src/api/index.ts +87 -0
  59. package/src/api/token-key.ts +42 -0
  60. package/src/api/token.ts +312 -0
  61. package/src/core/a11y-keys.ts +60 -0
  62. package/src/core/a11y.ts +160 -0
  63. package/src/core/boot-state.ts +6 -0
  64. package/src/core/color.ts +42 -0
  65. package/src/core/context.ts +108 -0
  66. package/src/core/early-rules.ts +96 -0
  67. package/src/core/entries.ts +25 -0
  68. package/src/core/fold.ts +7 -0
  69. package/src/core/health.ts +26 -0
  70. package/src/core/hold.ts +74 -0
  71. package/src/core/i18n.ts +66 -0
  72. package/src/core/image-load.ts +102 -0
  73. package/src/core/image-tags.ts +26 -0
  74. package/src/core/images.ts +170 -0
  75. package/src/core/integration.ts +126 -0
  76. package/src/core/kept.ts +25 -0
  77. package/src/core/listings.ts +100 -0
  78. package/src/core/locales.ts +14 -0
  79. package/src/core/own.ts +49 -0
  80. package/src/core/page.ts +14 -0
  81. package/src/core/prefetch.ts +88 -0
  82. package/src/core/recommenders.ts +166 -0
  83. package/src/core/reconciler.ts +397 -0
  84. package/src/core/result.ts +73 -0
  85. package/src/core/routes.ts +115 -0
  86. package/src/core/scheduler.ts +83 -0
  87. package/src/core/sense.ts +107 -0
  88. package/src/core/speculation.ts +59 -0
  89. package/src/core/stash.ts +38 -0
  90. package/src/core/storage.ts +45 -0
  91. package/src/core/styles.ts +106 -0
  92. package/src/core/support.ts +12 -0
  93. package/src/core/takeover.ts +220 -0
  94. package/src/core/templates.ts +74 -0
  95. package/src/core/tiles.ts +137 -0
  96. package/src/core/turbo.ts +64 -0
  97. package/src/core/url.ts +164 -0
  98. package/src/core/widget-inits.ts +40 -0
  99. package/src/env.d.ts +3 -0
  100. package/src/lit.ts +11 -0
  101. package/src/locales/cs.json +94 -0
  102. package/src/locales/de.json +88 -0
  103. package/src/locales/en.json +88 -0
  104. package/src/main.ts +493 -0
  105. package/src/types.ts +778 -0
  106. package/src/widgets/autocomplete/index.ts +1387 -0
  107. package/src/widgets/autocomplete/style-autocomplete.css +266 -0
  108. package/src/widgets/autocomplete/template-autocomplete-box.ts +61 -0
  109. package/src/widgets/autocomplete/template-autocomplete-empty.ts +53 -0
  110. package/src/widgets/autocomplete/template-autocomplete-result-default.ts +82 -0
  111. package/src/widgets/autocomplete/template-autocomplete-sheet.ts +74 -0
  112. package/src/widgets/listing/element.ts +1772 -0
  113. package/src/widgets/listing/flow.ts +30 -0
  114. package/src/widgets/listing/instant.ts +133 -0
  115. package/src/widgets/listing/intent.ts +193 -0
  116. package/src/widgets/listing/query.ts +298 -0
  117. package/src/widgets/listing/stash.ts +97 -0
  118. package/src/widgets/listing/state.ts +131 -0
  119. package/src/widgets/listing/view.ts +257 -0
  120. package/src/widgets/plp/index.ts +11 -0
  121. package/src/widgets/plp/route.ts +263 -0
  122. package/src/widgets/recommender/index.ts +261 -0
  123. package/src/widgets/recommender/style-recommender.css +26 -0
  124. package/src/widgets/recommender/template-recommender-box-default.ts +11 -0
  125. package/src/widgets/recommender/template-recommender-popup.ts +12 -0
  126. package/src/widgets/recommender/template-recommender-result-default.ts +10 -0
  127. package/src/widgets/search/assets.ts +7 -0
  128. package/src/widgets/search/first.ts +15 -0
  129. package/src/widgets/search/form-url.ts +59 -0
  130. package/src/widgets/search/in-place.ts +226 -0
  131. package/src/widgets/search/index.ts +16 -0
  132. package/src/widgets/search/range.ts +260 -0
  133. package/src/widgets/search/route.ts +32 -0
  134. package/src/widgets/search/speculate.ts +202 -0
  135. package/src/widgets/search/style-search.css +65 -0
  136. package/src/widgets/search/template-search-box.ts +17 -0
  137. package/src/widgets/search/template-search-facets-default.ts +73 -0
  138. package/src/widgets/search/template-search-header.ts +14 -0
  139. package/src/widgets/search/template-search-pagination.ts +12 -0
  140. package/src/widgets/search/template-search-range-default.ts +48 -0
  141. package/src/widgets/search/template-search-result-default.ts +13 -0
  142. package/src/widgets/search/template-search-results-default.ts +8 -0
  143. package/tasks/model.mjs +116 -0
  144. package/tasks/run.mjs +305 -0
  145. package/tsconfig.json +14 -0
package/src/types.ts ADDED
@@ -0,0 +1,778 @@
1
+ // Integration config types. `_config.ts` holds JSON-safe
2
+ // values only (it is parsed as JSON), `_init.ts` sets anything that needs code
3
+ // (functions, computed attributes) and may mutate the config it is given.
4
+
5
+ import type { FilterNode, Hit } from './api';
6
+ import type { Position } from './core/reconciler';
7
+ import type { ImageInfo, ImageOptions } from './core/images';
8
+
9
+ export type { FilterNode, Hit, ImageInfo, ImageOptions, Position };
10
+
11
+ /** A result as templates see it: `a` = the hit's fields (flat, as discovery returns them), `c` = computed attributes. */
12
+ export interface Result {
13
+ /** `@id`, e.g. "product/<id>". */
14
+ id: string;
15
+ /** `@type`: product, category, brand, … */
16
+ type: string;
17
+ a: Hit;
18
+ c: Record<string, any>;
19
+ }
20
+
21
+ /**
22
+ * Nested dictionary; `{name}` placeholders. A plural is an object of
23
+ * `Intl.PluralRules` categories (`zero`, `one`, `two`, `few`, `many`,
24
+ * `other`; `other` required), picked by the `count` param in the locale:
25
+ * `{ one: '{count} produkt', few: '{count} produkty', many: '{count} produktów', other: '{count} produktu' }`.
26
+ */
27
+ export interface Dictionary {
28
+ [key: string]: string | Dictionary;
29
+ }
30
+ /** Keyed by locale: `{ de: { search: { sort: 'Sortieren' } } }`. */
31
+ export type Translations = Record<string, Dictionary>;
32
+
33
+ export interface GlobalConfigType {
34
+ /** Luigi's Box AI publishable key (pub_<env>_<region>_…), bound to the channel and the site's origins. */
35
+ publishableKey: string;
36
+ /** Channel (serving destination), lbn_… */
37
+ channelId: string;
38
+ /** API region, from the key: 'eu1'. */
39
+ region?: string;
40
+ /** Dictionary + number formatting locale ('de', 'cs', 'en'). Default 'en'. */
41
+ locale?: string;
42
+ /** ISO 4217, for price(). */
43
+ currency?: string;
44
+ /** Intl.NumberFormat options for price(), e.g. { trailingZeroDisplay: 'stripIfInteger' }. */
45
+ priceFormat?: Intl.NumberFormatOptions & { trailingZeroDisplay?: 'auto' | 'stripIfInteger' };
46
+ /** Catalog type of products (default 'product'). */
47
+ itemType?: string;
48
+ /** The site's search input(s); autocomplete binds to whatever matches. */
49
+ inputSelector?: string;
50
+ disabled?: boolean;
51
+ /**
52
+ * Speculation Rules (Chrome, Edge): our product links (listings,
53
+ * recommenders, the autocomplete's links) are prerendered, or prefetched,
54
+ * when the shopper shows intent (`moderate`: ~200 ms of hover, pointerdown on
55
+ * touch; the browser keeps two at a time). The product page is then shown at
56
+ * once on the click. It costs the site's server a page render per intent
57
+ * that may not be followed, and the product page's scripts run in it before
58
+ * it is shown. Default 'prerender'; `false` turns it off (e.g. a site whose
59
+ * servers or product-page scripts can't take speculative views), 'prefetch'
60
+ * fetches the HTML only.
61
+ */
62
+ speculation?: 'prerender' | 'prefetch' | false;
63
+ /** Links speculation applies to (CSS). Default: links in our result lists (`[data-lbx-list]`), recommenders and the autocomplete dropdown. */
64
+ speculationSelector?: string;
65
+ /** `moderate` (default: hover, pointerdown) or `conservative` (pointerdown / touchstart only). */
66
+ speculationEagerness?: 'moderate' | 'conservative';
67
+ /** Preconnect to the product image host (learnt from the first results) on later page loads. Default true. */
68
+ preconnectImages?: boolean;
69
+ /** Product images: srcset, the eager first row, the box's size, placeholders, preloads (`image()` / `img()` in templates). */
70
+ images?: ImagesConfig;
71
+ /**
72
+ * Filter clicks that render at once (search and category listings): when
73
+ * the shopper shows intent on one of our controls leading to another state
74
+ * of the listing (a facet value, a flag, a sort, a chip, "clear all", the
75
+ * next page: the pointer resting on it ~100 ms, pointerdown on touch,
76
+ * keyboard focus), page 1 of that state's products is fetched. The click
77
+ * renders it in the next frame (the facets of the new result follow). The
78
+ * control's state is its URL: a link's `href`, or `data-lbx-href` (the
79
+ * scope's `link.href`) on any other element, e.g. a checkbox's `<label>`.
80
+ * Two in flight, the latest intent wins; 20 per category or query; none
81
+ * offline, with Save-Data or on 2G; range sliders never. A hover that is
82
+ * not followed costs one search request (no render, no hook, no analytics
83
+ * event). Default true.
84
+ */
85
+ prefetchFilters?: boolean;
86
+ /**
87
+ * The high-contrast mode's switch (the previous-generation Luma's "High-contrast mode"
88
+ * button) at the start of every listing: visually hidden until it has focus,
89
+ * unless the visitor's OS asks for a contrast preference or the mode is on.
90
+ * The mode itself follows `prefers-contrast: more` without it (pure CSS,
91
+ * docs/ACCESSIBILITY.md). Default false.
92
+ */
93
+ contrastToggle?: boolean;
94
+ /** Set in _init.ts: templates read them as r.c.<name>. Defaults: title, url, image, price. */
95
+ computedAttributes?: Record<string, (r: Result) => unknown>;
96
+ translations?: Translations;
97
+ /**
98
+ * `false`: no analytics at all, whatever `_analytics.ts` says (README
99
+ * "Analytics": what is sent is that file's).
100
+ */
101
+ analytics?: false;
102
+ /**
103
+ * The site's dataLayer, for analytics' `dl()` (default `dataLayer`): here,
104
+ * not in `_analytics.ts`, because the entry keeps its pushes for the next
105
+ * page when the visitor leaves before analytics has run.
106
+ */
107
+ dataLayer?: string;
108
+ /**
109
+ * Puts a product in the site's cart (`_init.ts`, or `_global/_widget.ts`:
110
+ * only widgets call it): what a click on `[data-lbx-cart]` in one of our
111
+ * tiles runs (announced to analytics first: `lumaCarts()`). `count`: the
112
+ * attribute's value (default 1).
113
+ */
114
+ addToCart?: (r: Result, o: { count: number; el: HTMLElement; event: MouseEvent }) => unknown;
115
+ }
116
+
117
+ /**
118
+ * Product images, JSON. The template
119
+ * helpers `img(r, i)` / `image(r, i)` build every attribute from it.
120
+ */
121
+ export interface ImagesConfig {
122
+ /**
123
+ * srcset candidates built from the image URL (`r.c.image`): `match` is a
124
+ * regex on it (default: the whole URL); each candidate is the URL with
125
+ * `match` replaced (`$1`… are its groups). Either named sizes, descriptor ->
126
+ * replacement (`{ "240w": "/thumb_base_$1", "480w": "/thumb_$1" }`, or `1x` /
127
+ * `2x`), or a resizer: `pattern` with `{w}` for each of `widths`
128
+ * (`"$1?width={w}"`, `[200, 400, 800]`). A URL it does not match gets no srcset.
129
+ */
130
+ srcset?: { match?: string; candidates?: Record<string, string>; pattern?: string; widths?: number[] };
131
+ /** The `sizes` attribute for w descriptors: the tile image's rendered width per viewport (`(min-width: 992px) 232px, 50vw`). */
132
+ sizes?: string;
133
+ /** The image box's intrinsic size, as width/height attributes (its aspect ratio before the image arrives: no layout shift). Default 400 x 400. */
134
+ width?: number;
135
+ height?: number;
136
+ /**
137
+ * The first row of a listing: eager, fetchpriority=high, preloaded. A
138
+ * number (the first N), or columns per viewport min-width
139
+ * (`{ "0": 2, "576": 3, "1400": 4 }`, read with matchMedia). Default
140
+ * `{ "0": 2, "768": 3, "1200": 4 }`. The rest is lazy.
141
+ */
142
+ firstRow?: number | Record<string, number>;
143
+ /** Catalog fields (or computed attributes) holding a placeholder: a dominant colour (CSS hex / rgb / hsl), a tiny image URL. Default: the neutral skeleton. */
144
+ placeholder?: { color?: string; image?: string } | false;
145
+ /** Preload the first row (`<link rel=preload as=image>`): from the server's fragment, the bundle when it has the result, a Back restore. Default true. */
146
+ preload?: boolean;
147
+ /** Lazy images fade in on load (not under reduced motion; never the first row). Default true. */
148
+ fade?: boolean;
149
+ /** Results fetched ahead (typing, a hovered filter): warm the first N of their images at low priority. Default 0 (off). */
150
+ warm?: number;
151
+ }
152
+
153
+ interface AppConfig<Self> {
154
+ disabled?: boolean;
155
+ translations?: Translations;
156
+ /** Set in the app's _init.ts: overrides of _global.computedAttributes for this app only. */
157
+ computedAttributes?: Record<string, (r: Result) => unknown>;
158
+ /** The app's _init.ts. Return false to not start this app. */
159
+ initialize?: (config: Self, Luma: LumaApi) => void | boolean | Promise<void | boolean>;
160
+ }
161
+
162
+ export interface AutocompleteConfigType extends AppConfig<AutocompleteConfigType> {
163
+ /** Search surface (lbs_search_…); autocomplete is the same surface with a partial query. */
164
+ surface: string;
165
+ /** One request per type, in parallel; groups render in this order. */
166
+ types: { type: string; size: number; fields?: string[]; filters?: FilterNode }[];
167
+ /**
168
+ * The shortest phrase that asks anything (default 0: the first letter asks,
169
+ * on the keystroke; one-letter phrases are cached). An empty phrase never asks.
170
+ */
171
+ minChars?: number;
172
+ /**
173
+ * Request pacing. A phrase of up to `instantChars`
174
+ * characters (default 3) asks on the keystroke. A longer one asks once
175
+ * typing pauses for `debounceMs` (default 300: longer than the 150-250 ms
176
+ * between a steady typist's keys, so a word typed in one go is one request,
177
+ * not one per key), but at least every `maxWaitMs` (default 800) while the
178
+ * keys keep coming, so the dropdown follows a fast typist at a calm cadence.
179
+ * A phrase already answered (backspacing, retyping) shows at once.
180
+ */
181
+ instantChars?: number;
182
+ debounceMs?: number;
183
+ maxWaitMs?: number;
184
+ /** A host element whose colour the dropdown uses as accent (its background, else its text colour). */
185
+ accentFrom?: string;
186
+ /**
187
+ * The house design's colours, font and corners (`dom: 'shadow'`). Five are
188
+ * sampled from the
189
+ * page (font, fg, bg, accent, radius); the rest are derived from them. A
190
+ * value here replaces the sampled or derived one (`--lbx-<name>` in the
191
+ * dropdown's shadow root: an integration's `style-autocomplete.css` can set
192
+ * the same properties on `:host`). `onAccent` follows `accent` (white, or
193
+ * near-black on a light accent) unless set.
194
+ * `{ "accent": "#1a73e8", "font": "Inter, sans-serif", "line": "#d0d0d0" }`
195
+ */
196
+ tokens?: Partial<Record<'font' | 'fg' | 'bg' | 'accent' | 'radius' | 'muted' | 'line' | 'soft' | 'tint' | 'tintStrong' | 'onAccent' | 'ok' | 'radiusRow' | 'radiusTile', string>>;
197
+ /**
198
+ * The house design's top product (the first product result, its own
199
+ * column). `variant`: 'image' (default, a large picture) or 'spec'
200
+ * (technical catalogues: a small picture, then up to 4 of `specFields` as
201
+ * label / value rows; a label is the translation `autocomplete.specs.<field>`,
202
+ * else the field's name). `badgeField`: a result field shown as a pill on
203
+ * the picture (e.g. a catalog label: "New", "Sale"); none by default.
204
+ */
205
+ hero?: { variant?: 'image' | 'spec'; specFields?: string[]; badgeField?: string };
206
+ /** The products beside the top product: 'grid' (default, 2 columns of cards) or 'list' (rows: picture, title over meta, stock, price). */
207
+ productLayout?: 'grid' | 'list';
208
+ /**
209
+ * Result fields (or computed attributes, `r.c`) the house design's product
210
+ * cards show; each optional, a missing value is not drawn. `availability`:
211
+ * a text, or true / a positive number for "In stock" (`autocomplete.inStock`);
212
+ * `category`: the text after the stock; `oldPrice`: struck through beside
213
+ * the price; `meta`: the line under the title in the list layout (a part
214
+ * number); `price`: the price instead of `r.c.price`. A price that is a
215
+ * number is formatted (`price()`); a text (a computed attribute in the
216
+ * shop's own format, "1.099,-") is shown as it is. None by default.
217
+ */
218
+ productFields?: { availability?: string; category?: string; oldPrice?: string; meta?: string; price?: string };
219
+ /**
220
+ * "No results" in the house design (its own layout): `tryQueries`, phrases offered as chips (handed to the site's
221
+ * search); `browse`, links (`{ title, url }`); `fallback`, products from
222
+ * the whole shop in a strip apart, labelled as not results: `null`
223
+ * (default: no strip) or `{ query, size }` (a fixed product search, asked
224
+ * once per page). The help box shows when the translation
225
+ * `autocomplete.help.phone` is set.
226
+ */
227
+ emptyState?: { tryQueries?: string[]; browse?: { title: string; url: string }[]; fallback?: { query: string; size?: number } | null };
228
+ /**
229
+ * 'shadow' (default): our own markup and CSS in a shadow root.
230
+ * 'light': the dropdown is a light-DOM element, so the site's own CSS styles
231
+ * it (a 1:1 port of an existing design); `hostClass` / `hostId` go on it and
232
+ * the integration's autocomplete styles go into the document.
233
+ */
234
+ dom?: 'shadow' | 'light';
235
+ hostClass?: string;
236
+ hostId?: string;
237
+ /**
238
+ * What the dropdown lines up with (its left edge and width, just below it).
239
+ * By default the visible search field: the input when it draws its own
240
+ * border or shadow, else its nearest ancestor up to its form (at most 4
241
+ * levels) that draws one (a border, a shadow or a background) and is the
242
+ * field rather than the bar around it (at least the input's width, not much
243
+ * taller, not as wide as the page); else the input. A selector: the input's
244
+ * nearest ancestor matching it (across shadow roots), else the input.
245
+ * `'input'`: always the input. At least 360 px wide; on a phone (below
246
+ * 480 px) a field spanning most of the screen exactly, else the screen's
247
+ * width.
248
+ */
249
+ alignTo?: 'input' | string;
250
+ /** _init.ts hooks, for sites whose own code reacts to their dropdown opening, closing or a pick. */
251
+ onOpen?: () => void;
252
+ /** Why it closed: a pick, Enter / Escape in the input, focus leaving, a press outside, a route change, … */
253
+ onClose?: (reason: 'select' | 'enter' | 'escape' | 'back' | 'blur' | 'outside' | 'navigate' | 'empty' | 'hidden' | 'api') => void;
254
+ onSelect?: (r: Result | null) => void;
255
+ /** _init.ts: receives the dropdown's controls once it has started (e.g. for the site's own code that opens or closes it). */
256
+ onReady?: (controls: { close: () => void; reposition: () => void }) => void;
257
+ }
258
+
259
+ /** Where a listing lives, and what it shows. */
260
+ export interface ListingConfigType {
261
+ /** Anchor candidates, most specific first. */
262
+ anchor: string | string[];
263
+ position: Position;
264
+ /** Wait until the anchor is stable this long (frameworks that re-render after load). */
265
+ settleMs?: number;
266
+ /** Element that reserves our remembered height (default: the anchor). */
267
+ reserveSelector?: string;
268
+ /** Fail-safe: show the host's page if ours has not rendered after this long (default 4000). */
269
+ holdTimeoutMs?: number;
270
+ /** Discovery surface: lbs_search_… for search, lbs_collection_… for category pages. */
271
+ surface: string;
272
+ /** Hits per page: the first page, and each "show more". */
273
+ size: number;
274
+ /** Faceted fields, in sidebar order. Range facets (numbers) are detected from the facets response. */
275
+ facets: string[];
276
+ /** Values per terms facet (default 50). */
277
+ facetSize?: number;
278
+ /**
279
+ * With a filter on, also list each facet's values that the page has without
280
+ * the shopper's filters, marked `empty` (sites that grey them out rather than
281
+ * hide them). One more facets request per scope or query (and its fields).
282
+ */
283
+ emptyValues?: boolean;
284
+ /** Sorts as field:direction ('lbx:price:asc'); '' = relevance. Labels: translations <app>.sorts. */
285
+ sorts: string[];
286
+ /** return_fields for the tiles (default: the surface's). */
287
+ fields?: string[];
288
+ /** Always-on filters (e.g. what this storefront may show), AND-ed with the shopper's. */
289
+ baseFilters?: FilterNode;
290
+ /**
291
+ * _init.ts: after each render (e.g. to keep the site's document title in step).
292
+ * `shown`: the results loaded; `page`: the pages loaded (1, plus one per "show more"). */
293
+ onRender?: (info: { route: string; query: string; total: number; shown: number; page: number; url: URL }) => void;
294
+ /** Host classes for our element, so it sits in the site's own container/grid. */
295
+ rootClass?: string;
296
+ /** Show the query / category name as a heading. */
297
+ showTitle?: boolean;
298
+ /** On/off filters of our own (in the URL as lbx.flag=<id>); labels: translations <app>.flags.<id>. */
299
+ flags?: { id: string; filter: FilterNode }[];
300
+ /**
301
+ * A click on a link of ours to another page (a product) remembers the
302
+ * listing (every page loaded, trimmed to `fields`) and where the shopper was, in
303
+ * sessionStorage; Back to that URL then renders it at once, at the same
304
+ * tile, and refreshes it in the background. Default true.
305
+ */
306
+ restoreOnBack?: boolean;
307
+ /** Near the end of the loaded tiles (~70%), the next page is fetched from the cursor, so "show more" appends at once. Default true. */
308
+ prefetchNextPage?: boolean;
309
+ /**
310
+ * Tiles out of view skip their style, layout and paint (`content-visibility:
311
+ * auto`), each at its own measured size: a long list's re-render about
312
+ * halves (a production integration's unstyled stub page, a sort after 136 tiles: style,
313
+ * layout and paint 243 → 144 ms, longest task 124 -> 85 ms at 4x CPU).
314
+ * Opt-in: every tile is then contained, always (paint: nothing of it shows
315
+ * outside its box, a hover panel or a badge hanging out is cut; its own
316
+ * stacking context; a `position: fixed` inside it is fixed to the tile;
317
+ * margins no longer collapse through it), and tiles a "show more" added
318
+ * out of view stand in at the first tile's size until they come near.
319
+ * Default false.
320
+ */
321
+ contentVisibility?: boolean;
322
+ /**
323
+ * How a change inside the open filter drawer (`[data-lbx-drawer-panel]`,
324
+ * opened by `[data-lbx-drawer="open"]`) applies. 'instant' (default): at
325
+ * once, as in a sidebar. 'staged': held in the drawer (it shows the pending
326
+ * selection, the scope's `pending` is true, nothing is fetched) until its
327
+ * apply button (`[data-lbx-drawer="apply"]`) commits them in one request
328
+ * and one history entry; closing it otherwise (`[data-lbx-drawer="close"]`,
329
+ * Escape) discards them. Per viewport min-width, e.g. `{ "0": "staged",
330
+ * "768": "instant" }` (staged on phones). Outside an open drawer every
331
+ * change is instant.
332
+ */
333
+ facetsApply?: 'instant' | 'staged' | Record<string, 'instant' | 'staged'>;
334
+ /** Facet groups: collapsible (accordion) and searchable. */
335
+ facetGroups?: FacetGroupsConfig;
336
+ /**
337
+ * The step of a range facet's slider and inputs, by facet name (default 1):
338
+ * `{ "lbx:price": 10 }`. An arrow key moves a thumb by one step, Page Up /
339
+ * Page Down by a tenth of the range (whole steps).
340
+ */
341
+ rangeSteps?: Record<string, number>;
342
+ }
343
+
344
+ /** `facetGroups` of a listing (JSON). */
345
+ export interface FacetGroupsConfig {
346
+ /**
347
+ * Which groups start open: `true` (all, the default), `false` (none), or
348
+ * the first N of the sidebar; per viewport min-width (`{ "0": false,
349
+ * "1024": 3 }`: closed on phones, the first three open on desktops). A
350
+ * group with a selection starts open; the shopper's toggles are kept for
351
+ * the visit (another category or query starts anew). Templates get each
352
+ * group's `open`, `toggle` and `id`.
353
+ */
354
+ open?: boolean | number | Record<string, boolean | number>;
355
+ /** Facet fields whose group gets a search box over its values (client-side, as typed; case and diacritics ignored). */
356
+ search?: string[];
357
+ /** A group's search box only from this many values (default 10). */
358
+ searchMin?: number;
359
+ }
360
+
361
+ export interface SearchConfigType extends AppConfig<SearchConfigType>, ListingConfigType {
362
+ /** Pathname regex of search pages; omitted = any page with the query param. */
363
+ urlMatch?: string;
364
+ /** Query param holding the query ('q'). Without it, capture group 1 of urlMatch is the query. */
365
+ queryParam?: string;
366
+ /** _init.ts overrides for URLs a regex cannot describe. */
367
+ active?: (url: URL) => boolean;
368
+ readQuery?: (url: URL) => string;
369
+ /** Extra result types shown above the results (category and brand tiles), one request each. */
370
+ quick?: { type: string; size: number; fields?: string[] }[];
371
+ /**
372
+ * On the search page, a new query from the site's search form (the form
373
+ * holding `_global.inputSelector`: Enter, its button, and the autocomplete's
374
+ * Enter / "show all" / query suggestions, which submit it) is shown in
375
+ * place: a new history entry and our listing re-rendered, no page load.
376
+ * Default true. false: every search is a page load, e.g. where the site's
377
+ * search page has server-rendered parts outside our listing that must follow
378
+ * the query, or page-view analytics that must fire per search.
379
+ */
380
+ inPlaceQuery?: boolean;
381
+ /**
382
+ * While the shopper types in `_global.inputSelector` (on any page), once the
383
+ * phrase settles (`prefetchDelayMs`, default 300) the search page's result
384
+ * for the URL their form would submit to is fetched: Enter on the search page
385
+ * renders it without a request, and a page load of it (from another page)
386
+ * within `prefetchMaxAgeMs` renders it from sessionStorage. One in flight,
387
+ * the latest two kept; not offline, with Save-Data or on 2G. Default true.
388
+ */
389
+ prefetchWhileTyping?: boolean;
390
+ prefetchDelayMs?: number;
391
+ /** How long a result fetched while typing is used (ms; older: asked anew). Default 120000. */
392
+ prefetchMaxAgeMs?: number;
393
+ /**
394
+ * Also prerender the search page (Speculation Rules: Chrome, Edge) of the
395
+ * phrase whose result was just fetched while typing, if it is still the one
396
+ * typed, so Enter from another page activates a page already rendered. One
397
+ * at a time (a newer phrase replaces it). It costs a host page render on
398
+ * the site's server per settled phrase, and runs the site's scripts in it:
399
+ * analytics that do not wait for `prerenderingchange` count a page view the
400
+ * shopper may never see. Not on the search page when `inPlaceQuery` is on.
401
+ * Default false.
402
+ */
403
+ prerenderWhileTyping?: boolean;
404
+ /** Speculation Rules eagerness of that list rule: 'immediate' (default) or 'eager'. */
405
+ prerenderEagerness?: 'immediate' | 'eager';
406
+ }
407
+
408
+ /**
409
+ * A page's own facets (plp `facetsFor`): the fields to facet, in order, and
410
+ * optionally the sidebar's group order for templates (`layout`: fields plus
411
+ * the integration's own group keys, e.g. a group of flags).
412
+ */
413
+ export interface FacetLayout {
414
+ facets: string[];
415
+ layout?: string[];
416
+ }
417
+
418
+ /** A category page's scope (collection_filters) and title; null = not ours. */
419
+ export type CategoryFilter = { scope: FilterNode; title: string } | null;
420
+
421
+ /** How the host's own URL parameters map onto listing state (JSON-safe). */
422
+ export interface ParamMap {
423
+ /**
424
+ * The host's page param. Luma opens every listing on its first page and
425
+ * appends the next ones ("show more"; cursors cannot jump to page N): a URL
426
+ * with their page 1 is ours, a later page of theirs is left to their own
427
+ * listing. Our UI never writes it (it drops it with any change).
428
+ */
429
+ page?: string;
430
+ sort?: { param: string; values: Record<string, string> };
431
+ /** On/off host params with a fixed value; labels: translations plp.flags["param=value"]. */
432
+ flags?: { param: string; value: string; filter: FilterNode }[];
433
+ /**
434
+ * Facet field -> the host param carrying it. `values`: their value -> ours
435
+ * (a value mapped to '' is one we cannot express: such a URL stays theirs);
436
+ * `slug: false`: values as they are (with `values`: the unmapped ones);
437
+ * neither: slugs of our values (looked up in the category's facet values).
438
+ */
439
+ facetParams?: Record<string, { param: string; slug?: boolean; values?: Record<string, string> }>;
440
+ separator?: string;
441
+ /** Read-only host params turned into a range filter, passed through unchanged. */
442
+ ranges?: { min: string; max: string; field: string }[];
443
+ /** Regex of params that never block a takeover (tracking). */
444
+ ignore?: string;
445
+ }
446
+
447
+ export interface PlpConfigType extends AppConfig<PlpConfigType>, Partial<ListingConfigType> {
448
+ /** Pathname regex of category pages. */
449
+ urlMatch: string;
450
+ /**
451
+ * _init.ts: narrows urlMatch for URLs a regex cannot describe (the bundle
452
+ * only: the snippet's rules are JSON, so they match on urlMatch alone, and
453
+ * the bundle drops a hold on a page `active` rejects at boot).
454
+ */
455
+ active?: (url: URL) => boolean;
456
+ /**
457
+ * false: no early-hold rule for category pages. Needed only when urlMatch
458
+ * over-matches (`active` narrows it) and style-host.css would hide something
459
+ * on the pages it over-matches (it must hit only elements of the category
460
+ * page). Default: a rule.
461
+ */
462
+ earlyHold?: boolean;
463
+ params?: ParamMap;
464
+ /**
465
+ * Category record lookup: the record of `type` (default 'category') whose
466
+ * `field` equals the page URL's path (no trailing slash; `absolute`: origin + path),
467
+ * through the listing's own collection surface.
468
+ */
469
+ lookup?: { type?: string; field: string; absolute?: boolean };
470
+ /** _init.ts: category record -> filter. */
471
+ toFilter?: (category: Hit) => CategoryFilter;
472
+ /** _init.ts: filter straight from the URL (no lookup). */
473
+ fromUrl?: (url: URL) => CategoryFilter;
474
+ /**
475
+ * _init.ts: this page's facets, when they depend on the category (its own
476
+ * technical parameters), instead of `facets`. It runs in the server
477
+ * renderer too (no DOM there): what it returns must not depend on the
478
+ * browser, or a server-rendered page's markup changes on its first client
479
+ * render. May return a promise (e.g. waiting for the host's page); null or
480
+ * undefined = `facets`. Selected fields not in the list still get their group.
481
+ */
482
+ facetsFor?: (url: URL, category: { scope?: FilterNode; title: string; filters: Record<string, string[]> }) => FacetLayout | null | undefined | Promise<FacetLayout | null | undefined>;
483
+ }
484
+
485
+ export interface RecommenderConfigType extends AppConfig<RecommenderConfigType> {
486
+ /** Recommender surface (lbs_recommender_…): it decides the model. */
487
+ surface: string;
488
+ /** Object type recommended (default: _global.itemType). */
489
+ type?: string;
490
+ size: number;
491
+ /** return_fields the tiles need (keeps the response small). */
492
+ fields?: string[];
493
+ /** Inline widget: pathname regex, anchor, placement. */
494
+ urlMatch?: string;
495
+ anchor?: string | string[];
496
+ position?: Position;
497
+ settleMs?: number;
498
+ /** Height reserved while loading. */
499
+ reserve?: number;
500
+ /** Host classes for the widget's element, so it sits in the site's grid (e.g. 'col-12'). */
501
+ rootClass?: string;
502
+ /** Popup widget, opened on "added to cart": the host's success notice selector. */
503
+ confirm?: string;
504
+ /** Max time from the add-to-cart click to the notice (default 10000). */
505
+ windowMs?: number;
506
+ /** _init.ts overrides. */
507
+ active?: (url: URL) => boolean;
508
+ /** _init.ts: anchors read from the page, as typed identities ("product/<id>"); [] = no widget. */
509
+ items?: () => string[];
510
+ /** Anchors are also excluded (on top of the API's own exclusion); default false. */
511
+ excludeItems?: boolean;
512
+ /** _init.ts, popup: the anchor (typed identity) being added when this click target is an add-to-cart control. */
513
+ intent?: (target: Element) => string | null;
514
+ }
515
+
516
+ export interface LumaConfigType {
517
+ _global: GlobalConfigType;
518
+ autocomplete?: AutocompleteConfigType;
519
+ search?: SearchConfigType;
520
+ plp?: PlpConfigType;
521
+ recommenders?: Record<string, RecommenderConfigType>;
522
+ }
523
+
524
+ /** What inits receive as their second argument. */
525
+ export interface LumaApi {
526
+ version: string;
527
+ config: LumaConfigType;
528
+ }
529
+
530
+ export type LumaInitType = (config: LumaConfigType, Luma: LumaApi) => void | boolean | Promise<void | boolean>;
531
+
532
+ /**
533
+ * `_widget.ts` of a folder: loaded with the widget that uses the folder (its
534
+ * chunk), and run once, before that widget's context is made: the place for
535
+ * hooks only a widget calls (computedAttributes, toFilter, onRender, onOpen…),
536
+ * which in `_init.ts` ship and run on every page. `_global/_widget.ts` gets
537
+ * the whole config, a widget folder's its own. Its return value is ignored
538
+ * (a widget that loads runs; disable one in `_init.ts`).
539
+ */
540
+ export type LumaWidgetInitType<C = LumaConfigType> = (config: C, Luma: LumaApi) => void | Promise<void>;
541
+
542
+ // --- Template scopes: what a template-*.ts function receives. -------------
543
+
544
+ /** Available in every template. */
545
+ export interface Helpers {
546
+ /** Translate: the widget's keys first (search.sort, …), then the bare key. */
547
+ t: (key: string, params?: Record<string, unknown>) => string;
548
+ /** A number as a price in the site's locale and currency. */
549
+ price: (n: unknown) => string;
550
+ /** A field's text (its first value when it is a list), for attributes and alt text. */
551
+ plain: (s: unknown) => string;
552
+ first: (v: unknown) => any;
553
+ /** Another template of the widget, with this scope plus props. */
554
+ include: (name: string, props?: Record<string, unknown>) => unknown;
555
+ /** A result's image attributes (`_global.images`): src, srcset, sizes, width/height, loading, fetchpriority, `ph` (its box's placeholder style). */
556
+ image: (r: Result, i: number, opts?: ImageOptions) => ImageInfo;
557
+ /** A complete `<img>` in its placeholder box (`<span class="lbx-img" data-lbx-ph>`); nothing without an image. */
558
+ img: (r: Result, i: number, opts?: ImageOptions) => unknown;
559
+ }
560
+
561
+ /**
562
+ * A UI action: `go(link)` applies it; `href` is the URL it leads to. Put it on
563
+ * the control as a link's `href`, or as `data-lbx-href` on any other element
564
+ * (a checkbox's `<label>`, a button): intent on it then prefetches that state
565
+ * (`_global.prefetchFilters`).
566
+ */
567
+ export interface Link {
568
+ patch: Record<string, unknown>;
569
+ href: string;
570
+ }
571
+
572
+ export interface SearchScope extends Helpers {
573
+ message: string | null;
574
+ query: string;
575
+ title: string;
576
+ showTitle: boolean;
577
+ /** 'search' or 'plp'. */
578
+ route: string;
579
+ total: number;
580
+ /**
581
+ * What is loaded, in order: the first page, then every page "show more"
582
+ * appended (`showMore`). There is no numbered page: a listing opens on its
583
+ * first page and grows.
584
+ */
585
+ results: Result[];
586
+ /** Search pages: results of the `quick` types (category, brand tiles), by type. */
587
+ quick: Record<string, Result[]>;
588
+ /**
589
+ * Text facets (range facets are in `ranges`). Values are links: href = the
590
+ * URL with the value toggled. No counts: the API returns none; `rank` is the
591
+ * value's place in the API's order (most frequent first); `empty`
592
+ * (`emptyValues`): none of the filtered results has it.
593
+ */
594
+ facets: ({
595
+ name: string;
596
+ label: string;
597
+ selected: number;
598
+ /** `match`: the group's search text finds it (always true without text). */
599
+ values: ({ value: string; label: string; checked: boolean; rank: number; match: boolean; empty?: boolean } & Link)[];
600
+ /**
601
+ * The group's search box (`facetGroups.search`, from `searchMin` values),
602
+ * else null: its `text`, how many values it `matches`, and `set(text)`
603
+ * (re-renders, no request; the count is announced once typing pauses).
604
+ * Label the input (`t('facetSearch', { facet })`).
605
+ */
606
+ search: { text: string; matches: number; set: (text: string) => void } | null;
607
+ } & FacetGroup)[];
608
+ /** The sidebar's groups in order: facet names (text and range) plus any group keys of the page's `layout`. */
609
+ layout: string[];
610
+ ranges: ({
611
+ name: string;
612
+ label: string;
613
+ min: number;
614
+ max: number;
615
+ from: number;
616
+ to: number;
617
+ active: boolean;
618
+ /** Unit shown next to the values (translations: units.<facet>). */
619
+ unit: string;
620
+ /** The slider's and the inputs' step (`rangeSteps`, default 1). */
621
+ step: number;
622
+ clear: Link;
623
+ /**
624
+ * Commit a range: one request (or, in a staged drawer, held until its
625
+ * Apply). An end at its bound is open (`null`); both at their bounds
626
+ * clears the filter. Call it when the shopper lets go (a slider's
627
+ * `change`, an input's `change` or Enter), never on every move.
628
+ */
629
+ set: (from: number | null, to: number | null) => void;
630
+ setFrom: (from: number | null) => void;
631
+ setTo: (to: number | null) => void;
632
+ } & FacetGroup)[];
633
+ flags: ({ id: string; label: string; checked: boolean } & Link)[];
634
+ /**
635
+ * Active filters, each with the link that removes it: its facet's `title`
636
+ * and its `value` ("Colour" and "White": render "Title: value"; a flag is its
637
+ * own title, `value` ''). `label` is the value alone for a facet value, and
638
+ * "Title: from–to" for a range.
639
+ */
640
+ chips: ({ kind: 'facet' | 'range' | 'flag'; name: string; title: string; value: string; label: string } & Link)[];
641
+ /** Removes every filter; null when none is active. */
642
+ clear: Link | null;
643
+ sorts: ({ value: string; label: string; selected: boolean } & Link)[];
644
+ sort: string;
645
+ /** Another page can be appended (`showMore`): the result set goes on. */
646
+ more: boolean;
647
+ /** Path of the page (for host links such as a login redirect). */
648
+ path: string;
649
+ facetsOpen: boolean;
650
+ /** The products are shown, their facets still on the way (a render follows): a template may hold their place. */
651
+ facetsPending: boolean;
652
+ /** The quick types are on their way, not waited for (they do not sit above the products here): a render follows with them. */
653
+ quickPending: boolean;
654
+ /** How the open filter drawer applies changes at this width (`facetsApply`). */
655
+ facetsApply: 'instant' | 'staged';
656
+ /** The open drawer holds changes not applied yet (staged): its apply button commits them. */
657
+ pending: boolean;
658
+ /**
659
+ * View state of this listing that is not in the URL (a group's "show more",
660
+ * a facet's search text), by the template's own keys. Empty on the server
661
+ * and on a new visit (another category or query); `setUi` re-renders.
662
+ */
663
+ ui: Record<string, unknown>;
664
+ setUi: (key: string, value: unknown) => void;
665
+ /** Click handler for a link: applies its patch client-side (in the server render: a no-op). */
666
+ go: (link: Link | null) => (e: Event) => void;
667
+ toggleFacet: (name: string, value: string) => void;
668
+ toggleFlag: (id: string) => void;
669
+ setSort: (sort: string) => void;
670
+ /**
671
+ * "Show more": the next page appended to `results`, from the cursor (one
672
+ * request; prefetched as the shopper nears the end, `prefetchNextPage`).
673
+ * No URL change and no history entry. Bind it as `@click=${s.showMore}` on
674
+ * a `<button data-lbx-more>` (`data-lbx-more`: intent on it prefetches the
675
+ * page too): from the keyboard, focus moves on to the first new tile.
676
+ */
677
+ showMore: (e?: Event) => void;
678
+ /** Range facet: set from–to (null = open end); both null clears it. */
679
+ setRange: (name: string, from: number | null, to: number | null) => void;
680
+ }
681
+ /**
682
+ * A facet group's accordion state (`facetGroups.open`): open or not, the
683
+ * toggle, and an `id` for the region it shows (put `aria-controls=${id}` on
684
+ * a `<button aria-expanded=${open}>` and `id=${id}` on the region).
685
+ */
686
+ export interface FacetGroup {
687
+ id: string;
688
+ open: boolean;
689
+ toggle: () => void;
690
+ }
691
+
692
+ /**
693
+ * search-range-default: one range facet's dual-thumb slider and its two
694
+ * number inputs (`include('search-range-default', { range })`), kept in sync
695
+ * as the shopper moves them and committed on release (`range.set`).
696
+ */
697
+ export interface SearchRangeScope extends SearchScope {
698
+ range: SearchScope['ranges'][number];
699
+ }
700
+
701
+ /** search-result-default: one result, `i` its position (0 = first). */
702
+ export interface SearchResultScope extends SearchScope {
703
+ r: Result;
704
+ i: number;
705
+ }
706
+
707
+ export interface AutocompleteScope extends Helpers {
708
+ query: string;
709
+ /** One group per configured type that has results, in config order; `kind` is the type. */
710
+ groups: { kind: string; label: string; rows: { r: Result; i: number }[] }[];
711
+ /** Rows of one type ([] when none). */
712
+ byType: (type: string) => { r: Result; i: number }[];
713
+ count: number;
714
+ /** Index of the keyboard-selected row, -1 = none. */
715
+ active: number;
716
+ choose: (i: number) => void;
717
+ submitAll: () => void;
718
+ /**
719
+ * The search page of what is typed: the URL the site's search form would
720
+ * submit to ('' without a GET form). "Show all products" is a link to it
721
+ * (`href`), so it is a link to assistive technology, opens in a new tab, and
722
+ * is the dropdown's one tab stop: mark it `data-lbx-all` and Tab from the
723
+ * input reaches it (the options stay out of the Tab order).
724
+ */
725
+ allHref: string;
726
+ // --- The house design (dom: 'shadow'). -------
727
+ /** 'idle' (the phone sheet before a phrase is long enough), 'results', 'empty' (every type answered nothing). */
728
+ state: 'idle' | 'results' | 'empty';
729
+ /** The phone sheet (full screen, viewport up to 600 px) rather than the panel under the input. */
730
+ sheet: boolean;
731
+ /** The panel is at least 1000 px wide: the top product has its own column. */
732
+ wide: boolean;
733
+ /** Query suggestions (pills). */
734
+ phrases: { r: Result; i: number }[];
735
+ /** One group per other non-product type with results, in config order (the list column; on a phone, tabs). */
736
+ lists: { kind: string; label: string; rows: { r: Result; i: number }[] }[];
737
+ /** The first product (the top product), or null. */
738
+ hero: { r: Result; i: number } | null;
739
+ /** The products after it (at most 6 on the panel). */
740
+ products: { r: Result; i: number }[];
741
+ /** The phone sheet's tabs: one per type with results (products first); `select()` shows it. */
742
+ tabs: { kind: string; label: string; selected: boolean; select: () => void }[];
743
+ /** "No results": phrases to try (options), links to browse (options), fallback products (options), the help box. */
744
+ empty: { tries: { r: Result; i: number }[]; browse: { r: Result; i: number }[]; fallback: { r: Result; i: number }[]; help: { title: string; phone: string; tel: string; hours: string } | null };
745
+ /** Pointer over an option: it becomes the selection (the keyboard's too). */
746
+ hover: (i: number, e?: PointerEvent) => void;
747
+ /** Pointer off that option: a selection the pointer made is cleared (a keyboard one stays). */
748
+ unhover: (i: number, e?: PointerEvent) => void;
749
+ /** The house design's config: `hero`, `productLayout`, and a product's optional fields. */
750
+ heroVariant: 'image' | 'spec';
751
+ productLayout: 'grid' | 'list';
752
+ /** A result's optional fields (`productFields`, `hero`): '' when it has none. */
753
+ field: (r: Result, name: 'availability' | 'category' | 'oldPrice' | 'meta' | 'price' | 'badge') => string;
754
+ /** The top product's spec rows (`hero.specFields`, at most 4). */
755
+ specs: (r: Result) => { label: string; value: string }[];
756
+ /** The phone sheet: its input's value, placeholder and name (the site's input's), clearing it, and its back button. */
757
+ value: string;
758
+ placeholder: string;
759
+ inputLabel: string;
760
+ clear: () => void;
761
+ back: () => void;
762
+ }
763
+ export interface AutocompleteResultScope extends AutocompleteScope {
764
+ r: Result;
765
+ i: number;
766
+ /** How the house design draws the option: a pill, a list row, the top product, a product card or row, a phone row, a compact fallback card, a chip, a browse link. */
767
+ as?: 'pill' | 'row' | 'hero' | 'card' | 'line' | 'mhero' | 'mproduct' | 'mrow' | 'compact' | 'try' | 'browse';
768
+ }
769
+
770
+ export interface RecommenderScope extends Helpers {
771
+ results: Result[];
772
+ /** Popup only. */
773
+ close?: () => void;
774
+ }
775
+ export interface RecommenderResultScope extends RecommenderScope {
776
+ r: Result;
777
+ i: number;
778
+ }