@luigisbox/luma 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/LICENSE.md +90 -0
  2. package/README.md +615 -2
  3. package/a11y/audit.mjs +735 -0
  4. package/a11y/axe.mjs +73 -0
  5. package/a11y/report.mjs +347 -0
  6. package/a11y/run.mjs +256 -0
  7. package/a11y/wcag.mjs +233 -0
  8. package/bin/config.mjs +60 -0
  9. package/bin/luma.mjs +99 -0
  10. package/bin/paths.mjs +11 -0
  11. package/bin/tools.mjs +43 -0
  12. package/build/baseline.d.mts +8 -0
  13. package/build/baseline.mjs +167 -0
  14. package/build/tooling.mjs +102 -0
  15. package/build/vite.mjs +507 -0
  16. package/chrome/block.mjs +8 -0
  17. package/chrome/run.mjs +150 -0
  18. package/docs/ACCESSIBILITY.md +501 -0
  19. package/docs/INTEGRATION-PERFORMANCE.md +213 -0
  20. package/docs/PARITY.md +946 -0
  21. package/footprint/model.mjs +184 -0
  22. package/footprint/run.mjs +331 -0
  23. package/package.json +58 -4
  24. package/parity/alignment.mjs +109 -0
  25. package/parity/archive.mjs +223 -0
  26. package/parity/args.mjs +137 -0
  27. package/parity/behaviour.mjs +379 -0
  28. package/parity/compare.mjs +985 -0
  29. package/parity/engine.mjs +848 -0
  30. package/parity/hints.mjs +132 -0
  31. package/parity/hold.mjs +54 -0
  32. package/parity/init.mjs +703 -0
  33. package/parity/map.mjs +640 -0
  34. package/parity/owner.mjs +170 -0
  35. package/parity/pairing.mjs +110 -0
  36. package/parity/pixels.mjs +33 -0
  37. package/parity/probe.mjs +1239 -0
  38. package/parity/report.mjs +236 -0
  39. package/parity/run.mjs +267 -0
  40. package/snippet/early-hold.js +64 -0
  41. package/snippet/early-hold.min.js +1 -0
  42. package/src/analytics/chain.ts +137 -0
  43. package/src/analytics/collect.ts +64 -0
  44. package/src/analytics/datalayer.ts +67 -0
  45. package/src/analytics/from-ga4.ts +68 -0
  46. package/src/analytics/from-luma.ts +21 -0
  47. package/src/analytics/index.ts +24 -0
  48. package/src/analytics/outbox.ts +118 -0
  49. package/src/analytics/page.ts +46 -0
  50. package/src/analytics/pageview.ts +82 -0
  51. package/src/analytics/ref.ts +19 -0
  52. package/src/analytics/setup.ts +41 -0
  53. package/src/analytics/start.ts +53 -0
  54. package/src/analytics/triggers.ts +239 -0
  55. package/src/analytics/user.ts +12 -0
  56. package/src/analytics/wire.ts +67 -0
  57. package/src/api/ai.ts +98 -0
  58. package/src/api/index.ts +87 -0
  59. package/src/api/token-key.ts +42 -0
  60. package/src/api/token.ts +312 -0
  61. package/src/core/a11y-keys.ts +60 -0
  62. package/src/core/a11y.ts +160 -0
  63. package/src/core/boot-state.ts +6 -0
  64. package/src/core/color.ts +42 -0
  65. package/src/core/context.ts +108 -0
  66. package/src/core/early-rules.ts +96 -0
  67. package/src/core/entries.ts +25 -0
  68. package/src/core/fold.ts +7 -0
  69. package/src/core/health.ts +26 -0
  70. package/src/core/hold.ts +74 -0
  71. package/src/core/i18n.ts +66 -0
  72. package/src/core/image-load.ts +102 -0
  73. package/src/core/image-tags.ts +26 -0
  74. package/src/core/images.ts +170 -0
  75. package/src/core/integration.ts +126 -0
  76. package/src/core/kept.ts +25 -0
  77. package/src/core/listings.ts +100 -0
  78. package/src/core/locales.ts +14 -0
  79. package/src/core/own.ts +49 -0
  80. package/src/core/page.ts +14 -0
  81. package/src/core/prefetch.ts +88 -0
  82. package/src/core/recommenders.ts +166 -0
  83. package/src/core/reconciler.ts +397 -0
  84. package/src/core/result.ts +73 -0
  85. package/src/core/routes.ts +115 -0
  86. package/src/core/scheduler.ts +83 -0
  87. package/src/core/sense.ts +107 -0
  88. package/src/core/speculation.ts +59 -0
  89. package/src/core/stash.ts +38 -0
  90. package/src/core/storage.ts +45 -0
  91. package/src/core/styles.ts +106 -0
  92. package/src/core/support.ts +12 -0
  93. package/src/core/takeover.ts +220 -0
  94. package/src/core/templates.ts +74 -0
  95. package/src/core/tiles.ts +137 -0
  96. package/src/core/turbo.ts +64 -0
  97. package/src/core/url.ts +164 -0
  98. package/src/core/widget-inits.ts +40 -0
  99. package/src/env.d.ts +3 -0
  100. package/src/lit.ts +11 -0
  101. package/src/locales/cs.json +94 -0
  102. package/src/locales/de.json +88 -0
  103. package/src/locales/en.json +88 -0
  104. package/src/main.ts +493 -0
  105. package/src/types.ts +778 -0
  106. package/src/widgets/autocomplete/index.ts +1387 -0
  107. package/src/widgets/autocomplete/style-autocomplete.css +266 -0
  108. package/src/widgets/autocomplete/template-autocomplete-box.ts +61 -0
  109. package/src/widgets/autocomplete/template-autocomplete-empty.ts +53 -0
  110. package/src/widgets/autocomplete/template-autocomplete-result-default.ts +82 -0
  111. package/src/widgets/autocomplete/template-autocomplete-sheet.ts +74 -0
  112. package/src/widgets/listing/element.ts +1772 -0
  113. package/src/widgets/listing/flow.ts +30 -0
  114. package/src/widgets/listing/instant.ts +133 -0
  115. package/src/widgets/listing/intent.ts +193 -0
  116. package/src/widgets/listing/query.ts +298 -0
  117. package/src/widgets/listing/stash.ts +97 -0
  118. package/src/widgets/listing/state.ts +131 -0
  119. package/src/widgets/listing/view.ts +257 -0
  120. package/src/widgets/plp/index.ts +11 -0
  121. package/src/widgets/plp/route.ts +263 -0
  122. package/src/widgets/recommender/index.ts +261 -0
  123. package/src/widgets/recommender/style-recommender.css +26 -0
  124. package/src/widgets/recommender/template-recommender-box-default.ts +11 -0
  125. package/src/widgets/recommender/template-recommender-popup.ts +12 -0
  126. package/src/widgets/recommender/template-recommender-result-default.ts +10 -0
  127. package/src/widgets/search/assets.ts +7 -0
  128. package/src/widgets/search/first.ts +15 -0
  129. package/src/widgets/search/form-url.ts +59 -0
  130. package/src/widgets/search/in-place.ts +226 -0
  131. package/src/widgets/search/index.ts +16 -0
  132. package/src/widgets/search/range.ts +260 -0
  133. package/src/widgets/search/route.ts +32 -0
  134. package/src/widgets/search/speculate.ts +202 -0
  135. package/src/widgets/search/style-search.css +65 -0
  136. package/src/widgets/search/template-search-box.ts +17 -0
  137. package/src/widgets/search/template-search-facets-default.ts +73 -0
  138. package/src/widgets/search/template-search-header.ts +14 -0
  139. package/src/widgets/search/template-search-pagination.ts +12 -0
  140. package/src/widgets/search/template-search-range-default.ts +48 -0
  141. package/src/widgets/search/template-search-result-default.ts +13 -0
  142. package/src/widgets/search/template-search-results-default.ts +8 -0
  143. package/tasks/model.mjs +116 -0
  144. package/tasks/run.mjs +305 -0
  145. package/tsconfig.json +14 -0
@@ -0,0 +1,132 @@
1
+ // Where to fix a template difference `yarn luma parity` found: the integration's
2
+ // file, from our side's selector. A style (a computed property, a box, the
3
+ // rendered font) is fixed in CSS: the integration's stylesheet with a rule
4
+ // for that class or marker. Structure (a missing part, its text, the order)
5
+ // is fixed in a template: the integration's `template-*.ts` that renders the
6
+ // class or marker, else Luma's built-in template that does (copied into the
7
+ // integration under the same name, it overrides it). Pure: the files are
8
+ // passed in (run.mjs reads integration/), unit-tested.
9
+
10
+ /** Luma's built-in templates by the marker they render (src/widgets/…), and the integration folder that overrides them. */
11
+ export const BUILT_IN = Object.freeze({
12
+ tile: 'search/template-search-result-default.ts',
13
+ image: 'search/template-search-result-default.ts',
14
+ title: 'search/template-search-result-default.ts',
15
+ price: 'search/template-search-result-default.ts',
16
+ 'old-price': 'search/template-search-result-default.ts',
17
+ badge: 'search/template-search-result-default.ts',
18
+ discount: 'search/template-search-result-default.ts',
19
+ list: 'search/template-search-results-default.ts',
20
+ count: 'search/template-search-header.ts',
21
+ sort: 'search/template-search-header.ts',
22
+ facets: 'search/template-search-facets-default.ts',
23
+ 'facet-group': 'search/template-search-facets-default.ts',
24
+ 'facet-title': 'search/template-search-facets-default.ts',
25
+ 'facet-value': 'search/template-search-facets-default.ts',
26
+ chip: 'search/template-search-facets-default.ts',
27
+ pagination: 'search/template-search-pagination.ts',
28
+ more: 'search/template-search-pagination.ts',
29
+ empty: 'search/template-search-box.ts',
30
+ 'ac-panel': 'autocomplete/template-autocomplete-box.ts',
31
+ 'ac-section': 'autocomplete/template-autocomplete-box.ts',
32
+ 'ac-title': 'autocomplete/template-autocomplete-box.ts',
33
+ 'ac-all': 'autocomplete/template-autocomplete-box.ts',
34
+ 'ac-empty': 'autocomplete/template-autocomplete-empty.ts',
35
+ 'ac-row': 'autocomplete/template-autocomplete-result-default.ts',
36
+ 'range-slider': 'search/template-search-range-default.ts',
37
+ 'range-min': 'search/template-search-range-default.ts',
38
+ 'range-max': 'search/template-search-range-default.ts',
39
+ });
40
+
41
+ /** Properties fixed in a template (markup), not in CSS. */
42
+ const STRUCTURE = new Set(['presence', 'text', 'order', 'unpaired', 'controls']);
43
+
44
+ /**
45
+ * What a selector's last compound names: its classes, its data-lbx-part
46
+ * markers, its other attributes, its tag.
47
+ * @param {string | null | undefined} sel
48
+ */
49
+ export const namesOf = (sel) => {
50
+ const last = String(sel ?? '').trim().split(/\s*[\s>+~]\s*(?![^[]*\])/).filter(Boolean).pop() ?? '';
51
+ const classes = [...last.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)].map((m) => m[1]);
52
+ const markers = [...last.matchAll(/\[data-lbx-part[~*|^$]?=["']?([\w-]+)["']?\]/g)].map((m) => m[1]);
53
+ const attrs = [...last.matchAll(/\[([\w-]+)(?:[~*|^$]?=[^\]]*)?\]/g)].map((m) => m[1]).filter((a) => a !== 'data-lbx-part');
54
+ const tag = /^[a-z][\w-]*/i.exec(last)?.[0] ?? null;
55
+ return { last, classes, markers, attrs, tag };
56
+ };
57
+
58
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
59
+ /** The folder a role's templates and styles live in. */
60
+ const folderOf = (role) => (/^ac-/.test(role ?? '') ? 'autocomplete' : 'search');
61
+
62
+ /**
63
+ * Where to fix one template difference (a `findings` entry), as a sentence:
64
+ * the file, the selector there, and what theirs has.
65
+ * @param {{ prop: string, group?: string, part: string, theirs: any, ours: any, selector?: { ours?: string | null, part?: { ours?: string | null } }, role?: string | null }} f
66
+ * @param {{ path: string, text: string }[]} files the integration's files (integration/…), and Luma's own templates (src/…: only to read the class of the element a marker names)
67
+ * @returns {{ file: string | null, kind: 'css' | 'template', hint: string }}
68
+ */
69
+ export const hintFor = (f, files = []) => {
70
+ const sel = f.selector?.part?.ours ?? f.selector?.ours ?? null;
71
+ const n = namesOf(sel);
72
+ const role = f.role ?? f.selector?.role ?? n.markers[0] ?? null;
73
+ const kind = STRUCTURE.has(f.prop) ? 'template' : 'css';
74
+ const show = (v) => (typeof v === 'string' ? v : JSON.stringify(v));
75
+ const folder = folderOf(role);
76
+ // The widget's own folder first (a title marker is in the tile's template and in the autocomplete row's).
77
+ const mine = (x) => (x.path.startsWith(`integration/${folder}/`) ? 0 : 1);
78
+ const css = files.filter((x) => /\.css$/.test(x.path) && !x.path.startsWith('src/')).sort((a, b) => mine(a) - mine(b));
79
+ const ofLuma = files.filter((x) => x.path.startsWith('src/'));
80
+ const tpl = files.filter((x) => /(^|\/)template-[^/]*\.ts$/.test(x.path) && !x.path.startsWith('src/')).sort((a, b) => mine(a) - mine(b));
81
+ // A marker alone (ours named by data-lbx-part): the class the integration's template gives that element.
82
+ if (!n.classes.length && n.markers.length)
83
+ for (const x of [...tpl, ...ofLuma]) {
84
+ const tag = new RegExp(`<[a-z][^>]*data-lbx-part=["'][^"']*(?<![\\w-])${esc(n.markers[0])}(?![\\w-])[^>]*>`).exec(x.text)?.[0];
85
+ const cls = tag && /class=["']([^"'$]+)["']/.exec(tag)?.[1].trim().split(/\s+/)[0];
86
+ if (cls) {
87
+ n.classes.push(cls);
88
+ break;
89
+ }
90
+ }
91
+ const target = n.classes.length ? `.${n.classes[0]}` : n.markers.length ? `[data-lbx-part~="${n.markers[0]}"]` : (n.last || sel || '(our element)');
92
+ const inCss = (x) => n.classes.some((c) => new RegExp(`\\.${esc(c)}(?![\\w-])`).test(x.text)) || n.markers.some((m) => new RegExp(`data-lbx-part[~*]?=["']?${esc(m)}["']?`).test(x.text));
93
+ const inTpl = (x) => n.classes.some((c) => new RegExp(`class(Name)?=["'\`][^"'\`]*(?<![\\w-])${esc(c)}(?![\\w-])`).test(x.text)) || n.markers.some((m) => new RegExp(`data-lbx-part=["']?[^"']*(?<![\\w-])${esc(m)}(?![\\w-])`).test(x.text));
94
+ if (kind === 'css') {
95
+ const GRID = {
96
+ columns: 'grid-template-columns',
97
+ columnGap: 'column-gap',
98
+ rowGap: 'row-gap',
99
+ itemWidth: 'the items\' width',
100
+ 'sibling-row-gap': 'the space between one and the next below it (a margin, a `+ *` rule, the parent\'s row-gap)',
101
+ 'sibling-column-gap': 'the space between one and the next beside it (a margin, the parent\'s column-gap)',
102
+ };
103
+ if (f.prop === 'sibling-direction') GRID[f.prop] = 'stacked or side by side (the parent\'s display, flex-direction)';
104
+ const what =
105
+ f.prop === 'rendered-font' ? `the font theirs renders (${show(f.theirs)}): font-family, and the @font-face it needs`
106
+ : ['x', 'y', 'width', 'height'].includes(f.prop) ? `its ${f.prop} (theirs ${show(f.theirs)}px, ours ${show(f.ours)}px): the size, padding, margin or gap that sets it`
107
+ : f.prop === 'pixels' ? 'what the other differences leave (the crops in the HTML report)'
108
+ : GRID[f.prop] ? `${GRID[f.prop]}: theirs ${show(f.theirs)}${f.prop === 'columns' ? ' columns per row' : f.prop === 'sibling-direction' ? '' : 'px'}, ours ${show(f.ours)} (at the viewports listed)`
109
+ : `${f.prop}: ${show(f.theirs)}`;
110
+ const has = css.find(inCss);
111
+ if (has) return { file: has.path, kind, hint: `${has.path}: ${target} { ${what} }` };
112
+ const own = css.find((x) => x.path.startsWith(`integration/${folder}/`) && !/style-host\.css$/.test(x.path)) ?? null;
113
+ const file = own?.path ?? `integration/${folder}/style-${folder}.css`;
114
+ return { file, kind, hint: `${file}${own ? '' : ' (new)'}: a rule ${target} { ${what} }` };
115
+ }
116
+ const has = tpl.find(inTpl);
117
+ const what =
118
+ f.prop === 'presence' ? (f.theirs === 'present' ? `render the ${f.part === 'root' ? 'element' : f.part} theirs has` : `remove the ${f.part === 'root' ? 'element' : f.part} theirs does not have`)
119
+ : f.prop === 'text' ? `its text: theirs ${JSON.stringify(f.theirs)}, ours ${JSON.stringify(f.ours)}`
120
+ : f.prop === 'order' ? `the parts in their order: ${f.theirs}`
121
+ : f.prop === 'controls' ? `the controls theirs has in the ${f.part} (theirs ${show(f.theirs)}, ours ${show(f.ours)}): ${f.delta}`
122
+ : `${f.prop}: ${show(f.theirs)}`;
123
+ if (has) return { file: has.path, kind, hint: `${has.path} (${target}): ${what}` };
124
+ // A part ours lacks: the template that renders its component's root.
125
+ const rn = namesOf(f.selector?.ours ?? null);
126
+ const rootTpl = f.selector?.part ? tpl.find((x) => rn.classes.some((c) => new RegExp(`class(Name)?=["'\`][^"'\`]*(?<![\\w-])${esc(c)}(?![\\w-])`).test(x.text)) || rn.markers.some((m) => new RegExp(`data-lbx-part=["']?[^"']*(?<![\\w-])${esc(m)}(?![\\w-])`).test(x.text))) : null;
127
+ const builtIn = BUILT_IN[role ?? ''] ?? BUILT_IN[rn.markers[0] ?? ''] ?? null;
128
+ const own = rootTpl ?? (builtIn ? tpl.find((x) => x.path === `integration/${builtIn}`) : null);
129
+ if (own) return { file: own.path, kind, hint: `${own.path} (${target}${rootTpl ? `, inside ${rn.last}` : ''}): ${what}` };
130
+ if (builtIn) return { file: `integration/${builtIn}`, kind, hint: `integration/${builtIn} (new: a copy of Luma's src/widgets/${builtIn}, which renders ${target}): ${what}` };
131
+ return { file: null, kind, hint: `the template that renders ${target} (no integration file names it): ${what}` };
132
+ };
@@ -0,0 +1,54 @@
1
+ // Luma on a page as a customer installs it (README, "What the site includes"): the
2
+ // generic early-hold snippet (snippet/early-hold.min.js) first in <head>,
3
+ // then the build. `yarn luma parity` (ours in every mode: replay, --record,
4
+ // --live, --dev) and `yarn luma a11y` put the build in the storefront's own page
5
+ // this way: before, they imported the build alone, so every full page load
6
+ // painted their listing first (the snippet never held it), which production
7
+ // never shows once the rules are stored (docs/PARITY.md, "The hold").
8
+ //
9
+ // No HTML is rewritten (a recording replays byte for byte, a live page is the
10
+ // browser's own response): an init script (addInitScript, before any script
11
+ // of the page) watches for <head> and inserts the snippet as its first child,
12
+ // an inline <script> that runs at once, before the first script of their
13
+ // <head> (the parser runs a microtask checkpoint before each of its scripts)
14
+ // and before the first paint; then it imports the build.
15
+ import { readFileSync } from 'node:fs';
16
+
17
+ /** The snippet customers paste first in <head> (README's copy is the same: `yarn snippet:check`). */
18
+ export const SNIPPET = readFileSync(new URL('../snippet/early-hold.min.js', import.meta.url), 'utf8').trim();
19
+
20
+ /**
21
+ * In the page, before any of its scripts (addInitScript; self-contained):
22
+ * on `origin`'s top document, the snippet (`snippet`, its source) first in
23
+ * <head>, then `import(bundle)` (the build's entry). `snippet: ''` leaves it
24
+ * out (the build alone, as before). `window.__lbx.hold` (Luma's one global) says what was put
25
+ * in: `{ snippet: true|false, bundle }`.
26
+ * @param {{ origin: string, bundle: string | null, snippet: string, tag?: string }} cfg
27
+ */
28
+ export function holdInit(cfg) {
29
+ if (location.origin !== cfg.origin || window.top !== window) return;
30
+ const L = (window.__lbx = window.__lbx || {});
31
+ if (L.hold) return;
32
+ const state = (L.hold = { snippet: false, bundle: cfg.bundle || null });
33
+ let done = false;
34
+ const inject = () => {
35
+ if (done) return true;
36
+ const head = document.head;
37
+ if (!head) return false;
38
+ done = true;
39
+ if (cfg.snippet) {
40
+ const s = document.createElement('script');
41
+ s.setAttribute('data-lbx-parity', 'early-hold');
42
+ s.textContent = cfg.snippet;
43
+ head.insertBefore(s, head.firstChild);
44
+ state.snippet = true;
45
+ }
46
+ if (cfg.bundle) import(cfg.bundle).catch((e) => console.error(`[${cfg.tag || 'parity'}] the integration did not load`, e));
47
+ return true;
48
+ };
49
+ if (inject()) return;
50
+ const mo = new MutationObserver(() => {
51
+ if (inject()) mo.disconnect();
52
+ });
53
+ mo.observe(document, { childList: true, subtree: true });
54
+ }