@luigisbox/luma 0.4.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 (61) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +75 -8
  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 +1 -1
  8. package/src/analytics/pageview.ts +1 -1
  9. package/src/analytics/start.ts +3 -4
  10. package/src/analytics/wire.ts +1 -1
  11. package/src/api/ai.ts +32 -20
  12. package/src/api/token.ts +7 -7
  13. package/src/core/a11y.ts +6 -35
  14. package/src/core/context.ts +2 -0
  15. package/src/core/contrast-toggle.ts +32 -0
  16. package/src/core/default-button.ts +66 -0
  17. package/src/core/early-rules.ts +3 -2
  18. package/src/core/features.ts +109 -0
  19. package/src/core/frameworks-check.ts +32 -0
  20. package/src/core/frameworks.ts +35 -0
  21. package/src/core/image-load.ts +0 -26
  22. package/src/core/image-preload.ts +29 -0
  23. package/src/core/image-tags.ts +7 -1
  24. package/src/core/images.ts +3 -2
  25. package/src/core/integration.ts +42 -26
  26. package/src/core/listings.ts +9 -5
  27. package/src/core/locales.ts +2 -1
  28. package/src/core/own.ts +7 -3
  29. package/src/core/recommenders.ts +2 -2
  30. package/src/core/reconciler.ts +32 -23
  31. package/src/core/routes.ts +7 -6
  32. package/src/core/scheduler.ts +39 -5
  33. package/src/core/speculation.ts +2 -0
  34. package/src/core/stash.ts +15 -6
  35. package/src/core/styles.ts +6 -5
  36. package/src/core/support.ts +1 -1
  37. package/src/core/takeover.ts +6 -5
  38. package/src/core/templates.ts +5 -2
  39. package/src/core/tiles.ts +33 -10
  40. package/src/core/url.ts +6 -3
  41. package/src/core/widget-inits.ts +4 -1
  42. package/src/lit.ts +2 -1
  43. package/src/locales/cs.json +0 -1
  44. package/src/locales/de.json +0 -1
  45. package/src/locales/en.json +0 -1
  46. package/src/main.ts +70 -53
  47. package/src/types.ts +29 -9
  48. package/src/widgets/autocomplete/index.ts +58 -19
  49. package/src/widgets/listing/element.ts +81 -58
  50. package/src/widgets/listing/query.ts +6 -3
  51. package/src/widgets/listing/stash.ts +33 -16
  52. package/src/widgets/listing/state.ts +12 -0
  53. package/src/widgets/listing/view.ts +2 -1
  54. package/src/widgets/plp/route.ts +3 -2
  55. package/src/widgets/recommender/index.ts +3 -2
  56. package/src/widgets/search/direct.ts +39 -0
  57. package/src/widgets/search/form-url.ts +38 -34
  58. package/src/widgets/search/in-place.ts +43 -108
  59. package/src/widgets/search/index.ts +7 -2
  60. package/src/widgets/search/speculate.ts +48 -27
  61. package/src/widgets/search/submit.ts +136 -0
@@ -28,12 +28,36 @@ let framesRunning = false;
28
28
  const firstPaintDone = () =>
29
29
  framesRunning || (framesRunning = performance.getEntriesByType('paint').length > 0 || document.readyState === 'complete');
30
30
 
31
+ /** Longest wait for a frame: past it the reconcile runs in a task (afterFrame too, once frames are stalled). */
32
+ const FRAME_FALLBACK_MS = 100;
33
+ /** A frame asked for this long ago that has not come: frames are stalled (WebKit on a busy machine: none for ~5 s). */
34
+ const STALL_MS = 250;
35
+ let asked = 0; // our requestAnimationFrame callbacks not yet run
36
+ let askedAt = 0; // when the earliest of them was asked for, or the last frame of ours ran
37
+ /** requestAnimationFrame, counted: whether a frame is overdue is known to every caller. */
38
+ const frame = (cb: () => void) => {
39
+ if (!asked++) askedAt = performance.now();
40
+ requestAnimationFrame(() => {
41
+ asked--;
42
+ askedAt = performance.now(); // a frame came: the asks still waiting start the clock anew
43
+ cb();
44
+ });
45
+ };
46
+ const framesStalled = () => asked > 0 && performance.now() - askedAt > STALL_MS;
47
+
31
48
  export const schedule = () => {
32
49
  if (pending) return;
33
50
  pending = true;
34
51
  // rAF does not run in background tabs or before the first paint; use a timeout there.
35
- if (document.visibilityState === 'visible' && firstPaintDone()) requestAnimationFrame(flush);
36
- else setTimeout(flush, document.visibilityState === 'visible' ? 16 : 50);
52
+ if (document.visibilityState !== 'visible' || !firstPaintDone()) return void setTimeout(flush, document.visibilityState === 'visible' ? 16 : 50);
53
+ // A visible page can still run no frames for seconds while its timers run
54
+ // (WebKit on a busy 2-CPU machine: ~5 s after typing). Waiting for one
55
+ // then outlasted the hold's timeout: their listing came back, ours stayed
56
+ // hidden. Whichever comes first, once.
57
+ let done = false;
58
+ const run = () => !done && ((done = true), flush());
59
+ frame(run);
60
+ setTimeout(run, FRAME_FALLBACK_MS);
37
61
  };
38
62
 
39
63
  export const scheduleIn = (ms: number) => setTimeout(schedule, ms);
@@ -43,7 +67,11 @@ export const scheduleIn = (ms: number) => setTimeout(schedule, ms);
43
67
  * are clean then, so reading computed styles or sizes costs only what we read,
44
68
  * not a style pass over the whole (host) document. Where no frames run (a
45
69
  * hidden tab, before the first paint) in a task of its own; a tab hidden
46
- * meanwhile (rAF waits until it is shown) by a timeout.
70
+ * meanwhile (rAF waits until it is shown) by a timeout: 500 ms, so a slow
71
+ * frame (a large page at 4x CPU) still comes first and the read costs no
72
+ * forced style pass; FRAME_FALLBACK_MS once frames are stalled (none for
73
+ * STALL_MS while asked for: WebKit ran none for ~5 s on a busy machine, and a
74
+ * listing's first render, one step per frame, showed nothing for 2.5 s then).
47
75
  */
48
76
  export const afterFrame = (f: Task) => {
49
77
  if (document.visibilityState !== 'visible') {
@@ -57,8 +85,14 @@ export const afterFrame = (f: Task) => {
57
85
  const run = () => !done && ((done = true), f());
58
86
  // Ahead of the page's own queued tasks where the browser can (Chrome's scheduler).
59
87
  const post = (globalThis as any).scheduler?.postTask;
60
- requestAnimationFrame(() => (post ? post.call((globalThis as any).scheduler, run, { priority: 'user-blocking' }) : setTimeout(run, 0)));
61
- setTimeout(run, 500);
88
+ frame(() => (post ? post.call((globalThis as any).scheduler, run, { priority: 'user-blocking' }) : setTimeout(run, 0)));
89
+ const t0 = performance.now();
90
+ const tick = () => {
91
+ if (done) return;
92
+ if (framesStalled() || performance.now() - t0 >= 500) run();
93
+ else setTimeout(tick, FRAME_FALLBACK_MS);
94
+ };
95
+ setTimeout(tick, FRAME_FALLBACK_MS);
62
96
  };
63
97
 
64
98
  let pendingNow = false;
@@ -18,6 +18,7 @@
18
18
  // activation (core/takeover.ts).
19
19
 
20
20
  import { mark } from './health';
21
+ import { cspNonce } from './styles';
21
22
  import type { GlobalConfigType } from '../types';
22
23
 
23
24
  const LINKS = ':is(lbx-search, lbx-plp) [data-lbx-list] a[href], lbx-reco a[href], [data-lbx="ac-overlay"] a[href]';
@@ -49,6 +50,7 @@ export const speculate = (g: GlobalConfigType, onUrlChange: (f: () => void) => u
49
50
  el = document.createElement('script');
50
51
  el.type = 'speculationrules';
51
52
  el.setAttribute('data-lbx', 'speculation');
53
+ el.nonce = cspNonce(); // a strict script-src applies to inline speculation rules
52
54
  el.textContent = JSON.stringify(rules);
53
55
  document.head.append(el);
54
56
  };
package/src/core/stash.ts CHANGED
@@ -1,11 +1,17 @@
1
- // A listing result kept in sessionStorage for the next page load of its URL
2
- // (widgets/listing/stash.ts reads and writes them). The entry only asks
1
+ // A listing result kept in sessionStorage for the next page load that shows
2
+ // it (widgets/listing/stash.ts reads and writes them): a listing left for
3
+ // Back, under its page's URL; a typed query's search page, under the query
4
+ // (the URL its form submits to is often not the one its page loads at: a
5
+ // server that redirects `/search?q=x` to `/search/x/`). The entry only asks
3
6
  // whether this load can use one: a search page then does not send its own
4
7
  // request before the listing's chunk has read it.
5
8
  import { pageKey } from './page';
6
9
 
7
10
  export const STASH = 'lbx:r:';
11
+ /** A listing left for Back: its page. */
8
12
  export const stashKey = (href: string) => STASH + pageKey(href);
13
+ /** A typed query's result (`spec`): the query, on its route, whatever URL its page loads at. */
14
+ export const specKey = (route: string, q: string) => `${STASH}spec:${route}:${q}`;
9
15
 
10
16
  /** How this document was loaded, and its URL (a later SPA visit is not the load a stash is for). */
11
17
  export const navigation = () => {
@@ -47,11 +53,14 @@ export const stashFits = (kind: string, at = 0) => {
47
53
  return Date.now() - at < ROUND_TRIP_MS && (t === 'reload' || (t === 'navigate' && fromItself()));
48
54
  };
49
55
 
50
- /** A stash this load will likely use (the listing checks it fully). Reads only the start of the entry. */
51
- export const hasStash = (href: string) => {
56
+ /** A stash this load will likely use (the listing checks it fully): the page's for Back, or the search query's (`spec`). Reads only the start of the entry. */
57
+ export const hasStash = (href: string, spec?: { route: string; q: string }) => {
52
58
  try {
53
- const head = sessionStorage.getItem(stashKey(href))?.slice(0, 60);
54
- return !!head && stashFits(head.includes('"kind":"back"') ? 'back' : 'spec', Number(/"at":(\d+)/.exec(head)?.[1]) || 0);
59
+ const fits = (key: string) => {
60
+ const head = sessionStorage.getItem(key)?.slice(0, 60);
61
+ return !!head && stashFits(head.includes('"kind":"back"') ? 'back' : 'spec', Number(/"at":(\d+)/.exec(head)?.[1]) || 0);
62
+ };
63
+ return fits(stashKey(href)) || (!!spec?.q && fits(specKey(spec.route, spec.q)));
55
64
  } catch {
56
65
  return false;
57
66
  }
@@ -13,13 +13,14 @@
13
13
  // route-scoped. CSS survives host re-renders, DOM mutations would not.
14
14
  // Every sheet is put back when a host re-render drops it (onFlush).
15
15
 
16
+ import { DOCUMENT_REACT } from './frameworks';
16
17
  import { documentMayHydrate, hasFiber, watchOwnRemovals } from './own';
17
18
  import { onFlush } from './scheduler';
18
19
 
19
- // While <head> is still parsing, on every page: whole-document frameworks
20
- // announce themselves only at the end of <body>, and the body-start observer
21
- // puts ours in before anything of the body paints.
22
- const deferred = () => !document.body;
20
+ // While <head> is still parsing, on every page that may be whole-document
21
+ // React (DOCUMENT_REACT): it announces itself only at the end of <body>, and
22
+ // the body-start observer puts ours in before anything of the body paints.
23
+ const deferred = () => (DOCUMENT_REACT ? !document.body : false);
23
24
 
24
25
  const sheets = new Map<string, HTMLStyleElement>();
25
26
 
@@ -38,7 +39,7 @@ const declareLayer = () => {
38
39
  const head = document.head || document.documentElement;
39
40
  // React has it once it has claimed <head>, or <html> when its root renders
40
41
  // no <head> of its own (Next's app router: Next fills it): hydrated past it.
41
- const early = (document.readyState === 'loading' || documentMayHydrate()) && !hasFiber(head) && !hasFiber(document.documentElement);
42
+ const early = DOCUMENT_REACT ? (document.readyState === 'loading' || documentMayHydrate()) && !hasFiber(head) && !hasFiber(document.documentElement) : false;
42
43
  if (order?.isConnected && (early || head.firstElementChild === order)) return;
43
44
  order ??= styleEl();
44
45
  order.textContent = '@layer lbx;';
@@ -9,4 +9,4 @@ export { speculate } from './speculation';
9
9
  // Images (./images.ts, ./image-tags.ts, ./image-load.ts): every widget's templates and pages.
10
10
  export { imageHelpers } from './image-tags';
11
11
  export { firstRowCount, firstRowImages, imageOf } from './images';
12
- export { preloadImages, settleImages, warmImages } from './image-load';
12
+ export { settleImages, warmImages } from './image-load';
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { earlyHold } from './early-rules';
20
20
  import { canon, isOwnEntry } from './entries';
21
+ import { SSR } from './features';
21
22
  import { mark } from './health';
22
23
  import { failKey, failures as storedFailures, holding, LOADING, MAX_FAILURES, painted, reserveCss } from './hold';
23
24
  import { onUrlChange } from './sense';
@@ -104,7 +105,7 @@ export const takeover = (r: TakeoverRoute): Takeover => {
104
105
  // A server-rendered fragment carries its own CSS hiding their listing (for
105
106
  // pages without JavaScript). Once we write ours, per state, it goes: theirs
106
107
  // must come back when we hand the page back.
107
- for (const el of document.querySelectorAll(`style[data-lbx-ssr-host="${r.id}"]`)) el.remove();
108
+ if (SSR) for (const el of document.querySelectorAll(`style[data-lbx-ssr-host="${r.id}"]`)) el.remove();
108
109
  const key = `host-${r.id}`;
109
110
  if (!r.accepts(new URL(location.href))) return setCss(key, null);
110
111
  const reserve = reserveCss(r.reserveSelector);
@@ -116,7 +117,7 @@ export const takeover = (r: TakeoverRoute): Takeover => {
116
117
  else setCss(key, r.hostCss + reserve);
117
118
  };
118
119
 
119
- let ssr = ssrOf(r);
120
+ let ssr = SSR ? ssrOf(r) : null;
120
121
  let visitStartedAtBoot = true;
121
122
  const t: Takeover = {
122
123
  get state() {
@@ -126,7 +127,7 @@ export const takeover = (r: TakeoverRoute): Takeover => {
126
127
  return ssr;
127
128
  },
128
129
  serverRendered: () => {
129
- if (ssr === 'rendered' || !visitStartedAtBoot) return;
130
+ if (!SSR || ssr === 'rendered' || !visitStartedAtBoot) return;
130
131
  ssr = 'rendered';
131
132
  countFailures(0); // the page is ours after all (a late include is not a failure)
132
133
  if (state !== 'ours') set('ours');
@@ -155,7 +156,7 @@ export const takeover = (r: TakeoverRoute): Takeover => {
155
156
  // A server-rendered visit whose listing is on the page (the server's, or
156
157
  // ours after hydration) keeps it: with SSI there is no listing of theirs
157
158
  // in the page to hand back to.
158
- if (ssr === 'rendered' && document.querySelector(`${r.tag}[data-lbx]`)?.childElementCount) return;
159
+ if (SSR && ssr === 'rendered' && document.querySelector(`${r.tag}[data-lbx]`)?.childElementCount) return;
159
160
  if (failed) fail();
160
161
  else set('released');
161
162
  },
@@ -208,7 +209,7 @@ export const takeover = (r: TakeoverRoute): Takeover => {
208
209
  const early = earlyHold();
209
210
  // Failing: an early hold is not continued (it ends below, with drop()).
210
211
  const claimed = wasActive && !t.failing && !!early?.claim?.(r.id);
211
- if (wasActive && ssr) set(ssr === 'rendered' ? 'ours' : 'released');
212
+ if (SSR && wasActive && ssr) set(ssr === 'rendered' ? 'ours' : 'released');
212
213
  else if (wasActive) set(!t.failing && (claimed || r.held || !painted()) ? 'hold' : 'native');
213
214
  else css();
214
215
  if (claimed) {
@@ -5,6 +5,7 @@
5
5
  // scope plus props; lit does the DOM updates (only changed bindings touch
6
6
  // the DOM, so images, focus and open <details> survive re-renders).
7
7
 
8
+ import { SSR } from './features';
8
9
  import { nothing, render } from 'lit-html';
9
10
 
10
11
  export type Template<S = any> = (s: S) => unknown;
@@ -31,7 +32,7 @@ export const templateOf = (templates: Templates, name: string, data: Record<stri
31
32
  const s: any = { ...d };
32
33
  s.include = (n: string, props?: Record<string, unknown>) => {
33
34
  const t = templates[n];
34
- if (!t) throw new Error(`template "${n}" not found`);
35
+ if (!t) throw Error(`template "${n}" not found`);
35
36
  if (!props) return t(s);
36
37
  const r = props.r;
37
38
  if (!r || typeof r !== 'object') return t(scope({ ...s, ...props }));
@@ -70,5 +71,7 @@ export const view = (root: HTMLElement | ShadowRoot, templates: Templates) => ({
70
71
  * server-side rendering): binds to the existing DOM instead of re-creating it.
71
72
  * `hydrate` is @lit-labs/ssr-client's, loaded only on server-rendered pages.
72
73
  */
73
- hydrate: (name: string, data: Record<string, unknown>, hydrate: (v: unknown, c: HTMLElement | ShadowRoot) => void) => hydrate(templateOf(templates, name, data), root),
74
+ hydrate: SSR
75
+ ? (name: string, data: Record<string, unknown>, hydrate: (v: unknown, c: HTMLElement | ShadowRoot) => void) => hydrate(templateOf(templates, name, data), root)
76
+ : (undefined as never), // no server-rendered listings in this build (core/features.ts)
74
77
  });
package/src/core/tiles.ts CHANGED
@@ -22,8 +22,10 @@
22
22
  // the click may leave the page: so each is first queued in sessionStorage, in
23
23
  // the click's own task, where analytics claims it, on this page or the next.
24
24
  // Not where the integration has no analytics (`_global.analytics` false: the
25
- // entry sets it where there is no _analytics.ts).
25
+ // entry sets it where there is no _analytics.ts), and not in its build
26
+ // (core/features.ts ANALYTICS): there a tile only puts products in the cart.
26
27
 
28
+ import { ANALYTICS, CART } from './features';
27
29
  import { reportError } from './health';
28
30
  import { session } from './storage';
29
31
  import type { GlobalConfigType, Result } from '../types';
@@ -56,7 +58,7 @@ export interface Watched {
56
58
  /** The queue analytics claims from (src/analytics/triggers.ts). */
57
59
  export const TILES = 'lbx:tiles';
58
60
  const announce = (g: GlobalConfigType, e: Omit<TileEvent, 'id' | 'url' | 'at'>) => {
59
- if (g.analytics === false) return;
61
+ if (!ANALYTICS || g.analytics === false) return;
60
62
  const d = { ...e, id: crypto.randomUUID(), url: location.href, at: Date.now() };
61
63
  session.set(TILES, [...(session.get<TileEvent[]>(TILES) ?? []), d].slice(-20));
62
64
  try {
@@ -68,11 +70,33 @@ const announce = (g: GlobalConfigType, e: Omit<TileEvent, 'id' | 'url' | 'at'>)
68
70
  const reference = (r: Result) => `${r.type || 'product'}/@id:${r.id}`;
69
71
 
70
72
  /** A result clicked (a link to it, or our own navigation: the autocomplete). */
71
- export const picked = (r: Result, index: number, list: string, g: GlobalConfigType) => announce(g, { kind: 'click', reference: reference(r), result: r, index, list });
73
+ export const picked = (r: Result, index: number, list: string, g: GlobalConfigType) => (ANALYTICS ? announce(g, { kind: 'click', reference: reference(r), result: r, index, list }) : undefined);
74
+
75
+ /**
76
+ * A product click, announced: only ever announced, so not in a build without
77
+ * analytics (core/features.ts). Out here, not in watch()'s try: Rollup folds
78
+ * no flag inside a try (its tryCatchDeoptimization).
79
+ */
80
+ const clicked = ANALYTICS
81
+ ? (t: Element, tile: Element, at: number, n: number, results: readonly Result[], w: Watched) => {
82
+ const a = t.closest<HTMLAnchorElement>('a[href]');
83
+ if (!a || !tile.contains(a)) return;
84
+ const r = resultOf(a, at, n, results);
85
+ if (r) picked(r.r, r.i, w.list, w.global);
86
+ }
87
+ : null;
88
+
89
+ /**
90
+ * The cart button a click is on: only where the integration's templates have
91
+ * one (core/features.ts CART). Out of watch()'s try, as `clicked`.
92
+ */
93
+ const cartOf = CART ? (t: Element) => t.closest<HTMLElement>('[data-lbx-cart]') : null;
72
94
 
73
95
  const watched = new WeakSet<Element>();
74
96
  /** Product clicks and add-to-carts inside `root` (once per root). */
75
97
  export const watch = (root: Element, w: Watched) => {
98
+ // Nothing to see: no cart button in the integration's templates, no analytics (core/features.ts).
99
+ if (!CART && !ANALYTICS) return;
76
100
  if (watched.has(root)) return;
77
101
  watched.add(root);
78
102
  const on = (e: MouseEvent) => {
@@ -84,12 +108,9 @@ export const watch = (root: Element, w: Watched) => {
84
108
  const results = w.results();
85
109
  const tiles = [...tile.parentElement!.children];
86
110
  const at = tiles.indexOf(tile);
87
- const cart = t!.closest<HTMLElement>('[data-lbx-cart]');
111
+ const cart = cartOf?.(t!);
88
112
  if (cart && tile.contains(cart)) return void (e.type === 'click' && toCart(e, cart, tile, at, tiles.length, results, w));
89
- const a = t!.closest<HTMLAnchorElement>('a[href]');
90
- if (!a || !tile.contains(a)) return;
91
- const r = resultOf(a, at, tiles.length, results);
92
- if (r) picked(r.r, r.i, w.list, w.global);
113
+ if (clicked) clicked(t!, tile, at, tiles.length, results, w);
93
114
  } catch (err) {
94
115
  reportError('tile click', err);
95
116
  }
@@ -126,8 +147,10 @@ const toCart = (e: MouseEvent, el: HTMLElement, tile: Element, at: number, n: nu
126
147
  const hook = w.global.addToCart;
127
148
  if (typeof hook !== 'function') return reportError('add to cart', '[data-lbx-cart] needs _global.addToCart (_init.ts or _global/_widget.ts) to put the product in the site\'s cart');
128
149
  const count = Math.max(1, Math.round(Number(el.dataset.lbxCart) || 1));
129
- const price = typeof hit.r.c.price === 'number' ? hit.r.c.price : undefined;
130
- announce(w.global, { kind: 'add_to_cart', reference: reference(hit.r), result: hit.r, index: hit.i, list: w.list, count, ...(price !== undefined ? { price } : {}) });
150
+ if (ANALYTICS) {
151
+ const price = typeof hit.r.c.price === 'number' ? hit.r.c.price : undefined;
152
+ announce(w.global, { kind: 'add_to_cart', reference: reference(hit.r), result: hit.r, index: hit.i, list: w.list, count, ...(price !== undefined ? { price } : {}) });
153
+ }
131
154
  try {
132
155
  const p = hook(hit.r, { count, el, event: e });
133
156
  if (p && typeof (p as Promise<unknown>).catch === 'function') (p as Promise<unknown>).catch((err) => reportError('addToCart', err));
package/src/core/url.ts CHANGED
@@ -18,6 +18,7 @@
18
18
  // null-state entries altogether.
19
19
 
20
20
  import { canon, markOwnEntry } from './entries';
21
+ import { ASTRO, NEXT, SVELTEKIT, VUE } from './frameworks';
21
22
 
22
23
  interface HostRouter {
23
24
  push(to: string): unknown;
@@ -42,6 +43,7 @@ const samePath = (a: string, b: string) => {
42
43
  };
43
44
 
44
45
  const vueRouter = (): HostRouter | null => {
46
+ if (!VUE) return null;
45
47
  // Vue 3 (the app on its mount element: every one, a widget app may come first), Vue 2 (the root instance), Nuxt 2.
46
48
  const candidates = [
47
49
  ...[...document.querySelectorAll('#__nuxt, #app, [data-v-app]')].map((el: any) => el.__vue_app__?.config?.globalProperties?.$router ?? el.__vue__?.$router),
@@ -68,6 +70,7 @@ const vueRouter = (): HostRouter | null => {
68
70
 
69
71
  /** Next.js pages router. The app router (it sets window.next.router too) follows raw history writes by itself. */
70
72
  const nextRouter = (): HostRouter | null => {
73
+ if (!NEXT) return null;
71
74
  const w = window as any;
72
75
  if (w.next?.appDir || !w.__NEXT_DATA__) return null;
73
76
  const r = w.next?.router;
@@ -81,7 +84,7 @@ const hostRouter = () => vueRouter() ?? nextRouter();
81
84
  /** A mounted router found (usable, or hash mode), or waited for one in vain: writes are not queued from then on. */
82
85
  let settled = false;
83
86
  /** A server-rendered Vue app whose client app has not mounted yet (no router to talk to). vueRouter() may settle it (a hash-mode router: null, settled), hence `settled` is read again after it. */
84
- const vueNotMountedYet = () => !settled && !!((window as any).__NUXT__ || document.querySelector('#__nuxt, [data-server-rendered]')) && !vueRouter() && !settled;
87
+ const vueNotMountedYet = () => (VUE ? !settled && !!((window as any).__NUXT__ || document.querySelector('#__nuxt, [data-server-rendered]')) && !vueRouter() && !settled : false);
85
88
 
86
89
  /** The router's own path for a URL: without its base, which it adds itself. */
87
90
  const routerPath = (r: HostRouter, url: URL) => {
@@ -140,8 +143,8 @@ const SK_NAVIGATION = 'sveltekit:navigation';
140
143
  const entryState = () => {
141
144
  const s = history.state;
142
145
  if (!s || typeof s !== 'object') return null;
143
- if (s[SK_HISTORY] && SK_NAVIGATION in s) return { [SK_HISTORY]: s[SK_HISTORY], [SK_NAVIGATION]: s[SK_NAVIGATION] };
144
- if (typeof s.index === 'number' && document.querySelector('meta[name="astro-view-transitions-enabled"]')) return { index: s.index, scrollX, scrollY };
146
+ if (SVELTEKIT && s[SK_HISTORY] && SK_NAVIGATION in s) return { [SK_HISTORY]: s[SK_HISTORY], [SK_NAVIGATION]: s[SK_NAVIGATION] };
147
+ if (ASTRO && typeof s.index === 'number' && document.querySelector('meta[name="astro-view-transitions-enabled"]')) return { index: s.index, scrollX, scrollY };
145
148
  return null;
146
149
  };
147
150
 
@@ -3,6 +3,7 @@
3
3
  // entry: the hooks' modules are in the widget's own chunks anyway. No DOM:
4
4
  // the server renderer runs them for its listings too.
5
5
 
6
+ import { WIDGET_INITS } from './features';
6
7
  import { folderOf, slot, type IntegrationFiles } from './integration';
7
8
  import type { LumaApi, LumaConfigType, LumaWidgetInitType } from '../types';
8
9
 
@@ -10,9 +11,11 @@ const widgetInitsRun = new WeakMap<object, Map<string, Promise<void>>>();
10
11
  /**
11
12
  * The `_widget.ts` of a widget's folders, in folder order, each run once per
12
13
  * config (a folder shared by widgets, `_global`, runs for the first): before
13
- * the widget's context is made from the config they complete.
14
+ * the widget's context is made from the config they complete. An integration
15
+ * without a _widget.ts (core/features.ts) builds none of it: 121 B gzip.
14
16
  */
15
17
  export const loadWidgetInits = (config: LumaConfigType, files: Pick<IntegrationFiles, 'widgetInits'>, folders: readonly string[], version: string) => {
18
+ if (!WIDGET_INITS) return Promise.resolve();
16
19
  let ran = widgetInitsRun.get(config);
17
20
  if (!ran) widgetInitsRun.set(config, (ran = new Map()));
18
21
  const Luma: LumaApi = { version, config };
package/src/lit.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // lit-html for templates: `import { html, repeat } from '@/lit'` in an
2
2
  // integration, so every template uses the framework's one copy of lit.
3
- // Only what a template uses ends up in the bundle. Note for form controls:
3
+ // Only what a template uses ends up in the bundle (a directive no template
4
+ // uses too: build/vite.mjs pureDirectives). Note for form controls:
4
5
  // bind `.checked` / `.selected` plainly (not with live()): lit then leaves a
5
6
  // control the user just changed alone until the state it drives agrees.
6
7
  export { html, svg, nothing } from 'lit-html';
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "_name": "Čeština",
3
3
  "autocomplete": {
4
- "showAllResults": "Zobrazit všechny výsledky",
5
4
  "empty": "Pro „{query}“ jsme nic nenašli",
6
5
  "types": {
7
6
  "product": "Produkty",
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "_name": "Deutsch",
3
3
  "autocomplete": {
4
- "showAllResults": "Alle Ergebnisse anzeigen",
5
4
  "empty": "Keine Treffer für „{query}“",
6
5
  "types": {
7
6
  "product": "Produkte",
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "_name": "English",
3
3
  "autocomplete": {
4
- "showAllResults": "See all results",
5
4
  "empty": "No results for “{query}”",
6
5
  "types": {
7
6
  "product": "Products",