@luigisbox/luma 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/README.md +60 -4
- package/build/vite.mjs +6 -1
- package/docs/ACCESSIBILITY.md +52 -6
- package/package.json +1 -1
- package/src/api/ai.ts +12 -3
- package/src/api/index.ts +11 -1
- package/src/core/features.ts +7 -0
- package/src/core/integration.ts +7 -2
- package/src/locales/cs.json +1 -0
- package/src/locales/de.json +1 -0
- package/src/locales/en.json +1 -0
- package/src/types.ts +76 -17
- package/src/widgets/autocomplete/index.ts +180 -33
- package/src/widgets/autocomplete/style-autocomplete.css +16 -8
- package/src/widgets/autocomplete/template-autocomplete-box.ts +1 -1
- package/src/widgets/autocomplete/template-autocomplete-empty.ts +4 -4
- package/src/widgets/autocomplete/template-autocomplete-sheet.ts +14 -10
- package/src/widgets/listing/applied.ts +87 -0
- package/src/widgets/listing/element.ts +261 -26
- package/src/widgets/listing/intent.ts +15 -0
- package/src/widgets/listing/query.ts +7 -3
- package/src/widgets/listing/stash.ts +11 -2
- package/src/widgets/listing/variants.ts +111 -0
- package/src/widgets/listing/view.ts +34 -3
- package/src/widgets/search/first.ts +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,73 @@ before 1.0, a minor version may break the integration API.
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.7.0 — 2026-10-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `variants: { fields }` on a listing (search, plp; plp inherits it): each
|
|
13
|
+
result gets its variant group as `r.variants` (`siblings`, `total`) from
|
|
14
|
+
Luigi's Box. One request per page of results, never waited for: the
|
|
15
|
+
tiles are drawn again when it arrives. Off by default, built only where
|
|
16
|
+
a folder's `_config.ts` sets it.
|
|
17
|
+
- Light-DOM autocomplete full screen on a phone: `autocomplete.fullScreen`
|
|
18
|
+
(`true`, or a media query such as `'(max-width: 767px)'`). A tap on the
|
|
19
|
+
site's input opens it over the whole screen, with the integration's own
|
|
20
|
+
markup and CSS. Its template renders its own input, marked
|
|
21
|
+
`data-lbx-full-screen-input`, and gets `fullScreen`, `state`, `tabs`,
|
|
22
|
+
`value`, `back()` and `clear()`. Its CSS places it, keyed on
|
|
23
|
+
`[data-lbx-full-screen]`, with `--lbx-top` and `--lbx-cover` from Luma.
|
|
24
|
+
Off by default.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- The listing reads the result set's key from `request_key` (`guid` is its
|
|
29
|
+
deprecated name).
|
|
30
|
+
- Autocomplete templates: `sheet` is now `fullScreen`, and the full-screen
|
|
31
|
+
input's marker `data-lbx-sheet-input` (in an `autocomplete-sheet`
|
|
32
|
+
override) is now `data-lbx-full-screen-input`. Rename both in templates
|
|
33
|
+
copied from 0.6.0.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Full-screen autocomplete on a phone: focus leaving it for the page behind
|
|
38
|
+
(Tab past its last control, iOS's next-field arrow) closes it. Focus was
|
|
39
|
+
left on a control it hid.
|
|
40
|
+
|
|
41
|
+
## 0.6.0 — 2026-10-20
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Phone sheet: a swipe through the results puts the keyboard away. The sheet
|
|
46
|
+
stays open and grows to the full screen. A tap on its input brings the
|
|
47
|
+
keyboard back.
|
|
48
|
+
- Phone sheet: no tab bar when only one type has results.
|
|
49
|
+
- Phone sheet: "Show all products" is at the end of the list, no longer a bar
|
|
50
|
+
pinned above the keyboard.
|
|
51
|
+
- Phone sheet: the list (and the no-results column) runs on behind Safari's
|
|
52
|
+
floating bars and the keyboard, down to the bottom of the screen. Its end
|
|
53
|
+
scrolls clear of them.
|
|
54
|
+
- Staged filter drawer (`facetsApply: 'staged'`): the held selection is
|
|
55
|
+
fetched in the background once the changes pause, so the apply button
|
|
56
|
+
shows the results without waiting. The results still change only on
|
|
57
|
+
apply. Off with `_global.prefetchFilters: false`, Save-Data or 2G.
|
|
58
|
+
- Staged filter drawer: apply marks the swap. The old list fades out, the
|
|
59
|
+
new products fade in, and a status at the top of the listing says
|
|
60
|
+
"Filters applied · 9 products" for 2.5 s. Translation key
|
|
61
|
+
`search.applied`; CSS variables `--lbx-applied-bg`, `--lbx-applied-fg`,
|
|
62
|
+
`--lbx-applied-top`, `--lbx-applied-offset`, or
|
|
63
|
+
`[data-lbx-applied-cue]::part(pill)`;
|
|
64
|
+
`data-lbx-applied` on the listing meanwhile.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- Autocomplete "No results": `emptyState` phrases and browse links keep their
|
|
69
|
+
config text and URL when `computedAttributes` override `title` or `url`.
|
|
70
|
+
Before, a browse link could have no URL, and a tap on it did nothing.
|
|
71
|
+
- Autocomplete, phone sheet, no results: each "browse" link is one row again,
|
|
72
|
+
its chevron at the right, not on a line under the title. The links' wrapper
|
|
73
|
+
is now `.e-links` (was `.browse`, the class each link also has).
|
|
74
|
+
|
|
8
75
|
## 0.5.0 — 2026-10-19
|
|
9
76
|
|
|
10
77
|
Upgrading from 0.4.0: read **Changed** first.
|
package/README.md
CHANGED
|
@@ -10,9 +10,9 @@ This package is what an integration is built with. Each integration lives in
|
|
|
10
10
|
its own repository and depends on `@luigisbox/luma`
|
|
11
11
|
([Integration repository](#integration-repository)).
|
|
12
12
|
|
|
13
|
-
> **Pre-release.** Until 1.0, a minor version (0.
|
|
13
|
+
> **Pre-release.** Until 1.0, a minor version (0.8.0) may change the
|
|
14
14
|
> integration API (config keys, template scope, the build); a patch version
|
|
15
|
-
> (0.
|
|
15
|
+
> (0.7.1) only fixes. Depend on `^0.7.0` to take fixes without breaking
|
|
16
16
|
> changes. [`CHANGELOG.md`](CHANGELOG.md) says what each version changed.
|
|
17
17
|
|
|
18
18
|
## Core principles
|
|
@@ -391,6 +391,59 @@ export default ({ r, i, img, price }: SearchResultScope) => html`
|
|
|
391
391
|
and twice.
|
|
392
392
|
- **Pagination** is "show more" only: templates get `results`, `more` and
|
|
393
393
|
`showMore`; there are no numbered pages.
|
|
394
|
+
- **Variant groups** (`variants`, a listing's config, off by default): where
|
|
395
|
+
the surface collapses a product's variants into one result (one tile for a
|
|
396
|
+
shoe in eight sizes), `r.variants` holds the group's other products
|
|
397
|
+
(`siblings`, as results, at most 100) and its size with this one
|
|
398
|
+
(`total`). Luma asks Luigi's Box for them once per page of results, as
|
|
399
|
+
soon as the page is here, and draws the tiles again when they arrive: a
|
|
400
|
+
tile shows without them first (`r.variants` is undefined), and stays so if
|
|
401
|
+
the request fails; a listing restored on Back has them at once. Keep the
|
|
402
|
+
tile's box the same with and without them (a reserved row, an overlay, or
|
|
403
|
+
shown on hover and focus, `:hover, :focus-within`), or the rows below move
|
|
404
|
+
when they arrive; draw them inside the tile's own markup, not a different
|
|
405
|
+
template branch. `fields` names what each sibling carries; keep it to what
|
|
406
|
+
the tile shows.
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
// integration/search/_config.ts (plp inherits it): the product's own size in `fields` too
|
|
410
|
+
export default { /* … */ fields: ['@title', 'web_url', 'size'], variants: { fields: ['size'] } };
|
|
411
|
+
// integration/_global/_init.ts: the sizes in stock, the product's and its siblings'
|
|
412
|
+
export default (config) => {
|
|
413
|
+
config._global.computedAttributes = {
|
|
414
|
+
...config._global.computedAttributes,
|
|
415
|
+
sizes: (r) => [...new Set([r.a.size, ...(r.variants?.siblings ?? []).map((s) => s.a.size)].flat().filter(Boolean))],
|
|
416
|
+
};
|
|
417
|
+
};
|
|
418
|
+
```
|
|
419
|
+
- **A light-DOM autocomplete full screen on a phone**
|
|
420
|
+
(`autocomplete.dom: 'light'` with `fullScreen: true`, or a media query of
|
|
421
|
+
the site's own: `fullScreen: '(max-width: 767px)'`): a tap on the site's
|
|
422
|
+
input opens the autocomplete over the whole screen instead of a dropdown
|
|
423
|
+
under the input, and `autocomplete-box` gets `fullScreen: true`
|
|
424
|
+
(`state: 'idle'` until a phrase is long enough). The template renders its
|
|
425
|
+
own input, marked `data-lbx-full-screen-input` (`.value=${s.value}`): Luma
|
|
426
|
+
moves the focus into it and makes what is typed there the query and the
|
|
427
|
+
site's input's value. `back()` closes it (as Escape and the browser's
|
|
428
|
+
Back do) and `clear()` empties its input; `tabs` are one per type with
|
|
429
|
+
results. Keep the input one element in every state (not drawn anew for
|
|
430
|
+
results), and the tabs `role="tab"` siblings in their `role="tablist"`:
|
|
431
|
+
the arrow keys move between them. The arrow keys go through the options
|
|
432
|
+
it shows (the open tab's, whether the others are not drawn or hidden by
|
|
433
|
+
its CSS). While it is open the dropdown carries `data-lbx-full-screen` (a
|
|
434
|
+
phone in landscape too), and the integration's CSS places it full screen
|
|
435
|
+
there: `[data-lbx-full-screen] { inset: 0; top: var(--lbx-top, 0px) }`
|
|
436
|
+
(Luma sets `--lbx-top` to the visual viewport's top, which iOS moves when
|
|
437
|
+
it pans the page for the keyboard, and `--lbx-cover` to what Safari's
|
|
438
|
+
bars and the keyboard cover of its bottom: end the scrolling list with
|
|
439
|
+
that much space and give it `scroll-padding-bottom: var(--lbx-cover, 0px)`,
|
|
440
|
+
so its last options are not hidden). Focus leaving it for the page it
|
|
441
|
+
covers closes it. Without `fullScreen`, a light-DOM dropdown on a phone
|
|
442
|
+
stays a dropdown. The build includes this only where a file of the
|
|
443
|
+
integration's repository (a script or `.html` file, not in `node_modules`,
|
|
444
|
+
`dist` or a hidden folder) has `data-lbx-full-screen-input`. The house
|
|
445
|
+
design is always full screen on a phone; its templates get the same
|
|
446
|
+
`fullScreen`.
|
|
394
447
|
- **No results under filters**: the scope still holds the selected groups,
|
|
395
448
|
their `chips` and `clear`. A template that leaves the facets out when
|
|
396
449
|
`total` is 0 must keep them (or at least `clear`) while `chips.length` is
|
|
@@ -411,7 +464,9 @@ On by default unless noted; each falls back to what the page would do anyway.
|
|
|
411
464
|
`emptyValues`, `prerenderWhileTyping` and `contentVisibility`, off by
|
|
412
465
|
default, are in the build only where a file of the integration's repository
|
|
413
466
|
(a script or `.html` file, not in `node_modules`, `dist` or a hidden folder) names them, as `_config.ts` or `_init.ts` does when
|
|
414
|
-
it sets them;
|
|
467
|
+
it sets them; `variants` only where a folder's `_config.ts` sets it (set it
|
|
468
|
+
there, not in `_init.ts`); a switch on by default that no file names is
|
|
469
|
+
built as on.
|
|
415
470
|
|
|
416
471
|
| Config | What it does | Turn it off when |
|
|
417
472
|
|---|---|---|
|
|
@@ -422,7 +477,8 @@ it sets them; a switch on by default that no file names is built as on.
|
|
|
422
477
|
| `restoreOnBack` | Back from a product restores the listing (every page shown) and the followed tile's position, as does the page loaded again within a minute of following a link (a reload, a link whose server redirects back to it); Luma turns the browser's own scroll restoration (`history.scrollRestoration`) off for the listing's history entry until it has placed the page, unless the site set it | |
|
|
423
478
|
| `prefetchNextPage` | "show more" is fetched as the shopper nears the end | |
|
|
424
479
|
| `contentVisibility` (opt-in) | tiles out of view skip style, layout and paint, each at its own measured size: a long list's re-render about halves | keep it off where a tile draws outside its box (a hover panel, a badge hanging out) or holds a `position: fixed` element |
|
|
425
|
-
| `
|
|
480
|
+
| `variants: { fields }` (opt-in) | each tile's result gets its variant group (`r.variants`, "Templates" above): one request to Luigi's Box per page of results, never waited for | the surface does not collapse variants (every group is a group of one) |
|
|
481
|
+
| `_global.prefetchFilters` | a filter, sort or chip is fetched on hover, touch or focus; in a drawer with `facetsApply: 'staged'`, the selection it holds is fetched once the changes pause, so its apply button shows the results without waiting | |
|
|
426
482
|
| `_global.speculation` | product pages are prerendered on hover (`'prerender'`, `'prefetch'`, `false`) | the site's server or its product-page scripts can't take a render per unfollowed hover |
|
|
427
483
|
|
|
428
484
|
### Analytics
|
package/build/vite.mjs
CHANGED
|
@@ -91,6 +91,8 @@ export const chunkOf = (id) => {
|
|
|
91
91
|
if (/\/src\/core\/(reconciler|recommenders)\.ts$/.test(id)) return 'lbx-place';
|
|
92
92
|
// The listing's high-contrast toggle: only a site that turns it on loads it (widgets/listing/element.ts).
|
|
93
93
|
if (/\/src\/core\/contrast-toggle\.ts$/.test(id)) return 'lbx-contrast';
|
|
94
|
+
// An Apply's cue in a staged drawer: asked for when such a drawer opens (widgets/listing/element.ts).
|
|
95
|
+
if (/\/src\/widgets\/listing\/applied\.ts$/.test(id)) return 'lbx-applied';
|
|
94
96
|
// The search page's own: not in lbx-listing, which a category page loads too
|
|
95
97
|
// (2.1 kB gzip it never ran).
|
|
96
98
|
if (/\/src\/widgets\/search\/index\.ts$/.test(id)) return 'lbx-search';
|
|
@@ -560,11 +562,12 @@ export const featureDefines = (integrationDir) => {
|
|
|
560
562
|
__LBX_HOST_STYLES__: has((d) => d.some((f) => existsSync(resolve(integrationDir, f, 'style-host.css')))),
|
|
561
563
|
__LBX_TRANSLATIONS__: has((d) => d.some((f) => existsSync(resolve(integrationDir, f, '_translations.ts')))),
|
|
562
564
|
__LBX_INITS__: has((d) => d.some((f) => existsSync(resolve(integrationDir, f, '_init.ts')))),
|
|
563
|
-
// Only an integration's templates make a cart button, a filter drawer or a
|
|
565
|
+
// Only an integration's templates make a cart button, a filter drawer, a menu or a light-DOM phone sheet's input (Luma's own have none; its house sheet is not light DOM).
|
|
564
566
|
__LBX_CART__: has(() => inFiles('data-lbx-cart')),
|
|
565
567
|
__LBX_DRAWER__: has(() => inFiles('data-lbx-drawer')),
|
|
566
568
|
__LBX_MENU__: has(() => inFiles('data-lbx-menu')),
|
|
567
569
|
__LBX_DROPDOWN__: has(() => inFiles('data-lbx-dropdown')),
|
|
570
|
+
__LBX_LIGHT_FULL_SCREEN__: has(() => inFiles('data-lbx-full-screen-input')),
|
|
568
571
|
// Hooks only the integration's code sets (_init.ts, _widget.ts: functions, never in _config.ts).
|
|
569
572
|
__LBX_AC_HOOKS__: has(() => inFiles('onOpen', 'onClose', 'onSelect', 'onReady')),
|
|
570
573
|
__LBX_ON_RENDER__: has(() => inFiles('onRender')),
|
|
@@ -575,6 +578,8 @@ export const featureDefines = (integrationDir) => {
|
|
|
575
578
|
__LBX_EMPTY_VALUES__: has(() => inFiles('emptyValues')),
|
|
576
579
|
__LBX_PRERENDER_TYPING__: has(() => inFiles('prerenderWhileTyping', 'pageWhileTyping')),
|
|
577
580
|
__LBX_CONTENT_VISIBILITY__: has(() => inFiles('contentVisibility')),
|
|
581
|
+
// A listing's variant groups: the key in a folder's _config.ts (the word alone is common: a catalog field, a comment).
|
|
582
|
+
__LBX_VARIANTS__: has((d) => d.some((f) => /\bvariants\b["']?\s*[:,}]/.test(existsSync(resolve(integrationDir, f, '_config.ts')) ? readFileSync(resolve(integrationDir, f, '_config.ts'), 'utf8') : ''))),
|
|
578
583
|
// Options on by default: one no file names is on.
|
|
579
584
|
...Object.fromEntries(ON_BY_DEFAULT.map((k) => [`__LBX_NAMED_${k.replace(/[A-Z]/g, (c) => `_${c}`).toUpperCase()}__`, has(() => inFiles(k))])),
|
|
580
585
|
};
|
package/docs/ACCESSIBILITY.md
CHANGED
|
@@ -134,8 +134,18 @@ guessed alt text, and forced colours and the OS setting are honoured.
|
|
|
134
134
|
`aria-activedescendant` the selected option: the same shadow root); the
|
|
135
135
|
site's input is left as it was. The back button is named "Close search",
|
|
136
136
|
the clear button "Clear"; the type tabs are a `tablist` (arrow keys move
|
|
137
|
-
between them)
|
|
138
|
-
|
|
137
|
+
between them), shown for two types or more (one type: no tabs, its
|
|
138
|
+
listbox named by the type, "Products"); a finger's vertical swipe
|
|
139
|
+
through the list puts the on-screen keyboard away: focus moves from the
|
|
140
|
+
input to the sheet (`tabindex="-1"`), Escape still closes it, a tap on the
|
|
141
|
+
input brings the keyboard back; Tab is the browser's; targets are 44 px; Escape and the
|
|
142
|
+
browser's Back close it, and so does focus leaving it for the page it
|
|
143
|
+
covers (Tab past its last control, Shift+Tab from its first, iOS's
|
|
144
|
+
next-field arrow): it is never left on a control the sheet hides (2.4.11).
|
|
145
|
+
A light-DOM integration's full-screen autocomplete (`fullScreen`) gets the same focus, keys,
|
|
146
|
+
`aria-activedescendant` on its input and Back; a swipe moves the focus to
|
|
147
|
+
the host (`tabindex="-1"`, `role="group"` named as the input); its roles,
|
|
148
|
+
names and targets are its template's (§5).
|
|
139
149
|
- **Forced colours**: the selection in `Highlight`/`HighlightText` with an
|
|
140
150
|
outline, the panel (and the sheet) keep a `CanvasText` border, pills,
|
|
141
151
|
chips and "Show all products" an outline.
|
|
@@ -160,6 +170,10 @@ guessed alt text, and forced colours and the OS setting are honoured.
|
|
|
160
170
|
link marked `tabindex="-1"`); a render that removed the focused control (a
|
|
161
171
|
chip, the last "show more") moves it to the region, not `<body>`. A mouse
|
|
162
172
|
click keeps focus where it is.
|
|
173
|
+
- **Variant groups landing** (`variants`): the tiles are drawn again, in
|
|
174
|
+
steps, with nothing announced and `aria-busy` untouched (the results did
|
|
175
|
+
not change); focus stays where it is, and in a tile whose template drew
|
|
176
|
+
other markup with its group, goes to the region, not `<body>`.
|
|
163
177
|
- **Menus** (`[data-lbx-menu]` with `[data-lbx-toggle]`, the integration's
|
|
164
178
|
markup): `aria-expanded` on the menu's button (the toggle with
|
|
165
179
|
`aria-haspopup`/`aria-expanded`, else the first toggle) in every state;
|
|
@@ -180,6 +194,18 @@ guessed alt text, and forced colours and the OS setting are honoured.
|
|
|
180
194
|
focus staying on the control. With `facetsApply: 'staged'` its
|
|
181
195
|
changes wait for `[data-lbx-drawer="apply"]`; closing it otherwise discards
|
|
182
196
|
them (the scope's `pending` says whether there is anything to apply).
|
|
197
|
+
Apply's result is said in Luma's status region as "Filters applied" and
|
|
198
|
+
the count (translation `search.applied`, WCAG 4.1.3), and shown for 2.5 s
|
|
199
|
+
in a pill at the listing's top with the same words (hidden from assistive
|
|
200
|
+
technology: it repeats the status region; white on dark, at least 10:1
|
|
201
|
+
over any page behind it; opaque in the high-contrast mode and in forced
|
|
202
|
+
colours, with a border there; taps go through it; it wraps rather than
|
|
203
|
+
cuts its words at 320 px or 200% text, and follows a larger default font
|
|
204
|
+
size; it moves no content). While the applied results load, the
|
|
205
|
+
old list is dimmed as a busy one is: not at all in the high-contrast
|
|
206
|
+
mode, to 60% in forced colours. The new tiles fade in; they also rise
|
|
207
|
+
unless reduced motion is asked for. The pill may overlap the top of the
|
|
208
|
+
drawer's opener, where focus returns, for its 2.5 s; it never hides it.
|
|
183
209
|
The build includes the drawer only where a file of the integration's
|
|
184
210
|
repository (a script or `.html` file, not in `node_modules`, `dist` or a hidden folder) has `data-lbx-drawer` (written
|
|
185
211
|
literally, as in a template).
|
|
@@ -213,7 +239,8 @@ guessed alt text, and forced colours and the OS setting are honoured.
|
|
|
213
239
|
- Built-in templates: the pager is a native "Show more" button with a
|
|
214
240
|
"Showing N of M" line (no numbered pages); tile pictures are decorative
|
|
215
241
|
(`alt=""`: the link reads as its title and price once); targets of at least
|
|
216
|
-
24 px; no fade under reduced motion
|
|
242
|
+
24 px; no fade under reduced motion (except a staged drawer's Apply: its
|
|
243
|
+
new tiles fade in, without rising); tiles reflow to one column at 320 px.
|
|
217
244
|
|
|
218
245
|
### Recommenders (`widgets/recommender/`)
|
|
219
246
|
|
|
@@ -232,7 +259,9 @@ guessed alt text, and forced colours and the OS setting are honoured.
|
|
|
232
259
|
reset cannot remove it there).
|
|
233
260
|
- **Reduced motion**: the hold's loading bar stands still, the listing's
|
|
234
261
|
dim while a re-render loads comes without its fade and the new results
|
|
235
|
-
without their fade-in, no smooth scrolling of ours.
|
|
262
|
+
without their fade-in, no smooth scrolling of ours. One exception: after
|
|
263
|
+
a staged drawer's Apply the new tiles still fade in (opacity only, nothing
|
|
264
|
+
moves), so the swap the shopper confirmed is seen.
|
|
236
265
|
- `.lbx-sr-only`: a class for text that only screen readers read (e.g. "Was"
|
|
237
266
|
before an old price), defined for every widget and in server-rendered CSS.
|
|
238
267
|
|
|
@@ -311,7 +340,20 @@ drawer. The markup is the integration's, so:
|
|
|
311
340
|
`aria-selected`, `tabindex="-1"`; sections as `role="group"` named by their
|
|
312
341
|
heading; wrappers in between `role="presentation"`; nothing but options and
|
|
313
342
|
groups inside the listbox (a pointer-only "show all" duplicate of Enter:
|
|
314
|
-
`aria-hidden="true"`, no tab stop). Decorative pictures `alt=""`.
|
|
343
|
+
`aria-hidden="true"`, no tab stop). Decorative pictures `alt=""`. Full screen on a
|
|
344
|
+
phone (`fullScreen`): the input marked `data-lbx-full-screen-input` is the combobox
|
|
345
|
+
(`role="combobox"`, `aria-autocomplete="list"`, `aria-expanded`,
|
|
346
|
+
`aria-controls` naming the listbox, a name: `inputLabel`); its back and
|
|
347
|
+
clear controls are buttons with names (`t('closeSearch')`, `t('clear')`)
|
|
348
|
+
calling `back()` and `clear()`, 44 px; type tabs a `tablist` of
|
|
349
|
+
`role="tab"` buttons with `aria-selected` (Luma moves between them with the
|
|
350
|
+
arrow keys: siblings in the tablist; the arrows go through the options
|
|
351
|
+
it shows, the open tab's); the input one element in every state
|
|
352
|
+
(drawn anew, it loses the focus and the keyboard); the input's font at
|
|
353
|
+
least 16 px (iOS zooms into a smaller one); the scrolling list ends with
|
|
354
|
+
`var(--lbx-cover)` of space and has it as `scroll-padding-bottom`, so an
|
|
355
|
+
option the keys bring into view is not under the keyboard or Safari's
|
|
356
|
+
bars (2.4.11).
|
|
315
357
|
- **Listings**: filter groups as `fieldset` + `legend` (or `role="group"`
|
|
316
358
|
with a name); native checkboxes, or links with `role="checkbox"` and
|
|
317
359
|
`aria-checked` (never `aria-checked` on a plain link); chips named by what
|
|
@@ -326,7 +368,11 @@ drawer. The markup is the integration's, so:
|
|
|
326
368
|
a `role="button"` div (Luma adds the keys) but a `<button>` is better.
|
|
327
369
|
- **Tiles**: one link name (the picture `alt=""` when the title is in the same
|
|
328
370
|
link); a struck-through old price says so (`<span class="lbx-sr-only">Was
|
|
329
|
-
</span>`), and the current one "Now" next to it.
|
|
371
|
+
</span>`), and the current one "Now" next to it. A variant group
|
|
372
|
+
(`r.variants`) is drawn inside the tile's own markup, not a different
|
|
373
|
+
template branch (focus in the tile stays on its link); shown on hover, it
|
|
374
|
+
shows on focus too (`:hover, :focus-within`), and sibling links have a
|
|
375
|
+
name and 24 px targets.
|
|
330
376
|
- **Colour**: text at 4.5:1, states not by colour alone, 24 px targets; adopt
|
|
331
377
|
the high-contrast mode (§4) for the colours your CSS sets.
|
|
332
378
|
- **Don't** add `aria-live` to counts (Luma announces), set `tabindex="0"` on
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luigisbox/luma",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Luma: Luigi's Box frontend integrations (autocomplete, search, category listings, recommenders) on the Luigi's Box AI API. Pre-release.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
package/src/api/ai.ts
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
// Also runs in Node (the edge render service): there the
|
|
9
9
|
// storefront origin is sent explicitly and nothing touches the DOM.
|
|
10
10
|
|
|
11
|
-
import { PLP, RECOMMENDER, SEARCH } from '../core/features';
|
|
11
|
+
import { PLP, RECOMMENDER, SEARCH, VARIANTS } from '../core/features';
|
|
12
12
|
import { health } from '../core/health';
|
|
13
13
|
import type { GlobalConfigType } from '../types';
|
|
14
|
-
import type { Api, FacetData, Hit, Page } from '.';
|
|
14
|
+
import type { Api, FacetData, Hit, Page, VariantGroup } from '.';
|
|
15
15
|
import { fetchJson, TIMEOUT_MS, tokensFor, untilAborted, visitorId } from './token';
|
|
16
16
|
|
|
17
17
|
export { untilAborted };
|
|
@@ -78,7 +78,8 @@ export const aiApi = (g: GlobalConfigType, origin?: string): Api => {
|
|
|
78
78
|
return_fields: r.fields?.length ? r.fields : undefined,
|
|
79
79
|
});
|
|
80
80
|
const j = await post(r.endpoint, { channel_id: g.channelId, surface_id: r.surface }, body, signal);
|
|
81
|
-
|
|
81
|
+
// `guid` is the deprecated name of `request_key` (it will change meaning).
|
|
82
|
+
return { hits: j.hits ?? [], total: j.total ?? j.total_approx ?? 0, guid: j.request_key ?? j.guid, cursor: j.next_page_cursor ?? null, status: j.pagination_status ?? 'exhausted' };
|
|
82
83
|
},
|
|
83
84
|
|
|
84
85
|
// A listing's and a recommender's (core/features.ts): not in a build without one.
|
|
@@ -106,5 +107,13 @@ export const aiApi = (g: GlobalConfigType, origin?: string): Api => {
|
|
|
106
107
|
)
|
|
107
108
|
).hits ?? []
|
|
108
109
|
: unbuilt,
|
|
110
|
+
|
|
111
|
+
// A listing's `variants` (core/features.ts): not in a build whose integration names none.
|
|
112
|
+
...(VARIANTS
|
|
113
|
+
? {
|
|
114
|
+
variants: async (ids: string[], key: string, fields: string[] | undefined, signal?: AbortSignal): Promise<Record<string, VariantGroup>> =>
|
|
115
|
+
(await post('variants', { channel_id: g.channelId }, compact({ ids, request_key: key, return_fields: fields?.length ? fields : undefined }), signal)).variants ?? {},
|
|
116
|
+
}
|
|
117
|
+
: {}),
|
|
109
118
|
};
|
|
110
119
|
};
|
package/src/api/index.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface Page {
|
|
|
26
26
|
hits: Hit[];
|
|
27
27
|
/** Exact when known, else the API's approximation. */
|
|
28
28
|
total: number;
|
|
29
|
-
/** Identifies the result set; /facets
|
|
29
|
+
/** Identifies the result set (the API's `request_key`); /facets and /variants take it. */
|
|
30
30
|
guid: string;
|
|
31
31
|
/** Cursor of the next page; null when there is none (see status). */
|
|
32
32
|
cursor: string | null;
|
|
@@ -68,11 +68,21 @@ export interface RecoRequest {
|
|
|
68
68
|
fields?: string[];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** A product's variant group (/variants): the other members, and the group's size with the product. */
|
|
72
|
+
export interface VariantGroup {
|
|
73
|
+
/** The other members of the group, by `@id`; at most 100. */
|
|
74
|
+
siblings: Hit[];
|
|
75
|
+
/** The group's size, the product included (0: not in the catalog). */
|
|
76
|
+
total: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
export interface Api {
|
|
72
80
|
search(r: ListingRequest, signal?: AbortSignal): Promise<Page>;
|
|
73
81
|
/** Facets of a result set (its guid): values, no counts. */
|
|
74
82
|
facets(guid: string, facets: { field: string; size?: number }[], signal?: AbortSignal): Promise<Record<string, FacetData>>;
|
|
75
83
|
recommend(r: RecoRequest, signal?: AbortSignal): Promise<Hit[]>;
|
|
84
|
+
/** The variant groups of products of a result set (its key: `guid`), by `@id`: at most 200 ids. Built only for a listing's `variants`. */
|
|
85
|
+
variants?(ids: string[], key: string, fields: string[] | undefined, signal?: AbortSignal): Promise<Record<string, VariantGroup>>;
|
|
76
86
|
/** Mint the browser token now, so the first request does not wait for it. */
|
|
77
87
|
warm(): void;
|
|
78
88
|
}
|
package/src/core/features.ts
CHANGED
|
@@ -18,6 +18,7 @@ declare const __LBX_CART__: boolean | undefined;
|
|
|
18
18
|
declare const __LBX_DRAWER__: boolean | undefined;
|
|
19
19
|
declare const __LBX_MENU__: boolean | undefined;
|
|
20
20
|
declare const __LBX_DROPDOWN__: boolean | undefined;
|
|
21
|
+
declare const __LBX_LIGHT_FULL_SCREEN__: boolean | undefined;
|
|
21
22
|
declare const __LBX_AC_HOOKS__: boolean | undefined;
|
|
22
23
|
declare const __LBX_ON_RENDER__: boolean | undefined;
|
|
23
24
|
declare const __LBX_READ_QUERY__: boolean | undefined;
|
|
@@ -29,6 +30,7 @@ declare const __LBX_INITS__: boolean | undefined;
|
|
|
29
30
|
declare const __LBX_EMPTY_VALUES__: boolean | undefined;
|
|
30
31
|
declare const __LBX_PRERENDER_TYPING__: boolean | undefined;
|
|
31
32
|
declare const __LBX_CONTENT_VISIBILITY__: boolean | undefined;
|
|
33
|
+
declare const __LBX_VARIANTS__: boolean | undefined;
|
|
32
34
|
declare const __LBX_NAMED_PRECONNECT_IMAGES__: boolean | undefined;
|
|
33
35
|
declare const __LBX_NAMED_PREFETCH_FILTERS__: boolean | undefined;
|
|
34
36
|
declare const __LBX_NAMED_PRELOAD__: boolean | undefined;
|
|
@@ -60,6 +62,8 @@ export const DRAWER = typeof __LBX_DRAWER__ === 'boolean' ? __LBX_DRAWER__ : tru
|
|
|
60
62
|
export const MENU = typeof __LBX_MENU__ === 'boolean' ? __LBX_MENU__ : true;
|
|
61
63
|
/** `data-lbx-dropdown` in any file of the integration: `<details>` menus in the listing's markup, closed from outside. */
|
|
62
64
|
export const DROPDOWN = typeof __LBX_DROPDOWN__ === 'boolean' ? __LBX_DROPDOWN__ : true;
|
|
65
|
+
/** `data-lbx-full-screen-input` in any file of the integration: a light-DOM autocomplete's own phone sheet (widgets/autocomplete, `fullScreen`). */
|
|
66
|
+
export const LIGHT_FULL_SCREEN = typeof __LBX_LIGHT_FULL_SCREEN__ === 'boolean' ? __LBX_LIGHT_FULL_SCREEN__ : true;
|
|
63
67
|
/** The autocomplete's `onOpen`, `onClose`, `onSelect`, `onReady` named in the integration's code. */
|
|
64
68
|
export const AC_HOOKS = typeof __LBX_AC_HOOKS__ === 'boolean' ? __LBX_AC_HOOKS__ : true;
|
|
65
69
|
/** A listing's `onRender` named in the integration's code. */
|
|
@@ -81,10 +85,13 @@ export const INITS = typeof __LBX_INITS__ === 'boolean' ? __LBX_INITS__ : true;
|
|
|
81
85
|
* or code setting them): a key no file names keeps its default, as only
|
|
82
86
|
* code naming it can change it. `emptyValues`, `prerenderWhileTyping` (or
|
|
83
87
|
* `pageWhileTyping`: prerendering the search page typed for), `contentVisibility`.
|
|
88
|
+
* `variants` (a listing's products' variant groups, widgets/listing/variants.ts):
|
|
89
|
+
* the key in a folder's _config.ts only (the word alone is common).
|
|
84
90
|
*/
|
|
85
91
|
export const EMPTY_VALUES = typeof __LBX_EMPTY_VALUES__ === 'boolean' ? __LBX_EMPTY_VALUES__ : true;
|
|
86
92
|
export const PRERENDER_TYPING = typeof __LBX_PRERENDER_TYPING__ === 'boolean' ? __LBX_PRERENDER_TYPING__ : true;
|
|
87
93
|
export const CONTENT_VISIBILITY = typeof __LBX_CONTENT_VISIBILITY__ === 'boolean' ? __LBX_CONTENT_VISIBILITY__ : true;
|
|
94
|
+
export const VARIANTS = typeof __LBX_VARIANTS__ === 'boolean' ? __LBX_VARIANTS__ : true;
|
|
88
95
|
/**
|
|
89
96
|
* Server-rendered listings (Luigi's Box's server-side includes): adopting
|
|
90
97
|
* and hydrating the server's markup. Declared, not found in the folders:
|
package/src/core/integration.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// templates, translations and helpers the bundle would.
|
|
6
6
|
|
|
7
7
|
import type { LumaApi, LumaConfigType, LumaInitType } from '../types';
|
|
8
|
-
import { AUTOCOMPLETE, HOST_STYLES, INITS, PLP, RECOMMENDER, SEARCH, TRANSLATIONS } from './features';
|
|
8
|
+
import { AUTOCOMPLETE, HOST_STYLES, INITS, PLP, RECOMMENDER, SEARCH, TRANSLATIONS, VARIANTS } from './features';
|
|
9
9
|
|
|
10
10
|
export type Lazy<T> = Record<string, () => Promise<T>>;
|
|
11
11
|
|
|
@@ -106,9 +106,14 @@ export const loadConfig = async (files: Pick<IntegrationFiles, 'configs' | 'init
|
|
|
106
106
|
if (config._global.disabled || !config._global.publishableKey || !config._global.channelId) return null;
|
|
107
107
|
const apps = [AUTOCOMPLETE ? config.autocomplete : null, SEARCH ? config.search : null, PLP ? config.plp : null, ...(RECOMMENDER ? Object.values(config.recommenders ?? {}) : [])];
|
|
108
108
|
if (INITS) for (const app of apps) if (app && !app.disabled && app.initialize && (await (app.initialize as any)(app, Luma)) === false) app.disabled = true;
|
|
109
|
+
// `variants` comes into a build from a folder's _config.ts only (build/vite.mjs
|
|
110
|
+
// featureDefines): set by an _init.ts, a build would leave it out unsaid.
|
|
111
|
+
if (import.meta.env.DEV && (config.search?.variants || config.plp?.variants) && !Object.values(files.configs).some((v: any) => v && typeof v === 'object' && 'variants' in v))
|
|
112
|
+
console.error("[lbx] variants: set it in a folder's _config.ts; a build leaves it out otherwise");
|
|
109
113
|
// Anything a category page does not set is the search page's.
|
|
110
114
|
const plp = PLP ? config.plp : null;
|
|
111
|
-
|
|
115
|
+
// `variants` only where the integration names it (core/features.ts): not a key of every entry.
|
|
116
|
+
if (plp) for (const k of VARIANTS ? [...LISTING, 'variants' as const] : LISTING) if (plp[k] == null && config.search?.[k] != null) (plp as any)[k] = config.search[k];
|
|
112
117
|
if (SEARCH || PLP) for (const l of [config.search, config.plp]) if (l && l.reserveSelector == null && l.anchor) l.reserveSelector = [l.anchor].flat()[0];
|
|
113
118
|
return config;
|
|
114
119
|
};
|
package/src/locales/cs.json
CHANGED
package/src/locales/de.json
CHANGED
package/src/locales/en.json
CHANGED
package/src/types.ts
CHANGED
|
@@ -16,6 +16,14 @@ export interface Result {
|
|
|
16
16
|
type: string;
|
|
17
17
|
a: Hit;
|
|
18
18
|
c: Record<string, any>;
|
|
19
|
+
/**
|
|
20
|
+
* A listing's `variants`: the other products of this one's variant group
|
|
21
|
+
* (collapsed into it by the surface), as results (`siblings`, by `@id`, at
|
|
22
|
+
* most 100), and the group's size with this one (`total`). Undefined until
|
|
23
|
+
* Luigi's Box has answered (the tile is drawn again then), on the server,
|
|
24
|
+
* and outside listings.
|
|
25
|
+
*/
|
|
26
|
+
variants?: { siblings: Result[]; total: number };
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
/**
|
|
@@ -80,7 +88,9 @@ export interface GlobalConfigType {
|
|
|
80
88
|
* Two in flight, the latest intent wins; 20 per category or query; none
|
|
81
89
|
* offline, with Save-Data or on 2G; range sliders never. A hover that is
|
|
82
90
|
* not followed costs one search request (no render, no hook, no analytics
|
|
83
|
-
* event).
|
|
91
|
+
* event). In a drawer that stages (`facetsApply`), the selection it holds
|
|
92
|
+
* is fetched the same way once the changes pause, for its apply button.
|
|
93
|
+
* Default true.
|
|
84
94
|
*/
|
|
85
95
|
prefetchFilters?: boolean;
|
|
86
96
|
/**
|
|
@@ -235,6 +245,23 @@ export interface AutocompleteConfigType extends AppConfig<AutocompleteConfigType
|
|
|
235
245
|
dom?: 'shadow' | 'light';
|
|
236
246
|
hostClass?: string;
|
|
237
247
|
hostId?: string;
|
|
248
|
+
/**
|
|
249
|
+
* `dom: 'light'`: the autocomplete full screen on a phone, with the
|
|
250
|
+
* integration's own markup. `true` where the house design is full screen
|
|
251
|
+
* (a viewport up to 600 px, or a short touch screen), or a media query of
|
|
252
|
+
* the site's own (`'(max-width: 767px)'`). There, focus or a tap on the
|
|
253
|
+
* site's input opens it with what the input holds (templates get
|
|
254
|
+
* `fullScreen: true`, `state: 'idle'` until a phrase is long enough); the
|
|
255
|
+
* template renders its own input, marked `data-lbx-full-screen-input`,
|
|
256
|
+
* which takes the focus and is the combobox there.
|
|
257
|
+
* The integration's CSS places it, keyed on `[data-lbx-full-screen]` while open
|
|
258
|
+
* (`inset: 0; top: var(--lbx-top, 0px)`): Luma sets no box on it, only `--lbx-top` (the
|
|
259
|
+
* visual viewport's top) and `--lbx-cover` (what Safari's bars and the
|
|
260
|
+
* keyboard cover of its bottom: the list's end space and
|
|
261
|
+
* `scroll-padding-bottom`). Off by default: a light-DOM dropdown
|
|
262
|
+
* on a phone stays a dropdown. The house design is always full screen there.
|
|
263
|
+
*/
|
|
264
|
+
fullScreen?: boolean | string;
|
|
238
265
|
/**
|
|
239
266
|
* What the dropdown lines up with (its left edge and width, just below it).
|
|
240
267
|
* By default the visible search field: the input when it draws its own
|
|
@@ -295,6 +322,21 @@ export interface ListingConfigType {
|
|
|
295
322
|
sorts: string[];
|
|
296
323
|
/** return_fields for the tiles (default: the surface's). */
|
|
297
324
|
fields?: string[];
|
|
325
|
+
/**
|
|
326
|
+
* The variant groups of the products shown, for a surface that collapses
|
|
327
|
+
* them (one result per group: a shoe in eight sizes is one tile): each
|
|
328
|
+
* tile's result gets the group's other products as `r.variants` (their
|
|
329
|
+
* `fields`, e.g. the in-stock sizes on hover or focus). Asked from Luigi's
|
|
330
|
+
* Box once per page of results, as soon as it is here (a page restored on
|
|
331
|
+
* Back has them with it): the tiles show without them first and are drawn
|
|
332
|
+
* again when they come, never waited for; without them (an error) they
|
|
333
|
+
* stay as they are. A tile keeps its box with and without them (a reserved
|
|
334
|
+
* row, an overlay, on hover or focus): a tile growing when they come moves
|
|
335
|
+
* the rows below. `fields`: return_fields of the group's products (keep it
|
|
336
|
+
* to what the tiles show). Off by default; set it in `_config.ts` (the
|
|
337
|
+
* build includes it only where a folder's `_config.ts` does).
|
|
338
|
+
*/
|
|
339
|
+
variants?: { fields: string[] };
|
|
298
340
|
/** Always-on filters (e.g. what this storefront may show), AND-ed with the shopper's. */
|
|
299
341
|
baseFilters?: FilterNode;
|
|
300
342
|
/**
|
|
@@ -333,10 +375,22 @@ export interface ListingConfigType {
|
|
|
333
375
|
* How a change inside the open filter drawer (`[data-lbx-drawer-panel]`,
|
|
334
376
|
* opened by `[data-lbx-drawer="open"]`) applies. 'instant' (default): at
|
|
335
377
|
* once, as in a sidebar. 'staged': held in the drawer (it shows the pending
|
|
336
|
-
* selection, the scope's `pending` is true,
|
|
337
|
-
* apply button (`[data-lbx-drawer="apply"]`) commits them
|
|
338
|
-
* and one history entry; closing it otherwise
|
|
339
|
-
* Escape) discards them.
|
|
378
|
+
* selection, the scope's `pending` is true, the results behind it do not
|
|
379
|
+
* change) until its apply button (`[data-lbx-drawer="apply"]`) commits them
|
|
380
|
+
* in one request and one history entry; closing it otherwise
|
|
381
|
+
* (`[data-lbx-drawer="close"]`, Escape) discards them. Once the changes
|
|
382
|
+
* pause, the held selection's results are fetched in the background
|
|
383
|
+
* (unless `_global.prefetchFilters` is false, with Save-Data or on 2G),
|
|
384
|
+
* so Apply shows them without waiting; nothing is shown and `onRender`
|
|
385
|
+
* does not run before Apply. Apply's swap is marked: the old list fades
|
|
386
|
+
* out, the new tiles come in, and a status at the listing's top says what
|
|
387
|
+
* was applied ("Filters applied · 9 products", translation
|
|
388
|
+
* `search.applied`) for 2.5 s, in `[data-lbx-applied-cue]`'s shadow root
|
|
389
|
+
* (out of reach of the site's resets; themed with `--lbx-applied-bg`,
|
|
390
|
+
* `--lbx-applied-fg`, `--lbx-applied-top` for a sticky header's height,
|
|
391
|
+
* `--lbx-applied-offset` to sit below a heading at the listing's top, or
|
|
392
|
+
* restyled with `[data-lbx-applied-cue]::part(pill)`), with
|
|
393
|
+
* `data-lbx-applied` on the listing meanwhile for the site's own CSS. Per viewport min-width, e.g. `{ "0": "staged",
|
|
340
394
|
* "768": "instant" }` (staged on phones). Outside an open drawer every
|
|
341
395
|
* change is instant.
|
|
342
396
|
*/
|
|
@@ -752,11 +806,24 @@ export interface AutocompleteScope extends Helpers {
|
|
|
752
806
|
* input reaches it (the options stay out of the Tab order).
|
|
753
807
|
*/
|
|
754
808
|
allHref: string;
|
|
755
|
-
|
|
756
|
-
/** 'idle' (the phone sheet before a phrase is long enough), 'results', 'empty' (every type answered nothing). */
|
|
809
|
+
/** 'idle' (full screen, before a phrase is long enough), 'results', 'empty' (every type answered nothing). */
|
|
757
810
|
state: 'idle' | 'results' | 'empty';
|
|
758
|
-
/**
|
|
759
|
-
|
|
811
|
+
/** Full screen on a phone (a light-DOM integration's with the config's `fullScreen`) rather than the dropdown under the input. */
|
|
812
|
+
fullScreen: boolean;
|
|
813
|
+
/** Full screen: the tabs, one per type with results (products first); `select()` shows it. The arrow keys go through the open tab's options. */
|
|
814
|
+
tabs: { kind: string; label: string; selected: boolean; select: () => void }[];
|
|
815
|
+
/**
|
|
816
|
+
* Full screen: its own input (`data-lbx-full-screen-input`): its value, placeholder
|
|
817
|
+
* and name (the site's input's); `clear()` empties it (focus stays in it);
|
|
818
|
+
* `back()` closes it as Escape and the browser's Back do, the site's
|
|
819
|
+
* input back to what it held when it opened.
|
|
820
|
+
*/
|
|
821
|
+
value: string;
|
|
822
|
+
placeholder: string;
|
|
823
|
+
inputLabel: string;
|
|
824
|
+
clear: () => void;
|
|
825
|
+
back: () => void;
|
|
826
|
+
// --- The house design (dom: 'shadow'). -------
|
|
760
827
|
/** The panel is at least 1000 px wide: the top product has its own column. */
|
|
761
828
|
wide: boolean;
|
|
762
829
|
/** Query suggestions (pills). */
|
|
@@ -767,8 +834,6 @@ export interface AutocompleteScope extends Helpers {
|
|
|
767
834
|
hero: { r: Result; i: number } | null;
|
|
768
835
|
/** The products after it (at most 6 on the panel). */
|
|
769
836
|
products: { r: Result; i: number }[];
|
|
770
|
-
/** The phone sheet's tabs: one per type with results (products first); `select()` shows it. */
|
|
771
|
-
tabs: { kind: string; label: string; selected: boolean; select: () => void }[];
|
|
772
837
|
/** "No results": phrases to try (options), links to browse (options), fallback products (options), the help box. */
|
|
773
838
|
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 };
|
|
774
839
|
/** Pointer over an option: it becomes the selection (the keyboard's too). */
|
|
@@ -782,12 +847,6 @@ export interface AutocompleteScope extends Helpers {
|
|
|
782
847
|
field: (r: Result, name: 'availability' | 'category' | 'oldPrice' | 'meta' | 'price' | 'badge') => string;
|
|
783
848
|
/** The top product's spec rows (`hero.specFields`, at most 4). */
|
|
784
849
|
specs: (r: Result) => { label: string; value: string }[];
|
|
785
|
-
/** The phone sheet: its input's value, placeholder and name (the site's input's), clearing it, and its back button. */
|
|
786
|
-
value: string;
|
|
787
|
-
placeholder: string;
|
|
788
|
-
inputLabel: string;
|
|
789
|
-
clear: () => void;
|
|
790
|
-
back: () => void;
|
|
791
850
|
}
|
|
792
851
|
export interface AutocompleteResultScope extends AutocompleteScope {
|
|
793
852
|
r: Result;
|