@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.
- package/CHANGELOG.md +118 -0
- package/README.md +75 -8
- package/build/baseline.mjs +2 -2
- package/build/vite.mjs +253 -20
- package/docs/ACCESSIBILITY.md +9 -2
- package/package.json +3 -2
- package/src/analytics/chain.ts +1 -1
- package/src/analytics/pageview.ts +1 -1
- package/src/analytics/start.ts +3 -4
- package/src/analytics/wire.ts +1 -1
- package/src/api/ai.ts +32 -20
- package/src/api/token.ts +7 -7
- package/src/core/a11y.ts +6 -35
- package/src/core/context.ts +2 -0
- package/src/core/contrast-toggle.ts +32 -0
- package/src/core/default-button.ts +66 -0
- package/src/core/early-rules.ts +3 -2
- package/src/core/features.ts +109 -0
- package/src/core/frameworks-check.ts +32 -0
- package/src/core/frameworks.ts +35 -0
- package/src/core/image-load.ts +0 -26
- package/src/core/image-preload.ts +29 -0
- package/src/core/image-tags.ts +7 -1
- package/src/core/images.ts +3 -2
- package/src/core/integration.ts +42 -26
- package/src/core/listings.ts +9 -5
- package/src/core/locales.ts +2 -1
- package/src/core/own.ts +7 -3
- package/src/core/recommenders.ts +2 -2
- package/src/core/reconciler.ts +32 -23
- package/src/core/routes.ts +7 -6
- package/src/core/scheduler.ts +39 -5
- package/src/core/speculation.ts +2 -0
- package/src/core/stash.ts +15 -6
- package/src/core/styles.ts +6 -5
- package/src/core/support.ts +1 -1
- package/src/core/takeover.ts +6 -5
- package/src/core/templates.ts +5 -2
- package/src/core/tiles.ts +33 -10
- package/src/core/url.ts +6 -3
- package/src/core/widget-inits.ts +4 -1
- package/src/lit.ts +2 -1
- package/src/locales/cs.json +0 -1
- package/src/locales/de.json +0 -1
- package/src/locales/en.json +0 -1
- package/src/main.ts +70 -53
- package/src/types.ts +29 -9
- package/src/widgets/autocomplete/index.ts +58 -19
- package/src/widgets/listing/element.ts +81 -58
- package/src/widgets/listing/query.ts +6 -3
- package/src/widgets/listing/stash.ts +33 -16
- package/src/widgets/listing/state.ts +12 -0
- package/src/widgets/listing/view.ts +2 -1
- package/src/widgets/plp/route.ts +3 -2
- package/src/widgets/recommender/index.ts +3 -2
- package/src/widgets/search/direct.ts +39 -0
- package/src/widgets/search/form-url.ts +38 -34
- package/src/widgets/search/in-place.ts +43 -108
- package/src/widgets/search/index.ts +7 -2
- package/src/widgets/search/speculate.ts +48 -27
- package/src/widgets/search/submit.ts +136 -0
package/docs/ACCESSIBILITY.md
CHANGED
|
@@ -105,7 +105,8 @@ guessed alt text, and forced colours and the OS setting are honoured.
|
|
|
105
105
|
the fallback products another, named by their heading and the note that
|
|
106
106
|
they are not results; the help box's phone link is a plain link. "Show all
|
|
107
107
|
products" is a real link (`href`: the
|
|
108
|
-
search page the site's form would submit to
|
|
108
|
+
search page the site's form would submit to, or with `search.queryUrl` the
|
|
109
|
+
query's page, the one Enter loads) and the open dropdown's one
|
|
109
110
|
tab stop: Tab from the input reaches it (the options' selection cleared:
|
|
110
111
|
one thing lit), Shift+Tab goes back, Escape closes and goes back, Tab moves
|
|
111
112
|
on to the page's next control after the input. It is also the arrow keys'
|
|
@@ -165,7 +166,10 @@ guessed alt text, and forced colours and the OS setting are honoured.
|
|
|
165
166
|
opened from the keyboard, focus goes to its selected item
|
|
166
167
|
(`aria-selected`/`aria-current`/`aria-checked`), else the first; arrows move
|
|
167
168
|
through its items; Escape closes it and focus returns to the button; picking
|
|
168
|
-
an item returns focus to the button; Tab out of it closes it.
|
|
169
|
+
an item returns focus to the button; Tab out of it closes it. The build
|
|
170
|
+
includes menus only where a file of the integration's repository (a script or `.html` file, not in `node_modules`, `dist` or a hidden folder) has `data-lbx-menu` (written literally, as in a template). A
|
|
171
|
+
`<details data-lbx-dropdown>` menu closes on a press outside it and on
|
|
172
|
+
picking an item; the same holds for it with `data-lbx-dropdown`.
|
|
169
173
|
- **The drawer** (`[data-lbx-drawer="open|close"]`): `aria-expanded` on its
|
|
170
174
|
openers. With a panel marked `[data-lbx-drawer-panel]`, while open, the panel
|
|
171
175
|
is a modal dialog (`role="dialog"`, `aria-modal="true"`, named "Filters",
|
|
@@ -176,6 +180,9 @@ guessed alt text, and forced colours and the OS setting are honoured.
|
|
|
176
180
|
focus staying on the control. With `facetsApply: 'staged'` its
|
|
177
181
|
changes wait for `[data-lbx-drawer="apply"]`; closing it otherwise discards
|
|
178
182
|
them (the scope's `pending` says whether there is anything to apply).
|
|
183
|
+
The build includes the drawer only where a file of the integration's
|
|
184
|
+
repository (a script or `.html` file, not in `node_modules`, `dist` or a hidden folder) has `data-lbx-drawer` (written
|
|
185
|
+
literally, as in a template).
|
|
179
186
|
- **Facet groups** (built-in template): each group's title is a `<button
|
|
180
187
|
aria-expanded aria-controls>` controlling its values (WAI-ARIA APG
|
|
181
188
|
accordion), focus staying on it. A group's search box
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luigisbox/luma",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Luma: Luigi's Box frontend integrations (autocomplete, search, category listings, recommenders) on the Luigi's Box AI API. Pre-release.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"snippet/early-hold.min.js",
|
|
30
30
|
"docs/ACCESSIBILITY.md",
|
|
31
31
|
"docs/INTEGRATION-PERFORMANCE.md",
|
|
32
|
-
"docs/PARITY.md"
|
|
32
|
+
"docs/PARITY.md",
|
|
33
|
+
"CHANGELOG.md"
|
|
33
34
|
],
|
|
34
35
|
"publishConfig": {
|
|
35
36
|
"access": "public"
|
package/src/analytics/chain.ts
CHANGED
|
@@ -71,7 +71,7 @@ export const reader = <E extends EventName, C>(event: E, f: (ctx: C) => Reads[E]
|
|
|
71
71
|
/** `.read(r)` on event `name`: `r`, or an error when it is a reader of another event. */
|
|
72
72
|
export const checked = (name: string, r: { event?: string }) => {
|
|
73
73
|
const e = name.startsWith('recommender:') ? 'recommender' : name;
|
|
74
|
-
if (r.event && r.event !== e) throw
|
|
74
|
+
if (r.event && r.event !== e) throw Error(`analytics: collect.${e}.read(): a reader of ${r.event}`);
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
export interface Chain<E extends EventName, C> {
|
|
@@ -42,7 +42,7 @@ export const pageView = {
|
|
|
42
42
|
},
|
|
43
43
|
read(r) {
|
|
44
44
|
checked('pageView', r);
|
|
45
|
-
if (chained) throw
|
|
45
|
+
if (chained) throw Error('analytics: one collect.pageView chain per setup');
|
|
46
46
|
chained = true;
|
|
47
47
|
const ref = (ctx: C) => {
|
|
48
48
|
try {
|
package/src/analytics/start.ts
CHANGED
|
@@ -33,10 +33,9 @@ export const start = async (o: StartOptions) => {
|
|
|
33
33
|
if (started) return;
|
|
34
34
|
started = true;
|
|
35
35
|
state.g = o.config._global;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
env
|
|
39
|
-
env.kept = o.kept;
|
|
36
|
+
// One call, not a read per field: a setup that uses none of them leaves
|
|
37
|
+
// Rollup no property reads to keep (it keeps a read: it may be a getter).
|
|
38
|
+
Object.assign(env, o);
|
|
40
39
|
// The setup (`_global` first). Its chains register as it runs.
|
|
41
40
|
for (const [p, f] of Object.entries(o.files).sort(([a], [b]) => (a.includes('/_global/') ? -1 : b.includes('/_global/') ? 1 : 0)))
|
|
42
41
|
try {
|
package/src/analytics/wire.ts
CHANGED
|
@@ -51,7 +51,7 @@ export const readDl = (x: any): DlEntry | null => {
|
|
|
51
51
|
return null;
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
const IGNORE =
|
|
54
|
+
const IGNORE = RegExp(`${TRACKING}|^${P.replace('.', '\\.')}`);
|
|
55
55
|
/**
|
|
56
56
|
* The page a URL shows, for pageviews: without tracking params, the hash and
|
|
57
57
|
* our own `lbx.*` (a filter, a sort on our listing is the same page). A
|
package/src/api/ai.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// Also runs in Node (the edge render service): there the
|
|
9
9
|
// storefront origin is sent explicitly and nothing touches the DOM.
|
|
10
10
|
|
|
11
|
+
import { PLP, RECOMMENDER, SEARCH } from '../core/features';
|
|
11
12
|
import { health } from '../core/health';
|
|
12
13
|
import type { GlobalConfigType } from '../types';
|
|
13
14
|
import type { Api, FacetData, Hit, Page } from '.';
|
|
@@ -15,6 +16,11 @@ import { fetchJson, TIMEOUT_MS, tokensFor, untilAborted, visitorId } from './tok
|
|
|
15
16
|
|
|
16
17
|
export { untilAborted };
|
|
17
18
|
|
|
19
|
+
/** A method of a widget the build has no folder for (never called). */
|
|
20
|
+
const unbuilt = (): never => {
|
|
21
|
+
throw Error('not in this build');
|
|
22
|
+
};
|
|
23
|
+
|
|
18
24
|
export const aiApi = (g: GlobalConfigType, origin?: string): Api => {
|
|
19
25
|
const tk = tokensFor(g, origin);
|
|
20
26
|
const { API, originHeaders } = tk;
|
|
@@ -48,8 +54,8 @@ export const aiApi = (g: GlobalConfigType, origin?: string): Api => {
|
|
|
48
54
|
return post(path, params, body, signal, true);
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
|
-
if (!r.ok) throw
|
|
52
|
-
if (r.json == null) throw
|
|
57
|
+
if (!r.ok) throw Error(`${path}: HTTP ${r.status} ${r.body.slice(0, 300)}`);
|
|
58
|
+
if (r.json == null) throw Error(`${path}: not JSON`);
|
|
53
59
|
return r.json;
|
|
54
60
|
};
|
|
55
61
|
|
|
@@ -75,24 +81,30 @@ export const aiApi = (g: GlobalConfigType, origin?: string): Api => {
|
|
|
75
81
|
return { hits: j.hits ?? [], total: j.total ?? j.total_approx ?? 0, guid: j.guid, cursor: j.next_page_cursor ?? null, status: j.pagination_status ?? 'exhausted' };
|
|
76
82
|
},
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
// A listing's and a recommender's (core/features.ts): not in a build without one.
|
|
85
|
+
facets:
|
|
86
|
+
SEARCH || PLP
|
|
87
|
+
? async (guid, facets, signal): Promise<Record<string, FacetData>> =>
|
|
88
|
+
(await post('facets', { channel_id: g.channelId }, { guid, facets: facets.map((f) => compact(f)) }, signal)).facets ?? {}
|
|
89
|
+
: unbuilt,
|
|
80
90
|
|
|
81
|
-
recommend:
|
|
82
|
-
(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
recommend: RECOMMENDER
|
|
92
|
+
? async (r, signal): Promise<Hit[]> =>
|
|
93
|
+
(
|
|
94
|
+
await post(
|
|
95
|
+
'recommender',
|
|
96
|
+
{ channel_id: g.channelId, surface_id: r.surface },
|
|
97
|
+
compact({
|
|
98
|
+
type: r.type,
|
|
99
|
+
size: r.size,
|
|
100
|
+
anchor_ids: r.anchors.length ? r.anchors.slice(0, 50) : undefined,
|
|
101
|
+
exclude_ids: r.exclude?.length ? r.exclude.slice(0, 50) : undefined,
|
|
102
|
+
filters: nonEmpty(r.filters),
|
|
103
|
+
return_fields: r.fields?.length ? r.fields : undefined,
|
|
104
|
+
}),
|
|
105
|
+
signal,
|
|
106
|
+
)
|
|
107
|
+
).hits ?? []
|
|
108
|
+
: unbuilt,
|
|
97
109
|
};
|
|
98
110
|
};
|
package/src/api/token.ts
CHANGED
|
@@ -99,7 +99,7 @@ export async function solveHere(nonce: string, difficulty: number, until: number
|
|
|
99
99
|
let t0 = Date.now();
|
|
100
100
|
for (let n = 0; ; n++) {
|
|
101
101
|
if ((n & 1023) === 0 && Date.now() - t0 > (slice || 50)) {
|
|
102
|
-
if (Date.now() > until) throw
|
|
102
|
+
if (Date.now() > until) throw Error('proof of work: no answer in time');
|
|
103
103
|
if (slice) await new Promise((r) => setTimeout(r));
|
|
104
104
|
t0 = Date.now();
|
|
105
105
|
}
|
|
@@ -142,8 +142,8 @@ const solve = (nonce: string, difficulty: number): Promise<number> => {
|
|
|
142
142
|
return here(); // CSP worker-src
|
|
143
143
|
}
|
|
144
144
|
// A worker that never answers is stopped.
|
|
145
|
-
timer = window.setTimeout(() => (done(), reject(
|
|
146
|
-
w.onmessage = (e) => (done(), e.data == null ? reject(
|
|
145
|
+
timer = window.setTimeout(() => (done(), reject(Error('proof of work: no answer in time'))), POW_TIMEOUT_MS);
|
|
146
|
+
w.onmessage = (e) => (done(), e.data == null ? reject(Error('proof of work failed')) : resolve(e.data));
|
|
147
147
|
w.onerror = here;
|
|
148
148
|
w.postMessage([nonce, difficulty, until]);
|
|
149
149
|
});
|
|
@@ -236,9 +236,9 @@ const tokens = (g: GlobalConfigType, origin?: string): Tokens => {
|
|
|
236
236
|
const url = challengeUrl(g);
|
|
237
237
|
// The one the entry asked while this chunk loaded (the first try only: a retry needs a new one).
|
|
238
238
|
const ch = await fetchJson(url, { headers: originHeaders }, TIMEOUT_MS, undefined, browser && retries ? takeChallenge(url) : undefined);
|
|
239
|
-
if (!ch.ok) throw
|
|
239
|
+
if (!ch.ok) throw Error(`browser-challenge: HTTP ${ch.status}`);
|
|
240
240
|
const { nonce, difficulty } = ch.json ?? {};
|
|
241
|
-
if (typeof nonce !== 'string' || !(difficulty >= 0 && difficulty <= MAX_DIFFICULTY)) throw
|
|
241
|
+
if (typeof nonce !== 'string' || !(difficulty >= 0 && difficulty <= MAX_DIFFICULTY)) throw Error('browser-challenge: unexpected challenge');
|
|
242
242
|
const solution = await solve(nonce, difficulty);
|
|
243
243
|
const r = await fetchJson(
|
|
244
244
|
`${AUTH}/browser-token`,
|
|
@@ -260,10 +260,10 @@ const tokens = (g: GlobalConfigType, origin?: string): Tokens => {
|
|
|
260
260
|
if (!r.ok) {
|
|
261
261
|
const detail = r.json?.detail;
|
|
262
262
|
if (retries > 0 && detail === 'Nonce expired') return mint(retries - 1);
|
|
263
|
-
throw
|
|
263
|
+
throw Error(`browser-token: HTTP ${r.status} ${typeof detail === 'string' ? detail : ''}`);
|
|
264
264
|
}
|
|
265
265
|
const { access_token, expires_in } = r.json ?? {};
|
|
266
|
-
if (typeof access_token !== 'string' || !(expires_in > 30)) throw
|
|
266
|
+
if (typeof access_token !== 'string' || !(expires_in > 30)) throw Error('browser-token: unexpected response');
|
|
267
267
|
return { token: access_token, exp: Date.now() + (expires_in - 30) * 1000 };
|
|
268
268
|
};
|
|
269
269
|
|
package/src/core/a11y.ts
CHANGED
|
@@ -90,17 +90,17 @@ export const CONTRAST_CSS =
|
|
|
90
90
|
`@media (prefers-contrast:more){:root:not([data-lbx-contrast="off"]) ${OURS}{outline:3px solid!important;outline-offset:2px!important}}` +
|
|
91
91
|
`:root[data-lbx-contrast="more"] ${OURS}{outline:3px solid!important;outline-offset:2px!important}\n`;
|
|
92
92
|
|
|
93
|
-
const
|
|
93
|
+
export const CONTRAST_KEY = 'lbx:contrast';
|
|
94
94
|
const stored = () => {
|
|
95
95
|
try {
|
|
96
|
-
const v = localStorage.getItem(
|
|
96
|
+
const v = localStorage.getItem(CONTRAST_KEY);
|
|
97
97
|
return v === 'more' || v === 'off' ? v : null;
|
|
98
98
|
} catch {
|
|
99
99
|
return null;
|
|
100
100
|
}
|
|
101
101
|
};
|
|
102
102
|
const more = () => (typeof matchMedia === 'function' ? matchMedia('(prefers-contrast: more)') : null);
|
|
103
|
-
const prefersMore = () => !!more()?.matches;
|
|
103
|
+
export const prefersMore = () => !!more()?.matches;
|
|
104
104
|
|
|
105
105
|
/** The mode's state: the visitor's toggle, else their OS setting ('more'), else none. */
|
|
106
106
|
const state = () => stored() ?? (prefersMore() ? 'more' : null);
|
|
@@ -112,7 +112,7 @@ export const contrastOn = () => state() === 'more';
|
|
|
112
112
|
* is on (the toggle, or the OS setting: one selector for both), `"off"` when
|
|
113
113
|
* the visitor switched it off against their OS setting.
|
|
114
114
|
*/
|
|
115
|
-
const
|
|
115
|
+
export const applyContrast = () => {
|
|
116
116
|
const v = state();
|
|
117
117
|
const html = document.documentElement;
|
|
118
118
|
if (!v) html.removeAttribute('data-lbx-contrast');
|
|
@@ -125,36 +125,7 @@ export const useContrast = () => {
|
|
|
125
125
|
setCss('lbx:contrast', layered(CONTRAST_CSS));
|
|
126
126
|
if (!watching) {
|
|
127
127
|
watching = true;
|
|
128
|
-
more()?.addEventListener?.('change',
|
|
128
|
+
more()?.addEventListener?.('change', applyContrast);
|
|
129
129
|
}
|
|
130
|
-
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
/** Switch it (our toggle): remembered for this browser; back to "as the OS says" when that matches. */
|
|
134
|
-
export const setContrast = (on: boolean) => {
|
|
135
|
-
const v = on === prefersMore() ? null : on ? 'more' : 'off';
|
|
136
|
-
try {
|
|
137
|
-
if (v) localStorage.setItem(KEY, v);
|
|
138
|
-
else localStorage.removeItem(KEY);
|
|
139
|
-
} catch {}
|
|
140
|
-
apply();
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Modelled on the previous-generation Luma's switch (`_global.contrastToggle`): a button that turns the
|
|
145
|
-
* mode on and off. Visually hidden until focused unless the visitor's OS asks
|
|
146
|
-
* for a contrast preference or the mode is on (as that one was), so a
|
|
147
|
-
* keyboard user who reaches it always sees it.
|
|
148
|
-
*/
|
|
149
|
-
export const contrastToggle = (label: string) => {
|
|
150
|
-
const b = document.createElement('button');
|
|
151
|
-
b.type = 'button';
|
|
152
|
-
b.className = 'lbx-contrast-toggle';
|
|
153
|
-
b.textContent = label;
|
|
154
|
-
b.setAttribute('aria-pressed', String(contrastOn()));
|
|
155
|
-
b.addEventListener('click', (e) => {
|
|
156
|
-
e.stopPropagation();
|
|
157
|
-
setContrast(!contrastOn());
|
|
158
|
-
});
|
|
159
|
-
return b;
|
|
130
|
+
applyContrast();
|
|
160
131
|
};
|
package/src/core/context.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { Templates } from './templates';
|
|
7
7
|
import type { LumaConfigType } from '../types';
|
|
8
8
|
import type { WidgetContext } from '../main';
|
|
9
|
+
import { QUERY_URL } from './features';
|
|
9
10
|
import { nameOf, slot } from './integration';
|
|
10
11
|
|
|
11
12
|
type Support = typeof import('./support');
|
|
@@ -104,5 +105,6 @@ export const widgetContext = (
|
|
|
104
105
|
result: s.resultFactory({ ...g.computedAttributes, ...w.app.computedAttributes }),
|
|
105
106
|
// Images: a listing's first row is eager; elsewhere (recommenders, the autocomplete) every image is lazy.
|
|
106
107
|
helpers: { t, price, plain: s.plain, first: s.first, ...s.imageHelpers(g.images, w.id !== 'search' && w.id !== 'plp') },
|
|
108
|
+
queryUrl: QUERY_URL ? config.search?.queryUrl : undefined,
|
|
107
109
|
};
|
|
108
110
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The listing's high-contrast toggle (`contrastToggle` of a listing config):
|
|
2
|
+
// apart from ./a11y.ts (every widget's), so only the listing's chunk has it.
|
|
3
|
+
import { applyContrast, CONTRAST_KEY, contrastOn, prefersMore } from './a11y';
|
|
4
|
+
|
|
5
|
+
/** Switch it (our toggle): remembered for this browser; back to "as the OS says" when that matches. */
|
|
6
|
+
export const setContrast = (on: boolean) => {
|
|
7
|
+
const v = on === prefersMore() ? null : on ? 'more' : 'off';
|
|
8
|
+
try {
|
|
9
|
+
if (v) localStorage.setItem(CONTRAST_KEY, v);
|
|
10
|
+
else localStorage.removeItem(CONTRAST_KEY);
|
|
11
|
+
} catch {}
|
|
12
|
+
applyContrast();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Modelled on the previous-generation Luma's switch (`_global.contrastToggle`): a button that turns the
|
|
17
|
+
* mode on and off. Visually hidden until focused unless the visitor's OS asks
|
|
18
|
+
* for a contrast preference or the mode is on (as that one was), so a
|
|
19
|
+
* keyboard user who reaches it always sees it.
|
|
20
|
+
*/
|
|
21
|
+
export const contrastToggle = (label: string) => {
|
|
22
|
+
const b = document.createElement('button');
|
|
23
|
+
b.type = 'button';
|
|
24
|
+
b.className = 'lbx-contrast-toggle';
|
|
25
|
+
b.textContent = label;
|
|
26
|
+
b.setAttribute('aria-pressed', String(contrastOn()));
|
|
27
|
+
b.addEventListener('click', (e) => {
|
|
28
|
+
e.stopPropagation();
|
|
29
|
+
setContrast(!contrastOn());
|
|
30
|
+
});
|
|
31
|
+
return b;
|
|
32
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// A form's default button, what an implicit submission (Enter in a field)
|
|
2
|
+
// submits with. In lbx-core: the autocomplete's "show all" link (its href is
|
|
3
|
+
// the URL Enter would load) and the search form's URL (widgets/search/form-url.ts,
|
|
4
|
+
// lbx-nav) read the same button, so the link, the result fetched while typing
|
|
5
|
+
// and the page fetched ahead agree.
|
|
6
|
+
|
|
7
|
+
export type Submitter = HTMLButtonElement | HTMLInputElement;
|
|
8
|
+
|
|
9
|
+
/** FormData(form, submitter) is newer (Chrome 112, Safari 16.4, Firefox 111): before, the second argument is ignored (it does not throw). */
|
|
10
|
+
let withSubmitter: boolean | undefined;
|
|
11
|
+
export const submitterSupported = () => {
|
|
12
|
+
if (withSubmitter === undefined) {
|
|
13
|
+
try {
|
|
14
|
+
const f = document.createElement('form');
|
|
15
|
+
const b = f.appendChild(document.createElement('button'));
|
|
16
|
+
b.name = 'x';
|
|
17
|
+
b.value = '1';
|
|
18
|
+
withSubmitter = new FormData(f, b).get('x') === '1';
|
|
19
|
+
} catch {
|
|
20
|
+
withSubmitter = false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return withSubmitter;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The form's data as a submit by `btn` sends it (null: by no button). Where
|
|
28
|
+
* the engine ignores the submitter argument, its own entry is added by hand:
|
|
29
|
+
* name=value, or an image button's x and y (`name.x`, `name.y`; `x`, `y`
|
|
30
|
+
* without a name), 0 as a submit without a pointer.
|
|
31
|
+
*/
|
|
32
|
+
export const formDataOf = (form: HTMLFormElement, btn: Submitter | null): FormData => {
|
|
33
|
+
const native = submitterSupported();
|
|
34
|
+
let own = !native; // the submitter's entry is ours to add
|
|
35
|
+
let data: FormData;
|
|
36
|
+
try {
|
|
37
|
+
data = new FormData(form, native && btn ? btn : undefined);
|
|
38
|
+
} catch {
|
|
39
|
+
data = new FormData(form); // not a submit button of this form
|
|
40
|
+
own = true;
|
|
41
|
+
}
|
|
42
|
+
if (own && btn) {
|
|
43
|
+
if (btn.type === 'image') {
|
|
44
|
+
data.append(btn.name ? `${btn.name}.x` : 'x', '0');
|
|
45
|
+
data.append(btn.name ? `${btn.name}.y` : 'y', '0');
|
|
46
|
+
} else if (btn.name) data.append(btn.name, btn.value);
|
|
47
|
+
}
|
|
48
|
+
return data;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The form's default button: the first submit button it owns in tree order
|
|
53
|
+
* (inside it, or anywhere in its tree through `form=`), what an implicit
|
|
54
|
+
* submission (Enter in a field) submits with, so its `formaction`,
|
|
55
|
+
* `formmethod`, `formtarget`, `formnovalidate` and name=value count as on a
|
|
56
|
+
* click. null without one.
|
|
57
|
+
*/
|
|
58
|
+
export const defaultSubmitter = (form: HTMLFormElement): Submitter | null => {
|
|
59
|
+
// A button outside the form owns it through form="<id>": the whole tree only when the form has an id.
|
|
60
|
+
for (const el of (form.id ? (form.getRootNode() as ParentNode) : form).querySelectorAll<Submitter>('button, input')) {
|
|
61
|
+
if (el.form !== form) continue;
|
|
62
|
+
const type = (el.getAttribute('type') ?? '').trim().toLowerCase();
|
|
63
|
+
if (el instanceof HTMLButtonElement ? type === '' || type === 'submit' : type === 'submit' || type === 'image') return el;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
};
|
package/src/core/early-rules.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { holdCss } from './hold';
|
|
17
17
|
import { P, TRACKING } from './routes';
|
|
18
18
|
import type { PlpConfigType, SearchConfigType } from '../types';
|
|
19
|
+
import { NAMED_EARLY_HOLD } from './features';
|
|
19
20
|
|
|
20
21
|
const EARLY_KEY = 'lbx:early';
|
|
21
22
|
// Short: when the bundle stops running (switched off, blocked), holds end soon after.
|
|
@@ -36,7 +37,7 @@ const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
36
37
|
// reserveSelector: set by loadConfig (default: the first anchor).
|
|
37
38
|
|
|
38
39
|
/** Capture groups in a regex source (its alternation with the empty string always matches). */
|
|
39
|
-
const groups = (re: string) =>
|
|
40
|
+
const groups = (re: string) => RegExp(`${re}|`).exec('')!.length - 1;
|
|
40
41
|
|
|
41
42
|
/**
|
|
42
43
|
* Only what the snippet can express (no custom readQuery/active): a path
|
|
@@ -61,7 +62,7 @@ export const searchRule = (c: SearchConfigType, hostCss: string): EarlyRoute | n
|
|
|
61
62
|
* the unclaimed hold at boot; `earlyHold: false` opts out where it is not.
|
|
62
63
|
*/
|
|
63
64
|
export const plpRule = (c: PlpConfigType, hostCss: string): EarlyRoute | null => {
|
|
64
|
-
if (c.earlyHold === false) return null;
|
|
65
|
+
if ((NAMED_EARLY_HOLD ? c.earlyHold === false : false)) return null;
|
|
65
66
|
const pm = c.params ?? {};
|
|
66
67
|
// Their page param too: their page 1 is ours. A later page of theirs is
|
|
67
68
|
// theirs (plpAccepts), a value the snippet does not check: held until the
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// The widgets an integration has (build/vite.mjs `features`): a widget is its
|
|
2
|
+
// folder (integration/autocomplete/, search/, plp/, recommender <name>/; an
|
|
3
|
+
// _analytics.ts or _widget.ts in any folder); a feature only the
|
|
4
|
+
// integration's own files can ask for (a template's attribute, a hook its
|
|
5
|
+
// _init.ts sets) by that text in them. The build defines these from the folders, so
|
|
6
|
+
// the entry leaves out what a missing widget's triggers need. The dev server
|
|
7
|
+
// (folders come and go), the render service and unit tests: all true.
|
|
8
|
+
// As in core/frameworks.ts: a flag in `if (FLAG)` or `FLAG ? x : y` only.
|
|
9
|
+
|
|
10
|
+
declare const __LBX_AUTOCOMPLETE__: boolean | undefined;
|
|
11
|
+
declare const __LBX_SEARCH__: boolean | undefined;
|
|
12
|
+
declare const __LBX_PLP__: boolean | undefined;
|
|
13
|
+
declare const __LBX_RECOMMENDER__: boolean | undefined;
|
|
14
|
+
declare const __LBX_ANALYTICS__: boolean | undefined;
|
|
15
|
+
declare const __LBX_SSR__: boolean | undefined;
|
|
16
|
+
declare const __LBX_WIDGET_INITS__: boolean | undefined;
|
|
17
|
+
declare const __LBX_CART__: boolean | undefined;
|
|
18
|
+
declare const __LBX_DRAWER__: boolean | undefined;
|
|
19
|
+
declare const __LBX_MENU__: boolean | undefined;
|
|
20
|
+
declare const __LBX_DROPDOWN__: boolean | undefined;
|
|
21
|
+
declare const __LBX_AC_HOOKS__: boolean | undefined;
|
|
22
|
+
declare const __LBX_ON_RENDER__: boolean | undefined;
|
|
23
|
+
declare const __LBX_READ_QUERY__: boolean | undefined;
|
|
24
|
+
declare const __LBX_FACETS_FOR__: boolean | undefined;
|
|
25
|
+
declare const __LBX_QUERY_URL__: boolean | undefined;
|
|
26
|
+
declare const __LBX_HOST_STYLES__: boolean | undefined;
|
|
27
|
+
declare const __LBX_TRANSLATIONS__: boolean | undefined;
|
|
28
|
+
declare const __LBX_INITS__: boolean | undefined;
|
|
29
|
+
declare const __LBX_EMPTY_VALUES__: boolean | undefined;
|
|
30
|
+
declare const __LBX_PRERENDER_TYPING__: boolean | undefined;
|
|
31
|
+
declare const __LBX_CONTENT_VISIBILITY__: boolean | undefined;
|
|
32
|
+
declare const __LBX_NAMED_PRECONNECT_IMAGES__: boolean | undefined;
|
|
33
|
+
declare const __LBX_NAMED_PREFETCH_FILTERS__: boolean | undefined;
|
|
34
|
+
declare const __LBX_NAMED_PRELOAD__: boolean | undefined;
|
|
35
|
+
declare const __LBX_NAMED_FADE__: boolean | undefined;
|
|
36
|
+
declare const __LBX_NAMED_RESTORE_ON_BACK__: boolean | undefined;
|
|
37
|
+
declare const __LBX_NAMED_PREFETCH_NEXT_PAGE__: boolean | undefined;
|
|
38
|
+
declare const __LBX_NAMED_IN_PLACE_QUERY__: boolean | undefined;
|
|
39
|
+
declare const __LBX_NAMED_PREFETCH_WHILE_TYPING__: boolean | undefined;
|
|
40
|
+
declare const __LBX_NAMED_EARLY_HOLD__: boolean | undefined;
|
|
41
|
+
declare const __LBX_NAMED_SPECULATION__: boolean | undefined;
|
|
42
|
+
|
|
43
|
+
/** integration/autocomplete/. */
|
|
44
|
+
export const AUTOCOMPLETE = typeof __LBX_AUTOCOMPLETE__ === 'boolean' ? __LBX_AUTOCOMPLETE__ : true;
|
|
45
|
+
/** integration/search/. */
|
|
46
|
+
export const SEARCH = typeof __LBX_SEARCH__ === 'boolean' ? __LBX_SEARCH__ : true;
|
|
47
|
+
/** integration/plp/. */
|
|
48
|
+
export const PLP = typeof __LBX_PLP__ === 'boolean' ? __LBX_PLP__ : true;
|
|
49
|
+
/** An integration/recommender <name>/. */
|
|
50
|
+
export const RECOMMENDER = typeof __LBX_RECOMMENDER__ === 'boolean' ? __LBX_RECOMMENDER__ : true;
|
|
51
|
+
/** An _analytics.ts in any folder (without one, analytics is off: main.ts). */
|
|
52
|
+
export const ANALYTICS = typeof __LBX_ANALYTICS__ === 'boolean' ? __LBX_ANALYTICS__ : true;
|
|
53
|
+
/** A _widget.ts in any folder (without one, core/widget-inits.ts has nothing to run). */
|
|
54
|
+
export const WIDGET_INITS = typeof __LBX_WIDGET_INITS__ === 'boolean' ? __LBX_WIDGET_INITS__ : true;
|
|
55
|
+
/** `data-lbx-cart` in any file of the integration (its templates): a tile's add-to-cart (core/tiles.ts). */
|
|
56
|
+
export const CART = typeof __LBX_CART__ === 'boolean' ? __LBX_CART__ : true;
|
|
57
|
+
/** `data-lbx-drawer` in any file of the integration: the listing's filter drawer (widgets/listing/element.ts). */
|
|
58
|
+
export const DRAWER = typeof __LBX_DRAWER__ === 'boolean' ? __LBX_DRAWER__ : true;
|
|
59
|
+
/** `data-lbx-menu` in any file of the integration: menus in the listing's markup (widgets/listing/element.ts). */
|
|
60
|
+
export const MENU = typeof __LBX_MENU__ === 'boolean' ? __LBX_MENU__ : true;
|
|
61
|
+
/** `data-lbx-dropdown` in any file of the integration: `<details>` menus in the listing's markup, closed from outside. */
|
|
62
|
+
export const DROPDOWN = typeof __LBX_DROPDOWN__ === 'boolean' ? __LBX_DROPDOWN__ : true;
|
|
63
|
+
/** The autocomplete's `onOpen`, `onClose`, `onSelect`, `onReady` named in the integration's code. */
|
|
64
|
+
export const AC_HOOKS = typeof __LBX_AC_HOOKS__ === 'boolean' ? __LBX_AC_HOOKS__ : true;
|
|
65
|
+
/** A listing's `onRender` named in the integration's code. */
|
|
66
|
+
export const ON_RENDER = typeof __LBX_ON_RENDER__ === 'boolean' ? __LBX_ON_RENDER__ : true;
|
|
67
|
+
/** The search page's `readQuery` named in the integration's code. */
|
|
68
|
+
export const READ_QUERY = typeof __LBX_READ_QUERY__ === 'boolean' ? __LBX_READ_QUERY__ : true;
|
|
69
|
+
/** A category page's `facetsFor` named in the integration's code. */
|
|
70
|
+
export const FACETS_FOR = typeof __LBX_FACETS_FOR__ === 'boolean' ? __LBX_FACETS_FOR__ : true;
|
|
71
|
+
/** The search page's `queryUrl` named in the integration's code: Enter straight to the query's page (widgets/search/direct.ts), the hook's page as every search's. */
|
|
72
|
+
export const QUERY_URL = typeof __LBX_QUERY_URL__ === 'boolean' ? __LBX_QUERY_URL__ : true;
|
|
73
|
+
/** A style-host.css in any folder (core/integration.ts hostStyle: the host's own to hide or hold). */
|
|
74
|
+
export const HOST_STYLES = typeof __LBX_HOST_STYLES__ === 'boolean' ? __LBX_HOST_STYLES__ : true;
|
|
75
|
+
/** A _translations.ts in any folder (core/integration.ts: over Luma's dictionary). */
|
|
76
|
+
export const TRANSLATIONS = typeof __LBX_TRANSLATIONS__ === 'boolean' ? __LBX_TRANSLATIONS__ : true;
|
|
77
|
+
/** An _init.ts in any folder (core/integration.ts loadConfig). */
|
|
78
|
+
export const INITS = typeof __LBX_INITS__ === 'boolean' ? __LBX_INITS__ : true;
|
|
79
|
+
/**
|
|
80
|
+
* Options off by default, named in the integration's code (its _config.ts,
|
|
81
|
+
* or code setting them): a key no file names keeps its default, as only
|
|
82
|
+
* code naming it can change it. `emptyValues`, `prerenderWhileTyping` (or
|
|
83
|
+
* `pageWhileTyping`: prerendering the search page typed for), `contentVisibility`.
|
|
84
|
+
*/
|
|
85
|
+
export const EMPTY_VALUES = typeof __LBX_EMPTY_VALUES__ === 'boolean' ? __LBX_EMPTY_VALUES__ : true;
|
|
86
|
+
export const PRERENDER_TYPING = typeof __LBX_PRERENDER_TYPING__ === 'boolean' ? __LBX_PRERENDER_TYPING__ : true;
|
|
87
|
+
export const CONTENT_VISIBILITY = typeof __LBX_CONTENT_VISIBILITY__ === 'boolean' ? __LBX_CONTENT_VISIBILITY__ : true;
|
|
88
|
+
/**
|
|
89
|
+
* Server-rendered listings (Luigi's Box's server-side includes): adopting
|
|
90
|
+
* and hydrating the server's markup. Declared, not found in the folders:
|
|
91
|
+
* `clientConfig(url, { serverRendering: false })` for a site without them.
|
|
92
|
+
*/
|
|
93
|
+
export const SSR = typeof __LBX_SSR__ === 'boolean' ? __LBX_SSR__ : true;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Options on by default, named in the integration's code: where none names
|
|
97
|
+
* one it is on (only code naming a key sets it), and a read of it is
|
|
98
|
+
* `NAMED_X ? cfg.x !== false : true` (build/vite.mjs ON_BY_DEFAULT).
|
|
99
|
+
*/
|
|
100
|
+
export const NAMED_PRECONNECT_IMAGES = typeof __LBX_NAMED_PRECONNECT_IMAGES__ === 'boolean' ? __LBX_NAMED_PRECONNECT_IMAGES__ : true;
|
|
101
|
+
export const NAMED_PREFETCH_FILTERS = typeof __LBX_NAMED_PREFETCH_FILTERS__ === 'boolean' ? __LBX_NAMED_PREFETCH_FILTERS__ : true;
|
|
102
|
+
export const NAMED_PRELOAD = typeof __LBX_NAMED_PRELOAD__ === 'boolean' ? __LBX_NAMED_PRELOAD__ : true;
|
|
103
|
+
export const NAMED_FADE = typeof __LBX_NAMED_FADE__ === 'boolean' ? __LBX_NAMED_FADE__ : true;
|
|
104
|
+
export const NAMED_RESTORE_ON_BACK = typeof __LBX_NAMED_RESTORE_ON_BACK__ === 'boolean' ? __LBX_NAMED_RESTORE_ON_BACK__ : true;
|
|
105
|
+
export const NAMED_PREFETCH_NEXT_PAGE = typeof __LBX_NAMED_PREFETCH_NEXT_PAGE__ === 'boolean' ? __LBX_NAMED_PREFETCH_NEXT_PAGE__ : true;
|
|
106
|
+
export const NAMED_IN_PLACE_QUERY = typeof __LBX_NAMED_IN_PLACE_QUERY__ === 'boolean' ? __LBX_NAMED_IN_PLACE_QUERY__ : true;
|
|
107
|
+
export const NAMED_PREFETCH_WHILE_TYPING = typeof __LBX_NAMED_PREFETCH_WHILE_TYPING__ === 'boolean' ? __LBX_NAMED_PREFETCH_WHILE_TYPING__ : true;
|
|
108
|
+
export const NAMED_EARLY_HOLD = typeof __LBX_NAMED_EARLY_HOLD__ === 'boolean' ? __LBX_NAMED_EARLY_HOLD__ : true;
|
|
109
|
+
export const NAMED_SPECULATION = typeof __LBX_NAMED_SPECULATION__ === 'boolean' ? __LBX_NAMED_SPECULATION__ : true;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Development only (src/main.ts, import.meta.env.DEV): a framework on the
|
|
2
|
+
// page whose support the build left out (clientConfig `framework`,
|
|
3
|
+
// core/frameworks.ts) is said in window.__lbx.health.errors. Without its
|
|
4
|
+
// support Luma does not wait for its hydration or talk to its router: a
|
|
5
|
+
// re-rendered page, ours dropped, a router putting its URL back.
|
|
6
|
+
import { SSR } from './features';
|
|
7
|
+
import { ANGULAR, ASTRO, DOCUMENT_REACT, NEXT, REACT, SVELTEKIT, TURBO, VUE } from './frameworks';
|
|
8
|
+
import { reportError } from './health';
|
|
9
|
+
import { reactDocumentSigns } from './own';
|
|
10
|
+
|
|
11
|
+
const keyed = (prefixes: string[]) => [...document.querySelectorAll('*')].some((el) => Object.keys(el).some((k) => prefixes.some((p) => k.startsWith(p))));
|
|
12
|
+
|
|
13
|
+
/** Per part of the support (build/vite.mjs SUPPORTS): left out of this build, and the page shows its framework. */
|
|
14
|
+
const CHECKS: [boolean, string, () => boolean][] = [
|
|
15
|
+
[DOCUMENT_REACT, 'React hydrating the whole document (next, remix or react-router)', reactDocumentSigns],
|
|
16
|
+
[REACT, 'React (react)', () => !!document.querySelector('#__next, #___gatsby, [data-reactroot]') || keyed(['__reactContainer$', '__reactFiber$'])],
|
|
17
|
+
[NEXT, 'the Next.js pages router (next)', () => !!(window as any).__NEXT_DATA__ && !(window as any).next?.appDir],
|
|
18
|
+
[VUE, 'Vue (vue or nuxt)', () => !!((window as any).__NUXT__ || (window as any).$nuxt || document.querySelector('[data-v-app], #__nuxt, [data-server-rendered]')) || keyed(['__vue_app__', '__vue__'])],
|
|
19
|
+
[ANGULAR, 'Angular (angular)', () => !!document.querySelector('[ng-version], [ng-server-context]')],
|
|
20
|
+
[SVELTEKIT, 'SvelteKit (sveltekit)', () => [...document.scripts].some((s) => s.text.includes('__sveltekit_')) || !!document.querySelector('link[href*="/immutable/entry/start."]')],
|
|
21
|
+
[ASTRO, 'Astro (astro)', () => !!document.querySelector('astro-island, meta[name="astro-view-transitions-enabled"]')],
|
|
22
|
+
[TURBO, 'Turbo (turbo)', () => !!((window as any).Turbo || (window as any).Turbolinks)],
|
|
23
|
+
[SSR, 'server-rendered listings (serverRendering)', () => !!document.querySelector('[data-lbx-ssr]')],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/** The frameworks on the page this build has no support for (reported once). */
|
|
27
|
+
export const checkFrameworks = () => {
|
|
28
|
+
const missing = CHECKS.filter(([has, , seen]) => !has && seen()).map(([, name]) => name);
|
|
29
|
+
if (missing.length)
|
|
30
|
+
reportError('boot', `this page uses ${missing.join(', ')}, which the build leaves out (clientConfig \`framework\`, \`serverRendering\`). Declare every framework the site uses, or leave them out.`);
|
|
31
|
+
return missing;
|
|
32
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// What of each framework's support a build has (build/vite.mjs SUPPORTS):
|
|
2
|
+
// all of it unless the integration declares its framework(s). Constants the
|
|
3
|
+
// build replaces, so code behind a false one is left out of every chunk.
|
|
4
|
+
// Not built by clientConfig (the render service, unit tests): all true.
|
|
5
|
+
//
|
|
6
|
+
// Use them in `if (FLAG)` or `FLAG ? x : y`, not as a value (`FLAG && x`):
|
|
7
|
+
// Rollup keeps such a reference, and a chunk other than the entry then imports
|
|
8
|
+
// the flag from it and keeps the code (test/browser/specs/budgets.spec.ts,
|
|
9
|
+
// .claude/skills/tree-shaking/SKILL.md).
|
|
10
|
+
|
|
11
|
+
declare const __LBX_REACT__: boolean | undefined;
|
|
12
|
+
declare const __LBX_DOCUMENT_REACT__: boolean | undefined;
|
|
13
|
+
declare const __LBX_NEXT__: boolean | undefined;
|
|
14
|
+
declare const __LBX_VUE__: boolean | undefined;
|
|
15
|
+
declare const __LBX_ANGULAR__: boolean | undefined;
|
|
16
|
+
declare const __LBX_SVELTEKIT__: boolean | undefined;
|
|
17
|
+
declare const __LBX_ASTRO__: boolean | undefined;
|
|
18
|
+
declare const __LBX_TURBO__: boolean | undefined;
|
|
19
|
+
|
|
20
|
+
/** React roots and islands: createRoot / hydrateRoot, Suspense boundaries, Gatsby. */
|
|
21
|
+
export const REACT = typeof __LBX_REACT__ === 'boolean' ? __LBX_REACT__ : true;
|
|
22
|
+
/** React hydrating the whole document (Next.js, Remix, React Router 7 / Hydrogen): the entry's checks (core/own.ts, core/styles.ts). */
|
|
23
|
+
export const DOCUMENT_REACT = typeof __LBX_DOCUMENT_REACT__ === 'boolean' ? __LBX_DOCUMENT_REACT__ : true;
|
|
24
|
+
/** Next.js pages router: its router (core/url.ts), its hydration measure (core/reconciler.ts). */
|
|
25
|
+
export const NEXT = typeof __LBX_NEXT__ === 'boolean' ? __LBX_NEXT__ : true;
|
|
26
|
+
/** Vue 2 / 3 and Nuxt: their mount (core/reconciler.ts), their router (core/url.ts). */
|
|
27
|
+
export const VUE = typeof __LBX_VUE__ === 'boolean' ? __LBX_VUE__ : true;
|
|
28
|
+
/** Angular's hydration (core/reconciler.ts). */
|
|
29
|
+
export const ANGULAR = typeof __LBX_ANGULAR__ === 'boolean' ? __LBX_ANGULAR__ : true;
|
|
30
|
+
/** SvelteKit: its root (core/reconciler.ts), its history entries (core/url.ts). */
|
|
31
|
+
export const SVELTEKIT = typeof __LBX_SVELTEKIT__ === 'boolean' ? __LBX_SVELTEKIT__ : true;
|
|
32
|
+
/** Astro: islands (core/reconciler.ts), ClientRouter history entries (core/url.ts). */
|
|
33
|
+
export const ASTRO = typeof __LBX_ASTRO__ === 'boolean' ? __LBX_ASTRO__ : true;
|
|
34
|
+
/** Turbo Drive / Turbolinks: snapshots (core/reconciler.ts), history and prefetch (core/turbo.ts). */
|
|
35
|
+
export const TURBO = typeof __LBX_TURBO__ === 'boolean' ? __LBX_TURBO__ : true;
|