@luigisbox/luma 0.0.0-stage → 0.1.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/LICENSE.md +90 -0
- package/README.md +488 -2
- package/a11y/audit.mjs +735 -0
- package/a11y/axe.mjs +73 -0
- package/a11y/report.mjs +347 -0
- package/a11y/run.mjs +256 -0
- package/a11y/wcag.mjs +233 -0
- package/bin/config.mjs +60 -0
- package/bin/luma.mjs +99 -0
- package/bin/paths.mjs +11 -0
- package/bin/tools.mjs +43 -0
- package/build/baseline.d.mts +8 -0
- package/build/baseline.mjs +167 -0
- package/build/tooling.mjs +102 -0
- package/build/vite.mjs +498 -0
- package/chrome/run.mjs +145 -0
- package/docs/ACCESSIBILITY.md +502 -0
- package/docs/INTEGRATION-PERFORMANCE.md +214 -0
- package/docs/PARITY.md +946 -0
- package/footprint/model.mjs +184 -0
- package/footprint/run.mjs +331 -0
- package/package.json +58 -4
- package/parity/alignment.mjs +109 -0
- package/parity/archive.mjs +223 -0
- package/parity/args.mjs +137 -0
- package/parity/behaviour.mjs +379 -0
- package/parity/compare.mjs +985 -0
- package/parity/engine.mjs +848 -0
- package/parity/hints.mjs +132 -0
- package/parity/hold.mjs +54 -0
- package/parity/init.mjs +703 -0
- package/parity/map.mjs +640 -0
- package/parity/owner.mjs +170 -0
- package/parity/pairing.mjs +110 -0
- package/parity/pixels.mjs +33 -0
- package/parity/probe.mjs +1239 -0
- package/parity/report.mjs +236 -0
- package/parity/run.mjs +267 -0
- package/snippet/early-hold.js +64 -0
- package/snippet/early-hold.min.js +1 -0
- package/src/api/ai.ts +98 -0
- package/src/api/index.ts +87 -0
- package/src/api/token-key.ts +42 -0
- package/src/api/token.ts +311 -0
- package/src/core/a11y-keys.ts +60 -0
- package/src/core/a11y.ts +160 -0
- package/src/core/boot-state.ts +6 -0
- package/src/core/color.ts +42 -0
- package/src/core/context.ts +108 -0
- package/src/core/early-rules.ts +96 -0
- package/src/core/entries.ts +25 -0
- package/src/core/fold.ts +7 -0
- package/src/core/health.ts +26 -0
- package/src/core/hold.ts +74 -0
- package/src/core/i18n.ts +66 -0
- package/src/core/image-load.ts +102 -0
- package/src/core/image-tags.ts +26 -0
- package/src/core/images.ts +170 -0
- package/src/core/integration.ts +120 -0
- package/src/core/listings.ts +100 -0
- package/src/core/locales.ts +14 -0
- package/src/core/own.ts +49 -0
- package/src/core/page.ts +14 -0
- package/src/core/prefetch.ts +88 -0
- package/src/core/recommenders.ts +166 -0
- package/src/core/reconciler.ts +397 -0
- package/src/core/result.ts +73 -0
- package/src/core/routes.ts +115 -0
- package/src/core/scheduler.ts +83 -0
- package/src/core/sense.ts +107 -0
- package/src/core/speculation.ts +59 -0
- package/src/core/stash.ts +38 -0
- package/src/core/storage.ts +45 -0
- package/src/core/styles.ts +106 -0
- package/src/core/support.ts +12 -0
- package/src/core/takeover.ts +220 -0
- package/src/core/templates.ts +74 -0
- package/src/core/turbo.ts +64 -0
- package/src/core/url.ts +164 -0
- package/src/core/widget-inits.ts +40 -0
- package/src/env.d.ts +3 -0
- package/src/lit.ts +11 -0
- package/src/locales/cs.json +94 -0
- package/src/locales/de.json +88 -0
- package/src/locales/en.json +88 -0
- package/src/main.ts +443 -0
- package/src/types.ts +760 -0
- package/src/widgets/autocomplete/index.ts +1367 -0
- package/src/widgets/autocomplete/style-autocomplete.css +266 -0
- package/src/widgets/autocomplete/template-autocomplete-box.ts +61 -0
- package/src/widgets/autocomplete/template-autocomplete-empty.ts +53 -0
- package/src/widgets/autocomplete/template-autocomplete-result-default.ts +82 -0
- package/src/widgets/autocomplete/template-autocomplete-sheet.ts +74 -0
- package/src/widgets/listing/element.ts +1769 -0
- package/src/widgets/listing/flow.ts +30 -0
- package/src/widgets/listing/instant.ts +133 -0
- package/src/widgets/listing/intent.ts +193 -0
- package/src/widgets/listing/query.ts +298 -0
- package/src/widgets/listing/stash.ts +97 -0
- package/src/widgets/listing/state.ts +131 -0
- package/src/widgets/listing/view.ts +257 -0
- package/src/widgets/plp/index.ts +11 -0
- package/src/widgets/plp/route.ts +263 -0
- package/src/widgets/recommender/index.ts +250 -0
- package/src/widgets/recommender/style-recommender.css +26 -0
- package/src/widgets/recommender/template-recommender-box-default.ts +11 -0
- package/src/widgets/recommender/template-recommender-popup.ts +12 -0
- package/src/widgets/recommender/template-recommender-result-default.ts +10 -0
- package/src/widgets/search/assets.ts +7 -0
- package/src/widgets/search/first.ts +15 -0
- package/src/widgets/search/form-url.ts +59 -0
- package/src/widgets/search/in-place.ts +226 -0
- package/src/widgets/search/index.ts +16 -0
- package/src/widgets/search/range.ts +260 -0
- package/src/widgets/search/route.ts +32 -0
- package/src/widgets/search/speculate.ts +202 -0
- package/src/widgets/search/style-search.css +65 -0
- package/src/widgets/search/template-search-box.ts +17 -0
- package/src/widgets/search/template-search-facets-default.ts +73 -0
- package/src/widgets/search/template-search-header.ts +14 -0
- package/src/widgets/search/template-search-pagination.ts +12 -0
- package/src/widgets/search/template-search-range-default.ts +48 -0
- package/src/widgets/search/template-search-result-default.ts +13 -0
- package/src/widgets/search/template-search-results-default.ts +8 -0
- package/tasks/model.mjs +116 -0
- package/tasks/run.mjs +305 -0
- package/tsconfig.json +14 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
LUIGI'S BOX COMMERCIAL SOURCE LICENSE
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Luigi's Box s.r.o., registered in Slovakia.
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
1. GRANT OF LICENSE
|
|
7
|
+
|
|
8
|
+
This software and associated documentation files (the "Software") are
|
|
9
|
+
proprietary to Luigi's Box s.r.o. ("Licensor"). Subject to the terms of
|
|
10
|
+
a valid, current commercial agreement between the Licensor and the
|
|
11
|
+
licensee ("Agreement"), the Licensor grants the licensee a limited,
|
|
12
|
+
non-exclusive, non-transferable, non-sublicensable license to use the
|
|
13
|
+
Software solely for the purposes defined in the Agreement.
|
|
14
|
+
|
|
15
|
+
2. CONDITIONS OF USE
|
|
16
|
+
|
|
17
|
+
Use of the Software is permitted only:
|
|
18
|
+
(a) by a party that has entered into a valid Agreement with the Licensor;
|
|
19
|
+
(b) for the duration of that Agreement; and
|
|
20
|
+
(c) solely for the purposes expressly permitted under that Agreement.
|
|
21
|
+
|
|
22
|
+
3. RESTRICTIONS
|
|
23
|
+
|
|
24
|
+
The licensee may not:
|
|
25
|
+
(a) copy, distribute, sublicense, sell, or transfer the Software or any
|
|
26
|
+
portion thereof to any third party;
|
|
27
|
+
(b) modify, adapt, translate, reverse engineer, decompile, or create
|
|
28
|
+
derivative works based on the core Software library. For the
|
|
29
|
+
avoidance of doubt, the licensee is permitted to write, modify,
|
|
30
|
+
and maintain integration code that interfaces with the Software
|
|
31
|
+
through its published public API, provided such integration code
|
|
32
|
+
does not incorporate, derive from, or replicate any portion of
|
|
33
|
+
the Software's internal implementation
|
|
34
|
+
(c) remove or alter any copyright, trademark, or proprietary notices
|
|
35
|
+
contained in the Software.
|
|
36
|
+
(d) use the Software, or any integration code built against it, to
|
|
37
|
+
communicate with, connect to, or interoperate with any backend
|
|
38
|
+
API, service, or infrastructure other than those operated by
|
|
39
|
+
Luigi's Box s.r.o. The Software is licensed exclusively for use
|
|
40
|
+
with Luigi's Box services. Any attempt to adapt, reconfigure, or
|
|
41
|
+
redirect the Software to function with a third-party service is
|
|
42
|
+
a material breach of this license and the Agreement.
|
|
43
|
+
|
|
44
|
+
4. TERMINATION AND RETURN OF SOURCE CODE
|
|
45
|
+
|
|
46
|
+
This license is effective until terminated. It terminates automatically,
|
|
47
|
+
without notice, upon expiry or termination of the Agreement. Upon
|
|
48
|
+
termination, the licensee must immediately cease all use of the Software
|
|
49
|
+
and permanently destroy all copies of the Software in its possession or
|
|
50
|
+
control, including source code, compiled versions, and any backups.
|
|
51
|
+
Upon request, the licensee shall provide the Licensor with written
|
|
52
|
+
confirmation of such destruction within 14 days.
|
|
53
|
+
|
|
54
|
+
5. DISCLAIMER OF WARRANTIES
|
|
55
|
+
|
|
56
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
57
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
58
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
|
|
59
|
+
THE LICENSOR DOES NOT WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE OR
|
|
60
|
+
UNINTERRUPTED.
|
|
61
|
+
|
|
62
|
+
6. LIMITATION OF LIABILITY
|
|
63
|
+
|
|
64
|
+
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL
|
|
65
|
+
THE LICENSOR BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL,
|
|
66
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
67
|
+
LOSS OF PROFITS, DATA, OR BUSINESS INTERRUPTION) ARISING OUT OF OR IN
|
|
68
|
+
CONNECTION WITH THE USE OR INABILITY TO USE THE SOFTWARE, EVEN IF
|
|
69
|
+
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
70
|
+
|
|
71
|
+
7. GOVERNING LAW AND JURISDICTION
|
|
72
|
+
|
|
73
|
+
This license shall be governed by and construed in accordance with the
|
|
74
|
+
laws of the Slovak Republic, without regard to its conflict of law
|
|
75
|
+
provisions. Any disputes arising under this license shall be subject
|
|
76
|
+
to the exclusive jurisdiction of the courts of Bratislava, Slovakia.
|
|
77
|
+
|
|
78
|
+
8. SEVERABILITY
|
|
79
|
+
|
|
80
|
+
If any provision of this license is found to be unenforceable or invalid
|
|
81
|
+
under applicable law, that provision shall be limited or eliminated to
|
|
82
|
+
the minimum extent necessary, and the remaining provisions shall continue
|
|
83
|
+
in full force and effect.
|
|
84
|
+
|
|
85
|
+
9. ENTIRE AGREEMENT
|
|
86
|
+
|
|
87
|
+
This license, together with the Agreement, constitutes the entire
|
|
88
|
+
agreement between the parties with respect to the Software and
|
|
89
|
+
supersedes all prior or contemporaneous understandings regarding
|
|
90
|
+
such subject matter.
|
package/README.md
CHANGED
|
@@ -1,3 +1,489 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Luma
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The framework for Luigi's Box frontend integrations: autocomplete, search
|
|
4
|
+
results, category listings (PLP) and recommenders on the Luigi's Box AI API,
|
|
5
|
+
for sites that can only include our script. Optionally, listings arrive
|
|
6
|
+
server-rendered through the site's own cache, from a rendering service Luigi's
|
|
7
|
+
Box runs.
|
|
8
|
+
|
|
9
|
+
> **Pre-release.** Until 1.0, a minor version (0.2.0) may change the
|
|
10
|
+
> integration API (config keys, template scope, the build); a patch version
|
|
11
|
+
> (0.1.1) only fixes. Depend on `^0.1.0` to take fixes without breaking
|
|
12
|
+
> changes.
|
|
13
|
+
|
|
14
|
+
## Core principles
|
|
15
|
+
|
|
16
|
+
### The problem
|
|
17
|
+
|
|
18
|
+
Luma runs on storefronts we don't own, often included as a single script tag.
|
|
19
|
+
That sets the constraints:
|
|
20
|
+
|
|
21
|
+
1. **The page is someone else's.** Their framework, router, CSS and scripts
|
|
22
|
+
were there first and keep changing. We work around them without breaking
|
|
23
|
+
anything.
|
|
24
|
+
2. **We are judged by how the page feels.** If search or listings feel slower,
|
|
25
|
+
flicker or jump compared with the site's own, the integration is a
|
|
26
|
+
regression, however good the results are.
|
|
27
|
+
3. **The shopper should not notice us.** Results look like the site's own UI
|
|
28
|
+
and are usable by everyone.
|
|
29
|
+
4. **One framework, many integrations.** Every change to `src/` ships to every
|
|
30
|
+
client, so the rules must hold in general, not for one site.
|
|
31
|
+
5. **Measured, not claimed.** We cannot check every client's site by hand, so
|
|
32
|
+
each principle below has a test, a gate or a tool behind it.
|
|
33
|
+
|
|
34
|
+
### A. A guest on the page
|
|
35
|
+
|
|
36
|
+
- **Do no harm, always fall back.** Luma adds only its own nodes
|
|
37
|
+
(`data-lbx`), hides the host's UI with CSS and never removes their nodes. It
|
|
38
|
+
never throws into the page, every network call has a deadline, and nothing
|
|
39
|
+
of ours keeps a page out of the back/forward cache. A failed request, a URL
|
|
40
|
+
we cannot serve, a listing not rendered within 4 s, a non-HTTPS page or a
|
|
41
|
+
missing browser feature: the site's own UI comes back as if we were never
|
|
42
|
+
there.
|
|
43
|
+
- **Navigation stays the host's.** The site's router owns the URL and history.
|
|
44
|
+
Our history entries carry no state of ours (null, or for SvelteKit and
|
|
45
|
+
Astro only the router's own bookkeeping keys); we never rewrite the host's.
|
|
46
|
+
Anything we cannot do exactly as the browser would (POST, a new window, a
|
|
47
|
+
modifier key) stays the browser's. The one exception: a new query from the
|
|
48
|
+
site's search form while on our search page is shown in place.
|
|
49
|
+
- **Sense and reconcile, not imperative patching.** Host signals (URL changes,
|
|
50
|
+
DOM mutations, our nodes removed) only mean "reconcile soon". At most once
|
|
51
|
+
per frame the reconciler makes the page match declarative rules: it removes
|
|
52
|
+
nodes whose rule is inactive, puts the same instance back if the host
|
|
53
|
+
removed it, and waits for the anchor to settle. A missed signal is harmless:
|
|
54
|
+
the next reconcile computes everything from scratch.
|
|
55
|
+
- **Works with any frontend stack.** React/Next.js, Remix/React Router 7,
|
|
56
|
+
Vue/Nuxt, Angular, SvelteKit, Astro, Turbo and htmx, with no code change on
|
|
57
|
+
the site. Inside a server-rendered root, Luma waits for that framework's own
|
|
58
|
+
hydration signal before inserting anything. Every framework host has a
|
|
59
|
+
contract suite on Chromium, Firefox and WebKit.
|
|
60
|
+
- **Listen at the window, not on host elements.** Input, submit and keydown are
|
|
61
|
+
window-capture listeners matched against selectors; inputs in shadow roots
|
|
62
|
+
are read from the composed path. Nothing is bound to the host's input or
|
|
63
|
+
form, so a re-created input keeps working.
|
|
64
|
+
|
|
65
|
+
### B. Faster than the site it replaces
|
|
66
|
+
|
|
67
|
+
- **No flicker, no layout shift.** With the inline snippet in `<head>`, the
|
|
68
|
+
host's listing is held before first paint and swapped for ours in one frame.
|
|
69
|
+
Its height is reserved from the last visit. Target: zero layout shift after
|
|
70
|
+
ours is shown.
|
|
71
|
+
- **A small entry, one round trip.** The entry is at most 14 kB brotli (one TCP
|
|
72
|
+
initial congestion window). Each widget's code, templates, styles and
|
|
73
|
+
dictionary are 2–7 kB chunks loaded only when that widget can run: a site
|
|
74
|
+
without recommenders never downloads that code.
|
|
75
|
+
- **Never block the main thread.** Target: no task of ours over 16 ms (a frame
|
|
76
|
+
at 60 fps) at 4x CPU; the test gate is 30 ms. Typing in autocomplete reaches
|
|
77
|
+
the next paint within 50 ms (INP). Visual feedback in the handler; fetches
|
|
78
|
+
and history writes in later tasks. No layout reads after our DOM writes in
|
|
79
|
+
the same task. Large listings render in steps that each fit a frame.
|
|
80
|
+
- **Instant.** Results are prefetched while the shopper types or hovers a
|
|
81
|
+
filter, Back restores the listing at the tile they followed, product pages
|
|
82
|
+
are prerendered on intent (Speculation Rules, Chromium).
|
|
83
|
+
- **Server-side rendering, same code.** The same integration and templates
|
|
84
|
+
render in the browser and on our servers. The site can serve our listing in
|
|
85
|
+
its own HTML through SSI/ESI in its web server or CDN; the browser takes the
|
|
86
|
+
server-rendered listing over. The server renderer and everything it imports
|
|
87
|
+
stay free of DOM access.
|
|
88
|
+
|
|
89
|
+
### C. Looks native, works for everyone
|
|
90
|
+
|
|
91
|
+
- **The site's own look.** Listing templates reuse the site's markup and CSS
|
|
92
|
+
classes. Luma's built-in CSS sits in `@layer lbx`, so the site's CSS always
|
|
93
|
+
wins. `yarn luma parity` measures how close the port is.
|
|
94
|
+
- **Web components, DOM mode per widget.** Custom elements are the unit of
|
|
95
|
+
placement and lifecycle. Listings are light DOM, so the site's CSS applies.
|
|
96
|
+
Autocomplete is shadow DOM by default: a robust house design isolated from
|
|
97
|
+
host CSS, with a light-DOM mode for sites that want their own styling.
|
|
98
|
+
- **Accessible by design.** WCAG 2.2 AA (EN 301 549, the European
|
|
99
|
+
Accessibility Act). Combobox ARIA on the input (`aria-expanded`,
|
|
100
|
+
`aria-controls`, `aria-activedescendant`), modal dialogs with managed focus,
|
|
101
|
+
`aria-busy` while a listing renders. `yarn luma a11y` checks an integration.
|
|
102
|
+
- **The modern platform, no polyfills.** Baseline Widely available (custom
|
|
103
|
+
elements, shadow DOM, cascade layers, `<dialog>`, `inert`, `color-mix()`).
|
|
104
|
+
Anything newer (Popover API, Navigation API, Speculation Rules) is
|
|
105
|
+
feature-detected and the page works without it. `yarn luma baseline`
|
|
106
|
+
enforces it ([Browser support](#browser-support)).
|
|
107
|
+
|
|
108
|
+
### D. How we work
|
|
109
|
+
|
|
110
|
+
- **One framework, many integrations.** Nothing site-specific in this
|
|
111
|
+
repository; each integration has its own, with Luma as the `@luigisbox/luma`
|
|
112
|
+
package. `_config.ts` is JSON; code goes in `_init.ts`.
|
|
113
|
+
- **Measured, not claimed.** Every principle has a gate: budgets and
|
|
114
|
+
main-thread limits, request counts, the bfcache test, the baseline lint,
|
|
115
|
+
framework contracts, the accessibility and parity checks. The suites run on
|
|
116
|
+
Chromium, Firefox and WebKit, desktop and phones, on every change. Every fix
|
|
117
|
+
lands with a test that fails without it.
|
|
118
|
+
|
|
119
|
+
What we accept in exchange: [Known limitations](#known-limitations).
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## Repository layout
|
|
123
|
+
|
|
124
|
+
| Path | What |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `src/` | the framework: config, sensing and placement, takeover, templates, API client, widgets |
|
|
127
|
+
| `build/vite.mjs` | the Vite config every integration builds with |
|
|
128
|
+
| `snippet/` | the inline early-hold snippet (identical for every client) |
|
|
129
|
+
| `a11y/` | `yarn luma a11y`: an integration's WCAG 2.2 AA check |
|
|
130
|
+
| `parity/` | `yarn luma parity`: an integration compared with the site it replaces |
|
|
131
|
+
| `bin/` | `yarn luma <tool>`: every tool below, from an integration repository, with one script |
|
|
132
|
+
| `footprint/` | `yarn luma footprint`: what Luma itself costs each page type of an integration |
|
|
133
|
+
| `chrome/` | `yarn luma chrome`: the live storefront in your Chrome with an integration on it, before the site includes it |
|
|
134
|
+
| `tasks/` | `yarn luma tasks`: an integration's main-thread tasks over a frame's budget on the storefront, by source line |
|
|
135
|
+
|
|
136
|
+
Integrations do not live here: each has its own repository with Luma as the
|
|
137
|
+
`@luigisbox/luma` package (older ones: the `luma/` submodule;
|
|
138
|
+
[Integration repository](#integration-repository)).
|
|
139
|
+
|
|
140
|
+
## What the customer includes
|
|
141
|
+
|
|
142
|
+
Everything goes in the site's HTML, in `<head>`, on every page.
|
|
143
|
+
`<distribution id>` is the site's distribution (`lbdr_…`).
|
|
144
|
+
|
|
145
|
+
**Loader only** (a tag manager is fine): the site's own listing may flash
|
|
146
|
+
before ours on a hard load.
|
|
147
|
+
|
|
148
|
+
```html
|
|
149
|
+
<script async src="https://cdn.eu1.luigisbox.ai/b/<distribution id>/loader.js"></script>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Snippet + loader** (recommended): the inline snippet hides the site's listing
|
|
153
|
+
before first paint, from rules the bundle stored on earlier page views; it makes
|
|
154
|
+
no request and gives up after a timeout. The first page view in a browser has
|
|
155
|
+
no rules yet. Put both first in `<head>`, before the site's own scripts and
|
|
156
|
+
stylesheets (not through a tag manager: too late). With a strict CSP, give the
|
|
157
|
+
snippet's tag the page's `nonce` or allow its hash. On a site whose React
|
|
158
|
+
hydrates the whole document (Remix, React Router 7, Hydrogen), put the loader
|
|
159
|
+
first and the snippet as the last element of `<head>`.
|
|
160
|
+
|
|
161
|
+
<!-- lbx-snippet: the minified form is snippet/early-hold.min.js (generated from snippet/early-hold.js); this copy must not differ from it -->
|
|
162
|
+
```html
|
|
163
|
+
<script>
|
|
164
|
+
"use strict";(function(){try{if(window.__lbx&&window.__lbx.early)return;var n=JSON.parse(localStorage.getItem("lbx:early")||"null");if(!n||n.v!==1||!(n.exp>Date.now())||performance.getEntriesByName&&performance.getEntriesByName("first-contentful-paint").length)return;for(var o=new URL(location.href),i=0;i<n.routes.length;i++){var e=n.routes[i];if(new RegExp(e.path).test(o.pathname)){for(var r=!0,l=0;e.need&&l<e.need.length;l++)(o.searchParams.get(e.need[l])||"").trim()||(r=!1);if(r&&e.allow){var d=new RegExp(e.allow);o.searchParams.forEach(function(a,p){a&&!d.test(p)&&(r=!1)})}if(r){var t=document.createElement("style"),c=document.currentScript;c&&c.nonce&&(t.nonce=c.nonce),t.setAttribute("data-lbx","early"),t.textContent=e.css,(document.head||document.documentElement).appendChild(t);var f=function(){clearTimeout(u),t.parentNode&&t.parentNode.removeChild(t)},u=setTimeout(f,n.ms||1500),m=window.__lbx=window.__lbx||{};m.early={t:performance.now(),route:e.id,snippet:1,claim:function(a){return!t.parentNode||a!==e.id?!1:(clearTimeout(u),!0)},drop:f};return}}}}catch(a){}})();
|
|
165
|
+
</script>
|
|
166
|
+
<script async src="https://cdn.eu1.luigisbox.ai/b/<distribution id>/loader.js"></script>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Connection hints** (recommended, first in `<head>`, before the snippet): the
|
|
170
|
+
connections our requests need, opened while the HTML is still arriving instead
|
|
171
|
+
of after the loader and the entry have run (the entry adds the same hints, but
|
|
172
|
+
only once `<body>` starts). On a first visit they take the TCP and TLS setup
|
|
173
|
+
(one to three round trips per host) off the path to the listing. Use the
|
|
174
|
+
region of the site's distribution (`eu1` here) and the host of the
|
|
175
|
+
catalog's product images.
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
<link rel="preconnect" href="https://cdn.eu1.luigisbox.ai">
|
|
179
|
+
<link rel="preconnect" href="https://cdn.eu1.luigisbox.ai" crossorigin>
|
|
180
|
+
<link rel="preconnect" href="https://api.eu1.luigisbox.ai" crossorigin>
|
|
181
|
+
<link rel="preconnect" href="https://auth.eu1.luigisbox.ai" crossorigin>
|
|
182
|
+
<link rel="preconnect" href="https://<product image host>">
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- The CDN twice: the loader is a classic script (no CORS) and the chunks are
|
|
186
|
+
modules loaded with CORS. A browser does not share connections between the two
|
|
187
|
+
modes. Drop the plain one if the Network panel shows a single connection.
|
|
188
|
+
- The API and auth hosts with `crossorigin`: our requests are CORS `fetch()`es.
|
|
189
|
+
A preconnect without it opens a connection they cannot use.
|
|
190
|
+
- The image host without `crossorigin` (images are no-CORS). Leave it out if
|
|
191
|
+
it is the site's own host (connected already).
|
|
192
|
+
- At most these five. More preconnects compete with the page's own
|
|
193
|
+
connections, and an unused one is closed after about 10 s.
|
|
194
|
+
- On listing and search pages, `fetchpriority="high"` on the loader's tag moves
|
|
195
|
+
it ahead of the page's images (an `async` script is otherwise fetched at low
|
|
196
|
+
priority). Our listing is those pages' main content. Measure it with
|
|
197
|
+
`yarn luma footprint` and DevTools before keeping it.
|
|
198
|
+
|
|
199
|
+
The whole `<head>` start of a listing page, in order:
|
|
200
|
+
|
|
201
|
+
```html
|
|
202
|
+
<head>
|
|
203
|
+
<meta charset="utf-8">
|
|
204
|
+
<link rel="preconnect" href="https://cdn.eu1.luigisbox.ai">
|
|
205
|
+
<link rel="preconnect" href="https://cdn.eu1.luigisbox.ai" crossorigin>
|
|
206
|
+
<link rel="preconnect" href="https://api.eu1.luigisbox.ai" crossorigin>
|
|
207
|
+
<link rel="preconnect" href="https://auth.eu1.luigisbox.ai" crossorigin>
|
|
208
|
+
<link rel="preconnect" href="https://<product image host>">
|
|
209
|
+
<script>/* the early-hold snippet above, verbatim */</script>
|
|
210
|
+
<script async fetchpriority="high" src="https://cdn.eu1.luigisbox.ai/b/<distribution id>/loader.js"></script>
|
|
211
|
+
<!-- the site's own stylesheets and scripts -->
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
What to measure, and the numbers to optimize for:
|
|
215
|
+
[docs/INTEGRATION-PERFORMANCE.md](docs/INTEGRATION-PERFORMANCE.md).
|
|
216
|
+
|
|
217
|
+
**Server-side includes**: the listing arrives in the site's HTML, through
|
|
218
|
+
their web server or CDN config, with the loader as above. The site adds an
|
|
219
|
+
include (nginx SSI or Varnish/CDN ESI) and a marker at its listing, and a short
|
|
220
|
+
snippet to its server config; Luigi's Box provides both for the site. The
|
|
221
|
+
include sits outside any root the site's own framework hydrates, and the site's
|
|
222
|
+
own listing stays the fallback.
|
|
223
|
+
|
|
224
|
+
If our listing does not render within 4 s, the request fails or the URL is one
|
|
225
|
+
we cannot serve, the site's own listing is shown and the page stays theirs
|
|
226
|
+
until the next visit.
|
|
227
|
+
|
|
228
|
+
## Integration repository
|
|
229
|
+
|
|
230
|
+
An integration repository has the shape of the distribution platform's
|
|
231
|
+
scaffold: Luma as the `@luigisbox/luma` package (a devDependency, in
|
|
232
|
+
`node_modules/@luigisbox/luma/`), the integration in `integration/`, a Vite
|
|
233
|
+
build that emits `dist/luma-integration.js` (what the platform's loader
|
|
234
|
+
imports) plus on-demand chunks. Its `vite.config.ts`:
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
import mkcert from 'vite-plugin-mkcert';
|
|
238
|
+
import { clientConfig } from '@luigisbox/luma/build/vite.mjs';
|
|
239
|
+
|
|
240
|
+
export default clientConfig(import.meta.url, { plugins: [mkcert()], server: { host: 'localhost', https: {} } });
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Its `tsconfig.json` extends `@luigisbox/luma/tsconfig.json`, includes
|
|
244
|
+
`integration/**/*` and `node_modules/@luigisbox/luma/src/**/*`, and maps `@/*`
|
|
245
|
+
to `./node_modules/@luigisbox/luma/src/*`. Its `package.json` depends on
|
|
246
|
+
`@luigisbox/luma`, `vite` 7, `typescript` and `vite-plugin-mkcert` (Luma brings
|
|
247
|
+
`lit-html`, `@lit-labs/ssr-client` and its tools' dependencies). An older
|
|
248
|
+
repository has Luma as the `luma/` submodule instead (`./luma/…` for
|
|
249
|
+
`@luigisbox/luma/…`, and `yarn --cwd luma install` for the tools); both build
|
|
250
|
+
the same.
|
|
251
|
+
|
|
252
|
+
The package holds what an integration uses: the framework's source, the build
|
|
253
|
+
config, `yarn luma` and the client tools, and their docs.
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
integration/
|
|
257
|
+
__integration.ts hands the folder to the framework (startIntegration)
|
|
258
|
+
_global/ _config.ts _init.ts [_widget.ts] _translations.ts style-*.css
|
|
259
|
+
template-*.ts shared by every widget (a tile, …)
|
|
260
|
+
autocomplete/ _config.ts [_init.ts] [_widget.ts] [style-host.css] [template-autocomplete-*.ts]
|
|
261
|
+
search/ _config.ts _translations.ts style-host.css template-search-*.ts
|
|
262
|
+
plp/ _config.ts _init.ts style-host.css (templates fall back to search/)
|
|
263
|
+
recommender <name>/ _config.ts [_init.ts] _translations.ts
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
- **`_config.ts` is JSON** (checked at boot); anything that needs code goes in
|
|
267
|
+
`_init.ts`. `_global/_init.ts` runs first and can change any config or return
|
|
268
|
+
`false` to stop; a widget's `_init.ts` gets that widget's config. Every config
|
|
269
|
+
key is typed and documented in `src/types.ts`.
|
|
270
|
+
- **`_init.ts` ships and runs on every page; `_widget.ts` loads with its
|
|
271
|
+
widget.** Keep in `_init.ts` only what decides whether and where a widget
|
|
272
|
+
runs (`disabled`, `active`, `readQuery`, `items`, `intent`, selectors set in
|
|
273
|
+
code, switching the site's own widgets off). Hooks only a widget calls
|
|
274
|
+
(`computedAttributes`, `toFilter`, `facetsFor`, `onRender`, `onOpen`, …) go
|
|
275
|
+
in the folder's optional `_widget.ts`, same signature (its return value is
|
|
276
|
+
ignored): it is loaded with the widget's chunk and run once before its
|
|
277
|
+
context is made, `_global/_widget.ts` first. `__integration.ts` hands it
|
|
278
|
+
over as a lazy glob: `widgetInits: import.meta.glob('./*/_widget.ts', { import: 'default' })`.
|
|
279
|
+
- **`style-host.css`** hides the host's own listing or suggestions, only while
|
|
280
|
+
the page is ours. Other `style-*.css` load with their widget. Luma's built-in
|
|
281
|
+
CSS sits in `@layer lbx`, so the integration's and the site's CSS win.
|
|
282
|
+
- **Templates override by name**: a widget's built-in templates, then
|
|
283
|
+
`_global/`, then the widget's own folder. Names: `search-box`, `search-header`,
|
|
284
|
+
`search-facets-default`, `search-results-default`, `search-result-default`,
|
|
285
|
+
`search-pagination`, `autocomplete-box`, `autocomplete-result-default`,
|
|
286
|
+
`autocomplete-sheet`, `autocomplete-empty`, `recommender-box-default`,
|
|
287
|
+
`recommender-result-default`, `recommender-popup`.
|
|
288
|
+
|
|
289
|
+
### Templates
|
|
290
|
+
|
|
291
|
+
Templates are [lit-html](https://lit.dev/docs/templates/overview/): each
|
|
292
|
+
`template-<name>.ts` default-exports a function of the template's scope that
|
|
293
|
+
returns `` html`…` ``. Only changed bindings touch the DOM, so images, focus and
|
|
294
|
+
open `<details>` survive re-renders; no `eval`, CSP-safe. Import lit from `@/lit`
|
|
295
|
+
and the scope types from `@/types` (`SearchScope`, `SearchResultScope`,
|
|
296
|
+
`AutocompleteScope`, `RecommenderScope`, …).
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
// integration/search/template-search-result-default.ts
|
|
300
|
+
import { html, nothing } from '@/lit';
|
|
301
|
+
import type { SearchResultScope } from '@/types';
|
|
302
|
+
|
|
303
|
+
export default ({ r, i, img, price }: SearchResultScope) => html`
|
|
304
|
+
<a class="tile" href=${r.c.url}>
|
|
305
|
+
${img(r, i)}
|
|
306
|
+
<span>${r.c.title}</span>
|
|
307
|
+
${r.c.price ? html`<b>${price(r.c.price)}</b>` : nothing}
|
|
308
|
+
</a>`;
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
| lit | |
|
|
312
|
+
|---|---|
|
|
313
|
+
| `${expr}` | text (escaped) |
|
|
314
|
+
| `` ${cond ? html`…` : nothing} `` | conditions |
|
|
315
|
+
| `` ${repeat(list, (x) => x.id, (x, i) => html`…`)} `` | keyed lists (`list.map(…)` for unkeyed) |
|
|
316
|
+
| `attr=${v}`, `?disabled=${b}`, `.checked=${b}` | attribute, boolean attribute, property |
|
|
317
|
+
| `@click=${s.showMore}`, `@change=${() => s.toggleFacet(f.name, v.value)}` | event handlers |
|
|
318
|
+
| `${include('product-tile', { extra })}` | another template of the widget, with this scope plus props |
|
|
319
|
+
|
|
320
|
+
- **Results**: `r.a` is the hit's attributes, `r.c` its computed attributes
|
|
321
|
+
(functions in `_global/_init.ts`, `computedAttributes`). `r.c.url` is always a
|
|
322
|
+
web link; a `javascript:` or `data:` value becomes `''`. Links you bind from
|
|
323
|
+
`r.a` or your own computed attributes, and `unsafeHTML`, get no such check:
|
|
324
|
+
keep catalog data out of them, or check it. With Trusted Types enforced, the
|
|
325
|
+
site's `trusted-types` directive must allow the `lit-html` policy.
|
|
326
|
+
- **Helpers** in every scope: `t(key, params)`, `price(n)`, `plain()`,
|
|
327
|
+
`first()`, `include()`, and `img(r, i)` (a complete `<img>` in its placeholder
|
|
328
|
+
box) / `image(r, i)` (its attributes, for your own markup), configured by
|
|
329
|
+
`_global.images` (srcset, sizes, the eager first row, placeholders).
|
|
330
|
+
- **Pagination** is "show more" only: templates get `results`, `more` and
|
|
331
|
+
`showMore`; there are no numbered pages.
|
|
332
|
+
- **Translations** are JSON dictionaries per locale (`src/locales/<locale>.json`,
|
|
333
|
+
`{name}` placeholders, plural forms by `Intl.PluralRules` category); only the
|
|
334
|
+
site's locale is loaded. Override keys in `_translations.ts`, keyed by locale:
|
|
335
|
+
`_global` for all widgets, a widget's own for that widget. Give
|
|
336
|
+
`__integration.ts` their glob without `eager`
|
|
337
|
+
(`translations: import.meta.glob('./*/_translations.ts', { import: 'default' })`)
|
|
338
|
+
and they load with their widget instead of on every page; `_init.ts` then
|
|
339
|
+
does not see them.
|
|
340
|
+
|
|
341
|
+
### Behaviour switches
|
|
342
|
+
|
|
343
|
+
On by default unless noted; each falls back to what the page would do anyway.
|
|
344
|
+
|
|
345
|
+
| Config | What it does | Turn it off when |
|
|
346
|
+
|---|---|---|
|
|
347
|
+
| `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 |
|
|
348
|
+
| `search.prefetchWhileTyping` | the search result is fetched once the typed phrase settles, so Enter shows it at once | |
|
|
349
|
+
| `restoreOnBack` | Back from a product restores the listing and the followed tile's position | |
|
|
350
|
+
| `prefetchNextPage` | "show more" is fetched as the shopper nears the end | |
|
|
351
|
+
| `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 |
|
|
352
|
+
| `_global.prefetchFilters` | a filter, sort or chip is fetched on hover, touch or focus | |
|
|
353
|
+
| `_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 |
|
|
354
|
+
|
|
355
|
+
## Browser support
|
|
356
|
+
|
|
357
|
+
Luma targets [Baseline](https://web.dev/baseline) **Widely available**; anything
|
|
358
|
+
newer (Popover API, Navigation API, Speculation Rules) is
|
|
359
|
+
feature-detected and optional. **HTTPS only**: on a plain-HTTP page Luma does
|
|
360
|
+
not start and the site keeps its own search. Tested on Chromium, Firefox and
|
|
361
|
+
WebKit, desktop and phone, on every change.
|
|
362
|
+
|
|
363
|
+
An integration repository runs the same check on its `integration/` folder:
|
|
364
|
+
`yarn luma baseline`, and `yarn luma
|
|
365
|
+
baseline --built dist` over a build's output (below, "Tools for an
|
|
366
|
+
integration repository").
|
|
367
|
+
|
|
368
|
+
## Known limitations
|
|
369
|
+
|
|
370
|
+
- **Autocomplete needs the Popover API**: without it (Safari < 17, Firefox
|
|
371
|
+
< 125) the site keeps its own suggestions.
|
|
372
|
+
- **Loader without the snippet**: the site's own listing may flash before ours
|
|
373
|
+
on a hard load.
|
|
374
|
+
- **Analytics**: Luma sends no tracking events. A search shown in place fires no page
|
|
375
|
+
view of the host's. Speculative requests (prefetches, prerenders) count in
|
|
376
|
+
the API's request numbers, though never as searches or impressions.
|
|
377
|
+
- **Prerendering** costs the site a page render per unfollowed hover
|
|
378
|
+
(`speculation: false` turns it off).
|
|
379
|
+
- **Early hold**: the snippet carries no client's rules, so the first page
|
|
380
|
+
view is not held.
|
|
381
|
+
- **Host routers out of reach**: where Luma cannot find the host's router (Vue
|
|
382
|
+
Router 4 when its app is not discoverable, Angular in production), the
|
|
383
|
+
router can overwrite our history entry or show a stale query in its search
|
|
384
|
+
box. `inPlaceQuery: false` for such a site.
|
|
385
|
+
|
|
386
|
+
## Development
|
|
387
|
+
|
|
388
|
+
Node 24 or later, Yarn 4 (corepack).
|
|
389
|
+
|
|
390
|
+
### Seeing an integration on the storefront
|
|
391
|
+
|
|
392
|
+
An integration is developed in its own repository. How you see it on the live
|
|
393
|
+
site depends on whether the site includes our loader yet.
|
|
394
|
+
|
|
395
|
+
**The site includes the loader.** Serve the integration and add
|
|
396
|
+
`?_lbx_env=development` to any page of the site:
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
yarn dev # serves the integration on https://localhost:8069
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
```
|
|
403
|
+
https://www.shop.example/some-category?_lbx_env=development
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
The loader then imports the integration from your dev server instead of the
|
|
407
|
+
published build (a red banner says so); an edit shows on reload.
|
|
408
|
+
`LBX_DEV_ORIGINS` (comma-separated) limits which sites may load from it.
|
|
409
|
+
|
|
410
|
+
**The site does not include the loader yet.** Open the storefront with the
|
|
411
|
+
integration injected as a customer would install it (the early-hold snippet,
|
|
412
|
+
then the build):
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
yarn luma chrome # builds, opens the home page in your Chrome
|
|
416
|
+
yarn luma chrome /some-category # another page
|
|
417
|
+
yarn luma chrome --dev # from yarn dev: an edit shows on reload, no build
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
It needs the storefront's host in `luma.config.mjs` (`export const host =
|
|
421
|
+
'www.shop.example'`). The site's own Luigi's Box scripts are blocked, nothing
|
|
422
|
+
else of the site changes, and the browser profile is kept per storefront
|
|
423
|
+
(`~/.lbx-chrome/<host>`), apart from your own. `--check` prints what Luma did
|
|
424
|
+
and exits; `--help` lists the rest ([`chrome/run.mjs`](chrome/run.mjs)).
|
|
425
|
+
|
|
426
|
+
Either way, `window.__lbx` in the console is Luma's state (`health.errors`,
|
|
427
|
+
`health.timeline`). `yarn build` emits `dist/`.
|
|
428
|
+
|
|
429
|
+
### Working on Luma itself
|
|
430
|
+
|
|
431
|
+
Luma is developed by Luigi's Box. Every change is type-checked and runs the
|
|
432
|
+
unit and browser suites and the performance gates on Chromium, Firefox and
|
|
433
|
+
WebKit before it ships.
|
|
434
|
+
|
|
435
|
+
### Tools for an integration repository
|
|
436
|
+
|
|
437
|
+
An integration repository's `package.json` has one script for
|
|
438
|
+
all of them, `"luma": "luma"` (the package's command; with the submodule,
|
|
439
|
+
`"node luma/bin/luma.mjs"` after `yarn --cwd luma install`), and its
|
|
440
|
+
`luma.config.mjs` the storefront's host (`export const host =
|
|
441
|
+
'www.shop.example'`): a tool Luma adds is there once Luma is updated, nothing to
|
|
442
|
+
change in the integration. `yarn luma` lists them; `yarn luma tooling --write`
|
|
443
|
+
sets an integration up
|
|
444
|
+
([`bin/tools.mjs`](bin/tools.mjs), [`build/tooling.mjs`](build/tooling.mjs)).
|
|
445
|
+
|
|
446
|
+
| Command | What |
|
|
447
|
+
|---|---|
|
|
448
|
+
| `yarn luma chrome [path]` | the live storefront in your Chrome with the integration on it as a customer installs it (the early-hold snippet, then the build), whether or not the site includes it yet; the site's own Luigi's Box scripts blocked, nothing else changed; `--dev` from `yarn dev`, `--check` prints what Luma did and exits ([`chrome/run.mjs`](chrome/run.mjs)) |
|
|
449
|
+
| `yarn luma a11y` | WCAG 2.2 AA check of the integration's widgets, by keyboard too ([`docs/ACCESSIBILITY.md`](docs/ACCESSIBILITY.md) §8) |
|
|
450
|
+
| `yarn luma parity` | compares the site's search and category UI with ours, component by component ([`docs/PARITY.md`](docs/PARITY.md)) |
|
|
451
|
+
| `yarn luma footprint` | what Luma itself costs per page type: the entry's size and round trips, scripts and bytes loaded, the script chain, main-thread and compile time, API requests, time to ready; on the integration's stub page ([`footprint/run.mjs`](footprint/run.mjs)) |
|
|
452
|
+
| `yarn luma tasks` | our main-thread tasks per scenario (pages, a state change, show more, Back, typing) at 4x CPU on the storefront: the worst task's own time against a 16 ms budget, its functions by source line, the site's code under our calls apart ([`tasks/run.mjs`](tasks/run.mjs)) |
|
|
453
|
+
| `yarn luma baseline` | nothing in `integration/` newer than Baseline Widely available (`--built dist`: the build) |
|
|
454
|
+
|
|
455
|
+
The integration's settings for the tools are its `luma.config.mjs`
|
|
456
|
+
([`bin/config.mjs`](bin/config.mjs) loads it, with Luma's defaults). Every
|
|
457
|
+
export is optional:
|
|
458
|
+
|
|
459
|
+
```js
|
|
460
|
+
// The storefront's host name: the tools open it (LBX_HOST overrides it). Without
|
|
461
|
+
// it they use the stub page (`yarn luma chrome` needs it).
|
|
462
|
+
export const host = 'www.shop.example';
|
|
463
|
+
// The site's name (LBX_SITE overrides it); default: package.json's name.
|
|
464
|
+
export const site = 'shop';
|
|
465
|
+
// A page in the site's own markup and stylesheet, for the tools' runs on the
|
|
466
|
+
// stub page (a string, or a promise of one).
|
|
467
|
+
export const stubPage = (url) => `<!DOCTYPE html>…`;
|
|
468
|
+
// What the tools' scenarios load: a category page, a search, a product page,
|
|
469
|
+
// the header search input.
|
|
470
|
+
export const fixtures = { category: '/some-category', query: 'shoes', product: '/some-product', input: 'input#search' };
|
|
471
|
+
// The pages footprint and tasks measure, { label: path }; default: home,
|
|
472
|
+
// fixtures.category, a search for fixtures.query, fixtures.product.
|
|
473
|
+
export const perf = { pages: { home: '/', category: '/some-category', search: '/search?q=shoes' } };
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Without a `stubPage`: a neutral page with a header search form (`input
|
|
477
|
+
name=q`), a search anchor `<div id="lbx-search-anchor">` on `/search`, and
|
|
478
|
+
elsewhere a category listing `<div id="lbx-listing">`. Older integrations kept
|
|
479
|
+
these settings in `edge/rig.mjs`: the tools refuse that name, `yarn luma
|
|
480
|
+
tooling --write` moves it.
|
|
481
|
+
|
|
482
|
+
How to make an integration fast and light, and which tool measures what:
|
|
483
|
+
[`docs/INTEGRATION-PERFORMANCE.md`](docs/INTEGRATION-PERFORMANCE.md).
|
|
484
|
+
|
|
485
|
+
## Documentation
|
|
486
|
+
|
|
487
|
+
- [`docs/INTEGRATION-PERFORMANCE.md`](docs/INTEGRATION-PERFORMANCE.md): what to measure in integration work, and the metrics to optimize.
|
|
488
|
+
- [`docs/ACCESSIBILITY.md`](docs/ACCESSIBILITY.md): what Luma guarantees, what templates must do.
|
|
489
|
+
- [`docs/PARITY.md`](docs/PARITY.md): the parity check.
|