@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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { html, nothing, repeat } from '../../lit';
|
|
2
|
+
import type { ConvoAction, ConvoPreviewScope } from '../../types';
|
|
3
|
+
import { avatar, icon } from './icons';
|
|
4
|
+
|
|
5
|
+
const NONE: ConvoAction[] = [];
|
|
6
|
+
|
|
7
|
+
// The search page's box (`convo.assist`): the assistant's answer to the query
|
|
8
|
+
// above the results, as it streams in: its first lines (fading out), three of
|
|
9
|
+
// its products, its suggestions; "Continue with the assistant" opens the
|
|
10
|
+
// panel with this conversation, a suggestion its next turn.
|
|
11
|
+
export default (s: ConvoPreviewScope) => {
|
|
12
|
+
const text = s.blocks.find((b) => b.type === 'text');
|
|
13
|
+
const list = s.blocks.find((b) => Array.isArray(b.products));
|
|
14
|
+
const rs = list ? s.products(list).slice(0, 3) : [];
|
|
15
|
+
const chips = s.done ? (s.blocks.find((b) => b.type === 'suggestions')?.actions ?? NONE).filter((a: ConvoAction) => a.action_ref).slice(0, 3) : NONE;
|
|
16
|
+
return html`
|
|
17
|
+
<section class="lbx-cv__pv" aria-labelledby="lbx-cv-pv-title" aria-busy=${s.done ? 'false' : 'true'}>
|
|
18
|
+
<header class="lbx-cv__pv-head">
|
|
19
|
+
${avatar}
|
|
20
|
+
<div class="lbx-cv__titles">
|
|
21
|
+
<h2 id="lbx-cv-pv-title">${s.t('title')}</h2>
|
|
22
|
+
<p>${s.t('disclaimer')}</p>
|
|
23
|
+
</div>
|
|
24
|
+
</header>
|
|
25
|
+
<div class="lbx-cv__pv-text lbx-cv__text">${text ? s.md(text.text) : html`<p class="lbx-cv__wait">${s.t('thinking')}</p>`}</div>
|
|
26
|
+
${rs.length
|
|
27
|
+
? html`<ul class="lbx-cv__list lbx-cv__pv-products" data-lbx-list>
|
|
28
|
+
${repeat(rs, (r) => r.id, (r, i) => html`<li>${s.include('convo-result-default', { r, i, actions: NONE })}</li>`)}
|
|
29
|
+
</ul>`
|
|
30
|
+
: nothing}
|
|
31
|
+
<div class="lbx-cv__pv-foot">
|
|
32
|
+
${chips.length ? html`<div class="lbx-cv__chips">${chips.map((a: ConvoAction) => html`<button type="button" class="lbx-cv__chip" @click=${(e: Event) => s.open(a, e.currentTarget as Element)}>${a.label}</button>`)}</div>` : nothing}
|
|
33
|
+
<button type="button" class="lbx-cv__continue" @click=${(e: Event) => s.open(undefined, e.currentTarget as Element)}>${icon('M5 12h14M13 6l6 6-6 6')}${s.t('continue')}</button>
|
|
34
|
+
</div>
|
|
35
|
+
</section>
|
|
36
|
+
`;
|
|
37
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { html, nothing } from '../../lit';
|
|
2
|
+
import type { ConvoResultScope } from '../../types';
|
|
3
|
+
|
|
4
|
+
export default ({ r, i, img, plain, price, actions, act }: ConvoResultScope) => html`
|
|
5
|
+
<a class="lbx-cv__tile" href=${r.c.url}>
|
|
6
|
+
${r.c.image ? img(r, i, { sizes: '64px' }) : html`<span class="lbx-img"></span>`}
|
|
7
|
+
<span class="lbx-cv__info">
|
|
8
|
+
<span class="lbx-cv__title">${plain(r.c.title)}</span>
|
|
9
|
+
${r.c.price ? html`<span class="lbx-cv__price">${price(r.c.price) || plain(r.c.price)}</span>` : nothing}
|
|
10
|
+
</span>
|
|
11
|
+
</a>
|
|
12
|
+
${actions.map((a) => (a.action_ref ? html`<button type="button" class="lbx-cv__more" aria-label=${`${a.label}: ${plain(r.c.title)}`} @click=${() => act(a)}>${a.label}</button>` : nothing))}
|
|
13
|
+
`;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Luma's sparkle in the site's search field (`convo.trigger`): a button that
|
|
2
|
+
// opens the panel with the phrase typed (main.ts's click listener, which
|
|
3
|
+
// knows it by identity: `created`). In lbx-place with the reconciler that
|
|
4
|
+
// places it (build/vite.mjs), loaded at boot on the pages with it: it waits
|
|
5
|
+
// for the anchor's framework to hydrate, is put back when the host re-renders
|
|
6
|
+
// the field, and stays out of Turbo's snapshots (data-turbo-temporary).
|
|
7
|
+
|
|
8
|
+
import { addRule } from '../../core/reconciler';
|
|
9
|
+
import { created } from '../../core/own';
|
|
10
|
+
import { schedule } from '../../core/scheduler';
|
|
11
|
+
import { layered, setCss } from '../../core/styles';
|
|
12
|
+
import type { ConvoConfigType, GlobalConfigType } from '../../types';
|
|
13
|
+
|
|
14
|
+
export const place = (tr: NonNullable<ConvoConfigType['trigger']>, g: GlobalConfigType) => {
|
|
15
|
+
const b = document.createElement('button');
|
|
16
|
+
b.type = 'button';
|
|
17
|
+
b.className = 'lbx-cv-trigger';
|
|
18
|
+
b.setAttribute('data-lbx', 'convo-trigger');
|
|
19
|
+
b.setAttribute('data-lbx-convo', '');
|
|
20
|
+
// Its name too (its only content is an aria-hidden icon), in the site's language where Luma has it.
|
|
21
|
+
b.title = tr.label ?? ({ cs: 'Zeptat se nákupního asistenta', de: 'Den Einkaufsassistenten fragen' } as Record<string, string>)[(g.locale ?? 'en').split('-')[0]] ?? 'Ask the shopping assistant';
|
|
22
|
+
created.add(b);
|
|
23
|
+
// A sparkle (elements, not markup: Trusted Types).
|
|
24
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
25
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
26
|
+
svg.setAttribute('viewBox', '0 0 24 24');
|
|
27
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
28
|
+
path.setAttribute('d', 'M10 3l1.6 4.4L16 9l-4.4 1.6L10 15l-1.6-4.4L4 9l4.4-1.6zM18 13l.9 2.1L21 16l-2.1.9L18 19l-.9-2.1L15 16l2.1-.9z');
|
|
29
|
+
path.setAttribute('fill', 'currentColor');
|
|
30
|
+
svg.append(path);
|
|
31
|
+
b.append(svg);
|
|
32
|
+
// In @layer lbx: the integration's style-host.css (applied by the entry at boot, before it is
|
|
33
|
+
// placed) lays it over the field; in flow by default, never over the site's own controls. Its
|
|
34
|
+
// focus ring !important (a site's reset of outlines loses to it).
|
|
35
|
+
setCss(
|
|
36
|
+
'convo:trigger',
|
|
37
|
+
layered(
|
|
38
|
+
'.lbx-cv-trigger{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:32px;height:32px;padding:0;border:1px solid;border-radius:50%;background:none;color:inherit;cursor:pointer}.lbx-cv-trigger svg{width:18px;height:18px}' +
|
|
39
|
+
'.lbx-cv-trigger:focus-visible{outline:2px solid!important;outline-offset:2px!important}@media (forced-colors:active){.lbx-cv-trigger:focus-visible{outline:3px solid Highlight!important}}',
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
addRule({ id: 'convo-trigger', active: () => true, anchors: [tr.anchor], position: tr.position ?? 'append', create: () => b });
|
|
43
|
+
schedule();
|
|
44
|
+
};
|
|
@@ -403,7 +403,7 @@ export const startListing = async (ctx: WidgetContext<ListingConfig>, tk: Takeov
|
|
|
403
403
|
private hiddenAt = 0;
|
|
404
404
|
/** scrollY when a Back stash was last written (leave()): the page hidden from there is stashed already. */
|
|
405
405
|
private leftAt = -1;
|
|
406
|
-
private onPageHide = () => {
|
|
406
|
+
private onPageHide = (e: PageTransitionEvent) => {
|
|
407
407
|
this.hiddenAt = Date.now();
|
|
408
408
|
// Left with more than the first page shown, not by a link of ours (a
|
|
409
409
|
// reload, their menu, the address bar): stashed too, so a reload within
|
|
@@ -412,6 +412,8 @@ export const startListing = async (ctx: WidgetContext<ListingConfig>, tk: Takeov
|
|
|
412
412
|
// shorter than where the shopper was). Into the back/forward cache
|
|
413
413
|
// too: it may be evicted there (then Back is a page load).
|
|
414
414
|
if (restoreOn && (this.last?.page ?? 0) > 1 && this.leftAt !== Math.round(scrollY)) this.leave();
|
|
415
|
+
// Into the back/forward cache: the browser's restoration on again (instant.ts `holdScroll`).
|
|
416
|
+
if (e.persisted) releaseScroll();
|
|
415
417
|
};
|
|
416
418
|
/**
|
|
417
419
|
* Restored from the back/forward cache (pageshow, persisted): the page is as
|
|
@@ -424,14 +426,13 @@ export const startListing = async (ctx: WidgetContext<ListingConfig>, tk: Takeov
|
|
|
424
426
|
*/
|
|
425
427
|
private onPageShow = (e: PageTransitionEvent) => {
|
|
426
428
|
if (!e.persisted) return;
|
|
427
|
-
// Where the shopper was:
|
|
428
|
-
//
|
|
429
|
-
//
|
|
429
|
+
// Where the shopper was: iOS Safari reads scrollY 0 at this event even
|
|
430
|
+
// when it restores it. Placed again (the browser's restoration is on
|
|
431
|
+
// again since pagehide, a host's own setting kept).
|
|
430
432
|
const st = peekStash(location.href);
|
|
431
433
|
dropStash(location.href);
|
|
432
434
|
mark(`${route.id} restored from the back/forward cache`);
|
|
433
|
-
if (st?.kind === 'back' && st.scroll && tk.state === 'ours') restoreScroll(this, st.scroll
|
|
434
|
-
else releaseScroll();
|
|
435
|
+
if (st?.kind === 'back' && st.scroll && tk.state === 'ours') restoreScroll(this, st.scroll);
|
|
435
436
|
const away = this.hiddenAt ? Date.now() - this.hiddenAt : 0;
|
|
436
437
|
if (away > BFCACHE_STALE_MS && this.last && this.lastQ && this.lastQ === this.current) this.revalidate(this.lastQ, this.last, new URL(location.href), this.href);
|
|
437
438
|
};
|
|
@@ -106,8 +106,10 @@ export const restoreScroll = (root: Element, s: Spot, after?: () => void) => {
|
|
|
106
106
|
* after its render. The browser's restoration ran before it, on a page not
|
|
107
107
|
* grown to the stashed pages yet, and could land after Luma's place too.
|
|
108
108
|
* Only from `auto`: a host that set it (Turbo, a router) keeps its own.
|
|
109
|
-
* Back on (`releaseScroll`) once Luma has placed the page,
|
|
110
|
-
* document moves on within itself (an SPA's navigation: theirs to restore)
|
|
109
|
+
* Back on (`releaseScroll`) once Luma has placed the page, when the
|
|
110
|
+
* document moves on within itself (an SPA's navigation: theirs to restore),
|
|
111
|
+
* or as the page goes into the back/forward cache (restored from memory:
|
|
112
|
+
* iOS Safari's swipe Back shows the page behind only with it on).
|
|
111
113
|
*/
|
|
112
114
|
let ownScroll = false;
|
|
113
115
|
/** Off for this entry, when it is the browser's (or already ours): 1, for the stash's `sr`. */
|
|
@@ -121,8 +123,16 @@ export const holdScroll = (): 1 | undefined => {
|
|
|
121
123
|
return undefined;
|
|
122
124
|
}
|
|
123
125
|
};
|
|
124
|
-
/**
|
|
125
|
-
|
|
126
|
+
/**
|
|
127
|
+
* A page loaded on an entry Luma turned it off for (the stash's `sr`): Luma's
|
|
128
|
+
* to turn back on. Off again where it was turned on as the page went into
|
|
129
|
+
* the back/forward cache (the page evicted from it since): the browser's
|
|
130
|
+
* restoration stops here, Luma places the page.
|
|
131
|
+
*/
|
|
132
|
+
export const adoptScroll = () => {
|
|
133
|
+
ownScroll = true;
|
|
134
|
+
holdScroll();
|
|
135
|
+
};
|
|
126
136
|
/** Back to the browser's, if Luma turned it off. */
|
|
127
137
|
export const releaseScroll = () => {
|
|
128
138
|
if (!ownScroll) return;
|