@luigisbox/luma 0.3.0 → 0.5.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 (65) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +95 -14
  3. package/build/baseline.mjs +2 -2
  4. package/build/vite.mjs +253 -20
  5. package/docs/ACCESSIBILITY.md +9 -2
  6. package/package.json +3 -2
  7. package/src/analytics/chain.ts +10 -2
  8. package/src/analytics/collect.ts +14 -7
  9. package/src/analytics/from-ga4.ts +5 -2
  10. package/src/analytics/from-luma.ts +6 -4
  11. package/src/analytics/index.ts +1 -1
  12. package/src/analytics/pageview.ts +1 -1
  13. package/src/analytics/start.ts +3 -4
  14. package/src/analytics/wire.ts +1 -1
  15. package/src/api/ai.ts +32 -20
  16. package/src/api/token.ts +7 -7
  17. package/src/core/a11y.ts +6 -35
  18. package/src/core/context.ts +2 -0
  19. package/src/core/contrast-toggle.ts +32 -0
  20. package/src/core/default-button.ts +66 -0
  21. package/src/core/early-rules.ts +18 -4
  22. package/src/core/features.ts +109 -0
  23. package/src/core/frameworks-check.ts +32 -0
  24. package/src/core/frameworks.ts +35 -0
  25. package/src/core/image-load.ts +0 -26
  26. package/src/core/image-preload.ts +29 -0
  27. package/src/core/image-tags.ts +7 -1
  28. package/src/core/images.ts +3 -2
  29. package/src/core/integration.ts +42 -26
  30. package/src/core/listings.ts +9 -5
  31. package/src/core/locales.ts +2 -1
  32. package/src/core/own.ts +7 -3
  33. package/src/core/recommenders.ts +2 -2
  34. package/src/core/reconciler.ts +32 -23
  35. package/src/core/routes.ts +7 -6
  36. package/src/core/scheduler.ts +39 -5
  37. package/src/core/speculation.ts +2 -0
  38. package/src/core/stash.ts +15 -6
  39. package/src/core/styles.ts +6 -5
  40. package/src/core/support.ts +1 -1
  41. package/src/core/takeover.ts +6 -5
  42. package/src/core/templates.ts +5 -2
  43. package/src/core/tiles.ts +33 -10
  44. package/src/core/url.ts +6 -3
  45. package/src/core/widget-inits.ts +4 -1
  46. package/src/lit.ts +2 -1
  47. package/src/locales/cs.json +0 -1
  48. package/src/locales/de.json +0 -1
  49. package/src/locales/en.json +0 -1
  50. package/src/main.ts +70 -53
  51. package/src/types.ts +29 -9
  52. package/src/widgets/autocomplete/index.ts +58 -19
  53. package/src/widgets/listing/element.ts +81 -58
  54. package/src/widgets/listing/query.ts +6 -3
  55. package/src/widgets/listing/stash.ts +33 -16
  56. package/src/widgets/listing/state.ts +12 -0
  57. package/src/widgets/listing/view.ts +2 -1
  58. package/src/widgets/plp/route.ts +3 -2
  59. package/src/widgets/recommender/index.ts +3 -2
  60. package/src/widgets/search/direct.ts +39 -0
  61. package/src/widgets/search/form-url.ts +38 -34
  62. package/src/widgets/search/in-place.ts +43 -108
  63. package/src/widgets/search/index.ts +7 -2
  64. package/src/widgets/search/speculate.ts +48 -27
  65. package/src/widgets/search/submit.ts +136 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,118 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@luigisbox/luma`. Follows [semver](https://semver.org);
4
+ before 1.0, a minor version may break the integration API.
5
+
6
+ ## Unreleased
7
+
8
+ ## 0.5.0 — 2026-10-19
9
+
10
+ Upgrading from 0.4.0: read **Changed** first.
11
+
12
+ ### Added
13
+
14
+ - `search.queryUrl` hook: the search page URL for a phrase. Use it when the
15
+ site's server redirects its search form (e.g. `/search?q=x` → `/search/x/`).
16
+ Enter, suggestions and "show all results" then go straight to that page.
17
+ - `search.pageWhileTyping`: `'prefetch'` (default), `'prerender'` or `false`.
18
+ `prerenderWhileTyping: true` equals `'prerender'`.
19
+ - `clientConfig` option `framework` (name or list): build in support for these
20
+ frameworks only. List every framework that renders part of a page Luma runs
21
+ on. Unknown names fail the build.
22
+ - `clientConfig` option `serverRendering: false`: leave out hydration of
23
+ server-rendered listings.
24
+ - `clientConfig` option `locales`: ship only these dictionaries (plus English).
25
+ Unknown locales or an empty list fail the build.
26
+
27
+ ### Changed
28
+
29
+ - The build includes only widgets that have a folder (`autocomplete/`,
30
+ `search/`, `plp/`, `recommender <name>/`). Config for a widget without a
31
+ folder is ignored. A `recommender` folder without a name fails the build.
32
+ `yarn dev` still includes every widget.
33
+ - These are built only if their name appears literally in a `.ts`, `.js` (or
34
+ variant) or `.html` file of the integration's repository, outside
35
+ `node_modules`, `dist` and hidden folders:
36
+ - attributes `data-lbx-cart`, `data-lbx-drawer`, `data-lbx-menu`,
37
+ `data-lbx-dropdown`
38
+ - hooks `onRender`, `readQuery`, `queryUrl`, `facetsFor`, `onOpen`,
39
+ `onClose`, `onSelect`, `onReady`
40
+ - options `emptyValues`, `prerenderWhileTyping` / `pageWhileTyping`,
41
+ `contentVisibility`
42
+
43
+ A name that only appears in a `.vue`, `.svelte` or `.hbs` file or in a
44
+ package does not count.
45
+ - The search page for the typed phrase is prefetched by default. Set
46
+ `search.pageWhileTyping: false` to turn it off.
47
+ - Results fetched while typing are cached per query, not per URL.
48
+ - Enter in the search form follows the browser's implicit submission rules.
49
+ - The `_config.ts` check (no functions) runs in development only.
50
+
51
+ ### Fixed
52
+
53
+ - WebKit: when frames stalled, the site's listing could show instead of
54
+ Luma's.
55
+ - Safari: listing images were all fetched on render, twice. Bind `src` last
56
+ in your own `<img>` templates too.
57
+ - WebKit: two chunks were fetched twice per page.
58
+ - Autocomplete: ARIA attributes could stay on the site's input after focus
59
+ left.
60
+
61
+ ## 0.4.0 — 2026-10-16
62
+
63
+ ### Added
64
+
65
+ - Readers can return `{ reference, attributedToList: true }` for clicks and
66
+ add-to-carts on the site's own lists.
67
+
68
+ ### Fixed
69
+
70
+ - Analytics: clicks and add-to-carts from Luma's widgets were never credited
71
+ to the search or recommendation that showed the product. They now carry
72
+ `attributed_to_list: true`, as does GA4 `select_item`.
73
+ - Early hold now covers search routes with the query in the path (`urlMatch`
74
+ with a capture group). The snippet is unchanged.
75
+
76
+ ## 0.3.0 — 2026-10-15
77
+
78
+ ### Added
79
+
80
+ - `alignTo: { field, column }` for the autocomplete panel. By default the
81
+ panel stays within the page's content column.
82
+
83
+ ### Changed
84
+
85
+ - Cancelling the phone autocomplete sheet restores the site's input value.
86
+ - Back closes an open staged filter drawer (`facetsApply`) and discards its
87
+ changes.
88
+ - Reloading a listing within a minute after "show more" restores it as left.
89
+
90
+ ### Fixed
91
+
92
+ - Phone autocomplete sheet: sized to the visible viewport, shown in
93
+ landscape, `autocorrect="off"`, keeps letters typed while it opens.
94
+ - Autocomplete: lists scroll to top on new results; top product's prices
95
+ sit under its title.
96
+ - iOS Safari: Back to a listing restores position and "show more" pages.
97
+ - Listing position survives a reload or a redirect back within a minute.
98
+ - Position is restored at the clicked link, not a hidden copy.
99
+ - Staged drawer: Apply scrolls to the listing's top; crossing a breakpoint
100
+ closes the drawer.
101
+ - A filter group opened by a selection stays open when the value is
102
+ unticked.
103
+ - Range slider: touch drag on the track follows the finger; vertical scroll
104
+ starting on the track doesn't move a thumb.
105
+
106
+ ## 0.2.0 — 2026-10-13
107
+
108
+ ### Added
109
+
110
+ - Analytics, configured in `_global/_analytics.ts`: one `collect.<event>` line
111
+ per event, with triggers (`lumaPages`, `lumaClicks`, `lumaCarts`, `dl`,
112
+ `domClick`, `urlChange`, `mutationOf`, `xhrMatching`, `manual`) and readers
113
+ (`fromLuma`, `fromGa4` or a function). `analytics: false` turns it off.
114
+ - `yarn luma chrome --allow HOST`: keep that host's Luigi's Box scripts.
115
+
116
+ ## 0.1.0 — 2026-10-12
117
+
118
+ First published version.
package/README.md CHANGED
@@ -10,10 +10,10 @@ 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.4.0) may change the
13
+ > **Pre-release.** Until 1.0, a minor version (0.6.0) may change the
14
14
  > integration API (config keys, template scope, the build); a patch version
15
- > (0.3.1) only fixes. Depend on `^0.3.0` to take fixes without breaking
16
- > changes.
15
+ > (0.5.1) only fixes. Depend on `^0.5.0` to take fixes without breaking
16
+ > changes. [`CHANGELOG.md`](CHANGELOG.md) says what each version changed.
17
17
 
18
18
  ## Core principles
19
19
 
@@ -245,6 +245,51 @@ import { clientConfig } from '@luigisbox/luma/build/vite.mjs';
245
245
  export default clientConfig(import.meta.url, { plugins: [mkcert()], server: { host: 'localhost', https: {} } });
246
246
  ```
247
247
 
248
+ `framework` names the site's frontend framework, so the build carries Luma's
249
+ support for that framework only. Without it, every integration carries the
250
+ support for all of them (waiting for each one's hydration, writing URLs through
251
+ its router, keeping out of its page snapshots), and Luma tells them apart on
252
+ each page. Declared, a listing page loads about 1.4 to 2.3 kB gzip less, and
253
+ every page about 0.2 kB less unless React hydrates the whole document. It is
254
+ one of the following, or a list of them:
255
+
256
+ | `framework` | the site |
257
+ |---|---|
258
+ | `next` | Next.js (app or pages router) |
259
+ | `remix`, `react-router` | Remix, React Router 7, Hydrogen |
260
+ | `react` | a React app in a root element (Vite, Create React App) |
261
+ | `vue`, `nuxt` | Vue 2 or 3, Nuxt |
262
+ | `angular` | Angular |
263
+ | `svelte`, `sveltekit` | a Svelte app, SvelteKit |
264
+ | `astro` | Astro (add the framework of its islands that hold Luma's anchors) |
265
+ | `turbo` | Turbo Drive or Turbolinks |
266
+ | `none` | server-rendered pages: jQuery, htmx, Alpine |
267
+
268
+ ```ts
269
+ export default clientConfig(import.meta.url, { framework: 'vue', plugins: [mkcert()], server: { host: 'localhost', https: {} } });
270
+ ```
271
+
272
+ `serverRendering: false` declares that the site has no server-rendered
273
+ listings (the server-side includes above): the build then leaves out adopting
274
+ and hydrating the server's markup, about 0.9 kB gzip on a listing page. Leave
275
+ it out when the site uses them or may; in development Luma reports
276
+ server-rendered markup a build without them meets.
277
+
278
+ `locales` lists the locales the site uses (`['cs', 'sk']`): every page then
279
+ carries the map of those dictionaries and English only (about 20 B gzip per
280
+ locale left out). A widget in a locale not listed reads English. Without it,
281
+ every locale Luma has a dictionary for. A locale Luma has no dictionary for
282
+ fails the build.
283
+
284
+ List every framework that renders a part of the page Luma places into or
285
+ navigates with, on every page Luma runs on (`['astro', 'react']` for an Astro
286
+ site with React islands). Without its framework's support, Luma does not
287
+ wait for that framework's hydration: React or Svelte renders the page anew,
288
+ Vue drops Luma's element, Angular binds the wrong element, a router puts its
289
+ own URL back. In development (`yarn dev`) Luma reports a framework the page
290
+ uses and the build leaves out in `window.__lbx.health.errors`. An unknown name
291
+ fails the build.
292
+
248
293
  Its `tsconfig.json` extends `@luigisbox/luma/tsconfig.json`, includes
249
294
  `integration/**/*` and `node_modules/@luigisbox/luma/src/**/*`, and maps `@/*`
250
295
  to `./node_modules/@luigisbox/luma/src/*`. Its `package.json` depends on
@@ -265,19 +310,31 @@ integration/
265
310
  recommender <name>/ _config.ts [_init.ts] _translations.ts
266
311
  ```
267
312
 
268
- - **`_config.ts` is JSON** (checked at boot); anything that needs code goes in
313
+ - **`_config.ts` is JSON** (checked at boot in development: `yarn dev`);
314
+ anything that needs code goes in
269
315
  `_init.ts`. `_global/_init.ts` runs first and can change any config or return
270
316
  `false` to stop; a widget's `_init.ts` gets that widget's config. Every config
271
317
  key is typed and documented in `src/types.ts`.
318
+ - **A widget is its folder.** The build carries only the widgets the
319
+ integration has folders for (`autocomplete/`, `search/`, `plp/`,
320
+ `recommender <name>/`) and analytics only where a folder has an
321
+ `_analytics.ts`: an integration with an autocomplete only loads about half
322
+ as much on every page as one with every widget. A config `_global/_init.ts`
323
+ creates for a widget without its folder is ignored. The development server
324
+ (`yarn dev`) carries every widget.
272
325
  - **`_init.ts` ships and runs on every page; `_widget.ts` loads with its
273
326
  widget.** Keep in `_init.ts` only what decides whether and where a widget
274
- runs (`disabled`, `active`, `readQuery`, `items`, `intent`, selectors set in
327
+ runs (`disabled`, `active`, `readQuery`, `queryUrl`, `items`, `intent`, selectors set in
275
328
  code, switching the site's own widgets off). Hooks only a widget calls
276
329
  (`computedAttributes`, `toFilter`, `facetsFor`, `onRender`, `onOpen`, …) go
277
330
  in the folder's optional `_widget.ts`, same signature (its return value is
278
331
  ignored): it is loaded with the widget's chunk and run once before its
279
332
  context is made, `_global/_widget.ts` first. `__integration.ts` hands it
280
333
  over as a lazy glob: `widgetInits: import.meta.glob('./*/_widget.ts', { import: 'default' })`.
334
+ The build calls `onRender`, `readQuery`, `queryUrl`, `facetsFor` and the autocomplete's
335
+ `onOpen`, `onClose`, `onSelect`, `onReady` only where a file of the
336
+ integration's repository (a script or `.html` file, not in `node_modules`, `dist` or a hidden folder) names them, as setting
337
+ them does (`config.search.onRender = …`).
281
338
  - **`style-host.css`** hides the host's own listing or suggestions, only while
282
339
  Luma has the page. Other `style-*.css` load with their widget. Luma's built-in
283
340
  CSS sits in `@layer lbx`, so the integration's and the site's CSS win.
@@ -328,7 +385,10 @@ export default ({ r, i, img, price }: SearchResultScope) => html`
328
385
  - **Helpers** in every scope: `t(key, params)`, `price(n)`, `plain()`,
329
386
  `first()`, `include()`, and `img(r, i)` (a complete `<img>` in its placeholder
330
387
  box) / `image(r, i)` (its attributes, for your own markup), configured by
331
- `_global.images` (srcset, sizes, the eager first row, placeholders).
388
+ `_global.images` (srcset, sizes, the eager first row, placeholders). In
389
+ your own `<img>`, bind `src` last: Safari starts the fetch when `src` is
390
+ set, so a `loading` or `srcset` bound after it loads every image at once,
391
+ and twice.
332
392
  - **Pagination** is "show more" only: templates get `results`, `more` and
333
393
  `showMore`; there are no numbered pages.
334
394
  - **No results under filters**: the scope still holds the selected groups,
@@ -348,11 +408,17 @@ export default ({ r, i, img, price }: SearchResultScope) => html`
348
408
  ### Behaviour switches
349
409
 
350
410
  On by default unless noted; each falls back to what the page would do anyway.
411
+ `emptyValues`, `prerenderWhileTyping` and `contentVisibility`, off by
412
+ default, are in the build only where a file of the integration's repository
413
+ (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; a switch on by default that no file names is built as on.
351
415
 
352
416
  | Config | What it does | Turn it off when |
353
417
  |---|---|---|
354
418
  | `search.inPlaceQuery` | a new query on the search page renders in place, without a page load | the host's page-view analytics or server-rendered parts of the search page must follow every query |
355
419
  | `search.prefetchWhileTyping` | the search result is fetched once the typed phrase settles, so Enter shows it at once | |
420
+ | `search.pageWhileTyping` (`'prefetch'`, `'prerender'`, `false`) | the search page of the typed phrase is fetched ahead (Chrome, Edge; Firefox and Safari as they ship it), so Enter from another page skips the site's server render; `'prerender'` renders it ahead as well (Chrome, Edge) | the site cannot take a document request per typed phrase; `'prerender'` only where its tags wait for `prerenderingchange` |
421
+ | `search.queryUrl` (an `_init.ts` hook, off unless set) | where the site's server redirects its search form to the query's page (`/search?q=x` to `/search/x/`), names that page: Enter, the autocomplete's "show all" and a picked query load it directly, without the redirect, and the result fetched while typing is shown there | the form's other fields decide the page, or the site must handle every search itself |
356
422
  | `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 | |
357
423
  | `prefetchNextPage` | "show more" is fetched as the shopper nears the end | |
358
424
  | `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 |
@@ -405,8 +471,8 @@ collect.purchase.on(dl('purchase')).read(fromGa4.purchase);
405
471
  | Event | What `.read()` returns |
406
472
  |---|---|
407
473
  | `pageView` | the page's reference, or `null` for a page without one. On `lumaPages()`, one pageview per page, sent at once. On any other trigger, one per page that waits for its reference from that trigger (`.timeout(ms)`, default 1000 ms) and is sent without one after that. One `pageView` line per file |
408
- | `click` | a product click: a reference, or `{ reference }` |
409
- | `addToCart` | `{ reference, count?, price? }`, or a list of them |
474
+ | `click` | a product click: a reference, or `{ reference, attributedToList? }` |
475
+ | `addToCart` | `{ reference, count?, price?, attributedToList? }`, or a list of them |
410
476
  | `purchase` | `{ id?, currency?, items: [{ reference, title, count, total_price, was_discounted? }] }`; sent once per `id`, so a reloaded thank-you page does not count twice |
411
477
  | `search`, `autocomplete`, `recommender(name)` | a list from the site's own service: `{ query?, items, filters? }`. Not sent on a page where Luma runs that service itself (the API records Luma's own results) |
412
478
 
@@ -416,7 +482,7 @@ collect.purchase.on(dl('purchase')).read(fromGa4.purchase);
416
482
  |---|---|---|
417
483
  | `lumaPages()` | each page: the first, the next page of a single-page app, a page restored from the back/forward cache, a prerendered page once it is shown. Filtering or sorting a Luma listing is not a new page | `{ url, referrer }` |
418
484
  | `lumaClicks()` | a click on a product in a Luma widget: any link to it, middle and modifier clicks included, and a pick in the autocomplete | `{ reference, result, index, list }` |
419
- | `lumaCarts()` | a click on a `[data-lbx-cart]` button in a Luma product tile (its value: the quantity). `_global.addToCart(result, { count, el, event })` adds the product to the site's cart, with or without analytics | `{ reference, result, index, list, count, price? }` |
485
+ | `lumaCarts()` | a click on a `[data-lbx-cart]` button in a Luma product tile (its value: the quantity). `_global.addToCart(result, { count, el, event })` adds the product to the site's cart, with or without analytics. The attribute is written in a template of the integration as `data-lbx-cart`: the build includes add-to-cart only where a file of the integration's repository (a script or `.html` file, not in `node_modules`, `dist` or a hidden folder) has it | `{ reference, result, index, list, count, price? }` |
420
486
  | `dl(event)` | a dataLayer push with that event name, object or `gtag()` form, including pushes made before Luma loaded. `_global.dataLayer` in `_config.ts` names another layer | `{ event, eventType, data, ecommerce }` |
421
487
  | `domClick(selector)` | a click (or middle click) on an element of the site's that matches `selector` | `{ element, event }` |
422
488
  | `urlChange()` | a navigation in a single-page app | `{ url, previousUrl }` |
@@ -428,12 +494,26 @@ collect.purchase.on(dl('purchase')).read(fromGa4.purchase);
428
494
 
429
495
  | Reader | Reads |
430
496
  |---|---|
431
- | `fromLuma.pageView`, `.click`, `.addToCart` | what Luma's own triggers saw (`lumaPages()`, `lumaClicks()`, `lumaCarts()`). Products are referenced by their catalog `@id` |
432
- | `fromGa4.addToCart`, `.purchase`, `.click`, `.pageView`, `.search`, `.autocomplete`, `.recommender` | GA4 ecommerce: `add_to_cart`, `purchase`, `select_item`, `view_item`, `view_item_list`. The data is taken from the push's `ecommerce` (or `gtag()`'s parameters), or from the push itself when its `items` are there. Items are referenced by `ref.product(item_id)`; purchases get line totals, and `was_discounted` from `discount` |
497
+ | `fromLuma.pageView`, `.click`, `.addToCart` | what Luma's own triggers saw (`lumaPages()`, `lumaClicks()`, `lumaCarts()`). Products are referenced by their catalog `@id`. Clicks and add-to-carts are in a list (`attributedToList: true`) |
498
+ | `fromGa4.addToCart`, `.purchase`, `.click`, `.pageView`, `.search`, `.autocomplete`, `.recommender` | GA4 ecommerce: `add_to_cart`, `purchase`, `select_item`, `view_item`, `view_item_list`. The data is taken from the push's `ecommerce` (or `gtag()`'s parameters), or from the push itself when its `items` are there. Items are referenced by `ref.product(item_id)`; purchases get line totals, and `was_discounted` from `discount`. `select_item` is a pick from a list (`attributedToList: true`); `add_to_cart` is not |
433
499
 
434
500
  Each reader belongs to one event: `collect.purchase…read(fromGa4.addToCart)`
435
501
  is a type error, and throws when the file runs.
436
502
 
503
+ **Clicks and add-to-carts in a list.** Luigi's Box credits a click or an
504
+ add-to-cart to the last search, suggestions, recommendation or listing the
505
+ shopper saw the product in only when the event says it happened in a list:
506
+ `attributedToList: true` from `.read()`, sent as `attributed_to_list: true`.
507
+ A click without it (a bare reference) counts for the product, never for a
508
+ list, so a click on a product page's own link is not credited to a search the
509
+ shopper made earlier. Luma's own tiles always say it (`fromLuma.click`,
510
+ `fromLuma.addToCart`, autocomplete picks included), and so does GA4's
511
+ `select_item` (`fromGa4.click`). A click on the site's own list, read with a
512
+ function of yours, says it when the element is one of that list's results:
513
+ return `{ reference, attributedToList: true }` (below). The list itself must
514
+ be known too: Luma's own results are; the site's own are when the setup
515
+ collects them (`collect.search`, `.autocomplete`, `.recommender(name)`).
516
+
437
517
  **References** follow `<type>/<field>:<value>`
438
518
  ([docs](https://docs.luigisbox.ai/analytics/references/)).
439
519
  `ref.product(value)` uses the field set by `ref.product.by(field)` (default
@@ -473,8 +553,8 @@ collect.search.on(mutationOf('#results')).once().read(({ element }) => ({
473
553
  query: { string: new URLSearchParams(location.search).get('q') ?? '' },
474
554
  items: [...element.querySelectorAll('[data-sku]')].map((tile, n) => ({ title: tile.textContent ?? '', type: 'product', reference: ref.product(tile.getAttribute('data-sku')!), position: n + 1 })),
475
555
  }));
476
- // clicks on those results
477
- collect.click.on(domClick('#results [data-sku] a')).read(({ element }) => ref.product(element.closest('[data-sku]')!.getAttribute('data-sku')!));
556
+ // clicks on those results: in that list
557
+ collect.click.on(domClick('#results [data-sku] a')).read(({ element }) => ({ reference: ref.product(element.closest('[data-sku]')!.getAttribute('data-sku')!), attributedToList: true }));
478
558
  // the site's suggestions, from its own API
479
559
  collect.autocomplete.on(xhrMatching(({ url }) => url.includes('/suggest'))).read(({ response }) => ({ items: /* … */ [] }));
480
560
  // the product page's product, from GA4's view_item
@@ -508,7 +588,8 @@ integration repository").
508
588
  add-to-carts on Luma's products and dataLayer pushes are kept for the next
509
589
  page. A/B tests are not supported yet (`listing_ab_test` is always `false`).
510
590
  - **Prerendering** costs the site a page render per unfollowed hover
511
- (`speculation: false` turns it off).
591
+ (`speculation: false` turns it off), and the search page fetched ahead
592
+ while typing a document per settled phrase (`search.pageWhileTyping: false`).
512
593
  - **Early hold**: the snippet carries no site's rules, so the first page
513
594
  view is not held.
514
595
  - **Host routers out of reach**: where Luma cannot find the host's router (Vue
@@ -53,8 +53,8 @@ export const ALLOW = [
53
53
  {
54
54
  // Limited availability (Chromium only). document.prerendering through `any`, as above.
55
55
  feature: 'speculation-rules',
56
- files: ['src/core/speculation.ts', 'src/core/takeover.ts', 'src/main.ts'],
57
- why: "The prerender rule is added only when HTMLScriptElement.supports('speculationrules'); document.prerendering is undefined (falsy) elsewhere, so a hold's timer starts at once.",
56
+ files: ['src/core/speculation.ts', 'src/core/takeover.ts', 'src/main.ts', 'src/widgets/search/speculate.ts'],
57
+ why: "The product-page prerender rule, and the search page of the phrase typed fetched ahead (prerendered with pageWhileTyping 'prerender'), are added only when HTMLScriptElement.supports('speculationrules'); without it Enter loads the page as usual and its result comes from the stash. document.prerendering is undefined (falsy) elsewhere, so a hold's timer starts at once.",
58
58
  },
59
59
  ];
60
60