@luigisbox/luma 0.7.0 → 0.8.0-beta.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 +20 -0
- package/README.md +115 -4
- package/build/vite.mjs +28 -9
- package/docs/ACCESSIBILITY.md +56 -1
- package/package.json +1 -1
- package/src/core/a11y.ts +11 -4
- package/src/core/context.ts +2 -1
- package/src/core/features.ts +10 -1
- package/src/core/image-load.ts +21 -14
- package/src/core/integration.ts +3 -3
- package/src/core/own.ts +9 -0
- package/src/locales/cs.json +5 -1
- package/src/locales/de.json +5 -1
- package/src/locales/en.json +5 -1
- package/src/main.ts +87 -2
- package/src/types.ts +124 -0
- package/src/widgets/autocomplete/index.ts +64 -8
- package/src/widgets/autocomplete/style-autocomplete.css +31 -0
- package/src/widgets/autocomplete/template-autocomplete-assist.ts +16 -0
- package/src/widgets/autocomplete/template-autocomplete-box.ts +2 -0
- package/src/widgets/autocomplete/template-autocomplete-sheet.ts +2 -0
- package/src/widgets/convo/assist.ts +216 -0
- package/src/widgets/convo/icons.ts +8 -0
- package/src/widgets/convo/index.ts +713 -0
- package/src/widgets/convo/markdown.ts +76 -0
- package/src/widgets/convo/protocol.ts +154 -0
- package/src/widgets/convo/strings.ts +93 -0
- package/src/widgets/convo/style-convo.css +173 -0
- package/src/widgets/convo/template-convo-box.ts +125 -0
- package/src/widgets/convo/template-convo-preview.ts +37 -0
- package/src/widgets/convo/template-convo-result-default.ts +13 -0
- package/src/widgets/convo/trigger.ts +44 -0
- package/src/widgets/listing/element.ts +7 -6
- package/src/widgets/listing/instant.ts +14 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ before 1.0, a minor version may break the integration API.
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Conversational search: a `convo/` folder adds a dialog where the shopper
|
|
11
|
+
asks in their own words and Luigi's Box's assistant answers with text and
|
|
12
|
+
products. Opened by a click on any element with `data-lbx-convo`; its
|
|
13
|
+
code loads only when the shopper goes near one. Products come from the
|
|
14
|
+
Luigi's Box AI catalog (`surface`, `idField`); the conversation from
|
|
15
|
+
Luigi's Box's conversation service (`trackerId`).
|
|
16
|
+
- `convo.trigger`: Luma's own assistant button in the site's search field.
|
|
17
|
+
- `convo.assist`: for sites Luigi's Box has set up for it, the assistant's
|
|
18
|
+
answer to a phrase in the autocomplete (a row on top) and on the search
|
|
19
|
+
page (a box above the results); either continues in the panel.
|
|
20
|
+
|
|
21
|
+
## 0.7.1 — 2026-11-02
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- iOS Safari: swiping Back from a product to a listing showed a blank page
|
|
26
|
+
for a moment; the swipe now shows the listing.
|
|
27
|
+
|
|
8
28
|
## 0.7.0 — 2026-10-29
|
|
9
29
|
|
|
10
30
|
### Added
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ its own repository and depends on `@luigisbox/luma`
|
|
|
12
12
|
|
|
13
13
|
> **Pre-release.** Until 1.0, a minor version (0.8.0) may change the
|
|
14
14
|
> integration API (config keys, template scope, the build); a patch version
|
|
15
|
-
> (0.7.
|
|
15
|
+
> (0.7.2) only fixes. Depend on `^0.7.1` to take fixes without breaking
|
|
16
16
|
> changes. [`CHANGELOG.md`](CHANGELOG.md) says what each version changed.
|
|
17
17
|
|
|
18
18
|
## Core principles
|
|
@@ -308,6 +308,7 @@ integration/
|
|
|
308
308
|
search/ _config.ts _translations.ts style-host.css template-search-*.ts
|
|
309
309
|
plp/ _config.ts _init.ts style-host.css (templates fall back to search/)
|
|
310
310
|
recommender <name>/ _config.ts [_init.ts] _translations.ts
|
|
311
|
+
convo/ _config.ts [_init.ts] [style-host.css] [style-convo.css] [template-convo-*.ts]
|
|
311
312
|
```
|
|
312
313
|
|
|
313
314
|
- **`_config.ts` is JSON** (checked at boot in development: `yarn dev`);
|
|
@@ -317,7 +318,7 @@ integration/
|
|
|
317
318
|
key is typed and documented in `src/types.ts`.
|
|
318
319
|
- **A widget is its folder.** The build carries only the widgets the
|
|
319
320
|
integration has folders for (`autocomplete/`, `search/`, `plp/`,
|
|
320
|
-
`recommender <name
|
|
321
|
+
`recommender <name>/`, `convo/`) and analytics only where a folder has an
|
|
321
322
|
`_analytics.ts`: an integration with an autocomplete only loads about half
|
|
322
323
|
as much on every page as one with every widget. A config `_global/_init.ts`
|
|
323
324
|
creates for a widget without its folder is ignored. The development server
|
|
@@ -343,7 +344,114 @@ integration/
|
|
|
343
344
|
`search-facets-default`, `search-results-default`, `search-result-default`,
|
|
344
345
|
`search-pagination`, `autocomplete-box`, `autocomplete-result-default`,
|
|
345
346
|
`autocomplete-sheet`, `autocomplete-empty`, `recommender-box-default`,
|
|
346
|
-
`recommender-result-default`, `recommender-popup
|
|
347
|
+
`recommender-result-default`, `recommender-popup`, `convo-box`,
|
|
348
|
+
`convo-result-default`, and with `convo.assist`, `autocomplete-assist` and
|
|
349
|
+
`convo-preview`.
|
|
350
|
+
|
|
351
|
+
### Conversational search
|
|
352
|
+
|
|
353
|
+
The shopper asks in their own words, in a dialog, and Luigi's Box's shopping
|
|
354
|
+
assistant answers: streamed text, the products it picked, follow-up
|
|
355
|
+
suggestions, questions with choices, comparisons. Only an integration with a
|
|
356
|
+
`convo/` folder has it, and a page loads its code only when the shopper goes
|
|
357
|
+
near one of its triggers (about 11 kB gzip, once).
|
|
358
|
+
|
|
359
|
+
A click on any element with `data-lbx-convo` opens the dialog: the site's
|
|
360
|
+
own button, or one in a template. Its value, when not empty, is a question:
|
|
361
|
+
from the autocomplete's templates (passing the phrase typed,
|
|
362
|
+
`data-lbx-convo=${s.query}`) it is asked at once; from anything else (the
|
|
363
|
+
site's markup, a listing's or recommender's tile) it is put in the dialog's
|
|
364
|
+
input for the shopper to send (text on a page never speaks in the
|
|
365
|
+
shopper's name).
|
|
366
|
+
|
|
367
|
+
```html
|
|
368
|
+
<button type="button" data-lbx-convo>Ask our shopping assistant</button>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Or Luma's own sparkle in the site's search field: `trigger: { anchor,
|
|
372
|
+
position, label }` places a 32 px round button (`[data-lbx="convo-trigger"]`,
|
|
373
|
+
`position` as a listing's anchor, default `append`; named "Ask the shopping
|
|
374
|
+
assistant" in English, Czech or German by `_global.locale`, or `label`)
|
|
375
|
+
that opens the dialog with the phrase typed in the site's search input
|
|
376
|
+
(`_global.inputSelector`). In flow by default; the folder's
|
|
377
|
+
`style-host.css` (applied on every page with the folder, before the button
|
|
378
|
+
is placed) lays it over the field and keeps the field's text clear of it.
|
|
379
|
+
It is in the build only where `convo/_config.ts` names `trigger` (about
|
|
380
|
+
0.8 kB gzip on every page, loaded after the page's own scripts).
|
|
381
|
+
|
|
382
|
+
```css
|
|
383
|
+
/* integration/convo/style-host.css */
|
|
384
|
+
.search-form { position: relative; }
|
|
385
|
+
.search-form > [data-lbx="convo-trigger"] { position: absolute; top: 6px; right: 6px; color: #7b2cbf; background: #f3eafb; border-color: #b48ad9; border-radius: 50%; }
|
|
386
|
+
.search-form:has(> [data-lbx="convo-trigger"]) input[name="q"] { padding-right: 44px; }
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
// integration/convo/_config.ts
|
|
391
|
+
export default {
|
|
392
|
+
trackerId: '1234-5678', // the site in Luigi's Box's conversation service
|
|
393
|
+
surface: 'lbs_collection_main', // the collection surface its products are looked up on
|
|
394
|
+
idField: 'id', // the catalog field holding the identity the assistant names a product by
|
|
395
|
+
fields: ['@title', 'web_url', 'lbx:image', 'lbx:price'],
|
|
396
|
+
locale: 'de', // the language it answers in (default: _global.locale)
|
|
397
|
+
trigger: { anchor: 'header form.search' }, // optional: Luma's sparkle in the site's search field
|
|
398
|
+
assist: true, // optional: its answer in the autocomplete and on the search page
|
|
399
|
+
};
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
- **The conversation** is Luigi's Box's conversation service
|
|
403
|
+
(`conversational-proxy.luigisbox.com`, `endpoint` to change it), keyed by
|
|
404
|
+
`trackerId`. A site with a strict Content Security Policy allows that host
|
|
405
|
+
in `connect-src`. The assistant answers in `en`, `sk`, `cs`, `pl`, `de` or
|
|
406
|
+
`hu`: set `locale` to the shop's language when the interface's differs.
|
|
407
|
+
- **Its products** are the catalog's: the identities the assistant names are
|
|
408
|
+
looked up on `surface` by `idField`, and drawn with the
|
|
409
|
+
`convo-result-default` template (the integration's, like every other tile:
|
|
410
|
+
`r.c.*`, `img`, `price`). A product the channel does not serve gets no
|
|
411
|
+
tile.
|
|
412
|
+
- **It lasts the tab's session**: a product opened from the dialog and Back,
|
|
413
|
+
or a trigger on the next page, shows the same conversation, for about an
|
|
414
|
+
hour. "Start over" begins a new one.
|
|
415
|
+
- **Where the site's assistant is off** (over its monthly budget, no
|
|
416
|
+
catalog), the dialog says so, and the triggers are hidden on the session's
|
|
417
|
+
later pages.
|
|
418
|
+
- **Its look**: a panel from the right over the dimmed page (the whole
|
|
419
|
+
screen on a phone), in a shadow root (the site's CSS does not reach in).
|
|
420
|
+
`style-convo.css` in `convo/` styles it, starting with its tokens: the
|
|
421
|
+
site's brand colour is enough, the tints of the shopper's bubbles, chips
|
|
422
|
+
and the assistant's mark come from it, and Luma darkens the text drawn in
|
|
423
|
+
it until it reads (4.5:1) and picks white or black for what stands
|
|
424
|
+
on it: `:host { --lbx-cv-accent: #8740bf;
|
|
425
|
+
--lbx-cv-font: 'Inter', sans-serif; }` (`--lbx-cv-fg`, `-bg`,
|
|
426
|
+
`-muted`, `-line`, `-field`, `-soft`, `-width`). The `convo-box` template draws the whole dialog
|
|
427
|
+
(`ConvoScope`). Its words come in English, Czech and German with its code;
|
|
428
|
+
`_translations.ts` overrides them under `convo` (`{ sk: { convo: { title:
|
|
429
|
+
'Nákupný asistent' } } }`).
|
|
430
|
+
- **The assistant's answer where the shopper searches** (`assist: true`,
|
|
431
|
+
for a site Luigi's Box has set up for it; elsewhere nothing is shown and
|
|
432
|
+
Luma stops asking for the session): Luigi's Box decides per phrase whether
|
|
433
|
+
the assistant serves it better than the results, and answers it when so.
|
|
434
|
+
The autocomplete asks about a phrase of 3 or more words once the shopper
|
|
435
|
+
pauses (600 ms), and shows a row on top with the answer's first words
|
|
436
|
+
(`autocomplete-assist`; in the site's own `autocomplete-box`,
|
|
437
|
+
`s.assist`: a button with `data-lbx-convo=${s.assist.query}` and
|
|
438
|
+
`data-lbx-assist`). The search page asks about its query (3 or more
|
|
439
|
+
words, for a search made on the site: a search page reached from another
|
|
440
|
+
site shows only an answer the tab already has, never asks in the
|
|
441
|
+
shopper's name) and shows a box above the listing with the answer, three
|
|
442
|
+
of its products, its suggestions and "Continue with the assistant"
|
|
443
|
+
(`convo-preview`, `ConvoPreviewScope`). An answer that ends in an error
|
|
444
|
+
shows nothing.
|
|
445
|
+
Either opens the panel with that answer as the conversation's first turn:
|
|
446
|
+
one answer per phrase, kept for the tab's session, so a phrase answered in
|
|
447
|
+
the autocomplete and then searched is not asked again (one still streaming
|
|
448
|
+
is asked again with its own conversation, which Luigi's Box replays). Its
|
|
449
|
+
code is a chunk of its own (about 2.7 kB gzip), loaded when a phrase
|
|
450
|
+
settles or on a search page; the panel's only once there is an answer. The search box is placed
|
|
451
|
+
before Luma's listing (`lbx-search`): CSS of the integration's that hides
|
|
452
|
+
the rest of the listing's container lets `lbx-convo-preview` through.
|
|
453
|
+
- **Not yet**: adding products to a comparison from their tiles (the
|
|
454
|
+
assistant's comparisons are shown), feedback on an answer.
|
|
347
455
|
|
|
348
456
|
### Templates
|
|
349
457
|
|
|
@@ -474,7 +582,7 @@ built as on.
|
|
|
474
582
|
| `search.prefetchWhileTyping` | the search result is fetched once the typed phrase settles, so Enter shows it at once | |
|
|
475
583
|
| `search.pageWhileTyping` (`'prefetch'`, `'prerender'`, `false`) | the search page of the typed phrase is fetched ahead (Chrome, Edge; Firefox and Safari as they ship it), so Enter from another page skips the site's server render; `'prerender'` renders it ahead as well (Chrome, Edge) | the site cannot take a document request per typed phrase; `'prerender'` only where its tags wait for `prerenderingchange` |
|
|
476
584
|
| `search.queryUrl` (an `_init.ts` hook, off unless set) | where the site's server redirects its search form to the query's page (`/search?q=x` to `/search/x/`), names that page: Enter, the autocomplete's "show all" and a picked query load it directly, without the redirect, and the result fetched while typing is shown there | the form's other fields decide the page, or the site must handle every search itself |
|
|
477
|
-
| `restoreOnBack` | Back from a product restores the listing (every page shown) and the followed tile's position, as does the page loaded again within a minute of following a link (a reload, a link whose server redirects back to it); Luma turns the browser's own scroll restoration (`history.scrollRestoration`) off for the listing's history entry until it has placed the page, unless the site set it | |
|
|
585
|
+
| `restoreOnBack` | Back from a product restores the listing (every page shown) and the followed tile's position, as does the page loaded again within a minute of following a link (a reload, a link whose server redirects back to it); Luma turns the browser's own scroll restoration (`history.scrollRestoration`) off for the listing's history entry until it has placed the page (not while the page is in the browser's back/forward cache, which restores it from memory), unless the site set it | |
|
|
478
586
|
| `prefetchNextPage` | "show more" is fetched as the shopper nears the end | |
|
|
479
587
|
| `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 |
|
|
480
588
|
| `variants: { fields }` (opt-in) | each tile's result gets its variant group (`r.variants`, "Templates" above): one request to Luigi's Box per page of results, never waited for | the surface does not collapse variants (every group is a group of one) |
|
|
@@ -648,6 +756,9 @@ integration repository").
|
|
|
648
756
|
while typing a document per settled phrase (`search.pageWhileTyping: false`).
|
|
649
757
|
- **Early hold**: the snippet carries no site's rules, so the first page
|
|
650
758
|
view is not held.
|
|
759
|
+
- **Conversational search** runs on Luigi's Box's conversation service, keyed
|
|
760
|
+
by the site's tracker id, until Luigi's Box AI serves conversations; its
|
|
761
|
+
products are already the Luigi's Box AI catalog's.
|
|
651
762
|
- **Host routers out of reach**: where Luma cannot find the host's router (Vue
|
|
652
763
|
Router 4 when its app is not discoverable, Angular in production), the
|
|
653
764
|
router can overwrite Luma's history entry or show a stale query in its search
|
package/build/vite.mjs
CHANGED
|
@@ -22,7 +22,7 @@ const FRAMEWORK = resolve(dirname(fileURLToPath(import.meta.url)), '../src');
|
|
|
22
22
|
// (soleUser: the listing's contrast toggle, fold, …)
|
|
23
23
|
// lbx-listing search results and category pages (one listing: plp is
|
|
24
24
|
// a route of it) with the integration's search/ templates
|
|
25
|
-
// lbx-autocomplete lbx-recommender each widget with its templates
|
|
25
|
+
// lbx-autocomplete lbx-recommender lbx-convo each widget with its templates
|
|
26
26
|
// lbx-plp lbx-place lbx-search lbx-search-in-place lbx-speculate lbx-token
|
|
27
27
|
// the plp widget and its templates, placement and
|
|
28
28
|
// recommender triggers, the search page's own, a new query
|
|
@@ -46,7 +46,7 @@ const FRAMEWORK = resolve(dirname(fileURLToPath(import.meta.url)), '../src');
|
|
|
46
46
|
// entry needs no change here. _global/ files named after a widget
|
|
47
47
|
// (template-recommender-*.ts) go with that widget only. Integration folders
|
|
48
48
|
// are named after their widget (`recommender similar`): the first word says which.
|
|
49
|
-
const KIND_CHUNK = { autocomplete: 'lbx-autocomplete', search: 'lbx-listing', plp: 'lbx-plp', recommender: 'lbx-recommender' };
|
|
49
|
+
const KIND_CHUNK = { autocomplete: 'lbx-autocomplete', search: 'lbx-listing', plp: 'lbx-plp', recommender: 'lbx-recommender', convo: 'lbx-convo' };
|
|
50
50
|
const NAV = /\/src\/(widgets\/listing\/(stash|query|state|flow)|widgets\/search\/(form-url|route|first)|core\/(url|listings|takeover|turbo|prefetch|stash|page))\.ts$/;
|
|
51
51
|
const WIDGET = /\/src\/widgets\/([a-z]+)\//;
|
|
52
52
|
const CORE = /\/src\/(lit\.ts|core\/|api\/)|\/node_modules\/lit-html\//;
|
|
@@ -86,9 +86,10 @@ export const chunkOf = (id) => {
|
|
|
86
86
|
// Hydration of server-rendered listings: only those pages load it.
|
|
87
87
|
// lit-html's private SSR support is the hydrator's alone (not every widget page's lbx-core).
|
|
88
88
|
if (/\/node_modules\/(@lit-labs\/ssr-client\/|lit-html\/private-ssr-support\.js)/.test(id)) return 'lbx-hydrate';
|
|
89
|
-
// Placing our elements (core/reconciler.ts)
|
|
90
|
-
// pages with a listing
|
|
91
|
-
|
|
89
|
+
// Placing our elements (core/reconciler.ts), the recommenders' triggers and the
|
|
90
|
+
// conversation's sparkle (widgets/convo/trigger.ts): pages with a listing, a
|
|
91
|
+
// recommender or the sparkle, in parallel with their other chunks.
|
|
92
|
+
if (/\/src\/(core\/(reconciler|recommenders)|widgets\/convo\/trigger)\.ts$/.test(id)) return 'lbx-place';
|
|
92
93
|
// The listing's high-contrast toggle: only a site that turns it on loads it (widgets/listing/element.ts).
|
|
93
94
|
if (/\/src\/core\/contrast-toggle\.ts$/.test(id)) return 'lbx-contrast';
|
|
94
95
|
// An Apply's cue in a staged drawer: asked for when such a drawer opens (widgets/listing/element.ts).
|
|
@@ -132,9 +133,14 @@ const soleUser = (id, getModuleInfo, entry, seen = new Set([id])) => {
|
|
|
132
133
|
}
|
|
133
134
|
return users;
|
|
134
135
|
};
|
|
136
|
+
// The assistant's answer to what is typed (widgets/convo/assist.ts) and the wire it shares with the
|
|
137
|
+
// panel (protocol.ts): their own chunk where the build has it (the autocomplete and the search page
|
|
138
|
+
// load it without the panel), else in the panel's.
|
|
139
|
+
const ASSIST = /\/src\/widgets\/convo\/(assist|protocol)\.ts$/;
|
|
135
140
|
const manualChunks = (id, { getModuleInfo, getModuleIds }) => {
|
|
136
141
|
const entry = entryGraph(getModuleInfo, getModuleIds);
|
|
137
142
|
if (entry.has(id)) return 'integration';
|
|
143
|
+
if (ASSIST.test(id)) return [...getModuleIds()].some((m) => /\/src\/widgets\/convo\/assist\.ts$/.test(m) && getModuleInfo(m)?.isIncluded) ? 'lbx-assist' : 'lbx-convo';
|
|
138
144
|
const c = chunkOf(id);
|
|
139
145
|
if (c !== 'lbx-core') return c;
|
|
140
146
|
const users = soleUser(id, getModuleInfo, entry);
|
|
@@ -224,7 +230,9 @@ const rawCss = () => ({
|
|
|
224
230
|
* `integration`: { folder: { name: source } }; `builtIns`: { widget: { name: source } }.
|
|
225
231
|
* Returns { widget: [built-in names to leave out] }.
|
|
226
232
|
*/
|
|
227
|
-
|
|
233
|
+
/** Built-in templates a feature of the integration renders (core/features.ts): left out of a build without it. */
|
|
234
|
+
export const FEATURE_TEMPLATES = { autocomplete: { 'autocomplete-assist': '__LBX_CONVO_ASSIST__' }, convo: { 'convo-preview': '__LBX_CONVO_ASSIST__' } };
|
|
235
|
+
export const ENTRY_TEMPLATES = { autocomplete: ['autocomplete-box'], search: ['search-box'], recommender: ['recommender-box-default', 'recommender-popup'], convo: ['convo-box', 'convo-preview'] };
|
|
228
236
|
const includes = (src) => {
|
|
229
237
|
const calls = [...src.matchAll(/\binclude\(\s*(?:(['"])([\w-]+)\1)?/g)];
|
|
230
238
|
return calls.some((m) => !m[2]) ? null : calls.map((m) => m[2]);
|
|
@@ -240,6 +248,7 @@ export const unrenderedBuiltIns = (integration, builtIns) => {
|
|
|
240
248
|
...(folders.includes('plp') ? [{ folders: ['_global', 'search', 'plp'], kinds: ['search', 'plp'] }] : []),
|
|
241
249
|
],
|
|
242
250
|
recommender: folders.filter((f) => f.split(/\s+/)[0] === 'recommender').map((f) => ({ folders: ['_global', f], kinds: ['recommender'] })),
|
|
251
|
+
convo: folders.includes('convo') ? [{ folders: ['_global', 'convo'], kinds: ['convo'] }] : [],
|
|
243
252
|
};
|
|
244
253
|
const out = {};
|
|
245
254
|
for (const [widget, instances] of Object.entries(users)) {
|
|
@@ -285,7 +294,7 @@ const templateSources = (dir) => {
|
|
|
285
294
|
}
|
|
286
295
|
return out;
|
|
287
296
|
};
|
|
288
|
-
const BUILT_INS = /\/src\/widgets\/(autocomplete|search|recommender)\/(?:index|assets)\.ts$/;
|
|
297
|
+
const BUILT_INS = /\/src\/widgets\/(autocomplete|search|recommender|convo)\/(?:index|assets)\.ts$/;
|
|
289
298
|
|
|
290
299
|
/**
|
|
291
300
|
* A built-in template's own rules in its widget's stylesheet, marked
|
|
@@ -319,6 +328,10 @@ const unusedBuiltIns = (root) => {
|
|
|
319
328
|
const itg = templateSources(resolve(root, 'integration'));
|
|
320
329
|
const builtIns = templateSources(resolve(FRAMEWORK, 'widgets'));
|
|
321
330
|
drop = unrenderedBuiltIns(itg, builtIns);
|
|
331
|
+
// Those of a feature the integration does not name (an assistant's nudge without `assist`).
|
|
332
|
+
const defines = featureDefines(resolve(root, 'integration'));
|
|
333
|
+
for (const [widget, names] of Object.entries(FEATURE_TEMPLATES))
|
|
334
|
+
if (drop[widget]) for (const [name, flag] of Object.entries(names)) if (defines[flag] === 'false' && !drop[widget].includes(name)) drop[widget].push(name);
|
|
322
335
|
// The left-out templates' stylesheet parts no template of the widget's folders still uses.
|
|
323
336
|
cssDrop = {};
|
|
324
337
|
for (const [widget, names] of Object.entries(drop)) {
|
|
@@ -551,12 +564,18 @@ export const featureDefines = (integrationDir) => {
|
|
|
551
564
|
readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
|
|
552
565
|
e.name.startsWith('.') || e.name === 'node_modules' || e.name === 'dist' ? [] : e.isDirectory() ? read(resolve(dir, e.name)) : /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs|html)$/.test(e.name) ? [readFileSync(resolve(dir, e.name), 'utf8')] : [],
|
|
553
566
|
);
|
|
567
|
+
// A key in a folder's _config.ts (a word alone is common: a catalog field, a comment).
|
|
568
|
+
const configText = (f) => (existsSync(resolve(integrationDir, f, '_config.ts')) ? readFileSync(resolve(integrationDir, f, '_config.ts'), 'utf8') : '');
|
|
554
569
|
const inFiles = (...needles) => ((text ??= read(dirname(integrationDir)).join('\n')), needles.some((n) => text.includes(n)));
|
|
555
570
|
return {
|
|
556
571
|
__LBX_AUTOCOMPLETE__: has((d) => d.includes('autocomplete')),
|
|
557
572
|
__LBX_SEARCH__: has((d) => d.includes('search')),
|
|
558
573
|
__LBX_PLP__: has((d) => d.includes('plp')),
|
|
559
574
|
__LBX_RECOMMENDER__: has((d) => d.some((f) => f.split(/\s+/)[0] === 'recommender')),
|
|
575
|
+
__LBX_CONVO__: has((d) => d.includes('convo')),
|
|
576
|
+
// Its sparkle in the site's search field: the key in convo/_config.ts (the word alone is common).
|
|
577
|
+
__LBX_CONVO_TRIGGER__: has((d) => d.includes('convo') && /\btrigger\b["']?\s*:/.test(configText('convo'))),
|
|
578
|
+
__LBX_CONVO_ASSIST__: has((d) => d.includes('convo') && /\bassist\b["']?\s*:/.test(configText('convo'))),
|
|
560
579
|
__LBX_ANALYTICS__: has((d) => d.some((f) => existsSync(resolve(integrationDir, f, '_analytics.ts')))),
|
|
561
580
|
__LBX_WIDGET_INITS__: has((d) => d.some((f) => existsSync(resolve(integrationDir, f, '_widget.ts')))),
|
|
562
581
|
__LBX_HOST_STYLES__: has((d) => d.some((f) => existsSync(resolve(integrationDir, f, 'style-host.css')))),
|
|
@@ -579,13 +598,13 @@ export const featureDefines = (integrationDir) => {
|
|
|
579
598
|
__LBX_PRERENDER_TYPING__: has(() => inFiles('prerenderWhileTyping', 'pageWhileTyping')),
|
|
580
599
|
__LBX_CONTENT_VISIBILITY__: has(() => inFiles('contentVisibility')),
|
|
581
600
|
// A listing's variant groups: the key in a folder's _config.ts (the word alone is common: a catalog field, a comment).
|
|
582
|
-
__LBX_VARIANTS__: has((d) => d.some((f) => /\bvariants\b["']?\s*[:,}]/.test(
|
|
601
|
+
__LBX_VARIANTS__: has((d) => d.some((f) => /\bvariants\b["']?\s*[:,}]/.test(configText(f)))),
|
|
583
602
|
// Options on by default: one no file names is on.
|
|
584
603
|
...Object.fromEntries(ON_BY_DEFAULT.map((k) => [`__LBX_NAMED_${k.replace(/[A-Z]/g, (c) => `_${c}`).toUpperCase()}__`, has(() => inFiles(k))])),
|
|
585
604
|
};
|
|
586
605
|
};
|
|
587
606
|
/** Luma's dictionaries' namespaces (src/locales/*.json) a build without the widget leaves out: plp reads search's too. */
|
|
588
|
-
export const LOCALE_NAMESPACES = { autocomplete: ['AUTOCOMPLETE'], search: ['SEARCH', 'PLP'], plp: ['PLP'], recommender: ['RECOMMENDER'] };
|
|
607
|
+
export const LOCALE_NAMESPACES = { autocomplete: ['AUTOCOMPLETE'], search: ['SEARCH', 'PLP'], plp: ['PLP'], recommender: ['RECOMMENDER'], assist: ['CONVO_ASSIST'] };
|
|
589
608
|
export const pruneDictionary = (json, defines) => {
|
|
590
609
|
const dict = JSON.parse(json);
|
|
591
610
|
for (const [ns, flags] of Object.entries(LOCALE_NAMESPACES)) if (flags.every((f) => defines[`__LBX_${f}__`] === 'false')) delete dict[ns];
|
package/docs/ACCESSIBILITY.md
CHANGED
|
@@ -120,7 +120,8 @@ guessed alt text, and forced colours and the OS setting are honoured.
|
|
|
120
120
|
- **Keys** as before: arrows move the selection, Enter picks it (or is the
|
|
121
121
|
host's search), Escape closes ours first, as whole gestures. Focus never
|
|
122
122
|
leaves the input on a click or a tap (`pointerdown` on the dropdown is
|
|
123
|
-
prevented); only Tab moves it, to "Show all products"
|
|
123
|
+
prevented); only Tab moves it, to "Show all products" (or first to the
|
|
124
|
+
assistant's row, when there is one: below).
|
|
124
125
|
- **Colours**: the host's text and accent colours are sampled only where they
|
|
125
126
|
read at 4.5:1 on our white panel (`readable`, `core/a11y.ts`; a dark site's
|
|
126
127
|
light text or a pale brand colour falls back); text on an accent fill is
|
|
@@ -251,6 +252,60 @@ guessed alt text, and forced colours and the OS setting are honoured.
|
|
|
251
252
|
appears after the shopper's own add-to-cart click; moving focus there would
|
|
252
253
|
pull them away from the host's notice).
|
|
253
254
|
|
|
255
|
+
### The conversational search (`widgets/convo/`)
|
|
256
|
+
|
|
257
|
+
- A modal `<dialog>` (`showModal()`, a panel from the right) named by its
|
|
258
|
+
title ("Shopping assistant", `convo.title`): the page behind is inert, so
|
|
259
|
+
Tab and the screen reader's cursor stay in it; Escape, the 40 px close
|
|
260
|
+
button named "Close" and a press on the backdrop close it, and focus goes
|
|
261
|
+
back to the trigger. Focus starts in its input (named "Ask anything…").
|
|
262
|
+
Its icon buttons (Start over, Close, Send / Stop) are named and titled;
|
|
263
|
+
the log is a region named "Conversation"; Enter in the input while an
|
|
264
|
+
answer streams does nothing (only the Stop button stops it).
|
|
265
|
+
- The answer streams in word by word: the log is not a live region (it
|
|
266
|
+
would be read in pieces) but `aria-busy` while it streams. "Thinking" is
|
|
267
|
+
said as the turn starts, the answer once, politely, when it is done: its
|
|
268
|
+
words without the Markdown's marks, up to the sentence ending past 500
|
|
269
|
+
characters (an error after it). Each question and answer starts with a
|
|
270
|
+
visually hidden "You:" / "Assistant:"; a question the assistant ignored
|
|
271
|
+
says "not answered", and that is said too.
|
|
272
|
+
- Focus never falls to the page: Send and Stop are one button, and a
|
|
273
|
+
control that goes away with a render (a suggestion used, a retry, the
|
|
274
|
+
input disabled when the conversation ends) gives focus to the log.
|
|
275
|
+
The fields' edges are 3:1, their placeholder (their visible label) 4.5:1.
|
|
276
|
+
- Luma's sparkle in the site's search field (`trigger`) is a 32 px button
|
|
277
|
+
named "Ask the shopping assistant" in the site's language (`label`), with
|
|
278
|
+
a focus ring of its own that a site's outline reset cannot remove (3 px
|
|
279
|
+
Highlight in forced colours).
|
|
280
|
+
- The assistant's answer in the autocomplete (`assist`) is a button above
|
|
281
|
+
the dropdown's results, named by its title ("The assistant has an
|
|
282
|
+
answer"), its first words (up to 160 characters, not the whole answer)
|
|
283
|
+
and "Continue"; Tab from the search input reaches it first, then "Show
|
|
284
|
+
all products" (Shift+Tab back), and its title is said once, politely,
|
|
285
|
+
when it appears, with the result count if that is still to be said. The
|
|
286
|
+
arrow keys' last stop is still "Show all products". Its words are in the
|
|
287
|
+
text colour (the muted grey and the accent can fall under 4.5:1 on the
|
|
288
|
+
accent's tint). Closing the dialog opened from it (its dropdown gone)
|
|
289
|
+
gives focus to the site's search field. Its one-time shimmer and slide-in run only without reduced
|
|
290
|
+
motion; until the answer's first words come, three dots (hidden from
|
|
291
|
+
assistive technology) bounce, or only fade in turn under reduced motion,
|
|
292
|
+
and keep the text colour in forced colours.
|
|
293
|
+
- The search page's answer box is a section named by its heading
|
|
294
|
+
("Shopping assistant"), `aria-busy` while the answer streams (not a live
|
|
295
|
+
region: the results are the page's main content); its suggestions and
|
|
296
|
+
"Continue with the assistant" are buttons that open the dialog, focus
|
|
297
|
+
going back to the one pressed when it closes. An answer that breaks off
|
|
298
|
+
removes the box; focus in it goes on to the listing after it. Its accent is made readable
|
|
299
|
+
as the dialog's (white or black on it, whichever reads better: at least
|
|
300
|
+
4.58:1 on any accent).
|
|
301
|
+
- Every control is a named button of at least 24 px (suggestions, choices,
|
|
302
|
+
a product's "Tell me more: <product>", each refinement's remove control
|
|
303
|
+
named by the assistant, Try again, Stop, Start over). A comparison table
|
|
304
|
+
scrolls in its own focusable region with row and column headers.
|
|
305
|
+
- On a phone (or a short touch screen: a phone in landscape) the dialog is the whole screen. Forced colours keep the
|
|
306
|
+
dialog's, the bubbles' and the controls' edges; under reduced motion the
|
|
307
|
+
"thinking" dots stand still.
|
|
308
|
+
|
|
254
309
|
### Every widget
|
|
255
310
|
|
|
256
311
|
- **Focus ring**: `:focus-visible` gets an outline in our subtrees (`@layer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luigisbox/luma",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0-beta.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",
|
package/src/core/a11y.ts
CHANGED
|
@@ -12,7 +12,9 @@ export const SR_ONLY =
|
|
|
12
12
|
/**
|
|
13
13
|
* A polite status region (WCAG 4.1.3). `say(text)` announces it once the
|
|
14
14
|
* caller has been quiet for `delay` ms (a burst of renders is one message);
|
|
15
|
-
* the same text twice is announced twice (it is cleared first).
|
|
15
|
+
* the same text twice is announced twice (it is cleared first). `keep`: a
|
|
16
|
+
* message a later one does not replace but is said with (two pieces of news
|
|
17
|
+
* that come together: the autocomplete's results and the assistant's answer).
|
|
16
18
|
*/
|
|
17
19
|
export const liveRegion = () => {
|
|
18
20
|
const el = document.createElement('div');
|
|
@@ -21,17 +23,22 @@ export const liveRegion = () => {
|
|
|
21
23
|
el.setAttribute('aria-atomic', 'true');
|
|
22
24
|
el.style.cssText = SR_ONLY;
|
|
23
25
|
let timer = 0;
|
|
26
|
+
let plain = '';
|
|
27
|
+
let kept = '';
|
|
24
28
|
return {
|
|
25
29
|
el,
|
|
26
|
-
say(text: string, delay = 150) {
|
|
30
|
+
say(text: string, delay = 150, keep = false) {
|
|
31
|
+
if (keep) kept = text;
|
|
32
|
+
else plain = text;
|
|
33
|
+
const out = plain && kept ? `${plain.replace(/[.\s]+$/, '')}. ${kept}` : plain || kept;
|
|
27
34
|
clearTimeout(timer);
|
|
28
35
|
el.textContent = '';
|
|
29
36
|
// In a later task: a region whose text is replaced in the same task as it appears is not announced.
|
|
30
|
-
timer = window.setTimeout(() => void (el.textContent =
|
|
37
|
+
timer = window.setTimeout(() => void ((el.textContent = out), (plain = kept = '')), delay);
|
|
31
38
|
},
|
|
32
39
|
clear() {
|
|
33
40
|
clearTimeout(timer);
|
|
34
|
-
el.textContent = '';
|
|
41
|
+
plain = kept = el.textContent = '';
|
|
35
42
|
},
|
|
36
43
|
};
|
|
37
44
|
};
|
package/src/core/context.ts
CHANGED
|
@@ -6,7 +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
|
+
import { CONVO_ASSIST, QUERY_URL } from './features';
|
|
10
10
|
import { nameOf, slot } from './integration';
|
|
11
11
|
|
|
12
12
|
type Support = typeof import('./support');
|
|
@@ -106,5 +106,6 @@ export const widgetContext = (
|
|
|
106
106
|
// Images: a listing's first row is eager; elsewhere (recommenders, the autocomplete) every image is lazy.
|
|
107
107
|
helpers: { t, price, plain: s.plain, first: s.first, ...s.imageHelpers(g.images, w.id !== 'search' && w.id !== 'plp') },
|
|
108
108
|
queryUrl: QUERY_URL ? config.search?.queryUrl : undefined,
|
|
109
|
+
assist: CONVO_ASSIST && config.convo?.assist && !config.convo.disabled ? config.convo : undefined,
|
|
109
110
|
};
|
|
110
111
|
};
|
package/src/core/features.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The widgets an integration has (build/vite.mjs `features`): a widget is its
|
|
2
|
-
// folder (integration/autocomplete/, search/, plp/, recommender <name
|
|
2
|
+
// folder (integration/autocomplete/, search/, plp/, recommender <name>/, convo/; an
|
|
3
3
|
// _analytics.ts or _widget.ts in any folder); a feature only the
|
|
4
4
|
// integration's own files can ask for (a template's attribute, a hook its
|
|
5
5
|
// _init.ts sets) by that text in them. The build defines these from the folders, so
|
|
@@ -11,6 +11,9 @@ declare const __LBX_AUTOCOMPLETE__: boolean | undefined;
|
|
|
11
11
|
declare const __LBX_SEARCH__: boolean | undefined;
|
|
12
12
|
declare const __LBX_PLP__: boolean | undefined;
|
|
13
13
|
declare const __LBX_RECOMMENDER__: boolean | undefined;
|
|
14
|
+
declare const __LBX_CONVO__: boolean | undefined;
|
|
15
|
+
declare const __LBX_CONVO_TRIGGER__: boolean | undefined;
|
|
16
|
+
declare const __LBX_CONVO_ASSIST__: boolean | undefined;
|
|
14
17
|
declare const __LBX_ANALYTICS__: boolean | undefined;
|
|
15
18
|
declare const __LBX_SSR__: boolean | undefined;
|
|
16
19
|
declare const __LBX_WIDGET_INITS__: boolean | undefined;
|
|
@@ -50,6 +53,12 @@ export const SEARCH = typeof __LBX_SEARCH__ === 'boolean' ? __LBX_SEARCH__ : tru
|
|
|
50
53
|
export const PLP = typeof __LBX_PLP__ === 'boolean' ? __LBX_PLP__ : true;
|
|
51
54
|
/** An integration/recommender <name>/. */
|
|
52
55
|
export const RECOMMENDER = typeof __LBX_RECOMMENDER__ === 'boolean' ? __LBX_RECOMMENDER__ : true;
|
|
56
|
+
/** integration/convo/: the conversational search (widgets/convo). */
|
|
57
|
+
export const CONVO = typeof __LBX_CONVO__ === 'boolean' ? __LBX_CONVO__ : true;
|
|
58
|
+
/** The conversational search's sparkle in the site's search field: `trigger` in convo/_config.ts (main.ts). */
|
|
59
|
+
export const CONVO_TRIGGER = typeof __LBX_CONVO_TRIGGER__ === 'boolean' ? __LBX_CONVO_TRIGGER__ : true;
|
|
60
|
+
/** The assistant asked about what is typed (/assist, lean trackers): `assist` in convo/_config.ts (widgets/convo/assist.ts). */
|
|
61
|
+
export const CONVO_ASSIST = typeof __LBX_CONVO_ASSIST__ === 'boolean' ? __LBX_CONVO_ASSIST__ : true;
|
|
53
62
|
/** An _analytics.ts in any folder (without one, analytics is off: main.ts). */
|
|
54
63
|
export const ANALYTICS = typeof __LBX_ANALYTICS__ === 'boolean' ? __LBX_ANALYTICS__ : true;
|
|
55
64
|
/** A _widget.ts in any folder (without one, core/widget-inits.ts has nothing to run). */
|
package/src/core/image-load.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// - load marks: `data-lbx-loaded` on an `img[data-lbx-img]` and its
|
|
3
3
|
// `[data-lbx-ph]` box once it has loaded (or failed: the alt text shows), so
|
|
4
4
|
// the placeholder goes and a lazy image fades in (style-search.css). One
|
|
5
|
-
// capture listener on the document for every widget
|
|
5
|
+
// capture listener on the document for every widget (and on a widget's
|
|
6
|
+
// shadow root: the conversation's dialog); an image already
|
|
6
7
|
// complete when a render puts it in (the memory cache, a persisted tile, a
|
|
7
8
|
// server-rendered one loaded before hydration) is marked in that task:
|
|
8
9
|
// no placeholder flash, no fade;
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
// filter), at low priority, behind a cap.
|
|
13
14
|
// Browser only, in the widgets' chunks: never in the entry or the server renderer.
|
|
14
15
|
|
|
16
|
+
import { CONVO } from './features';
|
|
15
17
|
import type { ImageInfo } from './images';
|
|
16
18
|
|
|
17
19
|
const SEL = 'img[data-lbx-img]:not([data-lbx-loaded])';
|
|
@@ -24,18 +26,18 @@ const loaded = (img: Element, how: '' | 'fade') => {
|
|
|
24
26
|
img.closest('[data-lbx-ph]')?.setAttribute('data-lbx-loaded', how);
|
|
25
27
|
};
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
const on = (e: Event) => {
|
|
30
|
+
const t = e.target as Element;
|
|
31
|
+
if (t?.nodeType !== 1 || t.localName !== 'img' || !t.hasAttribute('data-lbx-img') || t.hasAttribute('data-lbx-loaded')) return;
|
|
32
|
+
loaded(t, performance.now() - renderedAt < CACHED_MS ? '' : 'fade');
|
|
33
|
+
};
|
|
34
|
+
const watching = new WeakSet<Node>();
|
|
35
|
+
/** load and error do not bubble: capture sees every image of the document, or of a shadow root (theirs never leave it). */
|
|
36
|
+
const watch = (root: Node) => {
|
|
37
|
+
if (watching.has(root)) return;
|
|
38
|
+
watching.add(root);
|
|
39
|
+
root.addEventListener('load', on, true);
|
|
40
|
+
root.addEventListener('error', on, true);
|
|
39
41
|
};
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -44,7 +46,12 @@ const watch = () => {
|
|
|
44
46
|
* when `fade` is on. Server-rendered markup without JavaScript has no fade.
|
|
45
47
|
*/
|
|
46
48
|
export const settleImages = (root: Element, fade = true) => {
|
|
47
|
-
watch();
|
|
49
|
+
watch(document);
|
|
50
|
+
// Only the conversation's dialog renders images in a shadow root (core/features.ts).
|
|
51
|
+
if (CONVO) {
|
|
52
|
+
const sr = root.getRootNode();
|
|
53
|
+
if (sr instanceof ShadowRoot) watch(sr);
|
|
54
|
+
}
|
|
48
55
|
renderedAt = performance.now();
|
|
49
56
|
for (const img of root.querySelectorAll<HTMLImageElement>(SEL)) if (img.complete && img.getAttribute('src')) loaded(img, '');
|
|
50
57
|
if (fade && !root.hasAttribute('data-lbx-fade')) root.setAttribute('data-lbx-fade', '');
|
package/src/core/integration.ts
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
// templates, translations and helpers the bundle would.
|
|
6
6
|
|
|
7
7
|
import type { LumaApi, LumaConfigType, LumaInitType } from '../types';
|
|
8
|
-
import { AUTOCOMPLETE, HOST_STYLES, INITS, PLP, RECOMMENDER, SEARCH, TRANSLATIONS, VARIANTS } from './features';
|
|
8
|
+
import { AUTOCOMPLETE, CONVO, HOST_STYLES, INITS, PLP, RECOMMENDER, SEARCH, TRANSLATIONS, VARIANTS } from './features';
|
|
9
9
|
|
|
10
10
|
export type Lazy<T> = Record<string, () => Promise<T>>;
|
|
11
11
|
|
|
12
12
|
export const folderOf = (path: string) => path.split('/').slice(-2)[0];
|
|
13
13
|
export const nameOf = (path: string) => path.split('/').pop()!.replace(/^(template|style)-/, '').replace(/\.\w+$/, '');
|
|
14
|
-
const KINDS = ['autocomplete', 'search', 'plp', 'recommender'];
|
|
14
|
+
const KINDS = ['autocomplete', 'search', 'plp', 'recommender', 'convo'];
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Each listing's folders (template and style precedence) and translation
|
|
@@ -104,7 +104,7 @@ export const loadConfig = async (files: Pick<IntegrationFiles, 'configs' | 'init
|
|
|
104
104
|
const Luma: LumaApi = { version, config };
|
|
105
105
|
if (globalInit && (await globalInit(config, Luma)) === false) return null;
|
|
106
106
|
if (config._global.disabled || !config._global.publishableKey || !config._global.channelId) return null;
|
|
107
|
-
const apps = [AUTOCOMPLETE ? config.autocomplete : null, SEARCH ? config.search : null, PLP ? config.plp : null, ...(RECOMMENDER ? Object.values(config.recommenders ?? {}) : [])];
|
|
107
|
+
const apps = [AUTOCOMPLETE ? config.autocomplete : null, SEARCH ? config.search : null, PLP ? config.plp : null, CONVO ? config.convo : null, ...(RECOMMENDER ? Object.values(config.recommenders ?? {}) : [])];
|
|
108
108
|
if (INITS) for (const app of apps) if (app && !app.disabled && app.initialize && (await (app.initialize as any)(app, Luma)) === false) app.disabled = true;
|
|
109
109
|
// `variants` comes into a build from a folder's _config.ts only (build/vite.mjs
|
|
110
110
|
// featureDefines): set by an _init.ts, a build would leave it out unsaid.
|
package/src/core/own.ts
CHANGED
|
@@ -5,6 +5,15 @@ import { DOCUMENT_REACT } from './frameworks';
|
|
|
5
5
|
|
|
6
6
|
export const OWN_ATTR = 'data-lbx';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Elements Luma created, by identity (an attribute, `data-lbx`, can be put on
|
|
10
|
+
* the page by anyone who can put markup there: a review keeping data-*):
|
|
11
|
+
* what is inside one of them is ours (a template's), what is not is the
|
|
12
|
+
* site's. Read by the conversational search: a question asked at once only
|
|
13
|
+
* from a trigger of ours (widgets/convo).
|
|
14
|
+
*/
|
|
15
|
+
export const created = new WeakSet<Element>();
|
|
16
|
+
|
|
8
17
|
/**
|
|
9
18
|
* Whether a node of ours the host removes is put back before the next paint
|
|
10
19
|
* (core/sense.ts, scheduleNow): from when we have something to put back, a
|
package/src/locales/cs.json
CHANGED
package/src/locales/de.json
CHANGED
package/src/locales/en.json
CHANGED