@syntrologie/adapt-guide 0.0.0-stage → 2.8.0-canary.621
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/dist/cdn.d.ts +43 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.js +29 -0
- package/dist/cdn.js.map +7 -0
- package/dist/chunk-BVOACKL5.js +3545 -0
- package/dist/chunk-BVOACKL5.js.map +7 -0
- package/dist/chunk-IGIU6S7P.js +4051 -0
- package/dist/chunk-IGIU6S7P.js.map +7 -0
- package/dist/guide-widget.d.ts +26 -0
- package/dist/guide-widget.d.ts.map +1 -0
- package/dist/runtime.d.ts +41 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +12 -0
- package/dist/runtime.js.map +7 -0
- package/dist/schema.d.ts +47 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +51 -0
- package/dist/schema.js.map +7 -0
- package/package.json +55 -4
- package/README.md +0 -3
|
@@ -0,0 +1,3545 @@
|
|
|
1
|
+
import {
|
|
2
|
+
external_exports
|
|
3
|
+
} from "./chunk-IGIU6S7P.js";
|
|
4
|
+
|
|
5
|
+
// ../../../node_modules/@lit/context/lib/create-context.js
|
|
6
|
+
function n(n2) {
|
|
7
|
+
return n2;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// ../../sdk-contracts/dist/canvas-context.js
|
|
11
|
+
var canvasRuntimeContext = n("syntrologie:canvas-runtime");
|
|
12
|
+
|
|
13
|
+
// ../../sdk-contracts/dist/telemetry-events.js
|
|
14
|
+
var entry = (e2) => e2;
|
|
15
|
+
var TELEMETRY_EVENTS = {
|
|
16
|
+
// --- intervention metrics (April 2026) — wire names unchanged, all plan-scope
|
|
17
|
+
syntro_config_served: entry({
|
|
18
|
+
subject: "config",
|
|
19
|
+
verb: "served",
|
|
20
|
+
scope: "plan",
|
|
21
|
+
props: ["tiles", "actions"]
|
|
22
|
+
}),
|
|
23
|
+
syntro_intervention_seen: entry({
|
|
24
|
+
subject: "intervention",
|
|
25
|
+
verb: "seen",
|
|
26
|
+
scope: "plan",
|
|
27
|
+
props: ["intervention_id", "intervention_kind"]
|
|
28
|
+
}),
|
|
29
|
+
syntro_intervention_triggered: entry({
|
|
30
|
+
subject: "intervention",
|
|
31
|
+
verb: "triggered",
|
|
32
|
+
scope: "plan",
|
|
33
|
+
props: ["intervention_id", "intervention_kind"]
|
|
34
|
+
}),
|
|
35
|
+
syntro_intervention_interacted: entry({
|
|
36
|
+
subject: "intervention",
|
|
37
|
+
verb: "interacted",
|
|
38
|
+
scope: "plan",
|
|
39
|
+
props: ["intervention_id", "intervention_kind", "interaction_type"]
|
|
40
|
+
}),
|
|
41
|
+
// --- concierge (FEAT-1789150184) ---------------------------------------
|
|
42
|
+
syntro_concierge_opened: entry({
|
|
43
|
+
subject: "concierge",
|
|
44
|
+
verb: "opened",
|
|
45
|
+
scope: "open",
|
|
46
|
+
props: ["trigger", "page_path"]
|
|
47
|
+
// trigger: ConciergeOpenTrigger
|
|
48
|
+
}),
|
|
49
|
+
syntro_concierge_closed: entry({
|
|
50
|
+
subject: "concierge",
|
|
51
|
+
verb: "closed",
|
|
52
|
+
scope: "open",
|
|
53
|
+
props: ["duration_ms", "reason", "messages_sent", "tiles_dismissed", "pages_viewed"]
|
|
54
|
+
// reason: ConciergeCloseReason
|
|
55
|
+
}),
|
|
56
|
+
/**
|
|
57
|
+
* The shopper asked to leave the canvas by a link. Emitted on intent, before
|
|
58
|
+
* the host answers, so refused or denied opens are counted too (parity with
|
|
59
|
+
* syntro_concierge_closed reason navigation).
|
|
60
|
+
*
|
|
61
|
+
* On a merchant's own page a link tap closes the canvas, and
|
|
62
|
+
* `syntro_concierge_closed { reason: 'navigation' }` is the row that
|
|
63
|
+
* records it. Inside a chat app's card the frame is sandboxed: the canvas
|
|
64
|
+
* cannot navigate, does not close, and asks the host to open the link
|
|
65
|
+
* instead, so that close row never fires. This is its counterpart, and it
|
|
66
|
+
* carries the same `reason` so one query covers both surfaces.
|
|
67
|
+
*/
|
|
68
|
+
syntro_link_opened: entry({
|
|
69
|
+
subject: "link",
|
|
70
|
+
verb: "opened",
|
|
71
|
+
scope: "open",
|
|
72
|
+
props: ["kind", "reason"]
|
|
73
|
+
// kind: LinkOpenKind, reason: ConciergeCloseReason
|
|
74
|
+
}),
|
|
75
|
+
/**
|
|
76
|
+
* A store's product page opened inside a chat-app card instead of leaving
|
|
77
|
+
* through the host (the card's product view). `product` is the store's
|
|
78
|
+
* handle, never the display name; a rail switch is a new open. Its
|
|
79
|
+
* counterpart when the shopper goes back to the conversation's picks is
|
|
80
|
+
* syntro_product_view_closed.
|
|
81
|
+
*
|
|
82
|
+
* Plan scope, like the card's own syntro_card_lifecycle_seen: an inline
|
|
83
|
+
* card holds an open scope only while the host has it fullscreen, and a
|
|
84
|
+
* view opened from a tile fires before the host answers that request (or
|
|
85
|
+
* after it refused), so an open-scope row here would be dropped. The core
|
|
86
|
+
* still stamps surface and open_id whenever an open is live.
|
|
87
|
+
*/
|
|
88
|
+
syntro_product_view_opened: entry({
|
|
89
|
+
subject: "product_view",
|
|
90
|
+
verb: "opened",
|
|
91
|
+
scope: "plan",
|
|
92
|
+
props: ["product", "from"]
|
|
93
|
+
// from: ProductViewOpenSource
|
|
94
|
+
}),
|
|
95
|
+
syntro_product_view_closed: entry({
|
|
96
|
+
subject: "product_view",
|
|
97
|
+
verb: "closed",
|
|
98
|
+
scope: "plan",
|
|
99
|
+
props: ["product"]
|
|
100
|
+
}),
|
|
101
|
+
syntro_deck_paged: entry({
|
|
102
|
+
subject: "deck",
|
|
103
|
+
verb: "paged",
|
|
104
|
+
scope: "open",
|
|
105
|
+
props: ["from_index", "to_index", "method"]
|
|
106
|
+
// method: DeckPageMethod
|
|
107
|
+
}),
|
|
108
|
+
syntro_tile_dismissed: entry({
|
|
109
|
+
subject: "tile",
|
|
110
|
+
verb: "dismissed",
|
|
111
|
+
scope: "open",
|
|
112
|
+
props: ["tile_id", "tile_kind", "visible_ms"]
|
|
113
|
+
}),
|
|
114
|
+
// A tile's own data load finished: the network wait between the concierge
|
|
115
|
+
// mounting a tile and the shopper seeing its content (BUG-1784767817 was a
|
|
116
|
+
// 30 s reviews hang here). `outcome` is 'ok' | 'error'; `requested` is how
|
|
117
|
+
// many items the tile asked for, `received` how many came back.
|
|
118
|
+
syntro_tile_data_received: entry({
|
|
119
|
+
subject: "tile_data",
|
|
120
|
+
verb: "received",
|
|
121
|
+
scope: "open",
|
|
122
|
+
props: ["tile_id", "tile_kind", "total_ms", "outcome", "requested", "received"]
|
|
123
|
+
}),
|
|
124
|
+
// The three chat atoms are OPEN-scope: they carry `surface` / `open_id` and
|
|
125
|
+
// only make sense inside a concierge open. An INLINE chat bar (no concierge)
|
|
126
|
+
// still dispatches them on every turn, and the core drops each one and
|
|
127
|
+
// increments the `telemetry_dropped` health counter. That is EXPECTED on an
|
|
128
|
+
// inline-chat host page, not a bug — `telemetry_dropped` there mixes "real
|
|
129
|
+
// defect" with "inline chat has no open scope", so do not alert on it alone.
|
|
130
|
+
syntro_chat_message_sent: entry({
|
|
131
|
+
subject: "chat_message",
|
|
132
|
+
verb: "sent",
|
|
133
|
+
scope: "open",
|
|
134
|
+
props: ["turn", "chars", "via"]
|
|
135
|
+
// via: ChatSendVia
|
|
136
|
+
}),
|
|
137
|
+
syntro_chat_reply_received: entry({
|
|
138
|
+
subject: "chat_reply",
|
|
139
|
+
verb: "received",
|
|
140
|
+
scope: "open",
|
|
141
|
+
// mount_tools: the mount_* tool name of every mount this reply fired, in order (two product cards appear twice).
|
|
142
|
+
props: ["turn", "ttft_ms", "total_ms", "mounts", "cards", "mount_tools"]
|
|
143
|
+
}),
|
|
144
|
+
syntro_chat_interrupted: entry({
|
|
145
|
+
subject: "chat",
|
|
146
|
+
verb: "interrupted",
|
|
147
|
+
scope: "open",
|
|
148
|
+
props: ["turn", "after_ms"]
|
|
149
|
+
}),
|
|
150
|
+
syntro_chat_maximize_toggled: entry({
|
|
151
|
+
subject: "chat_maximize",
|
|
152
|
+
verb: "toggled",
|
|
153
|
+
scope: "open",
|
|
154
|
+
props: ["maximized"]
|
|
155
|
+
}),
|
|
156
|
+
syntro_chat_takeover_triggered: entry({
|
|
157
|
+
subject: "chat_takeover",
|
|
158
|
+
verb: "triggered",
|
|
159
|
+
scope: "open",
|
|
160
|
+
props: ["turn"]
|
|
161
|
+
}),
|
|
162
|
+
// --- search takeover (2026-09-17) ---------------------------------------
|
|
163
|
+
// All plan-scope: the search interceptor fires before any concierge open
|
|
164
|
+
// exists. No prop here ever carries query text (SEE the contract test's
|
|
165
|
+
// no-text-prop rule) — classification/signal_hits are the interceptor's
|
|
166
|
+
// own derived signals, never the string itself.
|
|
167
|
+
syntro_search_sent: entry({
|
|
168
|
+
subject: "search",
|
|
169
|
+
verb: "sent",
|
|
170
|
+
scope: "plan",
|
|
171
|
+
props: ["classification", "word_count", "signal_hits", "entry_point", "platform"]
|
|
172
|
+
// classification: SearchClassification, entry_point: SearchEntryPoint
|
|
173
|
+
}),
|
|
174
|
+
// `ms_to_skeleton` is intentionally the only timing prop: it is always
|
|
175
|
+
// knowable the instant the surface mounts. `ms_to_first_content` is NOT
|
|
176
|
+
// declared here (revision 1 defect) because a surface that never painted
|
|
177
|
+
// content still fires this event, and a required prop it cannot supply
|
|
178
|
+
// would make TelemetryCore.emit drop exactly the row worth measuring.
|
|
179
|
+
syntro_search_surface_seen: entry({
|
|
180
|
+
subject: "search_surface",
|
|
181
|
+
verb: "seen",
|
|
182
|
+
scope: "plan",
|
|
183
|
+
props: ["variant", "classification", "result_count", "ms_to_skeleton"]
|
|
184
|
+
// variant: SearchVariant, classification: SearchClassification
|
|
185
|
+
}),
|
|
186
|
+
syntro_search_result_tapped: entry({
|
|
187
|
+
subject: "search_result",
|
|
188
|
+
verb: "tapped",
|
|
189
|
+
scope: "plan",
|
|
190
|
+
props: ["variant", "region_id", "module", "position"]
|
|
191
|
+
// variant: SearchVariant
|
|
192
|
+
}),
|
|
193
|
+
syntro_search_followup_sent: entry({
|
|
194
|
+
subject: "search_followup",
|
|
195
|
+
verb: "sent",
|
|
196
|
+
scope: "plan",
|
|
197
|
+
props: ["delta_kind", "variant"]
|
|
198
|
+
// delta_kind: SearchDeltaKind, variant: SearchVariant
|
|
199
|
+
}),
|
|
200
|
+
// Declares `ms_since_boot` (not a "since surface mounted" timing prop) so a
|
|
201
|
+
// not-ready fallback the takeover never rendered is still measurable —
|
|
202
|
+
// revision 1 emitted nothing on this path.
|
|
203
|
+
syntro_search_native_served: entry({
|
|
204
|
+
subject: "search_native",
|
|
205
|
+
verb: "served",
|
|
206
|
+
scope: "plan",
|
|
207
|
+
props: ["reason", "platform", "ms_since_boot"]
|
|
208
|
+
// reason: SearchFallbackReason
|
|
209
|
+
}),
|
|
210
|
+
syntro_search_takeover_dismissed: entry({
|
|
211
|
+
subject: "search_takeover",
|
|
212
|
+
verb: "dismissed",
|
|
213
|
+
scope: "plan",
|
|
214
|
+
props: ["variant", "reason"]
|
|
215
|
+
// variant: SearchVariant, reason: SearchDismissReason
|
|
216
|
+
}),
|
|
217
|
+
// --- chat-app card lifecycle (2026-09-24) --------------------------------
|
|
218
|
+
// One row per stage a card reached, from `reportCardStage` in
|
|
219
|
+
// card-lifecycle.ts. Plan scope: most stages happen before a config is
|
|
220
|
+
// served and none happen inside a concierge open. `stage` is a
|
|
221
|
+
// CardLifecycleStage; `since_shell_ms` is the card shell's own clock, so a
|
|
222
|
+
// card's rows order themselves from the first byte of the page. The same
|
|
223
|
+
// stages reach the connector through the shell's beacon, which is the only
|
|
224
|
+
// record of a card whose SDK never booted.
|
|
225
|
+
syntro_card_lifecycle_seen: entry({
|
|
226
|
+
subject: "card_lifecycle",
|
|
227
|
+
verb: "seen",
|
|
228
|
+
scope: "plan",
|
|
229
|
+
props: ["stage", "since_shell_ms"]
|
|
230
|
+
// stage: CardLifecycleStage
|
|
231
|
+
})
|
|
232
|
+
};
|
|
233
|
+
var TELEMETRY_EVENT_NAMES = Object.keys(TELEMETRY_EVENTS);
|
|
234
|
+
|
|
235
|
+
// ../../sdk-contracts/dist/card-lifecycle.js
|
|
236
|
+
var MCP_APP_SURFACE_ATTRIBUTES = Object.freeze({
|
|
237
|
+
surface_type: "mcp-app",
|
|
238
|
+
surface: "personal_agent"
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ../../sdk-contracts/dist/detector-events.js
|
|
242
|
+
var DETECTOR_EVENT_NAMES = [
|
|
243
|
+
"ui.hover",
|
|
244
|
+
"ui.idle",
|
|
245
|
+
"ui.hesitate",
|
|
246
|
+
"ui.rage_click",
|
|
247
|
+
"ui.scroll_thrash",
|
|
248
|
+
"ui.focus_bounce"
|
|
249
|
+
];
|
|
250
|
+
var CANONICAL_BUS_EVENT_NAMES = [
|
|
251
|
+
...DETECTOR_EVENT_NAMES,
|
|
252
|
+
"nav.section_viewed",
|
|
253
|
+
"nav.scroll_depth"
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
// ../../sdk-contracts/dist/image-standin.js
|
|
257
|
+
var IMAGE_STANDIN_BACKGROUND = "linear-gradient(135deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.02) 45%, rgba(0, 0, 0, 0.22)), var(--sc-color-primary, #3a3a3a)";
|
|
258
|
+
function standinInitial(name) {
|
|
259
|
+
const first = [...name.trim()][0];
|
|
260
|
+
return first ? first.toUpperCase() : null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ../../sdk-contracts/dist/mount-plumbing.js
|
|
264
|
+
var MOUNT_PLUMBING_KEYS = ["instanceId", "runtime", "tileId"];
|
|
265
|
+
function stripMountPlumbing(config) {
|
|
266
|
+
if (!config || typeof config !== "object") {
|
|
267
|
+
return {};
|
|
268
|
+
}
|
|
269
|
+
const out = { ...config };
|
|
270
|
+
for (const key of MOUNT_PLUMBING_KEYS) {
|
|
271
|
+
delete out[key];
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ../../sdk-contracts/dist/routes.js
|
|
277
|
+
var RESERVED_BYTES = /* @__PURE__ */ new Set([
|
|
278
|
+
33,
|
|
279
|
+
// !
|
|
280
|
+
35,
|
|
281
|
+
// #
|
|
282
|
+
36,
|
|
283
|
+
// $
|
|
284
|
+
38,
|
|
285
|
+
// &
|
|
286
|
+
39,
|
|
287
|
+
// '
|
|
288
|
+
40,
|
|
289
|
+
// (
|
|
290
|
+
41,
|
|
291
|
+
// )
|
|
292
|
+
42,
|
|
293
|
+
// *
|
|
294
|
+
43,
|
|
295
|
+
// +
|
|
296
|
+
44,
|
|
297
|
+
// ,
|
|
298
|
+
47,
|
|
299
|
+
// /
|
|
300
|
+
58,
|
|
301
|
+
// :
|
|
302
|
+
59,
|
|
303
|
+
// ;
|
|
304
|
+
61,
|
|
305
|
+
// =
|
|
306
|
+
63,
|
|
307
|
+
// ?
|
|
308
|
+
64,
|
|
309
|
+
// @
|
|
310
|
+
91,
|
|
311
|
+
// [
|
|
312
|
+
93
|
|
313
|
+
// ]
|
|
314
|
+
]);
|
|
315
|
+
var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
|
|
316
|
+
function decodeUnreservedOnly(input) {
|
|
317
|
+
let out = "";
|
|
318
|
+
let pending = [];
|
|
319
|
+
const flushPending = () => {
|
|
320
|
+
if (pending.length === 0)
|
|
321
|
+
return;
|
|
322
|
+
const bytes = new Uint8Array(pending);
|
|
323
|
+
out += utf8Decoder.decode(bytes);
|
|
324
|
+
pending = [];
|
|
325
|
+
};
|
|
326
|
+
let i2 = 0;
|
|
327
|
+
while (i2 < input.length) {
|
|
328
|
+
const ch = input[i2];
|
|
329
|
+
if (ch === "%" && i2 + 2 < input.length && isHex(input[i2 + 1]) && isHex(input[i2 + 2])) {
|
|
330
|
+
const byte = parseInt(input.slice(i2 + 1, i2 + 3), 16);
|
|
331
|
+
if (RESERVED_BYTES.has(byte)) {
|
|
332
|
+
flushPending();
|
|
333
|
+
out += `%${input.slice(i2 + 1, i2 + 3).toUpperCase()}`;
|
|
334
|
+
i2 += 3;
|
|
335
|
+
} else {
|
|
336
|
+
pending.push(byte);
|
|
337
|
+
i2 += 3;
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
flushPending();
|
|
341
|
+
out += ch;
|
|
342
|
+
i2 += 1;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
flushPending();
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
function isHex(c2) {
|
|
349
|
+
return c2 >= "0" && c2 <= "9" || c2 >= "a" && c2 <= "f" || c2 >= "A" && c2 <= "F";
|
|
350
|
+
}
|
|
351
|
+
function stripQueryAndHash(s4) {
|
|
352
|
+
const q = s4.indexOf("?");
|
|
353
|
+
if (q !== -1)
|
|
354
|
+
s4 = s4.slice(0, q);
|
|
355
|
+
const h = s4.indexOf("#");
|
|
356
|
+
if (h !== -1)
|
|
357
|
+
s4 = s4.slice(0, h);
|
|
358
|
+
return s4;
|
|
359
|
+
}
|
|
360
|
+
function normalizeRoute(path) {
|
|
361
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
362
|
+
throw new TypeError("normalizeRoute: input must be a non-empty string");
|
|
363
|
+
}
|
|
364
|
+
if (!path.startsWith("/")) {
|
|
365
|
+
throw new TypeError(`normalizeRoute: input must be absolute (start with '/'); got ${JSON.stringify(path)}`);
|
|
366
|
+
}
|
|
367
|
+
let s4 = stripQueryAndHash(path);
|
|
368
|
+
s4 = decodeUnreservedOnly(s4);
|
|
369
|
+
s4 = s4.replace(/\/+/g, "/");
|
|
370
|
+
if (s4.length > 1 && s4.endsWith("/"))
|
|
371
|
+
s4 = s4.slice(0, -1);
|
|
372
|
+
return s4;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ../../sdk-contracts/dist/schemas.js
|
|
376
|
+
var NO_CSS_BREAKOUT_PATTERN = /^[^{}]*$/;
|
|
377
|
+
var AnchorIdZ = external_exports.object({
|
|
378
|
+
selector: external_exports.string().regex(NO_CSS_BREAKOUT_PATTERN, {
|
|
379
|
+
message: 'selector must not contain "{" or "}" \u2014 not valid CSS selector syntax, and content:hideBySelector injects this value directly into a <style> element where these characters would break out of the generated rule.'
|
|
380
|
+
}).describe("CSS selector for the target element"),
|
|
381
|
+
route: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).superRefine((value, ctx) => {
|
|
382
|
+
for (const route of Array.isArray(value) ? value : [value]) {
|
|
383
|
+
let canonical;
|
|
384
|
+
try {
|
|
385
|
+
canonical = normalizeRoute(route);
|
|
386
|
+
} catch (err) {
|
|
387
|
+
ctx.addIssue({
|
|
388
|
+
code: external_exports.ZodIssueCode.custom,
|
|
389
|
+
message: `route must be an absolute path starting with "/" (got ${JSON.stringify(route)}): ${err instanceof Error ? err.message : String(err)}`
|
|
390
|
+
});
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (canonical !== route) {
|
|
394
|
+
ctx.addIssue({
|
|
395
|
+
code: external_exports.ZodIssueCode.custom,
|
|
396
|
+
message: `route ${JSON.stringify(route)} is not canonical \u2014 use ${JSON.stringify(canonical)} (this must match what the backend's RouteCanonicalityCheck accepts)`
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}).describe("URL path(s) where this element exists")
|
|
401
|
+
}).strict().describe("DOM element target. selector = CSS selector, route = URL path(s) where the element exists.");
|
|
402
|
+
var AuthoringFieldsZ = {
|
|
403
|
+
id: external_exports.string().optional().describe('Stable action identifier (e.g. "act_3db6a14d2ab0").'),
|
|
404
|
+
title: external_exports.string().max(200).optional().describe("Authoring-only: short label shown on the action plan dashboard. Stripped before serving to the runtime SDK."),
|
|
405
|
+
description: external_exports.string().max(1e3).optional().describe("Authoring-only: one-sentence explanation of what this action does and why. Stripped before serving to the runtime SDK."),
|
|
406
|
+
validation: external_exports.array(external_exports.string().max(500)).max(10).optional().describe("Authoring-only: ordered steps a reviewer can follow to trigger this action and visually confirm it works. Each entry is one step. Stripped before serving to the runtime SDK.")
|
|
407
|
+
};
|
|
408
|
+
var COUNTABLE_EVENTS = [
|
|
409
|
+
// User interactions (from PostHog autocapture normalization)
|
|
410
|
+
"ui.click",
|
|
411
|
+
"ui.scroll",
|
|
412
|
+
"ui.input",
|
|
413
|
+
"ui.change",
|
|
414
|
+
"ui.submit",
|
|
415
|
+
// Behavioral detectors (from event-processor)
|
|
416
|
+
"ui.hover",
|
|
417
|
+
"ui.idle",
|
|
418
|
+
"ui.scroll_thrash",
|
|
419
|
+
"ui.focus_bounce",
|
|
420
|
+
"ui.hesitate",
|
|
421
|
+
"ui.rage_click",
|
|
422
|
+
// Navigation
|
|
423
|
+
"nav.page_view",
|
|
424
|
+
"nav.page_leave"
|
|
425
|
+
];
|
|
426
|
+
var CountableEventZ = external_exports.enum(COUNTABLE_EVENTS).describe("Event name to count. ui.* = user interactions and behavioral detectors (hesitate, rage_click, scroll_thrash, focus_bounce, idle, hover); nav.* = page navigation.");
|
|
427
|
+
var SESSION_METRIC_KEYS = ["time_on_page", "page_views", "scroll_depth"];
|
|
428
|
+
var SessionMetricKeyZ = external_exports.enum(SESSION_METRIC_KEYS).describe("Session metric key. time_on_page = seconds on current page, page_views = pages visited this session, scroll_depth = 0-100 percentage.");
|
|
429
|
+
var PageUrlConditionZ = external_exports.object({
|
|
430
|
+
type: external_exports.literal("page_url"),
|
|
431
|
+
url: external_exports.string().describe('URL path to match (e.g. "/pricing", "/dashboard")')
|
|
432
|
+
}).describe('Fires when the current page URL matches. Use for page-specific actions. Example: {"type": "page_url", "url": "/pricing"}');
|
|
433
|
+
var RouteConditionZ = external_exports.object({
|
|
434
|
+
type: external_exports.literal("route"),
|
|
435
|
+
routeId: external_exports.string().describe("Named route ID from the route filter")
|
|
436
|
+
}).describe("Fires when the current route matches a named route ID.");
|
|
437
|
+
var AnchorVisibleConditionZ = external_exports.object({
|
|
438
|
+
type: external_exports.literal("anchor_visible"),
|
|
439
|
+
anchorId: external_exports.string().describe("CSS selector of the anchor element"),
|
|
440
|
+
state: external_exports.enum(["visible", "present", "absent"]).describe('"visible" = in viewport, "present" = in DOM, "absent" = not in DOM')
|
|
441
|
+
}).describe(`Fires based on a DOM element's visibility state. Example: {"type": "anchor_visible", "anchorId": "#cta-button", "state": "visible"}`);
|
|
442
|
+
var EventOccurredConditionZ = external_exports.object({
|
|
443
|
+
type: external_exports.literal("event_occurred"),
|
|
444
|
+
eventName: external_exports.string().describe('Event name (e.g. "ui.click", "$pageview")'),
|
|
445
|
+
withinMs: external_exports.number().optional().describe("Time window in ms. Omit = any time this session.")
|
|
446
|
+
}).describe('Fires when a specific event has occurred during this session. Example: {"type": "event_occurred", "eventName": "ui.click", "withinMs": 5000}');
|
|
447
|
+
var StateEqualsConditionZ = external_exports.object({
|
|
448
|
+
type: external_exports.literal("state_equals"),
|
|
449
|
+
key: external_exports.string().describe("Key in the SDK persistent state store (localStorage). Only valid for keys the host app explicitly sets via syntro.state.set()."),
|
|
450
|
+
value: external_exports.unknown().describe("Expected value to match against")
|
|
451
|
+
}).describe("Checks the SDK persistent state store (localStorage). ONLY for host-app state set via syntro.state.set() \u2014 NOT for user attributes like region, device, or UTM params (those are handled by segment targeting). Do NOT use this for targeting. If you do not know the valid state keys, do not use this condition type.");
|
|
452
|
+
var ViewportConditionZ = external_exports.object({
|
|
453
|
+
type: external_exports.literal("viewport"),
|
|
454
|
+
minWidth: external_exports.number().optional().describe("Minimum viewport width in pixels"),
|
|
455
|
+
maxWidth: external_exports.number().optional().describe("Maximum viewport width in pixels"),
|
|
456
|
+
minHeight: external_exports.number().optional().describe("Minimum viewport height in pixels"),
|
|
457
|
+
maxHeight: external_exports.number().optional().describe("Maximum viewport height in pixels")
|
|
458
|
+
}).describe('Fires based on viewport (screen) size. Use for responsive behavior. Example: {"type": "viewport", "minWidth": 768} \u2014 fires on tablet and larger.');
|
|
459
|
+
var SessionMetricConditionZ = external_exports.object({
|
|
460
|
+
type: external_exports.literal("session_metric"),
|
|
461
|
+
key: SessionMetricKeyZ,
|
|
462
|
+
operator: external_exports.enum(["gte", "lte", "eq", "gt", "lt"]),
|
|
463
|
+
threshold: external_exports.number().describe("Numeric threshold to compare against")
|
|
464
|
+
}).describe('Fires when a session metric crosses a threshold. Valid keys: "time_on_page" (seconds), "page_views" (count), "scroll_depth" (0-100). Example: {"type": "session_metric", "key": "time_on_page", "operator": "gte", "threshold": 30}');
|
|
465
|
+
var DismissedConditionZ = external_exports.object({
|
|
466
|
+
type: external_exports.literal("dismissed"),
|
|
467
|
+
key: external_exports.string().describe("Dismissal key (usually a tile or action ID)"),
|
|
468
|
+
inverted: external_exports.boolean().optional().describe("When true, fires if NOT dismissed (default behavior)")
|
|
469
|
+
}).describe("Checks if an item has been dismissed by the user. Use with inverted: true to show only if not dismissed.");
|
|
470
|
+
var CooldownActiveConditionZ = external_exports.object({
|
|
471
|
+
type: external_exports.literal("cooldown_active"),
|
|
472
|
+
key: external_exports.string().describe("Cooldown key"),
|
|
473
|
+
inverted: external_exports.boolean().optional().describe("When true, fires if cooldown is NOT active")
|
|
474
|
+
}).describe("Checks if a cooldown timer is currently active. Use to prevent showing the same intervention too frequently.");
|
|
475
|
+
var FrequencyLimitConditionZ = external_exports.object({
|
|
476
|
+
type: external_exports.literal("frequency_limit"),
|
|
477
|
+
key: external_exports.string().describe("Frequency counter key"),
|
|
478
|
+
limit: external_exports.number().describe("Maximum allowed count"),
|
|
479
|
+
inverted: external_exports.boolean().optional().describe("When true, fires if limit NOT reached")
|
|
480
|
+
}).describe("Checks if a frequency limit has been reached. Use to cap how many times an action fires per session.");
|
|
481
|
+
var MatchOpZ = external_exports.object({
|
|
482
|
+
equals: external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()]).optional(),
|
|
483
|
+
contains: external_exports.string().optional()
|
|
484
|
+
}).refine((operator) => Number(operator.equals !== void 0) + Number(operator.contains !== void 0) === 1, {
|
|
485
|
+
message: "Exactly one of equals or contains must be specified."
|
|
486
|
+
}).describe("Match operator for counter filters. Exactly one of equals or contains must be specified.");
|
|
487
|
+
var CounterDefZ = external_exports.object({
|
|
488
|
+
events: external_exports.array(CountableEventZ).min(1).describe("Event names to count. Use values from the countable events enum."),
|
|
489
|
+
match: external_exports.record(external_exports.string(), MatchOpZ).optional().describe("Property filters. Keys are event prop names or element-chain fields (tag_name, $el_text, attr__*). All entries AND together.")
|
|
490
|
+
}).describe("Defines what events to count. Registered as an accumulator predicate at config-load time.");
|
|
491
|
+
var EventCountConditionZ = external_exports.object({
|
|
492
|
+
type: external_exports.literal("event_count"),
|
|
493
|
+
key: external_exports.string().describe("Unique key for this counter (used for accumulator registration)"),
|
|
494
|
+
operator: external_exports.enum(["gte", "lte", "eq", "gt", "lt"]),
|
|
495
|
+
count: external_exports.number().int().min(0).describe("Target count threshold"),
|
|
496
|
+
withinMs: external_exports.number().positive().optional().describe("Time window in ms. Omit = count across entire session."),
|
|
497
|
+
counter: CounterDefZ.optional().describe("Inline counter definition. Defines what events to count.")
|
|
498
|
+
}).describe('Fires when accumulated event count crosses a threshold. Most powerful trigger type. Example: {"type": "event_count", "key": "pricing-clicks", "operator": "gte", "count": 3, "counter": {"events": ["ui.click"], "match": {"attr__data-cta": {"contains": "pricing"}}}}');
|
|
499
|
+
var ConditionZ = external_exports.discriminatedUnion("type", [
|
|
500
|
+
PageUrlConditionZ,
|
|
501
|
+
RouteConditionZ,
|
|
502
|
+
AnchorVisibleConditionZ,
|
|
503
|
+
EventOccurredConditionZ,
|
|
504
|
+
StateEqualsConditionZ,
|
|
505
|
+
ViewportConditionZ,
|
|
506
|
+
SessionMetricConditionZ,
|
|
507
|
+
DismissedConditionZ,
|
|
508
|
+
CooldownActiveConditionZ,
|
|
509
|
+
FrequencyLimitConditionZ,
|
|
510
|
+
EventCountConditionZ
|
|
511
|
+
]);
|
|
512
|
+
var RuleZ = external_exports.object({
|
|
513
|
+
conditions: external_exports.array(ConditionZ).describe("Array of conditions \u2014 ALL must match (AND logic) for this rule to fire."),
|
|
514
|
+
value: external_exports.unknown().describe("Value returned when all conditions match. For triggerWhen: true = fire the action.")
|
|
515
|
+
}).describe("A single rule. ALL conditions must match (AND logic). Rules in a strategy are evaluated top-to-bottom \u2014 first rule where all conditions match wins and returns its value.");
|
|
516
|
+
var RuleStrategyZ = external_exports.object({
|
|
517
|
+
type: external_exports.literal("rules"),
|
|
518
|
+
rules: external_exports.array(RuleZ).describe("Ordered list of rules. Evaluated top-to-bottom \u2014 first match wins."),
|
|
519
|
+
default: external_exports.unknown().describe("Fallback value when no rule matches. For triggerWhen: false = do not fire by default.")
|
|
520
|
+
}).describe("Rule-based strategy. Evaluates rules top-to-bottom. First rule where ALL conditions match returns its value. If no rule matches, returns default. For triggerWhen: set value=true on matching rules, default=false.");
|
|
521
|
+
var ScoreStrategyZ = external_exports.object({
|
|
522
|
+
type: external_exports.literal("score"),
|
|
523
|
+
field: external_exports.string(),
|
|
524
|
+
threshold: external_exports.number(),
|
|
525
|
+
above: external_exports.unknown(),
|
|
526
|
+
below: external_exports.unknown()
|
|
527
|
+
}).describe("Score-based strategy. Compares a field value against a threshold.");
|
|
528
|
+
var ModelStrategyZ = external_exports.object({
|
|
529
|
+
type: external_exports.literal("model"),
|
|
530
|
+
modelId: external_exports.string(),
|
|
531
|
+
inputs: external_exports.array(external_exports.string()),
|
|
532
|
+
outputMapping: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
533
|
+
default: external_exports.unknown()
|
|
534
|
+
}).describe("ML model strategy. Sends inputs to a model and maps outputs.");
|
|
535
|
+
var ExternalStrategyZ = external_exports.object({
|
|
536
|
+
type: external_exports.literal("external"),
|
|
537
|
+
endpoint: external_exports.string(),
|
|
538
|
+
method: external_exports.enum(["GET", "POST"]).optional(),
|
|
539
|
+
default: external_exports.unknown(),
|
|
540
|
+
timeoutMs: external_exports.number().optional()
|
|
541
|
+
}).describe("External API strategy. Calls an endpoint to determine the value.");
|
|
542
|
+
var DecisionStrategyZ = external_exports.discriminatedUnion("type", [
|
|
543
|
+
RuleStrategyZ,
|
|
544
|
+
ScoreStrategyZ,
|
|
545
|
+
ModelStrategyZ,
|
|
546
|
+
ExternalStrategyZ
|
|
547
|
+
]);
|
|
548
|
+
var TriggerWhenZ = DecisionStrategyZ.nullable().optional();
|
|
549
|
+
var EventScopeZ = external_exports.object({
|
|
550
|
+
events: external_exports.array(external_exports.string()),
|
|
551
|
+
urlContains: external_exports.string().optional(),
|
|
552
|
+
props: external_exports.record(external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()])).optional()
|
|
553
|
+
});
|
|
554
|
+
var NotifyZ = external_exports.object({
|
|
555
|
+
title: external_exports.string().optional().describe("Notification title"),
|
|
556
|
+
body: external_exports.string().optional().describe("Notification body text"),
|
|
557
|
+
icon: external_exports.string().optional().describe("Notification icon (emoji or URL)")
|
|
558
|
+
}).describe("Optional toast notification shown when this action triggers.").nullable().optional();
|
|
559
|
+
|
|
560
|
+
// ../../sdk-contracts/dist/viewport.js
|
|
561
|
+
var SIZE_CLASS_ATTR = "data-size";
|
|
562
|
+
var MEDIUM_MIN_WIDTH_PX = 600;
|
|
563
|
+
var EXPANDED_MIN_WIDTH_PX = 1200;
|
|
564
|
+
function classifySize(widthPx) {
|
|
565
|
+
if (widthPx >= EXPANDED_MIN_WIDTH_PX)
|
|
566
|
+
return "expanded";
|
|
567
|
+
if (widthPx >= MEDIUM_MIN_WIDTH_PX)
|
|
568
|
+
return "medium";
|
|
569
|
+
return "compact";
|
|
570
|
+
}
|
|
571
|
+
var ContainerSizeController = class {
|
|
572
|
+
constructor(host) {
|
|
573
|
+
this.value = "compact";
|
|
574
|
+
this.width = 0;
|
|
575
|
+
this._off = null;
|
|
576
|
+
this._host = host;
|
|
577
|
+
host.addController(this);
|
|
578
|
+
}
|
|
579
|
+
_take(width) {
|
|
580
|
+
this.width = width;
|
|
581
|
+
const next = classifySize(width);
|
|
582
|
+
const stamped = this._host.getAttribute(SIZE_CLASS_ATTR);
|
|
583
|
+
this.value = next;
|
|
584
|
+
if (stamped !== next) {
|
|
585
|
+
this._host.setAttribute(SIZE_CLASS_ATTR, next);
|
|
586
|
+
this._host.requestUpdate();
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
hostConnected() {
|
|
590
|
+
const RO = typeof ResizeObserver === "function" ? ResizeObserver : void 0;
|
|
591
|
+
if (!RO) {
|
|
592
|
+
const onResize = () => this._take(window.innerWidth);
|
|
593
|
+
onResize();
|
|
594
|
+
window.addEventListener("resize", onResize, { passive: true });
|
|
595
|
+
this._off = () => window.removeEventListener("resize", onResize);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
this._take(this._host.getBoundingClientRect().width);
|
|
599
|
+
const ro = new RO((entries) => {
|
|
600
|
+
for (const entry2 of entries)
|
|
601
|
+
this._take(entry2.contentRect.width);
|
|
602
|
+
});
|
|
603
|
+
ro.observe(this._host);
|
|
604
|
+
this._off = () => ro.disconnect();
|
|
605
|
+
}
|
|
606
|
+
hostDisconnected() {
|
|
607
|
+
this._off?.();
|
|
608
|
+
this._off = null;
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// ../../sdk-contracts/dist/live-price.js
|
|
613
|
+
var PRODUCT_ADAPTER_KEY = "__SYNTRO_PRODUCT_ADAPTER__";
|
|
614
|
+
|
|
615
|
+
// ../../canvases/postcard-guide/dist/cart.js
|
|
616
|
+
function getCart() {
|
|
617
|
+
const adapter = window[PRODUCT_ADAPTER_KEY];
|
|
618
|
+
return adapter && typeof adapter.addToCart === "function" && typeof adapter.openCart === "function" ? adapter : null;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ../../canvases/postcard-guide/dist/host.js
|
|
622
|
+
var HINT_MAX = 60;
|
|
623
|
+
var PART_MAX = 20;
|
|
624
|
+
var PRIVATE_PATHS = /^\/(account|orders|checkouts?|cart)(\/|$)/i;
|
|
625
|
+
var LANDING_RECORD_KEY = "syntro_initial_attrs";
|
|
626
|
+
var NAME_MAX = 60;
|
|
627
|
+
function pathWords(path) {
|
|
628
|
+
if (PRIVATE_PATHS.test(path))
|
|
629
|
+
return "";
|
|
630
|
+
return path.split("/").map((segment) => segment.split(/[-_.]+/).filter((part) => /^[A-Za-z]+$/.test(part) && part.length <= PART_MAX).join("-")).filter(Boolean).join(" ").slice(-HINT_MAX).trim();
|
|
631
|
+
}
|
|
632
|
+
function landingRecord() {
|
|
633
|
+
try {
|
|
634
|
+
const raw = localStorage.getItem(LANDING_RECORD_KEY);
|
|
635
|
+
const data = raw ? JSON.parse(raw).data : null;
|
|
636
|
+
return data && typeof data === "object" ? data : {};
|
|
637
|
+
} catch {
|
|
638
|
+
return {};
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function landingHints() {
|
|
642
|
+
const record = landingRecord();
|
|
643
|
+
const hints = [];
|
|
644
|
+
const words = pathWords(typeof record.initial_page_path === "string" ? record.initial_page_path : location.pathname);
|
|
645
|
+
if (words)
|
|
646
|
+
hints.push(words);
|
|
647
|
+
const host = typeof record.referrer_hostname === "string" ? record.referrer_hostname : "";
|
|
648
|
+
if (/^[A-Za-z0-9.-]{1,60}$/.test(host) && host !== location.hostname)
|
|
649
|
+
hints.push(host);
|
|
650
|
+
return hints.slice(0, 3);
|
|
651
|
+
}
|
|
652
|
+
function consentGranted() {
|
|
653
|
+
try {
|
|
654
|
+
return window.SynOS?.consent?.getStatus?.() === "granted";
|
|
655
|
+
} catch {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function storeName() {
|
|
660
|
+
const meta = document.querySelector('meta[property="og:site_name"]');
|
|
661
|
+
return (meta?.content ?? "").trim().slice(0, NAME_MAX);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ../../canvases/postcard-guide/dist/page.js
|
|
665
|
+
import { html as html8, LitElement as LitElement2, nothing as nothing7 } from "lit";
|
|
666
|
+
import { repeat } from "lit/directives/repeat.js";
|
|
667
|
+
import { html as staticHtml, unsafeStatic } from "lit/static-html.js";
|
|
668
|
+
|
|
669
|
+
// ../../canvases/postcard-guide/dist/copy.js
|
|
670
|
+
var DEFAULT_COPY = Object.freeze({
|
|
671
|
+
ask_placeholder: "Tell us what you're looking for",
|
|
672
|
+
known_label: "What we know",
|
|
673
|
+
need_headline: "What you need",
|
|
674
|
+
best_fit: "Best fit for you",
|
|
675
|
+
because_label: "Why it's here",
|
|
676
|
+
back_label: "Back to your picks",
|
|
677
|
+
saved_failed: "Added to your cart. We couldn't save that just now.",
|
|
678
|
+
guide_name: "Shopping guide",
|
|
679
|
+
done_label: "That's everything",
|
|
680
|
+
also_label: "Also worth a look"
|
|
681
|
+
});
|
|
682
|
+
var filled = /* @__PURE__ */ new WeakMap();
|
|
683
|
+
function copyOf(view) {
|
|
684
|
+
const sent = view?.copy;
|
|
685
|
+
if (!sent || typeof sent !== "object")
|
|
686
|
+
return DEFAULT_COPY;
|
|
687
|
+
const known = filled.get(sent);
|
|
688
|
+
if (known)
|
|
689
|
+
return known;
|
|
690
|
+
const out = { ...DEFAULT_COPY };
|
|
691
|
+
for (const key of Object.keys(DEFAULT_COPY)) {
|
|
692
|
+
const word = sent[key];
|
|
693
|
+
if (typeof word === "string" && word.trim())
|
|
694
|
+
out[key] = word;
|
|
695
|
+
}
|
|
696
|
+
filled.set(sent, out);
|
|
697
|
+
return out;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// ../../canvases/postcard-guide/dist/templates/compare.js
|
|
701
|
+
import { css as css3, html as html3, nothing as nothing2 } from "lit";
|
|
702
|
+
|
|
703
|
+
// ../../canvases/postcard-guide/dist/templates/base.js
|
|
704
|
+
import { LitElement } from "lit";
|
|
705
|
+
var GUIDE_TAP_EVENT = "guide-tap";
|
|
706
|
+
var GUIDE_PICK_EVENT = "guide-pick";
|
|
707
|
+
var GUIDE_TURN_EVENT = "guide-turn";
|
|
708
|
+
var _GuideTemplate = class _GuideTemplate extends LitElement {
|
|
709
|
+
constructor() {
|
|
710
|
+
super();
|
|
711
|
+
this.size = new ContainerSizeController(this);
|
|
712
|
+
this.view = null;
|
|
713
|
+
this.turnId = "";
|
|
714
|
+
this.moveId = "";
|
|
715
|
+
this.cart = () => null;
|
|
716
|
+
this.busy = false;
|
|
717
|
+
this.copy = DEFAULT_COPY;
|
|
718
|
+
this._slots = {};
|
|
719
|
+
}
|
|
720
|
+
/** Replace one slot's text (the words stream). A path this template does not draw is kept and never shown. */
|
|
721
|
+
setSlot(path, text) {
|
|
722
|
+
this._slots = { ...this._slots, [path]: text };
|
|
723
|
+
}
|
|
724
|
+
/** Focus this template's heading. The frame calls it only after a shopper's own tap. */
|
|
725
|
+
async focusHeading() {
|
|
726
|
+
await this.updateComplete;
|
|
727
|
+
this.renderRoot.querySelector("[data-heading]")?.focus({ preventScroll: true });
|
|
728
|
+
}
|
|
729
|
+
slotText(path, fallback) {
|
|
730
|
+
return this._slots[path] ?? fallback ?? "";
|
|
731
|
+
}
|
|
732
|
+
busyAttr() {
|
|
733
|
+
return this.busy ? "true" : "false";
|
|
734
|
+
}
|
|
735
|
+
emitTap(tap) {
|
|
736
|
+
if (this.busy)
|
|
737
|
+
return;
|
|
738
|
+
this.dispatchEvent(new CustomEvent(GUIDE_TAP_EVENT, { detail: tap, bubbles: true, composed: true }));
|
|
739
|
+
}
|
|
740
|
+
emitPick(itemId, handle, variantId = null) {
|
|
741
|
+
if (this.busy)
|
|
742
|
+
return;
|
|
743
|
+
this.dispatchEvent(new CustomEvent(GUIDE_PICK_EVENT, {
|
|
744
|
+
detail: { itemId, handle, variantId },
|
|
745
|
+
bubbles: true,
|
|
746
|
+
composed: true
|
|
747
|
+
}));
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
_GuideTemplate.properties = {
|
|
751
|
+
view: { attribute: false },
|
|
752
|
+
turnId: { attribute: false },
|
|
753
|
+
moveId: { attribute: false },
|
|
754
|
+
cart: { attribute: false },
|
|
755
|
+
busy: { type: Boolean },
|
|
756
|
+
copy: { attribute: false },
|
|
757
|
+
_slots: { state: true }
|
|
758
|
+
};
|
|
759
|
+
var GuideTemplate = _GuideTemplate;
|
|
760
|
+
|
|
761
|
+
// ../../canvases/postcard-guide/dist/templates/fit.js
|
|
762
|
+
function fitOf(payload) {
|
|
763
|
+
const inStock = payload.variants.filter((v) => v.in_stock).length;
|
|
764
|
+
return inStock === 0 ? "soldout" : inStock === 1 ? "add" : "choose";
|
|
765
|
+
}
|
|
766
|
+
async function checkFit(cart, handle) {
|
|
767
|
+
if (!cart?.fetchVariants)
|
|
768
|
+
return "choose";
|
|
769
|
+
try {
|
|
770
|
+
return fitOf(await cart.fetchVariants(handle));
|
|
771
|
+
} catch {
|
|
772
|
+
return "choose";
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ../../canvases/postcard-guide/dist/templates/format.js
|
|
777
|
+
import { html } from "lit";
|
|
778
|
+
function picture(src, name) {
|
|
779
|
+
return src ? html`<img src=${src} alt="" loading="lazy" />` : html`<span class="standin" aria-hidden="true">${standinInitial(name) ?? ""}</span>`;
|
|
780
|
+
}
|
|
781
|
+
function formatPrice(price, currency) {
|
|
782
|
+
if (price == null || !(price > 0) || !currency)
|
|
783
|
+
return null;
|
|
784
|
+
try {
|
|
785
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(price);
|
|
786
|
+
} catch {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// ../../canvases/postcard-guide/dist/templates/parts.js
|
|
792
|
+
import { css, html as html2, nothing } from "lit";
|
|
793
|
+
function dots(value, label) {
|
|
794
|
+
return html2`<span class="dots" role="img" aria-label=${label}
|
|
795
|
+
>${[1, 2, 3].map((n2) => html2`<span class=${n2 <= value ? "dot on" : "dot"}></span>`)}</span
|
|
796
|
+
>`;
|
|
797
|
+
}
|
|
798
|
+
function meter(t2, value) {
|
|
799
|
+
return html2`<div class="meter">
|
|
800
|
+
<img src=${t2.image} alt="" />
|
|
801
|
+
<span class="meter-label" aria-hidden="true">${t2.label}</span>
|
|
802
|
+
${dots(value, `${t2.label}: ${value} of 3`)}
|
|
803
|
+
</div>`;
|
|
804
|
+
}
|
|
805
|
+
function stampRow(stamps, label) {
|
|
806
|
+
if (!stamps.length)
|
|
807
|
+
return nothing;
|
|
808
|
+
return html2`<ul class="because" aria-label=${label}>
|
|
809
|
+
${stamps.map((s4) => html2`<li><img class="stamp" src=${s4.image} alt=${s4.label} /></li>`)}
|
|
810
|
+
</ul>`;
|
|
811
|
+
}
|
|
812
|
+
var partStyles = css`
|
|
813
|
+
/* Read by a screen reader, not drawn. */
|
|
814
|
+
.sr {
|
|
815
|
+
position: absolute;
|
|
816
|
+
width: 1px;
|
|
817
|
+
height: 1px;
|
|
818
|
+
overflow: hidden;
|
|
819
|
+
clip-path: inset(50%);
|
|
820
|
+
white-space: nowrap;
|
|
821
|
+
}
|
|
822
|
+
.dots {
|
|
823
|
+
display: inline-flex;
|
|
824
|
+
gap: 4px;
|
|
825
|
+
flex: none;
|
|
826
|
+
}
|
|
827
|
+
.dot {
|
|
828
|
+
width: 10px;
|
|
829
|
+
height: 10px;
|
|
830
|
+
border-radius: 50%;
|
|
831
|
+
/* An empty dot is a ring in the text color, so it stays visible (WCAG 1.4.11). */
|
|
832
|
+
box-shadow: inset 0 0 0 1.5px var(--sc-tile-text-color, #4b5563);
|
|
833
|
+
}
|
|
834
|
+
.dot.on {
|
|
835
|
+
background: var(--sc-color-primary, #111827);
|
|
836
|
+
box-shadow: none;
|
|
837
|
+
}
|
|
838
|
+
.meter {
|
|
839
|
+
display: flex;
|
|
840
|
+
align-items: center;
|
|
841
|
+
gap: 10px;
|
|
842
|
+
min-height: 36px;
|
|
843
|
+
/* The dots stay near their label on a wide body. */
|
|
844
|
+
max-width: 420px;
|
|
845
|
+
}
|
|
846
|
+
.meter img {
|
|
847
|
+
flex: none;
|
|
848
|
+
width: 36px;
|
|
849
|
+
height: 36px;
|
|
850
|
+
border-radius: 8px;
|
|
851
|
+
}
|
|
852
|
+
.meter-label {
|
|
853
|
+
flex: 1;
|
|
854
|
+
min-width: 0;
|
|
855
|
+
font-weight: 700;
|
|
856
|
+
font-size: 15.2px;
|
|
857
|
+
line-height: 1.2;
|
|
858
|
+
}
|
|
859
|
+
.because {
|
|
860
|
+
display: flex;
|
|
861
|
+
flex-wrap: wrap;
|
|
862
|
+
gap: 6px;
|
|
863
|
+
}
|
|
864
|
+
/* Forced colors (Windows High Contrast) drops backgrounds and box-shadows, which
|
|
865
|
+
erased every dot: a filled dot is CanvasText, an empty one a CanvasText ring.
|
|
866
|
+
Filled buttons lose their fill too, so they get a border (:host raises them over
|
|
867
|
+
each template's own border: 0). */
|
|
868
|
+
@media (forced-colors: active) {
|
|
869
|
+
.dot {
|
|
870
|
+
border: 1.5px solid CanvasText;
|
|
871
|
+
box-shadow: none;
|
|
872
|
+
}
|
|
873
|
+
.dot.on {
|
|
874
|
+
background: CanvasText;
|
|
875
|
+
forced-color-adjust: none;
|
|
876
|
+
}
|
|
877
|
+
:host .add,
|
|
878
|
+
:host .choose,
|
|
879
|
+
:host .ghost {
|
|
880
|
+
border: 2px solid ButtonText;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
.because .stamp {
|
|
884
|
+
width: 40px;
|
|
885
|
+
height: 48px;
|
|
886
|
+
padding: 2px;
|
|
887
|
+
border: 2px solid var(--sc-color-primary, #111827);
|
|
888
|
+
border-radius: 5px;
|
|
889
|
+
/* A stamp's own white mount, in both schemes (the frame's stamps). */
|
|
890
|
+
background: #ffffff;
|
|
891
|
+
box-shadow:
|
|
892
|
+
inset 0 0 0 2px #ffffff,
|
|
893
|
+
0 2px 6px rgb(0 0 0 / 0.18);
|
|
894
|
+
object-fit: cover;
|
|
895
|
+
}
|
|
896
|
+
`;
|
|
897
|
+
|
|
898
|
+
// ../../canvases/postcard-guide/dist/templates/styles.js
|
|
899
|
+
import { css as css2, unsafeCSS } from "lit";
|
|
900
|
+
var templateStyles = css2`
|
|
901
|
+
:host {
|
|
902
|
+
display: block;
|
|
903
|
+
box-sizing: border-box;
|
|
904
|
+
min-height: 100%;
|
|
905
|
+
padding: 20px 16px 28px;
|
|
906
|
+
color: var(--sc-tile-title-color, #111827);
|
|
907
|
+
font-family: var(--sc-font-family, system-ui, sans-serif);
|
|
908
|
+
}
|
|
909
|
+
:host([data-size='medium']),
|
|
910
|
+
:host([data-size='expanded']) {
|
|
911
|
+
padding: 28px 32px 36px;
|
|
912
|
+
}
|
|
913
|
+
* {
|
|
914
|
+
box-sizing: border-box;
|
|
915
|
+
}
|
|
916
|
+
ul {
|
|
917
|
+
list-style: none;
|
|
918
|
+
margin: 0;
|
|
919
|
+
padding: 0;
|
|
920
|
+
}
|
|
921
|
+
button {
|
|
922
|
+
font: inherit;
|
|
923
|
+
color: inherit;
|
|
924
|
+
cursor: pointer;
|
|
925
|
+
}
|
|
926
|
+
button[aria-disabled='true'] {
|
|
927
|
+
cursor: progress;
|
|
928
|
+
}
|
|
929
|
+
[data-heading] {
|
|
930
|
+
outline: none;
|
|
931
|
+
}
|
|
932
|
+
button:focus-visible,
|
|
933
|
+
a:focus-visible,
|
|
934
|
+
[data-heading]:focus-visible {
|
|
935
|
+
outline: 3px solid var(--sc-color-primary, #111827);
|
|
936
|
+
outline-offset: 3px;
|
|
937
|
+
}
|
|
938
|
+
img {
|
|
939
|
+
display: block;
|
|
940
|
+
width: 100%;
|
|
941
|
+
height: 100%;
|
|
942
|
+
object-fit: cover;
|
|
943
|
+
}
|
|
944
|
+
.eyebrow {
|
|
945
|
+
margin: 0 0 6px;
|
|
946
|
+
font-size: 12.8px;
|
|
947
|
+
font-weight: 700;
|
|
948
|
+
letter-spacing: 0.08em;
|
|
949
|
+
text-transform: uppercase;
|
|
950
|
+
color: var(--sc-tile-text-color, #4b5563);
|
|
951
|
+
}
|
|
952
|
+
.headline {
|
|
953
|
+
margin: 0 0 16px;
|
|
954
|
+
font-size: 28px;
|
|
955
|
+
line-height: 1.1;
|
|
956
|
+
font-weight: 800;
|
|
957
|
+
letter-spacing: -0.01em;
|
|
958
|
+
}
|
|
959
|
+
:host([data-size='medium']) .headline,
|
|
960
|
+
:host([data-size='expanded']) .headline {
|
|
961
|
+
font-size: 36px;
|
|
962
|
+
}
|
|
963
|
+
.voice {
|
|
964
|
+
margin: -6px 0 18px;
|
|
965
|
+
font-size: 16.8px;
|
|
966
|
+
font-weight: 700;
|
|
967
|
+
line-height: 1.3;
|
|
968
|
+
color: var(--sc-color-primary, #111827);
|
|
969
|
+
}
|
|
970
|
+
.standin {
|
|
971
|
+
display: grid;
|
|
972
|
+
place-items: center;
|
|
973
|
+
width: 100%;
|
|
974
|
+
height: 100%;
|
|
975
|
+
background: ${unsafeCSS(IMAGE_STANDIN_BACKGROUND)};
|
|
976
|
+
color: var(--sc-launcher-color, #ffffff);
|
|
977
|
+
font-size: 40px;
|
|
978
|
+
font-weight: 800;
|
|
979
|
+
}
|
|
980
|
+
.on-photo {
|
|
981
|
+
position: absolute;
|
|
982
|
+
left: 0;
|
|
983
|
+
right: 0;
|
|
984
|
+
bottom: 0;
|
|
985
|
+
padding: 36px 14px 12px;
|
|
986
|
+
color: #ffffff;
|
|
987
|
+
font-weight: 800;
|
|
988
|
+
line-height: 1.15;
|
|
989
|
+
text-align: left;
|
|
990
|
+
background: linear-gradient(transparent, rgb(0 0 0 / 0.68));
|
|
991
|
+
}
|
|
992
|
+
@media (forced-colors: active) {
|
|
993
|
+
.on-photo {
|
|
994
|
+
background: Canvas;
|
|
995
|
+
color: CanvasText;
|
|
996
|
+
}
|
|
997
|
+
button {
|
|
998
|
+
border: 1px solid ButtonText;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
`;
|
|
1002
|
+
|
|
1003
|
+
// ../../canvases/postcard-guide/dist/templates/compare.js
|
|
1004
|
+
var GUIDE_COMPARE = "guide-compare";
|
|
1005
|
+
var _GuideCompare = class _GuideCompare extends GuideTemplate {
|
|
1006
|
+
constructor() {
|
|
1007
|
+
super();
|
|
1008
|
+
this._asked = /* @__PURE__ */ new Set();
|
|
1009
|
+
this._fits = {};
|
|
1010
|
+
}
|
|
1011
|
+
updated(changed) {
|
|
1012
|
+
if (changed.has("view") || changed.has("cart"))
|
|
1013
|
+
void this._checkVariants();
|
|
1014
|
+
}
|
|
1015
|
+
/** Ask the platform how many variants each product has in stock, once per product. */
|
|
1016
|
+
async _checkVariants() {
|
|
1017
|
+
const cart = this.cart();
|
|
1018
|
+
const view = this.view;
|
|
1019
|
+
if (!view || !cart?.fetchVariants)
|
|
1020
|
+
return;
|
|
1021
|
+
const pending = view.options.map((o) => o.product.handle).filter((h) => !this._asked.has(h));
|
|
1022
|
+
if (!pending.length)
|
|
1023
|
+
return;
|
|
1024
|
+
for (const handle of pending)
|
|
1025
|
+
this._asked.add(handle);
|
|
1026
|
+
const fits = {};
|
|
1027
|
+
await Promise.all(pending.map(async (handle) => {
|
|
1028
|
+
fits[handle] = await checkFit(cart, handle);
|
|
1029
|
+
}));
|
|
1030
|
+
this._fits = { ...this._fits, ...fits };
|
|
1031
|
+
}
|
|
1032
|
+
render() {
|
|
1033
|
+
const view = this.view;
|
|
1034
|
+
if (!view)
|
|
1035
|
+
return nothing2;
|
|
1036
|
+
return html3`<p class="eyebrow">${view.label}</p>
|
|
1037
|
+
<h2 class="headline" tabindex="-1" data-heading data-slot="headline">${this.slotText("headline", view.headline)}</h2>
|
|
1038
|
+
<ul class="legend">
|
|
1039
|
+
${view.tradeoffs.map((t2) => html3`<li><img src=${t2.image} alt="" /><span>${t2.label}</span></li>`)}
|
|
1040
|
+
</ul>
|
|
1041
|
+
<ul class="options" data-count=${view.options.length}>
|
|
1042
|
+
${view.options.map((o) => this._option(view, o))}
|
|
1043
|
+
</ul>`;
|
|
1044
|
+
}
|
|
1045
|
+
_option(view, o) {
|
|
1046
|
+
const handle = o.product.handle;
|
|
1047
|
+
const fit = this._fits[handle] ?? (this.cart()?.fetchVariants ? "checking" : "choose");
|
|
1048
|
+
const path = `options.${handle}.reason`;
|
|
1049
|
+
const price = formatPrice(o.product.price, o.product.currency);
|
|
1050
|
+
return html3`<li class="option" ?data-best=${o.best}>
|
|
1051
|
+
<div class="photo">
|
|
1052
|
+
${picture(o.product.image_url, o.product.name)}
|
|
1053
|
+
${o.best ? html3`<span class="best">${this.copy.best_fit}</span>` : nothing2}
|
|
1054
|
+
<div class="on-photo">
|
|
1055
|
+
<button
|
|
1056
|
+
class="name"
|
|
1057
|
+
aria-disabled=${this.busyAttr()}
|
|
1058
|
+
@click=${() => this.emitTap({ kind: "product", handle: o.product.handle })}
|
|
1059
|
+
>
|
|
1060
|
+
${o.product.name}
|
|
1061
|
+
</button>
|
|
1062
|
+
${price ? html3`<span class="price">${price}</span>` : nothing2}
|
|
1063
|
+
</div>
|
|
1064
|
+
</div>
|
|
1065
|
+
<div class="body">
|
|
1066
|
+
<div class="meters">
|
|
1067
|
+
${view.tradeoffs.map((t2) => {
|
|
1068
|
+
const value = o.values[t2.id] ?? 0;
|
|
1069
|
+
return html3`<div class="meter">
|
|
1070
|
+
<img src=${t2.image} alt=${t2.label} />
|
|
1071
|
+
${dots(value, `${value} of 3`)}
|
|
1072
|
+
</div>`;
|
|
1073
|
+
})}
|
|
1074
|
+
</div>
|
|
1075
|
+
<p class="reason" data-slot=${path}>${this.slotText(path, o.reason)}</p>
|
|
1076
|
+
${this._action(fit, view.item_id, handle, o.product.name)}
|
|
1077
|
+
</div>
|
|
1078
|
+
</li>`;
|
|
1079
|
+
}
|
|
1080
|
+
/** Visible words first (WCAG 2.5.3), then the product: three of these sit side by side. */
|
|
1081
|
+
_action(fit, itemId, handle, name) {
|
|
1082
|
+
if (fit === "soldout")
|
|
1083
|
+
return html3`<span class="sold-out">Sold out</span>`;
|
|
1084
|
+
if (fit === "choose")
|
|
1085
|
+
return html3`<button
|
|
1086
|
+
class="choose"
|
|
1087
|
+
aria-label=${`Choose options: ${name}`}
|
|
1088
|
+
aria-disabled=${this.busyAttr()}
|
|
1089
|
+
@click=${() => this.emitTap({ kind: "product", handle })}
|
|
1090
|
+
>
|
|
1091
|
+
Choose options
|
|
1092
|
+
</button>`;
|
|
1093
|
+
if (fit === "checking")
|
|
1094
|
+
return html3`<button class="add" aria-disabled="true">Checking stock</button>`;
|
|
1095
|
+
return html3`<button
|
|
1096
|
+
class="add"
|
|
1097
|
+
aria-label=${`Add to cart: ${name}`}
|
|
1098
|
+
aria-disabled=${this.busyAttr()}
|
|
1099
|
+
@click=${() => this.emitPick(itemId, handle)}
|
|
1100
|
+
>
|
|
1101
|
+
Add to cart
|
|
1102
|
+
</button>`;
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
_GuideCompare.properties = { _fits: { state: true } };
|
|
1106
|
+
_GuideCompare.styles = [
|
|
1107
|
+
templateStyles,
|
|
1108
|
+
partStyles,
|
|
1109
|
+
css3`
|
|
1110
|
+
.legend {
|
|
1111
|
+
display: flex;
|
|
1112
|
+
flex-wrap: wrap;
|
|
1113
|
+
gap: 8px 16px;
|
|
1114
|
+
margin: 0 0 16px;
|
|
1115
|
+
}
|
|
1116
|
+
.legend li {
|
|
1117
|
+
display: flex;
|
|
1118
|
+
align-items: center;
|
|
1119
|
+
gap: 8px;
|
|
1120
|
+
font-weight: 700;
|
|
1121
|
+
font-size: 14.4px;
|
|
1122
|
+
}
|
|
1123
|
+
.legend img {
|
|
1124
|
+
width: 32px;
|
|
1125
|
+
height: 32px;
|
|
1126
|
+
border-radius: 8px;
|
|
1127
|
+
}
|
|
1128
|
+
.options {
|
|
1129
|
+
display: flex;
|
|
1130
|
+
gap: 14px;
|
|
1131
|
+
overflow-x: auto;
|
|
1132
|
+
scroll-snap-type: x mandatory;
|
|
1133
|
+
overscroll-behavior-x: contain;
|
|
1134
|
+
margin: 0 -16px;
|
|
1135
|
+
padding: 0 16px 8px;
|
|
1136
|
+
scroll-padding-inline: 16px;
|
|
1137
|
+
}
|
|
1138
|
+
:host([data-size='medium']) .options,
|
|
1139
|
+
:host([data-size='expanded']) .options {
|
|
1140
|
+
display: grid;
|
|
1141
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1142
|
+
gap: 18px;
|
|
1143
|
+
overflow: visible;
|
|
1144
|
+
margin: 0;
|
|
1145
|
+
padding: 0;
|
|
1146
|
+
}
|
|
1147
|
+
:host([data-size='medium']) .options[data-count='2'],
|
|
1148
|
+
:host([data-size='expanded']) .options[data-count='2'] {
|
|
1149
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1150
|
+
}
|
|
1151
|
+
.option {
|
|
1152
|
+
flex: 0 0 82%;
|
|
1153
|
+
scroll-snap-align: start;
|
|
1154
|
+
display: flex;
|
|
1155
|
+
flex-direction: column;
|
|
1156
|
+
border-radius: var(--sc-border-radius, 18px);
|
|
1157
|
+
overflow: hidden;
|
|
1158
|
+
/* A shade off the page, so a card reads as a card in both schemes. */
|
|
1159
|
+
background: var(--sc-content-search-background, #f4f4f5);
|
|
1160
|
+
box-shadow: 0 8px 22px rgb(0 0 0 / 0.12);
|
|
1161
|
+
border: 2px solid transparent;
|
|
1162
|
+
}
|
|
1163
|
+
.option[data-best] {
|
|
1164
|
+
border-color: var(--sc-color-primary, #111827);
|
|
1165
|
+
}
|
|
1166
|
+
/* The headline is a sentence of voice, not a title: one size down. */
|
|
1167
|
+
.headline {
|
|
1168
|
+
font-size: 21.6px;
|
|
1169
|
+
line-height: 1.2;
|
|
1170
|
+
}
|
|
1171
|
+
:host([data-size='medium']) .headline,
|
|
1172
|
+
:host([data-size='expanded']) .headline {
|
|
1173
|
+
font-size: 28px;
|
|
1174
|
+
}
|
|
1175
|
+
.photo {
|
|
1176
|
+
position: relative;
|
|
1177
|
+
flex: none;
|
|
1178
|
+
aspect-ratio: 1 / 1;
|
|
1179
|
+
overflow: hidden;
|
|
1180
|
+
background: #ffffff;
|
|
1181
|
+
}
|
|
1182
|
+
/* Out of flow, so a tall store photo can never stretch the square. */
|
|
1183
|
+
.photo img {
|
|
1184
|
+
position: absolute;
|
|
1185
|
+
inset: 0;
|
|
1186
|
+
object-fit: contain;
|
|
1187
|
+
padding: 8% 8% 26%;
|
|
1188
|
+
}
|
|
1189
|
+
.photo .standin {
|
|
1190
|
+
position: absolute;
|
|
1191
|
+
inset: 0;
|
|
1192
|
+
}
|
|
1193
|
+
/* Store photos sit on white: the name sits on a solid caption plate. */
|
|
1194
|
+
.photo .on-photo {
|
|
1195
|
+
padding: 10px 12px 12px;
|
|
1196
|
+
background: rgb(12 20 18 / 0.86);
|
|
1197
|
+
}
|
|
1198
|
+
.best {
|
|
1199
|
+
position: absolute;
|
|
1200
|
+
top: 12px;
|
|
1201
|
+
left: 12px;
|
|
1202
|
+
padding: 4px 10px;
|
|
1203
|
+
border-radius: 999px;
|
|
1204
|
+
background: var(--sc-color-primary, #111827);
|
|
1205
|
+
color: var(--sc-accent-foreground, #ffffff);
|
|
1206
|
+
font-size: 11.52px;
|
|
1207
|
+
font-weight: 800;
|
|
1208
|
+
letter-spacing: 0.04em;
|
|
1209
|
+
text-transform: uppercase;
|
|
1210
|
+
}
|
|
1211
|
+
/* A button that reads as the name on the photo: it opens product detail. */
|
|
1212
|
+
.on-photo .name {
|
|
1213
|
+
display: block;
|
|
1214
|
+
padding: 0;
|
|
1215
|
+
border: 0;
|
|
1216
|
+
background: none;
|
|
1217
|
+
color: #ffffff;
|
|
1218
|
+
font-size: 17.6px;
|
|
1219
|
+
font-weight: 800;
|
|
1220
|
+
line-height: 1.15;
|
|
1221
|
+
text-align: left;
|
|
1222
|
+
}
|
|
1223
|
+
.on-photo .price {
|
|
1224
|
+
display: block;
|
|
1225
|
+
margin-top: 2px;
|
|
1226
|
+
font-weight: 700;
|
|
1227
|
+
}
|
|
1228
|
+
.body {
|
|
1229
|
+
display: flex;
|
|
1230
|
+
flex-direction: column;
|
|
1231
|
+
gap: 8px;
|
|
1232
|
+
flex: 1;
|
|
1233
|
+
padding: 12px 14px 14px;
|
|
1234
|
+
}
|
|
1235
|
+
.meters {
|
|
1236
|
+
display: flex;
|
|
1237
|
+
flex-direction: column;
|
|
1238
|
+
gap: 6px;
|
|
1239
|
+
}
|
|
1240
|
+
.meter {
|
|
1241
|
+
min-height: 0;
|
|
1242
|
+
}
|
|
1243
|
+
.meter img {
|
|
1244
|
+
width: 26px;
|
|
1245
|
+
height: 26px;
|
|
1246
|
+
border-radius: 6px;
|
|
1247
|
+
}
|
|
1248
|
+
.reason {
|
|
1249
|
+
margin: 0;
|
|
1250
|
+
font-weight: 600;
|
|
1251
|
+
}
|
|
1252
|
+
.add,
|
|
1253
|
+
.choose,
|
|
1254
|
+
.sold-out {
|
|
1255
|
+
display: block;
|
|
1256
|
+
width: 100%;
|
|
1257
|
+
min-height: 44px;
|
|
1258
|
+
margin-top: auto;
|
|
1259
|
+
padding: 12px;
|
|
1260
|
+
border-radius: 999px;
|
|
1261
|
+
text-align: center;
|
|
1262
|
+
font-weight: 800;
|
|
1263
|
+
}
|
|
1264
|
+
.add {
|
|
1265
|
+
border: 0;
|
|
1266
|
+
background: var(--sc-color-primary, #111827);
|
|
1267
|
+
color: var(--sc-accent-foreground, #ffffff);
|
|
1268
|
+
}
|
|
1269
|
+
.choose {
|
|
1270
|
+
border: 2px solid var(--sc-color-primary, #111827);
|
|
1271
|
+
background: transparent;
|
|
1272
|
+
color: var(--sc-tile-title-color, #111827);
|
|
1273
|
+
}
|
|
1274
|
+
.sold-out {
|
|
1275
|
+
border: 1px dashed var(--sc-tile-text-color, #4b5563);
|
|
1276
|
+
color: var(--sc-tile-text-color, #4b5563);
|
|
1277
|
+
}
|
|
1278
|
+
`
|
|
1279
|
+
];
|
|
1280
|
+
var GuideCompare = _GuideCompare;
|
|
1281
|
+
if (!customElements.get(GUIDE_COMPARE)) {
|
|
1282
|
+
customElements.define(GUIDE_COMPARE, GuideCompare);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// ../../canvases/postcard-guide/dist/templates/hero-pick.js
|
|
1286
|
+
import { css as css5, html as html4, nothing as nothing3 } from "lit";
|
|
1287
|
+
|
|
1288
|
+
// ../../canvases/postcard-guide/dist/templates/product-styles.js
|
|
1289
|
+
import { css as css4 } from "lit";
|
|
1290
|
+
var productStyles = css4`
|
|
1291
|
+
.photo {
|
|
1292
|
+
position: relative;
|
|
1293
|
+
overflow: hidden;
|
|
1294
|
+
border-radius: var(--sc-border-radius, 18px);
|
|
1295
|
+
/* Store photos are shot on white: their own paper, in both schemes. */
|
|
1296
|
+
background: #ffffff;
|
|
1297
|
+
box-shadow: 0 12px 30px rgb(0 0 0 / 0.16);
|
|
1298
|
+
}
|
|
1299
|
+
/* Out of flow, so a tall packshot never sizes the photo; contained, never cropped. */
|
|
1300
|
+
.photo img,
|
|
1301
|
+
.photo .standin {
|
|
1302
|
+
position: absolute;
|
|
1303
|
+
inset: 0;
|
|
1304
|
+
}
|
|
1305
|
+
/* The packshot clears the tag and stops at the plate. */
|
|
1306
|
+
.photo > img {
|
|
1307
|
+
object-fit: contain;
|
|
1308
|
+
padding: 44px 16px 80px;
|
|
1309
|
+
}
|
|
1310
|
+
.photo .because {
|
|
1311
|
+
position: absolute;
|
|
1312
|
+
top: 14px;
|
|
1313
|
+
right: 14px;
|
|
1314
|
+
justify-content: flex-end;
|
|
1315
|
+
max-width: 60%;
|
|
1316
|
+
}
|
|
1317
|
+
.photo .because img {
|
|
1318
|
+
position: static;
|
|
1319
|
+
}
|
|
1320
|
+
.best {
|
|
1321
|
+
position: absolute;
|
|
1322
|
+
top: 16px;
|
|
1323
|
+
left: 16px;
|
|
1324
|
+
padding: 5px 12px;
|
|
1325
|
+
border-radius: 999px;
|
|
1326
|
+
background: var(--sc-color-primary, #111827);
|
|
1327
|
+
color: var(--sc-accent-foreground, #ffffff);
|
|
1328
|
+
font-size: 12px;
|
|
1329
|
+
font-weight: 800;
|
|
1330
|
+
letter-spacing: 0.05em;
|
|
1331
|
+
text-transform: uppercase;
|
|
1332
|
+
}
|
|
1333
|
+
.plate {
|
|
1334
|
+
position: absolute;
|
|
1335
|
+
left: 0;
|
|
1336
|
+
right: 0;
|
|
1337
|
+
bottom: 0;
|
|
1338
|
+
display: flex;
|
|
1339
|
+
align-items: flex-end;
|
|
1340
|
+
justify-content: space-between;
|
|
1341
|
+
gap: 12px;
|
|
1342
|
+
padding: 12px 16px 14px;
|
|
1343
|
+
background: rgb(12 20 18 / 0.88);
|
|
1344
|
+
color: #ffffff;
|
|
1345
|
+
}
|
|
1346
|
+
.plate h2 {
|
|
1347
|
+
margin: 0;
|
|
1348
|
+
font-size: 18.4px;
|
|
1349
|
+
line-height: 1.15;
|
|
1350
|
+
font-weight: 800;
|
|
1351
|
+
display: -webkit-box;
|
|
1352
|
+
-webkit-line-clamp: 2;
|
|
1353
|
+
-webkit-box-orient: vertical;
|
|
1354
|
+
overflow: hidden;
|
|
1355
|
+
}
|
|
1356
|
+
.plate .price {
|
|
1357
|
+
flex: none;
|
|
1358
|
+
font-size: 18.4px;
|
|
1359
|
+
font-weight: 800;
|
|
1360
|
+
}
|
|
1361
|
+
:host([data-size='medium']) .plate,
|
|
1362
|
+
:host([data-size='expanded']) .plate {
|
|
1363
|
+
padding: 18px 24px 20px;
|
|
1364
|
+
}
|
|
1365
|
+
:host([data-size='medium']) .plate h2,
|
|
1366
|
+
:host([data-size='expanded']) .plate h2 {
|
|
1367
|
+
font-size: 26.4px;
|
|
1368
|
+
-webkit-line-clamp: 3;
|
|
1369
|
+
}
|
|
1370
|
+
:host([data-size='medium']) .photo > img,
|
|
1371
|
+
:host([data-size='expanded']) .photo > img {
|
|
1372
|
+
padding: 64px 40px 128px;
|
|
1373
|
+
}
|
|
1374
|
+
.eyebrow {
|
|
1375
|
+
margin: 0;
|
|
1376
|
+
}
|
|
1377
|
+
.reason {
|
|
1378
|
+
margin: 0;
|
|
1379
|
+
font-size: 19.2px;
|
|
1380
|
+
line-height: 1.25;
|
|
1381
|
+
font-weight: 800;
|
|
1382
|
+
letter-spacing: -0.005em;
|
|
1383
|
+
color: var(--sc-color-primary, #111827);
|
|
1384
|
+
}
|
|
1385
|
+
:host([data-size='medium']) .reason,
|
|
1386
|
+
:host([data-size='expanded']) .reason {
|
|
1387
|
+
font-size: 29.6px;
|
|
1388
|
+
line-height: 1.15;
|
|
1389
|
+
}
|
|
1390
|
+
.actions {
|
|
1391
|
+
display: flex;
|
|
1392
|
+
gap: 10px;
|
|
1393
|
+
}
|
|
1394
|
+
.add,
|
|
1395
|
+
.ghost,
|
|
1396
|
+
.sold-out {
|
|
1397
|
+
display: inline-flex;
|
|
1398
|
+
align-items: center;
|
|
1399
|
+
justify-content: center;
|
|
1400
|
+
min-height: 52px;
|
|
1401
|
+
padding: 0 22px;
|
|
1402
|
+
border-radius: 999px;
|
|
1403
|
+
font-weight: 800;
|
|
1404
|
+
font-size: 16px;
|
|
1405
|
+
text-decoration: none;
|
|
1406
|
+
}
|
|
1407
|
+
.add {
|
|
1408
|
+
flex: 1.4;
|
|
1409
|
+
border: 0;
|
|
1410
|
+
background: var(--sc-color-primary, #111827);
|
|
1411
|
+
color: var(--sc-accent-foreground, #ffffff);
|
|
1412
|
+
}
|
|
1413
|
+
.add[aria-disabled='true'] {
|
|
1414
|
+
opacity: 0.55;
|
|
1415
|
+
}
|
|
1416
|
+
.ghost {
|
|
1417
|
+
flex: 1;
|
|
1418
|
+
border: 2px solid var(--sc-color-primary, #111827);
|
|
1419
|
+
background: transparent;
|
|
1420
|
+
color: var(--sc-tile-title-color, #111827);
|
|
1421
|
+
}
|
|
1422
|
+
.sold-out {
|
|
1423
|
+
flex: 1.4;
|
|
1424
|
+
border: 1px dashed var(--sc-tile-text-color, #4b5563);
|
|
1425
|
+
color: var(--sc-tile-text-color, #4b5563);
|
|
1426
|
+
}
|
|
1427
|
+
:host([data-size='medium']) .actions .add,
|
|
1428
|
+
:host([data-size='expanded']) .actions .add,
|
|
1429
|
+
:host([data-size='medium']) .actions .sold-out,
|
|
1430
|
+
:host([data-size='expanded']) .actions .sold-out {
|
|
1431
|
+
flex: none;
|
|
1432
|
+
min-width: 200px;
|
|
1433
|
+
}
|
|
1434
|
+
:host([data-size='medium']) .actions .ghost,
|
|
1435
|
+
:host([data-size='expanded']) .actions .ghost {
|
|
1436
|
+
flex: none;
|
|
1437
|
+
}
|
|
1438
|
+
@media (forced-colors: active) {
|
|
1439
|
+
.plate {
|
|
1440
|
+
background: Canvas;
|
|
1441
|
+
color: CanvasText;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
`;
|
|
1445
|
+
|
|
1446
|
+
// ../../canvases/postcard-guide/dist/templates/hero-pick.js
|
|
1447
|
+
var GUIDE_HERO_PICK = "guide-hero-pick";
|
|
1448
|
+
var _GuideHeroPick = class _GuideHeroPick extends GuideTemplate {
|
|
1449
|
+
constructor() {
|
|
1450
|
+
super();
|
|
1451
|
+
this._fitSeq = 0;
|
|
1452
|
+
this._openDetail = () => {
|
|
1453
|
+
if (this.view)
|
|
1454
|
+
this.emitTap({ kind: "product", handle: this.view.product.handle });
|
|
1455
|
+
};
|
|
1456
|
+
this._add = () => {
|
|
1457
|
+
if (this.view && this._fit === "add") {
|
|
1458
|
+
this.emitPick(this.view.item_id, this.view.product.handle);
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
this._fit = "checking";
|
|
1462
|
+
}
|
|
1463
|
+
willUpdate(changed) {
|
|
1464
|
+
if (changed.has("view") || changed.has("cart"))
|
|
1465
|
+
void this._checkFit();
|
|
1466
|
+
}
|
|
1467
|
+
async _checkFit() {
|
|
1468
|
+
const seq = ++this._fitSeq;
|
|
1469
|
+
const handle = this.view?.product.handle;
|
|
1470
|
+
if (!handle)
|
|
1471
|
+
return;
|
|
1472
|
+
this._fit = "checking";
|
|
1473
|
+
const fit = await checkFit(this.cart(), handle);
|
|
1474
|
+
if (seq === this._fitSeq)
|
|
1475
|
+
this._fit = fit;
|
|
1476
|
+
}
|
|
1477
|
+
render() {
|
|
1478
|
+
const v = this.view;
|
|
1479
|
+
if (!v)
|
|
1480
|
+
return nothing3;
|
|
1481
|
+
const p = v.product;
|
|
1482
|
+
const price = formatPrice(p.price, p.currency);
|
|
1483
|
+
return html4`<article class="hero">
|
|
1484
|
+
<div class="photo">
|
|
1485
|
+
${picture(p.image_url, p.name)}
|
|
1486
|
+
<span class="best">${this.copy.best_fit}</span>
|
|
1487
|
+
${stampRow(v.because, this.copy.because_label)}
|
|
1488
|
+
<div class="plate">
|
|
1489
|
+
<h2 tabindex="-1" data-heading>${p.name}</h2>
|
|
1490
|
+
${price ? html4`<span class="price">${price}</span>` : nothing3}
|
|
1491
|
+
</div>
|
|
1492
|
+
</div>
|
|
1493
|
+
<div class="body">
|
|
1494
|
+
<p class="eyebrow">${v.label}</p>
|
|
1495
|
+
<p class="reason" data-slot="reason">${this.slotText("reason", v.reason)}</p>
|
|
1496
|
+
<ul class="strengths" aria-label="How it does">
|
|
1497
|
+
${v.tradeoffs.map((t2) => html4`<li>${meter(t2, v.values[t2.id] ?? 0)}</li>`)}
|
|
1498
|
+
</ul>
|
|
1499
|
+
<div class="actions">
|
|
1500
|
+
${this._action()}
|
|
1501
|
+
${this._fit === "choose" ? nothing3 : html4`<button
|
|
1502
|
+
class="ghost"
|
|
1503
|
+
aria-label=${this._named("See details")}
|
|
1504
|
+
aria-disabled=${this.busyAttr()}
|
|
1505
|
+
@click=${this._openDetail}
|
|
1506
|
+
>
|
|
1507
|
+
See details
|
|
1508
|
+
</button>`}
|
|
1509
|
+
</div>
|
|
1510
|
+
</div>
|
|
1511
|
+
</article>`;
|
|
1512
|
+
}
|
|
1513
|
+
/** The visible words first (WCAG 2.5.3), then which product: several heroes can share a feed. */
|
|
1514
|
+
_named(action) {
|
|
1515
|
+
return `${action}: ${this.view?.product.name ?? ""}`;
|
|
1516
|
+
}
|
|
1517
|
+
_action() {
|
|
1518
|
+
switch (this._fit) {
|
|
1519
|
+
case "checking":
|
|
1520
|
+
return html4`<button class="add" aria-disabled="true">Checking stock</button>`;
|
|
1521
|
+
case "soldout":
|
|
1522
|
+
return html4`<span class="sold-out">Sold out</span>`;
|
|
1523
|
+
case "add":
|
|
1524
|
+
return html4`<button
|
|
1525
|
+
class="add"
|
|
1526
|
+
aria-label=${this._named("Add to cart")}
|
|
1527
|
+
aria-disabled=${this.busyAttr()}
|
|
1528
|
+
@click=${this._add}
|
|
1529
|
+
>
|
|
1530
|
+
Add to cart
|
|
1531
|
+
</button>`;
|
|
1532
|
+
default:
|
|
1533
|
+
return html4`<button
|
|
1534
|
+
class="add"
|
|
1535
|
+
aria-label=${this._named("Choose options")}
|
|
1536
|
+
aria-disabled=${this.busyAttr()}
|
|
1537
|
+
@click=${this._openDetail}
|
|
1538
|
+
>
|
|
1539
|
+
Choose options
|
|
1540
|
+
</button>`;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
_GuideHeroPick.properties = { _fit: { state: true } };
|
|
1545
|
+
_GuideHeroPick.styles = [
|
|
1546
|
+
templateStyles,
|
|
1547
|
+
partStyles,
|
|
1548
|
+
productStyles,
|
|
1549
|
+
css5`
|
|
1550
|
+
:host {
|
|
1551
|
+
display: flex;
|
|
1552
|
+
flex-direction: column;
|
|
1553
|
+
}
|
|
1554
|
+
.hero {
|
|
1555
|
+
flex: 1;
|
|
1556
|
+
display: flex;
|
|
1557
|
+
flex-direction: column;
|
|
1558
|
+
gap: 16px;
|
|
1559
|
+
}
|
|
1560
|
+
.photo {
|
|
1561
|
+
flex: 1 1 auto;
|
|
1562
|
+
min-height: 260px;
|
|
1563
|
+
}
|
|
1564
|
+
.body {
|
|
1565
|
+
display: flex;
|
|
1566
|
+
flex-direction: column;
|
|
1567
|
+
gap: 12px;
|
|
1568
|
+
}
|
|
1569
|
+
.strengths {
|
|
1570
|
+
display: grid;
|
|
1571
|
+
gap: 8px 20px;
|
|
1572
|
+
}
|
|
1573
|
+
:host([data-size='medium']) .hero,
|
|
1574
|
+
:host([data-size='expanded']) .hero {
|
|
1575
|
+
display: grid;
|
|
1576
|
+
grid-template-columns: minmax(0, 1.2fr) minmax(300px, 1fr);
|
|
1577
|
+
align-items: center;
|
|
1578
|
+
gap: 44px;
|
|
1579
|
+
}
|
|
1580
|
+
:host([data-size='medium']) .photo,
|
|
1581
|
+
:host([data-size='expanded']) .photo {
|
|
1582
|
+
align-self: stretch;
|
|
1583
|
+
min-height: 520px;
|
|
1584
|
+
}
|
|
1585
|
+
:host([data-size='medium']) .body,
|
|
1586
|
+
:host([data-size='expanded']) .body {
|
|
1587
|
+
gap: 20px;
|
|
1588
|
+
}
|
|
1589
|
+
`
|
|
1590
|
+
];
|
|
1591
|
+
var GuideHeroPick = _GuideHeroPick;
|
|
1592
|
+
if (!customElements.get(GUIDE_HERO_PICK)) {
|
|
1593
|
+
customElements.define(GUIDE_HERO_PICK, GuideHeroPick);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// ../../canvases/postcard-guide/dist/templates/pick-from-pictures.js
|
|
1597
|
+
import { css as css6, html as html5, nothing as nothing4 } from "lit";
|
|
1598
|
+
var GUIDE_PICK_FROM_PICTURES = "guide-pick-from-pictures";
|
|
1599
|
+
function lastSpan(count, size) {
|
|
1600
|
+
if (count <= 1)
|
|
1601
|
+
return 1;
|
|
1602
|
+
const columns = size === "expanded" ? 4 : size === "medium" ? 3 : 2;
|
|
1603
|
+
const cells = (size === "compact" ? 2 : 4) + count - 1;
|
|
1604
|
+
const rest = cells % columns;
|
|
1605
|
+
return rest === 0 ? 1 : columns - rest + 1;
|
|
1606
|
+
}
|
|
1607
|
+
var _GuidePickFromPictures = class _GuidePickFromPictures extends GuideTemplate {
|
|
1608
|
+
render() {
|
|
1609
|
+
const view = this.view;
|
|
1610
|
+
if (!view)
|
|
1611
|
+
return nothing4;
|
|
1612
|
+
const span = lastSpan(view.choices.length, this.size.value);
|
|
1613
|
+
const last = view.choices.length - 1;
|
|
1614
|
+
return html5`<h2 class="headline" tabindex="-1" data-heading data-slot="question">${this.slotText("question", view.question)}</h2>
|
|
1615
|
+
<ul class="choices">
|
|
1616
|
+
${view.choices.map((c2, i2) => html5`<li class=${i2 === last && span > 1 ? `span-${span}` : nothing4}>
|
|
1617
|
+
<button
|
|
1618
|
+
class="choice"
|
|
1619
|
+
aria-label=${c2.title}
|
|
1620
|
+
aria-disabled=${this.busyAttr()}
|
|
1621
|
+
@click=${() => this.emitTap({ kind: "postcard", postcard_id: c2.id })}
|
|
1622
|
+
>
|
|
1623
|
+
<img src=${c2.image} alt="" loading="lazy" /><span class="on-photo" aria-hidden="true">${c2.title}</span>
|
|
1624
|
+
</button>
|
|
1625
|
+
</li>`)}
|
|
1626
|
+
</ul>`;
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
_GuidePickFromPictures.styles = [
|
|
1630
|
+
templateStyles,
|
|
1631
|
+
css6`
|
|
1632
|
+
.choices {
|
|
1633
|
+
display: grid;
|
|
1634
|
+
gap: 12px;
|
|
1635
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1636
|
+
grid-auto-rows: 176px;
|
|
1637
|
+
}
|
|
1638
|
+
.choices li:first-child {
|
|
1639
|
+
grid-column: span 2;
|
|
1640
|
+
}
|
|
1641
|
+
:host([data-size='medium']) .choices {
|
|
1642
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1643
|
+
grid-auto-rows: 192px;
|
|
1644
|
+
gap: 16px;
|
|
1645
|
+
}
|
|
1646
|
+
:host([data-size='expanded']) .choices {
|
|
1647
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1648
|
+
grid-auto-rows: 208px;
|
|
1649
|
+
gap: 18px;
|
|
1650
|
+
}
|
|
1651
|
+
:host([data-size='medium']) .choices li:first-child,
|
|
1652
|
+
:host([data-size='expanded']) .choices li:first-child {
|
|
1653
|
+
grid-column: span 2;
|
|
1654
|
+
grid-row: span 2;
|
|
1655
|
+
}
|
|
1656
|
+
/* The last picture closes the last row (lastSpan); a class, not a
|
|
1657
|
+
style attribute, so a strict host CSP cannot drop it. */
|
|
1658
|
+
.choices li.span-2 {
|
|
1659
|
+
grid-column: span 2;
|
|
1660
|
+
}
|
|
1661
|
+
.choices li.span-3 {
|
|
1662
|
+
grid-column: span 3;
|
|
1663
|
+
}
|
|
1664
|
+
.choices li.span-4 {
|
|
1665
|
+
grid-column: span 4;
|
|
1666
|
+
}
|
|
1667
|
+
.choice {
|
|
1668
|
+
position: relative;
|
|
1669
|
+
display: block;
|
|
1670
|
+
width: 100%;
|
|
1671
|
+
height: 100%;
|
|
1672
|
+
padding: 0;
|
|
1673
|
+
border: 0;
|
|
1674
|
+
border-radius: var(--sc-border-radius, 18px);
|
|
1675
|
+
overflow: hidden;
|
|
1676
|
+
background: var(--sc-content-search-background, #e7e2d6);
|
|
1677
|
+
box-shadow: 0 8px 20px rgb(0 0 0 / 0.12);
|
|
1678
|
+
}
|
|
1679
|
+
.choice .on-photo {
|
|
1680
|
+
font-size: 17.6px;
|
|
1681
|
+
}
|
|
1682
|
+
li:first-child .choice .on-photo {
|
|
1683
|
+
font-size: 24px;
|
|
1684
|
+
}
|
|
1685
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
1686
|
+
.choice img {
|
|
1687
|
+
transition: transform 300ms ease;
|
|
1688
|
+
}
|
|
1689
|
+
.choice:hover img {
|
|
1690
|
+
transform: scale(1.04);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
`
|
|
1694
|
+
];
|
|
1695
|
+
var GuidePickFromPictures = _GuidePickFromPictures;
|
|
1696
|
+
if (!customElements.get(GUIDE_PICK_FROM_PICTURES)) {
|
|
1697
|
+
customElements.define(GUIDE_PICK_FROM_PICTURES, GuidePickFromPictures);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// ../../canvases/postcard-guide/dist/templates/product-detail.js
|
|
1701
|
+
import { css as css7, html as html6, nothing as nothing5 } from "lit";
|
|
1702
|
+
import { styleMap } from "lit/directives/style-map.js";
|
|
1703
|
+
var GUIDE_PRODUCT_DETAIL = "guide-product-detail";
|
|
1704
|
+
var GUIDE_DETAIL_BACK_EVENT = "guide-detail-back";
|
|
1705
|
+
var COLOR = /^(#[0-9a-f]{3,8}|(rgb|rgba|hsl|hsla)\([0-9.,%\s/]+\)|[a-z]{3,20})$/i;
|
|
1706
|
+
function tilePrice(t2) {
|
|
1707
|
+
if (!t2.price || t2.price_cents === 0)
|
|
1708
|
+
return null;
|
|
1709
|
+
return /[1-9]/.test(t2.price) ? t2.price : null;
|
|
1710
|
+
}
|
|
1711
|
+
var _GuideProductDetail = class _GuideProductDetail extends GuideTemplate {
|
|
1712
|
+
constructor() {
|
|
1713
|
+
super();
|
|
1714
|
+
this._seq = 0;
|
|
1715
|
+
this._warned = /* @__PURE__ */ new Set();
|
|
1716
|
+
this._back = () => {
|
|
1717
|
+
this.dispatchEvent(new CustomEvent(GUIDE_DETAIL_BACK_EVENT, { detail: {}, bubbles: true, composed: true }));
|
|
1718
|
+
};
|
|
1719
|
+
this._variants = { state: "checking" };
|
|
1720
|
+
this._choice = {};
|
|
1721
|
+
}
|
|
1722
|
+
willUpdate(changed) {
|
|
1723
|
+
if (changed.has("view")) {
|
|
1724
|
+
this._choice = {};
|
|
1725
|
+
void this._loadVariants();
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
async _loadVariants() {
|
|
1729
|
+
const seq = ++this._seq;
|
|
1730
|
+
const handle = this.view?.product.handle;
|
|
1731
|
+
const cart = this.cart();
|
|
1732
|
+
if (!handle || !cart?.fetchVariants) {
|
|
1733
|
+
this._variants = { state: "unreadable" };
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
this._variants = { state: "checking" };
|
|
1737
|
+
try {
|
|
1738
|
+
const data = await cart.fetchVariants(handle);
|
|
1739
|
+
if (seq === this._seq)
|
|
1740
|
+
this._variants = { state: "ready", data };
|
|
1741
|
+
} catch {
|
|
1742
|
+
if (seq === this._seq)
|
|
1743
|
+
this._variants = { state: "unreadable" };
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
_add(variantId) {
|
|
1747
|
+
const v = this.view;
|
|
1748
|
+
if (v)
|
|
1749
|
+
this.emitPick(v.item_id, v.product.handle, variantId);
|
|
1750
|
+
}
|
|
1751
|
+
render() {
|
|
1752
|
+
const v = this.view;
|
|
1753
|
+
if (!v)
|
|
1754
|
+
return nothing5;
|
|
1755
|
+
const p = v.product;
|
|
1756
|
+
const price = formatPrice(p.price, p.currency);
|
|
1757
|
+
return html6`<article class="detail">
|
|
1758
|
+
<div class="photo">
|
|
1759
|
+
${picture(p.image_url, p.name)}
|
|
1760
|
+
${v.best ? html6`<span class="best">${this.copy.best_fit}</span>` : nothing5}
|
|
1761
|
+
${stampRow(v.because, this.copy.because_label)}
|
|
1762
|
+
<div class="plate">
|
|
1763
|
+
<h2 tabindex="-1" data-heading>${p.name}</h2>
|
|
1764
|
+
${price ? html6`<span class="price">${price}</span>` : nothing5}
|
|
1765
|
+
</div>
|
|
1766
|
+
</div>
|
|
1767
|
+
<div class="body">
|
|
1768
|
+
<p class="reason" data-slot="reason">${this.slotText("reason", v.reason)}</p>
|
|
1769
|
+
${this._buy(v)}
|
|
1770
|
+
<section aria-labelledby="how">
|
|
1771
|
+
<h3 id="how">How it does</h3>
|
|
1772
|
+
<ul class="meters">
|
|
1773
|
+
${v.tradeoffs.map((t2) => html6`<li>${meter(t2, v.values[t2.id] ?? 0)}</li>`)}
|
|
1774
|
+
</ul>
|
|
1775
|
+
</section>
|
|
1776
|
+
${v.modules.map((m) => this._module(m))}
|
|
1777
|
+
<button class="ghost back" @click=${this._back}>${this.copy.back_label}</button>
|
|
1778
|
+
</div>
|
|
1779
|
+
</article>`;
|
|
1780
|
+
}
|
|
1781
|
+
_buy(v) {
|
|
1782
|
+
const s4 = this._variants;
|
|
1783
|
+
if (s4.state === "checking") {
|
|
1784
|
+
return html6`<div class="buy">
|
|
1785
|
+
<button class="add" aria-disabled="true">Checking stock</button>
|
|
1786
|
+
</div>`;
|
|
1787
|
+
}
|
|
1788
|
+
const choose = html6`<div class="buy">
|
|
1789
|
+
<a class="add" href=${v.product.href}>Choose options</a>
|
|
1790
|
+
</div>`;
|
|
1791
|
+
if (s4.state === "unreadable")
|
|
1792
|
+
return choose;
|
|
1793
|
+
const inStock = s4.data.variants.filter((x) => x.in_stock);
|
|
1794
|
+
if (inStock.length === 0) {
|
|
1795
|
+
return html6`<div class="buy"><span class="sold-out">Sold out</span></div>`;
|
|
1796
|
+
}
|
|
1797
|
+
if (inStock.length === 1) {
|
|
1798
|
+
return html6`<div class="buy">
|
|
1799
|
+
<button
|
|
1800
|
+
class="add"
|
|
1801
|
+
aria-label=${`Add to cart: ${v.product.name}`}
|
|
1802
|
+
aria-disabled=${this.busyAttr()}
|
|
1803
|
+
@click=${() => this._add(inStock[0].id ?? null)}
|
|
1804
|
+
>
|
|
1805
|
+
Add to cart
|
|
1806
|
+
</button>
|
|
1807
|
+
</div>`;
|
|
1808
|
+
}
|
|
1809
|
+
const attrs = s4.data.attributes ?? [];
|
|
1810
|
+
if (!attrs.length)
|
|
1811
|
+
return choose;
|
|
1812
|
+
const chosen = inStock.find((x) => attrs.every((a) => x.selection?.[a.key] === this._choice[a.key]));
|
|
1813
|
+
const ready = !!chosen && !this.busy;
|
|
1814
|
+
return html6`<div class="buy">
|
|
1815
|
+
${attrs.map((a) => html6`<fieldset class="options">
|
|
1816
|
+
<legend>${a.label}</legend>
|
|
1817
|
+
${a.values.map((val) => {
|
|
1818
|
+
const available = inStock.some((x) => x.selection?.[a.key] === val.id);
|
|
1819
|
+
const color = val.swatch?.kind === "color" && COLOR.test(val.swatch.value) ? val.swatch.value : null;
|
|
1820
|
+
return html6`<label class=${available ? "pill" : "pill out"}>
|
|
1821
|
+
<input
|
|
1822
|
+
type="radio"
|
|
1823
|
+
name=${a.key}
|
|
1824
|
+
.value=${val.id}
|
|
1825
|
+
.checked=${this._choice[a.key] === val.id}
|
|
1826
|
+
?disabled=${!available}
|
|
1827
|
+
@change=${() => {
|
|
1828
|
+
this._choice = { ...this._choice, [a.key]: val.id };
|
|
1829
|
+
}}
|
|
1830
|
+
/>
|
|
1831
|
+
<span class="pill-face">
|
|
1832
|
+
${color ? html6`<span
|
|
1833
|
+
class="swatch"
|
|
1834
|
+
style=${styleMap({ backgroundColor: color })}
|
|
1835
|
+
></span>` : nothing5}
|
|
1836
|
+
<span>${val.label}</span>${available ? nothing5 : html6`<span class="sr">, sold out</span>`}
|
|
1837
|
+
</span>
|
|
1838
|
+
</label>`;
|
|
1839
|
+
})}
|
|
1840
|
+
</fieldset>`)}
|
|
1841
|
+
<button
|
|
1842
|
+
class="add"
|
|
1843
|
+
aria-label=${`Add to cart: ${v.product.name}`}
|
|
1844
|
+
aria-disabled=${ready ? "false" : "true"}
|
|
1845
|
+
aria-describedby=${chosen ? nothing5 : "need-choice"}
|
|
1846
|
+
@click=${() => {
|
|
1847
|
+
if (chosen)
|
|
1848
|
+
this._add(chosen.id ?? null);
|
|
1849
|
+
}}
|
|
1850
|
+
>
|
|
1851
|
+
Add to cart
|
|
1852
|
+
</button>
|
|
1853
|
+
${chosen ? nothing5 : html6`<p id="need-choice" class="hint">Choose your options first.</p>`}
|
|
1854
|
+
</div>`;
|
|
1855
|
+
}
|
|
1856
|
+
_module(m) {
|
|
1857
|
+
if (m.kind !== "other-products") {
|
|
1858
|
+
if (!this._warned.has(m.kind)) {
|
|
1859
|
+
this._warned.add(m.kind);
|
|
1860
|
+
console.warn(`[postcard-guide] product detail cannot draw a ${m.kind} module; left out`);
|
|
1861
|
+
}
|
|
1862
|
+
return nothing5;
|
|
1863
|
+
}
|
|
1864
|
+
const icons = new Map((this.view?.tradeoffs ?? []).map((t2) => [t2.id, t2]));
|
|
1865
|
+
return html6`<section class="others" aria-labelledby="others">
|
|
1866
|
+
<h3 id="others">${this.copy.also_label}</h3>
|
|
1867
|
+
<ul class="tiles">
|
|
1868
|
+
${m.payload.tiles.map((t2) => {
|
|
1869
|
+
const icon = icons.get(t2.badges?.[0] ?? "");
|
|
1870
|
+
const path = `modules.${m.kind}.tiles.${t2.recommended_product_id}.why_blurb`;
|
|
1871
|
+
const price = tilePrice(t2);
|
|
1872
|
+
return html6`<li>
|
|
1873
|
+
<button
|
|
1874
|
+
class="tile"
|
|
1875
|
+
aria-disabled=${this.busyAttr()}
|
|
1876
|
+
@click=${() => this.emitTap({ kind: "product", handle: t2.recommended_product_id })}
|
|
1877
|
+
>
|
|
1878
|
+
<span class="pic">
|
|
1879
|
+
${picture(t2.image_url, t2.name)}
|
|
1880
|
+
${price ? html6`<span class="tile-price">${price}</span>` : nothing5}
|
|
1881
|
+
</span>
|
|
1882
|
+
<span class="name">${t2.name}</span>
|
|
1883
|
+
<span class="why">
|
|
1884
|
+
${icon ? html6`<img src=${icon.image} alt="" />` : nothing5}
|
|
1885
|
+
<span data-slot=${path}>${this.slotText(path, t2.why_blurb)}</span>
|
|
1886
|
+
</span>
|
|
1887
|
+
</button>
|
|
1888
|
+
</li>`;
|
|
1889
|
+
})}
|
|
1890
|
+
</ul>
|
|
1891
|
+
</section>`;
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
_GuideProductDetail.properties = {
|
|
1895
|
+
_variants: { state: true },
|
|
1896
|
+
_choice: { state: true }
|
|
1897
|
+
};
|
|
1898
|
+
_GuideProductDetail.styles = [
|
|
1899
|
+
templateStyles,
|
|
1900
|
+
partStyles,
|
|
1901
|
+
productStyles,
|
|
1902
|
+
css7`
|
|
1903
|
+
.detail {
|
|
1904
|
+
display: grid;
|
|
1905
|
+
gap: 20px;
|
|
1906
|
+
}
|
|
1907
|
+
/* The first screen on a phone holds the photo, the reason and the buy row. */
|
|
1908
|
+
.photo {
|
|
1909
|
+
height: 340px;
|
|
1910
|
+
}
|
|
1911
|
+
.body {
|
|
1912
|
+
display: flex;
|
|
1913
|
+
flex-direction: column;
|
|
1914
|
+
gap: 22px;
|
|
1915
|
+
min-width: 0;
|
|
1916
|
+
}
|
|
1917
|
+
h3 {
|
|
1918
|
+
margin: 0 0 10px;
|
|
1919
|
+
font-size: 12.8px;
|
|
1920
|
+
font-weight: 800;
|
|
1921
|
+
letter-spacing: 0.08em;
|
|
1922
|
+
text-transform: uppercase;
|
|
1923
|
+
color: var(--sc-tile-text-color, #4b5563);
|
|
1924
|
+
}
|
|
1925
|
+
.buy {
|
|
1926
|
+
display: flex;
|
|
1927
|
+
flex-direction: column;
|
|
1928
|
+
gap: 14px;
|
|
1929
|
+
}
|
|
1930
|
+
.buy .add,
|
|
1931
|
+
.buy .sold-out {
|
|
1932
|
+
flex: none;
|
|
1933
|
+
}
|
|
1934
|
+
.options {
|
|
1935
|
+
display: flex;
|
|
1936
|
+
flex-wrap: wrap;
|
|
1937
|
+
gap: 8px;
|
|
1938
|
+
margin: 0;
|
|
1939
|
+
padding: 0;
|
|
1940
|
+
border: 0;
|
|
1941
|
+
min-width: 0;
|
|
1942
|
+
}
|
|
1943
|
+
.options legend {
|
|
1944
|
+
margin-bottom: 10px;
|
|
1945
|
+
padding: 0;
|
|
1946
|
+
font-weight: 800;
|
|
1947
|
+
}
|
|
1948
|
+
.pill {
|
|
1949
|
+
position: relative;
|
|
1950
|
+
display: inline-flex;
|
|
1951
|
+
}
|
|
1952
|
+
.pill input {
|
|
1953
|
+
position: absolute;
|
|
1954
|
+
inset: 0;
|
|
1955
|
+
margin: 0;
|
|
1956
|
+
opacity: 0;
|
|
1957
|
+
cursor: pointer;
|
|
1958
|
+
}
|
|
1959
|
+
.pill-face {
|
|
1960
|
+
display: inline-flex;
|
|
1961
|
+
align-items: center;
|
|
1962
|
+
justify-content: center;
|
|
1963
|
+
gap: 8px;
|
|
1964
|
+
min-width: 48px;
|
|
1965
|
+
min-height: 44px;
|
|
1966
|
+
padding: 0 16px;
|
|
1967
|
+
border-radius: 999px;
|
|
1968
|
+
border: 2px solid var(--sc-content-divider-color, rgb(0 0 0 / 0.2));
|
|
1969
|
+
background: var(--sc-content-search-background, transparent);
|
|
1970
|
+
font-weight: 700;
|
|
1971
|
+
}
|
|
1972
|
+
.pill input:checked + .pill-face {
|
|
1973
|
+
border-color: var(--sc-color-primary, #111827);
|
|
1974
|
+
background: var(--sc-color-primary, #111827);
|
|
1975
|
+
color: var(--sc-accent-foreground, #ffffff);
|
|
1976
|
+
}
|
|
1977
|
+
.pill input:focus-visible + .pill-face {
|
|
1978
|
+
outline: 3px solid var(--sc-color-primary, #111827);
|
|
1979
|
+
outline-offset: 3px;
|
|
1980
|
+
}
|
|
1981
|
+
.pill.out .pill-face {
|
|
1982
|
+
opacity: 0.5;
|
|
1983
|
+
text-decoration: line-through;
|
|
1984
|
+
}
|
|
1985
|
+
.pill.out input {
|
|
1986
|
+
cursor: not-allowed;
|
|
1987
|
+
}
|
|
1988
|
+
.swatch {
|
|
1989
|
+
width: 20px;
|
|
1990
|
+
height: 20px;
|
|
1991
|
+
border-radius: 50%;
|
|
1992
|
+
box-shadow:
|
|
1993
|
+
inset 0 0 0 1px rgb(0 0 0 / 0.25),
|
|
1994
|
+
0 0 0 2px #ffffff;
|
|
1995
|
+
}
|
|
1996
|
+
.hint {
|
|
1997
|
+
margin: -4px 0 0;
|
|
1998
|
+
font-size: 14.4px;
|
|
1999
|
+
font-weight: 600;
|
|
2000
|
+
color: var(--sc-tile-text-color, #4b5563);
|
|
2001
|
+
}
|
|
2002
|
+
.meters {
|
|
2003
|
+
display: grid;
|
|
2004
|
+
gap: 8px 24px;
|
|
2005
|
+
}
|
|
2006
|
+
.tiles {
|
|
2007
|
+
display: grid;
|
|
2008
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2009
|
+
gap: 12px;
|
|
2010
|
+
}
|
|
2011
|
+
.tile {
|
|
2012
|
+
position: relative;
|
|
2013
|
+
display: flex;
|
|
2014
|
+
flex-direction: column;
|
|
2015
|
+
width: 100%;
|
|
2016
|
+
height: 100%;
|
|
2017
|
+
padding: 0;
|
|
2018
|
+
border: 0;
|
|
2019
|
+
border-radius: var(--sc-border-radius, 16px);
|
|
2020
|
+
overflow: hidden;
|
|
2021
|
+
text-align: left;
|
|
2022
|
+
background: var(--sc-content-search-background, #f4f4f5);
|
|
2023
|
+
box-shadow: 0 6px 16px rgb(0 0 0 / 0.1);
|
|
2024
|
+
}
|
|
2025
|
+
.tile .pic {
|
|
2026
|
+
position: relative;
|
|
2027
|
+
display: block;
|
|
2028
|
+
aspect-ratio: 1 / 1;
|
|
2029
|
+
background: #ffffff;
|
|
2030
|
+
}
|
|
2031
|
+
.tile .pic img {
|
|
2032
|
+
position: absolute;
|
|
2033
|
+
inset: 0;
|
|
2034
|
+
object-fit: contain;
|
|
2035
|
+
padding: 10%;
|
|
2036
|
+
}
|
|
2037
|
+
.tile .pic .standin {
|
|
2038
|
+
position: absolute;
|
|
2039
|
+
inset: 0;
|
|
2040
|
+
}
|
|
2041
|
+
.tile .tile-price {
|
|
2042
|
+
position: absolute;
|
|
2043
|
+
top: 8px;
|
|
2044
|
+
right: 8px;
|
|
2045
|
+
padding: 3px 9px;
|
|
2046
|
+
border-radius: 999px;
|
|
2047
|
+
background: rgb(12 20 18 / 0.88);
|
|
2048
|
+
color: #ffffff;
|
|
2049
|
+
font-size: 12.8px;
|
|
2050
|
+
font-weight: 800;
|
|
2051
|
+
}
|
|
2052
|
+
.tile .name {
|
|
2053
|
+
display: -webkit-box;
|
|
2054
|
+
-webkit-line-clamp: 2;
|
|
2055
|
+
-webkit-box-orient: vertical;
|
|
2056
|
+
overflow: hidden;
|
|
2057
|
+
/* Margin, not padding: a clamped line would show through padding. */
|
|
2058
|
+
margin: 10px 12px 4px;
|
|
2059
|
+
font-weight: 800;
|
|
2060
|
+
font-size: 15.2px;
|
|
2061
|
+
line-height: 1.2;
|
|
2062
|
+
color: var(--sc-tile-title-color, #111827);
|
|
2063
|
+
}
|
|
2064
|
+
.tile .why {
|
|
2065
|
+
display: flex;
|
|
2066
|
+
align-items: center;
|
|
2067
|
+
gap: 8px;
|
|
2068
|
+
margin-top: auto;
|
|
2069
|
+
padding: 4px 12px 12px;
|
|
2070
|
+
font-size: 13.6px;
|
|
2071
|
+
font-weight: 700;
|
|
2072
|
+
color: var(--sc-color-primary, #111827);
|
|
2073
|
+
}
|
|
2074
|
+
.tile .why img {
|
|
2075
|
+
flex: none;
|
|
2076
|
+
width: 24px;
|
|
2077
|
+
height: 24px;
|
|
2078
|
+
border-radius: 6px;
|
|
2079
|
+
}
|
|
2080
|
+
.back {
|
|
2081
|
+
align-self: flex-start;
|
|
2082
|
+
flex: none;
|
|
2083
|
+
}
|
|
2084
|
+
:host([data-size='medium']) .detail,
|
|
2085
|
+
:host([data-size='expanded']) .detail {
|
|
2086
|
+
grid-template-columns: minmax(0, 1.1fr) minmax(320px, 1fr);
|
|
2087
|
+
align-items: start;
|
|
2088
|
+
gap: 44px;
|
|
2089
|
+
}
|
|
2090
|
+
:host([data-size='medium']) .photo,
|
|
2091
|
+
:host([data-size='expanded']) .photo {
|
|
2092
|
+
position: sticky;
|
|
2093
|
+
top: 0;
|
|
2094
|
+
height: 600px;
|
|
2095
|
+
}
|
|
2096
|
+
:host([data-size='medium']) .tiles,
|
|
2097
|
+
:host([data-size='expanded']) .tiles {
|
|
2098
|
+
gap: 16px;
|
|
2099
|
+
}
|
|
2100
|
+
`
|
|
2101
|
+
];
|
|
2102
|
+
var GuideProductDetail = _GuideProductDetail;
|
|
2103
|
+
if (!customElements.get(GUIDE_PRODUCT_DETAIL)) {
|
|
2104
|
+
customElements.define(GUIDE_PRODUCT_DETAIL, GuideProductDetail);
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// ../../canvases/postcard-guide/dist/templates/what-you-need.js
|
|
2108
|
+
import { css as css8, html as html7, nothing as nothing6 } from "lit";
|
|
2109
|
+
var GUIDE_WHAT_YOU_NEED = "guide-what-you-need";
|
|
2110
|
+
var _GuideWhatYouNeed = class _GuideWhatYouNeed extends GuideTemplate {
|
|
2111
|
+
render() {
|
|
2112
|
+
const view = this.view;
|
|
2113
|
+
if (!view)
|
|
2114
|
+
return nothing6;
|
|
2115
|
+
return html7`<h2 class="headline" tabindex="-1" data-heading>${this.copy.need_headline}</h2>
|
|
2116
|
+
<p class="voice" data-slot="voice">${this.slotText("voice", view.voice)}</p>
|
|
2117
|
+
<ul class="items">
|
|
2118
|
+
${view.items.map((item) => {
|
|
2119
|
+
const path = `items.${item.item_id}.reason`;
|
|
2120
|
+
return html7`<li>
|
|
2121
|
+
<button
|
|
2122
|
+
class="item"
|
|
2123
|
+
aria-disabled=${this.busyAttr()}
|
|
2124
|
+
@click=${() => this.emitTap({ kind: "open", item_id: item.item_id })}
|
|
2125
|
+
>
|
|
2126
|
+
<span class="pic">
|
|
2127
|
+
${picture(item.picked?.image_url ?? item.image, item.label)}
|
|
2128
|
+
<span class="on-photo"
|
|
2129
|
+
><span class="name">${item.label}</span
|
|
2130
|
+
><span class="reason" data-slot=${path}>${this.slotText(path, item.reason)}</span></span
|
|
2131
|
+
>
|
|
2132
|
+
<span class="why">${item.because.map((s4) => html7`<img src=${s4.image} alt=${s4.label} />`)}</span>
|
|
2133
|
+
${item.picked ? html7`<span class="in-cart">In your cart: ${item.picked.name}</span>` : nothing6}
|
|
2134
|
+
</span>
|
|
2135
|
+
</button>
|
|
2136
|
+
</li>`;
|
|
2137
|
+
})}
|
|
2138
|
+
</ul>`;
|
|
2139
|
+
}
|
|
2140
|
+
};
|
|
2141
|
+
_GuideWhatYouNeed.styles = [
|
|
2142
|
+
templateStyles,
|
|
2143
|
+
css8`
|
|
2144
|
+
.items {
|
|
2145
|
+
display: grid;
|
|
2146
|
+
gap: 12px;
|
|
2147
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2148
|
+
}
|
|
2149
|
+
:host([data-size='medium']) .items {
|
|
2150
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
2151
|
+
gap: 16px;
|
|
2152
|
+
}
|
|
2153
|
+
:host([data-size='expanded']) .items {
|
|
2154
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
2155
|
+
gap: 18px;
|
|
2156
|
+
}
|
|
2157
|
+
.item {
|
|
2158
|
+
display: block;
|
|
2159
|
+
width: 100%;
|
|
2160
|
+
padding: 0;
|
|
2161
|
+
border: 0;
|
|
2162
|
+
border-radius: var(--sc-border-radius, 16px);
|
|
2163
|
+
overflow: hidden;
|
|
2164
|
+
background: var(--sc-tile-background, #ffffff);
|
|
2165
|
+
box-shadow: 0 6px 16px rgb(0 0 0 / 0.1);
|
|
2166
|
+
text-align: left;
|
|
2167
|
+
}
|
|
2168
|
+
.pic {
|
|
2169
|
+
position: relative;
|
|
2170
|
+
display: block;
|
|
2171
|
+
aspect-ratio: 4 / 5;
|
|
2172
|
+
/* Store photos are shot on white: their own paper, in both schemes. */
|
|
2173
|
+
background: #ffffff;
|
|
2174
|
+
}
|
|
2175
|
+
.pic img {
|
|
2176
|
+
object-fit: contain;
|
|
2177
|
+
padding: 8% 8% 30%;
|
|
2178
|
+
}
|
|
2179
|
+
/* Store photos sit on white: a fade to black over white reads as a grey
|
|
2180
|
+
smudge, so the name sits on a solid caption plate instead. */
|
|
2181
|
+
.item .on-photo {
|
|
2182
|
+
padding: 10px 12px 12px;
|
|
2183
|
+
background: rgb(12 20 18 / 0.86);
|
|
2184
|
+
}
|
|
2185
|
+
.on-photo .name {
|
|
2186
|
+
display: block;
|
|
2187
|
+
font-size: 16.8px;
|
|
2188
|
+
}
|
|
2189
|
+
.on-photo .reason {
|
|
2190
|
+
display: block;
|
|
2191
|
+
margin-top: 4px;
|
|
2192
|
+
font-size: 13.6px;
|
|
2193
|
+
font-weight: 600;
|
|
2194
|
+
opacity: 0.92;
|
|
2195
|
+
}
|
|
2196
|
+
.why {
|
|
2197
|
+
position: absolute;
|
|
2198
|
+
top: 8px;
|
|
2199
|
+
right: 8px;
|
|
2200
|
+
display: flex;
|
|
2201
|
+
gap: 4px;
|
|
2202
|
+
}
|
|
2203
|
+
.why img {
|
|
2204
|
+
width: 30px;
|
|
2205
|
+
height: 36px;
|
|
2206
|
+
padding: 2px;
|
|
2207
|
+
border: 2px solid var(--sc-color-primary, #111827);
|
|
2208
|
+
border-radius: 4px;
|
|
2209
|
+
background: #ffffff;
|
|
2210
|
+
}
|
|
2211
|
+
.in-cart {
|
|
2212
|
+
position: absolute;
|
|
2213
|
+
top: 8px;
|
|
2214
|
+
left: 8px;
|
|
2215
|
+
max-width: calc(100% - 60px);
|
|
2216
|
+
padding: 4px 8px;
|
|
2217
|
+
border-radius: 999px;
|
|
2218
|
+
background: var(--sc-color-primary, #111827);
|
|
2219
|
+
color: var(--sc-accent-foreground, #ffffff);
|
|
2220
|
+
font-size: 11.52px;
|
|
2221
|
+
font-weight: 800;
|
|
2222
|
+
overflow: hidden;
|
|
2223
|
+
text-overflow: ellipsis;
|
|
2224
|
+
white-space: nowrap;
|
|
2225
|
+
}
|
|
2226
|
+
`
|
|
2227
|
+
];
|
|
2228
|
+
var GuideWhatYouNeed = _GuideWhatYouNeed;
|
|
2229
|
+
if (!customElements.get(GUIDE_WHAT_YOU_NEED)) {
|
|
2230
|
+
customElements.define(GUIDE_WHAT_YOU_NEED, GuideWhatYouNeed);
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// ../../canvases/postcard-guide/dist/templates/index.js
|
|
2234
|
+
var TEMPLATE_ELEMENTS = {
|
|
2235
|
+
pick_from_pictures: GUIDE_PICK_FROM_PICTURES,
|
|
2236
|
+
what_you_need: GUIDE_WHAT_YOU_NEED,
|
|
2237
|
+
compare: GUIDE_COMPARE,
|
|
2238
|
+
hero_pick: GUIDE_HERO_PICK,
|
|
2239
|
+
product_detail: GUIDE_PRODUCT_DETAIL
|
|
2240
|
+
};
|
|
2241
|
+
function templateElement(name) {
|
|
2242
|
+
return Object.hasOwn(TEMPLATE_ELEMENTS, name) ? TEMPLATE_ELEMENTS[name] ?? null : null;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
// ../../canvases/postcard-guide/dist/guide-client.js
|
|
2246
|
+
var GUIDE_PATH = "/api/adaptive/guide";
|
|
2247
|
+
var TAP_PATH = "/api/adaptive/guide/tap";
|
|
2248
|
+
var SAY_PATH = "/api/adaptive/guide/say";
|
|
2249
|
+
var SEEN_PATH = "/api/adaptive/guide/seen";
|
|
2250
|
+
var PAGE_SESSION_HEADER = "X-Syntro-Page-Session";
|
|
2251
|
+
var TURN_ID = /^[0-9a-f]{32}$/;
|
|
2252
|
+
var MOVE_ID = /^[a-z_]+(:[a-z0-9][a-z0-9_-]*)?$/;
|
|
2253
|
+
var MOVE_ID_MAX = 300;
|
|
2254
|
+
function newPageSession() {
|
|
2255
|
+
const bytes = new Uint8Array(16);
|
|
2256
|
+
crypto.getRandomValues(bytes);
|
|
2257
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
2258
|
+
}
|
|
2259
|
+
function turnQuery(params) {
|
|
2260
|
+
const parts = [`host=${params.host}`, `width=${params.width}`];
|
|
2261
|
+
if (params.consent)
|
|
2262
|
+
parts.push("consent=granted");
|
|
2263
|
+
if (params.turn && TURN_ID.test(params.turn))
|
|
2264
|
+
parts.push(`turn=${params.turn}`);
|
|
2265
|
+
if (params.move && params.move.length <= MOVE_ID_MAX && MOVE_ID.test(params.move)) {
|
|
2266
|
+
parts.push(`move=${encodeURIComponent(params.move)}`);
|
|
2267
|
+
}
|
|
2268
|
+
return parts;
|
|
2269
|
+
}
|
|
2270
|
+
var TEMPLATES_HEADER = "X-Syntro-Guide-Templates";
|
|
2271
|
+
function drawableTemplates(allowed) {
|
|
2272
|
+
const drawable = Object.keys(TEMPLATE_ELEMENTS);
|
|
2273
|
+
const names = allowed ? drawable.filter((name) => allowed.includes(name)) : drawable;
|
|
2274
|
+
return names.sort().join(",");
|
|
2275
|
+
}
|
|
2276
|
+
function headers(params) {
|
|
2277
|
+
return {
|
|
2278
|
+
[PAGE_SESSION_HEADER]: params.pageSession,
|
|
2279
|
+
[TEMPLATES_HEADER]: drawableTemplates(params.allowedTemplates)
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
function guideRequest(path, params) {
|
|
2283
|
+
const sep = path.includes("?") ? "&" : "?";
|
|
2284
|
+
return { path: `${path}${sep}${turnQuery(params).join("&")}`, headers: headers(params) };
|
|
2285
|
+
}
|
|
2286
|
+
function isView(body) {
|
|
2287
|
+
const v = body;
|
|
2288
|
+
return !!v && (v.stage === "postcard" || v.stage === "spread") && Array.isArray(v.stamps) && Array.isArray(v.feed);
|
|
2289
|
+
}
|
|
2290
|
+
async function call(fetchFn, path, init, on404) {
|
|
2291
|
+
let res;
|
|
2292
|
+
try {
|
|
2293
|
+
res = await fetchFn(path, init);
|
|
2294
|
+
} catch {
|
|
2295
|
+
return { kind: "unavailable" };
|
|
2296
|
+
}
|
|
2297
|
+
if (res.status === 404)
|
|
2298
|
+
return on404;
|
|
2299
|
+
if (!res.ok)
|
|
2300
|
+
return { kind: "unavailable" };
|
|
2301
|
+
const body = await res.json().catch(() => null);
|
|
2302
|
+
return isView(body) ? { kind: "view", view: body } : { kind: "unavailable" };
|
|
2303
|
+
}
|
|
2304
|
+
function loadGuide(fetchFn, params, hints = []) {
|
|
2305
|
+
const query = [...turnQuery(params), ...hints.map((h) => `hint=${encodeURIComponent(h)}`)];
|
|
2306
|
+
return call(fetchFn, `${GUIDE_PATH}?${query.join("&")}`, { method: "GET", headers: headers(params) }, { kind: "unpublished" });
|
|
2307
|
+
}
|
|
2308
|
+
function sendTap(fetchFn, tap, params) {
|
|
2309
|
+
return call(fetchFn, `${TAP_PATH}?${turnQuery(params).join("&")}`, { method: "POST", body: JSON.stringify(tap), headers: headers(params) }, { kind: "rejected" });
|
|
2310
|
+
}
|
|
2311
|
+
function sendMessage(fetchFn, text, params) {
|
|
2312
|
+
return call(fetchFn, `${SAY_PATH}?${turnQuery(params).join("&")}`, { method: "POST", body: JSON.stringify({ text }), headers: headers(params) }, { kind: "rejected" });
|
|
2313
|
+
}
|
|
2314
|
+
async function sendSeen(fetchFn, report, params, keepalive = false) {
|
|
2315
|
+
try {
|
|
2316
|
+
await fetchFn(`${SEEN_PATH}${params.consent ? "?consent=granted" : ""}`, {
|
|
2317
|
+
method: "POST",
|
|
2318
|
+
body: JSON.stringify(report),
|
|
2319
|
+
headers: headers(params),
|
|
2320
|
+
keepalive
|
|
2321
|
+
});
|
|
2322
|
+
} catch {
|
|
2323
|
+
console.warn("[postcard-guide] a seen report did not reach the server");
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// ../../canvases/postcard-guide/dist/product-history.js
|
|
2328
|
+
var STATE_KEY = "syntroGuideDetail";
|
|
2329
|
+
function isSitePath(href) {
|
|
2330
|
+
if (!/^\/(?![/\\])[^\s\\]*$/.test(href))
|
|
2331
|
+
return false;
|
|
2332
|
+
return ![...href].some((c2) => c2.charCodeAt(0) < 32 || c2.charCodeAt(0) === 127);
|
|
2333
|
+
}
|
|
2334
|
+
function ours(state) {
|
|
2335
|
+
const entry2 = state?.[STATE_KEY];
|
|
2336
|
+
return entry2 && typeof entry2 === "object" ? entry2 : null;
|
|
2337
|
+
}
|
|
2338
|
+
function sectionFor(root, turnId, moveId) {
|
|
2339
|
+
if (!turnId)
|
|
2340
|
+
return null;
|
|
2341
|
+
return [...root.querySelectorAll("section.snap")].find((s4) => s4.dataset.turnId === turnId && (moveId === null || s4.dataset.moveId === moveId)) ?? null;
|
|
2342
|
+
}
|
|
2343
|
+
function within(ancestor, node) {
|
|
2344
|
+
let n2 = node;
|
|
2345
|
+
while (n2) {
|
|
2346
|
+
if (n2 === ancestor)
|
|
2347
|
+
return true;
|
|
2348
|
+
n2 = n2.parentNode ?? (n2 instanceof ShadowRoot ? n2.host : null);
|
|
2349
|
+
}
|
|
2350
|
+
return false;
|
|
2351
|
+
}
|
|
2352
|
+
function show(section) {
|
|
2353
|
+
const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2354
|
+
section?.scrollIntoView({ block: "start", behavior: reduced ? "auto" : "smooth" });
|
|
2355
|
+
}
|
|
2356
|
+
var ProductDetailHistory = class {
|
|
2357
|
+
constructor(host, root) {
|
|
2358
|
+
this.host = host;
|
|
2359
|
+
this.root = root;
|
|
2360
|
+
this._pending = null;
|
|
2361
|
+
this._current = null;
|
|
2362
|
+
this._lastControl = null;
|
|
2363
|
+
this._openers = /* @__PURE__ */ new Map();
|
|
2364
|
+
this._onClick = (event) => {
|
|
2365
|
+
this._lastControl = event.composedPath().find((n2) => n2 instanceof HTMLElement && n2.matches("button, a[href], input")) ?? null;
|
|
2366
|
+
};
|
|
2367
|
+
this._onTap = (event) => {
|
|
2368
|
+
const tap = event.detail;
|
|
2369
|
+
if (tap?.kind !== "product")
|
|
2370
|
+
return;
|
|
2371
|
+
const path = event.composedPath();
|
|
2372
|
+
const section = path.find((n2) => n2 instanceof HTMLElement && n2.matches("section.snap"));
|
|
2373
|
+
const control = this._lastControl;
|
|
2374
|
+
const source = path[0] instanceof HTMLElement ? path[0] : null;
|
|
2375
|
+
this._pending = {
|
|
2376
|
+
turnId: section?.dataset.turnId ?? null,
|
|
2377
|
+
moveId: section?.dataset.moveId ?? null,
|
|
2378
|
+
opener: control && section && within(section, control) ? control : source
|
|
2379
|
+
};
|
|
2380
|
+
};
|
|
2381
|
+
this._onTurn = (event) => {
|
|
2382
|
+
const origin = this._pending;
|
|
2383
|
+
this._pending = null;
|
|
2384
|
+
const turnId = event.detail?.turnId;
|
|
2385
|
+
const root2 = this.root();
|
|
2386
|
+
if (!origin || !turnId || !root2)
|
|
2387
|
+
return;
|
|
2388
|
+
const section = [...root2.querySelectorAll("section.snap")].find((s4) => s4.dataset.turnId === turnId && s4.dataset.template === "product_detail");
|
|
2389
|
+
const detail = section?.firstElementChild;
|
|
2390
|
+
const href = detail?.view?.product?.href;
|
|
2391
|
+
if (!section || !detail || !href)
|
|
2392
|
+
return;
|
|
2393
|
+
if (!isSitePath(href)) {
|
|
2394
|
+
console.error("[postcard-guide] refusing to put a non-site path in the address bar:", href);
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
const entry2 = {
|
|
2398
|
+
turnId,
|
|
2399
|
+
moveId: section.dataset.moveId ?? "",
|
|
2400
|
+
originTurnId: origin.turnId,
|
|
2401
|
+
originMoveId: origin.moveId
|
|
2402
|
+
};
|
|
2403
|
+
history.pushState({ [STATE_KEY]: entry2 }, "", href);
|
|
2404
|
+
this._openers.set(`${entry2.turnId}|${entry2.moveId}`, origin.opener);
|
|
2405
|
+
this._current = entry2;
|
|
2406
|
+
void detail.focusHeading?.();
|
|
2407
|
+
};
|
|
2408
|
+
this._onPop = (event) => {
|
|
2409
|
+
const root2 = this.root();
|
|
2410
|
+
if (!root2)
|
|
2411
|
+
return;
|
|
2412
|
+
const entry2 = ours(event.state);
|
|
2413
|
+
if (entry2) {
|
|
2414
|
+
this._current = entry2;
|
|
2415
|
+
show(sectionFor(root2, entry2.turnId, entry2.moveId));
|
|
2416
|
+
return;
|
|
2417
|
+
}
|
|
2418
|
+
const left = this._current;
|
|
2419
|
+
this._current = null;
|
|
2420
|
+
if (!left)
|
|
2421
|
+
return;
|
|
2422
|
+
show(sectionFor(root2, left.originTurnId, left.originMoveId));
|
|
2423
|
+
const opener = this._openers.get(`${left.turnId}|${left.moveId}`);
|
|
2424
|
+
if (!opener?.isConnected)
|
|
2425
|
+
return;
|
|
2426
|
+
if (opener.focusHeading)
|
|
2427
|
+
void opener.focusHeading();
|
|
2428
|
+
else
|
|
2429
|
+
opener.focus({ preventScroll: true });
|
|
2430
|
+
};
|
|
2431
|
+
this._onBack = () => {
|
|
2432
|
+
if (ours(history.state))
|
|
2433
|
+
history.back();
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
attach() {
|
|
2437
|
+
this.host.addEventListener("click", this._onClick, true);
|
|
2438
|
+
this.host.addEventListener(GUIDE_TAP_EVENT, this._onTap);
|
|
2439
|
+
this.host.addEventListener(GUIDE_TURN_EVENT, this._onTurn);
|
|
2440
|
+
this.host.addEventListener(GUIDE_DETAIL_BACK_EVENT, this._onBack);
|
|
2441
|
+
window.addEventListener("popstate", this._onPop);
|
|
2442
|
+
}
|
|
2443
|
+
detach() {
|
|
2444
|
+
this.host.removeEventListener("click", this._onClick, true);
|
|
2445
|
+
this.host.removeEventListener(GUIDE_TAP_EVENT, this._onTap);
|
|
2446
|
+
this.host.removeEventListener(GUIDE_TURN_EVENT, this._onTurn);
|
|
2447
|
+
this.host.removeEventListener(GUIDE_DETAIL_BACK_EVENT, this._onBack);
|
|
2448
|
+
window.removeEventListener("popstate", this._onPop);
|
|
2449
|
+
this._pending = null;
|
|
2450
|
+
this._lastControl = null;
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
2453
|
+
|
|
2454
|
+
// ../../canvases/postcard-guide/dist/seen.js
|
|
2455
|
+
var SEEN_SHARE = 0.6;
|
|
2456
|
+
var MAX_VISIBLE_MS = 36e5;
|
|
2457
|
+
var THRESHOLDS = Array.from({ length: 21 }, (_, i2) => i2 / 20);
|
|
2458
|
+
var browserObserver = (cb, init) => new IntersectionObserver(cb, init);
|
|
2459
|
+
function visibleShare(entry2) {
|
|
2460
|
+
const shown = entry2.intersectionRect.height;
|
|
2461
|
+
const own = entry2.boundingClientRect.height;
|
|
2462
|
+
const base = Math.min(own, entry2.rootBounds?.height ?? own);
|
|
2463
|
+
return base > 0 ? shown / base : 0;
|
|
2464
|
+
}
|
|
2465
|
+
var SeenTracker = class {
|
|
2466
|
+
constructor(root, sink, now = () => performance.now(), factory = typeof IntersectionObserver === "function" ? browserObserver : null) {
|
|
2467
|
+
this._watch = /* @__PURE__ */ new Map();
|
|
2468
|
+
this._keys = /* @__PURE__ */ new Set();
|
|
2469
|
+
this._onEntries = (flag) => (entries) => {
|
|
2470
|
+
for (const entry2 of entries) {
|
|
2471
|
+
const watch = this._watch.get(entry2.target);
|
|
2472
|
+
if (!watch || watch.done)
|
|
2473
|
+
continue;
|
|
2474
|
+
watch[flag] = entry2.isIntersecting && visibleShare(entry2) >= SEEN_SHARE;
|
|
2475
|
+
const seen = watch.inFeed && watch.onScreen;
|
|
2476
|
+
if (seen && watch.since === null)
|
|
2477
|
+
watch.since = this._now();
|
|
2478
|
+
else if (!seen && watch.since !== null)
|
|
2479
|
+
this._report(entry2.target, watch, false);
|
|
2480
|
+
}
|
|
2481
|
+
};
|
|
2482
|
+
this._sink = sink;
|
|
2483
|
+
this._now = now;
|
|
2484
|
+
this._observer = factory ? factory(this._onEntries("inFeed"), { root, threshold: THRESHOLDS }) : null;
|
|
2485
|
+
this._screen = factory ? factory(this._onEntries("onScreen"), { root: null, threshold: THRESHOLDS }) : null;
|
|
2486
|
+
if (!this._observer) {
|
|
2487
|
+
console.warn("[postcard-guide] no IntersectionObserver here; time on each template is not reported");
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
observe(section) {
|
|
2491
|
+
const turnId = section.dataset.turnId ?? "";
|
|
2492
|
+
const moveId = section.dataset.moveId ?? "";
|
|
2493
|
+
const key = `${turnId}:${moveId}`;
|
|
2494
|
+
if (!turnId || !moveId || this._keys.has(key))
|
|
2495
|
+
return;
|
|
2496
|
+
this._keys.add(key);
|
|
2497
|
+
this._watch.set(section, {
|
|
2498
|
+
turnId,
|
|
2499
|
+
moveId,
|
|
2500
|
+
since: null,
|
|
2501
|
+
done: false,
|
|
2502
|
+
inFeed: false,
|
|
2503
|
+
onScreen: false
|
|
2504
|
+
});
|
|
2505
|
+
this._observer?.observe(section);
|
|
2506
|
+
this._screen?.observe(section);
|
|
2507
|
+
}
|
|
2508
|
+
/** The page is going away: report every section on screen now. */
|
|
2509
|
+
flush() {
|
|
2510
|
+
for (const [el, watch] of this._watch) {
|
|
2511
|
+
if (watch.since !== null && !watch.done)
|
|
2512
|
+
this._report(el, watch, true);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
disconnect() {
|
|
2516
|
+
this._observer?.disconnect();
|
|
2517
|
+
this._screen?.disconnect();
|
|
2518
|
+
this._watch.clear();
|
|
2519
|
+
}
|
|
2520
|
+
_report(el, watch, final) {
|
|
2521
|
+
watch.done = true;
|
|
2522
|
+
this._observer?.unobserve(el);
|
|
2523
|
+
this._screen?.unobserve(el);
|
|
2524
|
+
const ms = Math.round(this._now() - (watch.since ?? this._now()));
|
|
2525
|
+
this._sink({
|
|
2526
|
+
turn_id: watch.turnId,
|
|
2527
|
+
move_id: watch.moveId,
|
|
2528
|
+
visible_ms: Math.min(Math.max(ms, 0), MAX_VISIBLE_MS)
|
|
2529
|
+
}, final);
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2532
|
+
|
|
2533
|
+
// ../../canvases/postcard-guide/dist/styles.js
|
|
2534
|
+
import { css as css9 } from "lit";
|
|
2535
|
+
var pageStyles = css9`
|
|
2536
|
+
:host {
|
|
2537
|
+
display: block;
|
|
2538
|
+
height: 100%;
|
|
2539
|
+
color: var(--sc-content-text-color, #1f2937);
|
|
2540
|
+
font-family: var(--sc-font-family, system-ui, sans-serif);
|
|
2541
|
+
}
|
|
2542
|
+
* {
|
|
2543
|
+
box-sizing: border-box;
|
|
2544
|
+
}
|
|
2545
|
+
button {
|
|
2546
|
+
font: inherit;
|
|
2547
|
+
color: inherit;
|
|
2548
|
+
cursor: pointer;
|
|
2549
|
+
}
|
|
2550
|
+
button[aria-disabled='true'] {
|
|
2551
|
+
cursor: progress;
|
|
2552
|
+
opacity: 0.7;
|
|
2553
|
+
}
|
|
2554
|
+
button:focus-visible,
|
|
2555
|
+
input:focus-visible {
|
|
2556
|
+
outline: 3px solid var(--sc-color-primary, #111827);
|
|
2557
|
+
outline-offset: 2px;
|
|
2558
|
+
}
|
|
2559
|
+
ul {
|
|
2560
|
+
list-style: none;
|
|
2561
|
+
margin: 0;
|
|
2562
|
+
padding: 0;
|
|
2563
|
+
}
|
|
2564
|
+
img {
|
|
2565
|
+
display: block;
|
|
2566
|
+
width: 100%;
|
|
2567
|
+
height: 100%;
|
|
2568
|
+
object-fit: cover;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
.frame {
|
|
2572
|
+
height: 100%;
|
|
2573
|
+
display: grid;
|
|
2574
|
+
grid-template-columns: minmax(0, 1fr);
|
|
2575
|
+
grid-template-rows: auto auto minmax(0, 1fr) auto auto;
|
|
2576
|
+
grid-template-areas: 'brand' 'known' 'feed' 'notice' 'composer';
|
|
2577
|
+
background: var(--sc-tile-background, #ffffff);
|
|
2578
|
+
color: var(--sc-tile-title-color, #111827);
|
|
2579
|
+
}
|
|
2580
|
+
:host([data-size='expanded']) .frame {
|
|
2581
|
+
grid-template-columns: 272px minmax(0, 1fr);
|
|
2582
|
+
grid-template-rows: auto minmax(0, 1fr) auto auto;
|
|
2583
|
+
grid-template-areas: 'brand brand' 'known feed' 'known notice' 'known composer';
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
.brand {
|
|
2587
|
+
grid-area: brand;
|
|
2588
|
+
display: flex;
|
|
2589
|
+
align-items: center;
|
|
2590
|
+
justify-content: space-between;
|
|
2591
|
+
gap: 12px;
|
|
2592
|
+
padding: 14px 18px 8px;
|
|
2593
|
+
}
|
|
2594
|
+
.brand .name {
|
|
2595
|
+
font-weight: 800;
|
|
2596
|
+
font-size: 15.2px;
|
|
2597
|
+
letter-spacing: 0.04em;
|
|
2598
|
+
text-transform: uppercase;
|
|
2599
|
+
}
|
|
2600
|
+
.brand .cart {
|
|
2601
|
+
min-height: 32px;
|
|
2602
|
+
padding: 6px 12px;
|
|
2603
|
+
border: 0;
|
|
2604
|
+
border-radius: 999px;
|
|
2605
|
+
background: var(--sc-tile-title-color, #111827);
|
|
2606
|
+
color: var(--sc-tile-background, #ffffff);
|
|
2607
|
+
font-size: 12.8px;
|
|
2608
|
+
font-weight: 700;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
.known {
|
|
2612
|
+
grid-area: known;
|
|
2613
|
+
display: flex;
|
|
2614
|
+
align-items: center;
|
|
2615
|
+
gap: 10px;
|
|
2616
|
+
padding: 4px 14px 10px;
|
|
2617
|
+
border-bottom: 1px solid var(--sc-content-divider-color, rgb(0 0 0 / 0.08));
|
|
2618
|
+
overflow-x: auto;
|
|
2619
|
+
}
|
|
2620
|
+
.known-label,
|
|
2621
|
+
.words {
|
|
2622
|
+
display: none;
|
|
2623
|
+
}
|
|
2624
|
+
.stamps {
|
|
2625
|
+
display: flex;
|
|
2626
|
+
gap: 8px;
|
|
2627
|
+
}
|
|
2628
|
+
.stamps li {
|
|
2629
|
+
display: flex;
|
|
2630
|
+
align-items: center;
|
|
2631
|
+
gap: 10px;
|
|
2632
|
+
}
|
|
2633
|
+
.stamp {
|
|
2634
|
+
display: block;
|
|
2635
|
+
flex: none;
|
|
2636
|
+
width: 46px;
|
|
2637
|
+
height: 54px;
|
|
2638
|
+
padding: 3px;
|
|
2639
|
+
border: 2px solid var(--sc-color-primary, #111827);
|
|
2640
|
+
border-radius: 6px;
|
|
2641
|
+
/* A stamp's own white mount, in both schemes. */
|
|
2642
|
+
background: #ffffff;
|
|
2643
|
+
box-shadow:
|
|
2644
|
+
inset 0 0 0 3px #ffffff,
|
|
2645
|
+
0 2px 6px rgb(0 0 0 / 0.1);
|
|
2646
|
+
}
|
|
2647
|
+
.stamp.empty {
|
|
2648
|
+
border: 2px dashed var(--sc-content-divider-color, #d6d3cc);
|
|
2649
|
+
background: transparent;
|
|
2650
|
+
box-shadow: none;
|
|
2651
|
+
}
|
|
2652
|
+
.done {
|
|
2653
|
+
flex: none;
|
|
2654
|
+
min-height: 36px;
|
|
2655
|
+
margin-left: auto;
|
|
2656
|
+
padding: 8px 14px;
|
|
2657
|
+
border: 0;
|
|
2658
|
+
border-radius: 999px;
|
|
2659
|
+
background: var(--sc-color-primary, #111827);
|
|
2660
|
+
color: var(--sc-accent-foreground, #ffffff);
|
|
2661
|
+
font-weight: 700;
|
|
2662
|
+
white-space: nowrap;
|
|
2663
|
+
}
|
|
2664
|
+
:host([data-size='expanded']) .known {
|
|
2665
|
+
flex-direction: column;
|
|
2666
|
+
align-items: stretch;
|
|
2667
|
+
gap: 14px;
|
|
2668
|
+
padding: 18px;
|
|
2669
|
+
border-bottom: 0;
|
|
2670
|
+
border-right: 1px solid var(--sc-content-divider-color, rgb(0 0 0 / 0.08));
|
|
2671
|
+
overflow: hidden auto;
|
|
2672
|
+
}
|
|
2673
|
+
:host([data-size='expanded']) .known-label {
|
|
2674
|
+
display: block;
|
|
2675
|
+
margin: 0;
|
|
2676
|
+
font-size: 12px;
|
|
2677
|
+
font-weight: 700;
|
|
2678
|
+
letter-spacing: 0.08em;
|
|
2679
|
+
text-transform: uppercase;
|
|
2680
|
+
color: var(--sc-tile-text-color, #4b5563);
|
|
2681
|
+
}
|
|
2682
|
+
:host([data-size='expanded']) .stamps {
|
|
2683
|
+
flex-direction: column;
|
|
2684
|
+
gap: 12px;
|
|
2685
|
+
}
|
|
2686
|
+
:host([data-size='expanded']) .words {
|
|
2687
|
+
display: block;
|
|
2688
|
+
font-size: 15.2px;
|
|
2689
|
+
font-weight: 700;
|
|
2690
|
+
}
|
|
2691
|
+
:host([data-size='expanded']) .words.unknown {
|
|
2692
|
+
font-weight: 600;
|
|
2693
|
+
color: var(--sc-tile-text-color, #4b5563);
|
|
2694
|
+
}
|
|
2695
|
+
:host([data-size='expanded']) .done {
|
|
2696
|
+
align-self: flex-start;
|
|
2697
|
+
margin: 4px 0 0;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
.feed {
|
|
2701
|
+
grid-area: feed;
|
|
2702
|
+
position: relative;
|
|
2703
|
+
overflow-y: auto;
|
|
2704
|
+
scroll-behavior: smooth;
|
|
2705
|
+
}
|
|
2706
|
+
/* On the store the guide is a section of the page: at the feed's ends, scroll chains on
|
|
2707
|
+
to the page. No snapping there: Chrome does not chain past the end of a snapping feed
|
|
2708
|
+
whose last template is taller than it. A chat-app card is the whole surface, so it
|
|
2709
|
+
keeps the scroll to itself and snaps. A tap still lands its turn at the top (page.ts). */
|
|
2710
|
+
:host([data-card]) .feed {
|
|
2711
|
+
overscroll-behavior: contain;
|
|
2712
|
+
scroll-snap-type: y mandatory;
|
|
2713
|
+
}
|
|
2714
|
+
.snap {
|
|
2715
|
+
display: flex;
|
|
2716
|
+
flex-direction: column;
|
|
2717
|
+
min-height: 100%;
|
|
2718
|
+
scroll-snap-align: start;
|
|
2719
|
+
}
|
|
2720
|
+
.snap > * {
|
|
2721
|
+
flex: 1 0 auto;
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
.notice {
|
|
2725
|
+
grid-area: notice;
|
|
2726
|
+
margin: 0;
|
|
2727
|
+
padding: 0 16px;
|
|
2728
|
+
font-weight: 700;
|
|
2729
|
+
}
|
|
2730
|
+
/* Always mounted, so a screen reader announces the message when it arrives. */
|
|
2731
|
+
.notice:not(:empty) {
|
|
2732
|
+
padding: 8px 16px 0;
|
|
2733
|
+
}
|
|
2734
|
+
/* Heard, not shown: the line is already on screen in its template. */
|
|
2735
|
+
.notice.heard {
|
|
2736
|
+
position: absolute;
|
|
2737
|
+
width: 1px;
|
|
2738
|
+
height: 1px;
|
|
2739
|
+
padding: 0;
|
|
2740
|
+
overflow: hidden;
|
|
2741
|
+
clip-path: inset(50%);
|
|
2742
|
+
white-space: nowrap;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
.composer {
|
|
2746
|
+
grid-area: composer;
|
|
2747
|
+
display: flex;
|
|
2748
|
+
gap: 8px;
|
|
2749
|
+
margin: 8px 12px max(12px, env(safe-area-inset-bottom));
|
|
2750
|
+
padding: 6px;
|
|
2751
|
+
border-radius: 999px;
|
|
2752
|
+
background: var(--sc-content-search-background, #f3f4f6);
|
|
2753
|
+
box-shadow: 0 6px 20px rgb(0 0 0 / 0.12);
|
|
2754
|
+
}
|
|
2755
|
+
.composer input {
|
|
2756
|
+
flex: 1;
|
|
2757
|
+
min-width: 0;
|
|
2758
|
+
padding: 10px 14px;
|
|
2759
|
+
border: 0;
|
|
2760
|
+
/* 16 px or more, or iOS zooms the page on focus. */
|
|
2761
|
+
font: inherit;
|
|
2762
|
+
font-size: 16px;
|
|
2763
|
+
color: var(--sc-tile-title-color, #111827);
|
|
2764
|
+
background: transparent;
|
|
2765
|
+
}
|
|
2766
|
+
.composer input::placeholder {
|
|
2767
|
+
color: var(--sc-tile-text-color, #4b5563);
|
|
2768
|
+
}
|
|
2769
|
+
.composer input:focus-visible {
|
|
2770
|
+
border-radius: 999px;
|
|
2771
|
+
}
|
|
2772
|
+
.composer button {
|
|
2773
|
+
min-height: 40px;
|
|
2774
|
+
padding: 8px 18px;
|
|
2775
|
+
border: 0;
|
|
2776
|
+
border-radius: 999px;
|
|
2777
|
+
background: var(--sc-color-primary, #111827);
|
|
2778
|
+
color: var(--sc-accent-foreground, #ffffff);
|
|
2779
|
+
font-weight: 700;
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2783
|
+
.feed {
|
|
2784
|
+
scroll-behavior: auto;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
@media (forced-colors: active) {
|
|
2788
|
+
.stamp,
|
|
2789
|
+
.composer,
|
|
2790
|
+
.brand .cart,
|
|
2791
|
+
.done,
|
|
2792
|
+
.composer button {
|
|
2793
|
+
border: 1px solid CanvasText;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
`;
|
|
2797
|
+
|
|
2798
|
+
// ../../canvases/postcard-guide/dist/words-stream.js
|
|
2799
|
+
var WORDS_PATH = "/api/adaptive/guide/words";
|
|
2800
|
+
var WORDS_BUDGET_MS = 1e4;
|
|
2801
|
+
var STOPPED = /* @__PURE__ */ Symbol("words-stopped");
|
|
2802
|
+
function isRecord(value) {
|
|
2803
|
+
return typeof value === "object" && value !== null;
|
|
2804
|
+
}
|
|
2805
|
+
function readSlot(value) {
|
|
2806
|
+
if (!isRecord(value))
|
|
2807
|
+
return null;
|
|
2808
|
+
const { move_id, slot, form, text } = value;
|
|
2809
|
+
if (typeof move_id !== "string" || typeof slot !== "string")
|
|
2810
|
+
return null;
|
|
2811
|
+
if (typeof text !== "string" || !text)
|
|
2812
|
+
return null;
|
|
2813
|
+
if (form !== "short" && form !== "long")
|
|
2814
|
+
return null;
|
|
2815
|
+
return { move_id, slot, form, text };
|
|
2816
|
+
}
|
|
2817
|
+
function frameData(frame) {
|
|
2818
|
+
return frame.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.startsWith("data: ") ? line.slice(6) : line.slice(5)).join("");
|
|
2819
|
+
}
|
|
2820
|
+
async function streamWords(fetchFn, path, options) {
|
|
2821
|
+
const { signal, onSlot } = options;
|
|
2822
|
+
if (signal.aborted)
|
|
2823
|
+
return "aborted";
|
|
2824
|
+
const abort = new AbortController();
|
|
2825
|
+
const onAbort = () => abort.abort();
|
|
2826
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2827
|
+
const watchdog = setTimeout(() => abort.abort(), options.budgetMs ?? WORDS_BUDGET_MS);
|
|
2828
|
+
const stopped = new Promise((_resolve, reject) => {
|
|
2829
|
+
abort.signal.addEventListener("abort", () => reject(STOPPED), { once: true });
|
|
2830
|
+
});
|
|
2831
|
+
stopped.catch(() => {
|
|
2832
|
+
});
|
|
2833
|
+
let reader = null;
|
|
2834
|
+
try {
|
|
2835
|
+
const res = await Promise.race([
|
|
2836
|
+
fetchFn(path, {
|
|
2837
|
+
method: "GET",
|
|
2838
|
+
headers: { Accept: "text/event-stream", ...options.headers },
|
|
2839
|
+
signal: abort.signal
|
|
2840
|
+
}),
|
|
2841
|
+
stopped
|
|
2842
|
+
]);
|
|
2843
|
+
if (res.status === 204)
|
|
2844
|
+
return "empty";
|
|
2845
|
+
if (!res.ok || !res.body)
|
|
2846
|
+
return "failed";
|
|
2847
|
+
reader = res.body.getReader();
|
|
2848
|
+
const decoder = new TextDecoder();
|
|
2849
|
+
let buffer = "";
|
|
2850
|
+
for (; ; ) {
|
|
2851
|
+
const { done, value } = await Promise.race([reader.read(), stopped]);
|
|
2852
|
+
if (done)
|
|
2853
|
+
return "ended";
|
|
2854
|
+
buffer += decoder.decode(value, { stream: true });
|
|
2855
|
+
for (let sep = buffer.indexOf("\n\n"); sep >= 0; sep = buffer.indexOf("\n\n")) {
|
|
2856
|
+
const data = frameData(buffer.slice(0, sep));
|
|
2857
|
+
buffer = buffer.slice(sep + 2);
|
|
2858
|
+
if (!data)
|
|
2859
|
+
continue;
|
|
2860
|
+
const parsed = JSON.parse(data);
|
|
2861
|
+
if (isRecord(parsed) && parsed.done === true)
|
|
2862
|
+
return "done";
|
|
2863
|
+
const words = readSlot(parsed);
|
|
2864
|
+
if (words)
|
|
2865
|
+
onSlot(words);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
} catch {
|
|
2869
|
+
return signal.aborted ? "aborted" : "failed";
|
|
2870
|
+
} finally {
|
|
2871
|
+
clearTimeout(watchdog);
|
|
2872
|
+
signal.removeEventListener("abort", onAbort);
|
|
2873
|
+
reader?.cancel().catch(() => {
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// ../../canvases/postcard-guide/dist/words.js
|
|
2879
|
+
var TURN_ID2 = /^[0-9a-f]{32}$/;
|
|
2880
|
+
var SHORT = "data-words-short";
|
|
2881
|
+
var LONG = "data-words-long";
|
|
2882
|
+
function* slotNodes(root) {
|
|
2883
|
+
for (const el of root.querySelectorAll("*")) {
|
|
2884
|
+
if (el.hasAttribute("data-slot"))
|
|
2885
|
+
yield el;
|
|
2886
|
+
if (el.shadowRoot)
|
|
2887
|
+
yield* slotNodes(el.shadowRoot);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
function turnSections(root, turnId) {
|
|
2891
|
+
return [...root.querySelectorAll("section.snap")].filter((s4) => s4.dataset.turnId === turnId);
|
|
2892
|
+
}
|
|
2893
|
+
function findSlot(root, turnId, moveId, slot) {
|
|
2894
|
+
for (const section of turnSections(root, turnId)) {
|
|
2895
|
+
if (section.dataset.moveId !== moveId)
|
|
2896
|
+
continue;
|
|
2897
|
+
for (const node of slotNodes(section)) {
|
|
2898
|
+
if (node.getAttribute("data-slot") === slot)
|
|
2899
|
+
return node;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
return null;
|
|
2903
|
+
}
|
|
2904
|
+
function fillSlot(node, text) {
|
|
2905
|
+
const texts = [...node.childNodes].filter((c2) => c2.nodeType === Node.TEXT_NODE);
|
|
2906
|
+
const target = texts.find((t2) => t2.data.trim()) ?? texts[0];
|
|
2907
|
+
if (target)
|
|
2908
|
+
target.data = text;
|
|
2909
|
+
else
|
|
2910
|
+
node.append(text);
|
|
2911
|
+
}
|
|
2912
|
+
function show2(node, width) {
|
|
2913
|
+
const text = width === "desktop" && node.getAttribute(LONG) || node.getAttribute(SHORT);
|
|
2914
|
+
if (text)
|
|
2915
|
+
fillSlot(node, text);
|
|
2916
|
+
}
|
|
2917
|
+
var WordsController = class {
|
|
2918
|
+
constructor(host, options) {
|
|
2919
|
+
this._turn = null;
|
|
2920
|
+
this._abort = null;
|
|
2921
|
+
this._onTurn = (event) => {
|
|
2922
|
+
const turnId = event.detail?.turnId;
|
|
2923
|
+
if (typeof turnId !== "string" || !TURN_ID2.test(turnId) || turnId === this._turn)
|
|
2924
|
+
return;
|
|
2925
|
+
void this._stream(turnId);
|
|
2926
|
+
};
|
|
2927
|
+
this._host = host;
|
|
2928
|
+
this._options = options;
|
|
2929
|
+
host.addController(this);
|
|
2930
|
+
}
|
|
2931
|
+
hostConnected() {
|
|
2932
|
+
this._host.addEventListener("guide-turn", this._onTurn);
|
|
2933
|
+
}
|
|
2934
|
+
hostDisconnected() {
|
|
2935
|
+
this._host.removeEventListener("guide-turn", this._onTurn);
|
|
2936
|
+
this._abort?.abort();
|
|
2937
|
+
this._abort = null;
|
|
2938
|
+
this._turn = null;
|
|
2939
|
+
}
|
|
2940
|
+
/** Show each filled slot's form for the current width. Never a new request. */
|
|
2941
|
+
relayout() {
|
|
2942
|
+
const width = this._options.width();
|
|
2943
|
+
for (const node of slotNodes(this._host.renderRoot)) {
|
|
2944
|
+
if (node.hasAttribute(SHORT))
|
|
2945
|
+
show2(node, width);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
async _stream(turnId) {
|
|
2949
|
+
this._abort?.abort();
|
|
2950
|
+
const abort = new AbortController();
|
|
2951
|
+
this._abort = abort;
|
|
2952
|
+
this._turn = turnId;
|
|
2953
|
+
const fetchFn = this._options.fetchFn();
|
|
2954
|
+
if (!fetchFn)
|
|
2955
|
+
return;
|
|
2956
|
+
const root = this._host.renderRoot;
|
|
2957
|
+
const lead = turnSections(root, turnId)[0]?.dataset.moveId;
|
|
2958
|
+
const leadLines = [];
|
|
2959
|
+
const onSlot = (words) => {
|
|
2960
|
+
if (this._turn !== turnId)
|
|
2961
|
+
return;
|
|
2962
|
+
const node = findSlot(root, turnId, words.move_id, words.slot);
|
|
2963
|
+
if (!node)
|
|
2964
|
+
return;
|
|
2965
|
+
node.setAttribute(words.form === "long" ? LONG : SHORT, words.text);
|
|
2966
|
+
show2(node, this._options.width());
|
|
2967
|
+
if (words.form === "short" && words.move_id === lead)
|
|
2968
|
+
leadLines.push(words.text);
|
|
2969
|
+
};
|
|
2970
|
+
const { path, headers: headers2 } = this._options.request(turnId);
|
|
2971
|
+
const outcome = await streamWords(fetchFn, path, { signal: abort.signal, headers: headers2, onSlot });
|
|
2972
|
+
if (this._abort === abort)
|
|
2973
|
+
this._abort = null;
|
|
2974
|
+
if (this._turn === turnId && outcome === "done" && leadLines[0]) {
|
|
2975
|
+
this._options.announce(leadLines[0]);
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
};
|
|
2979
|
+
|
|
2980
|
+
// ../../canvases/postcard-guide/dist/page.js
|
|
2981
|
+
var PAGE_SESSION = newPageSession();
|
|
2982
|
+
var COPY = {
|
|
2983
|
+
saveFailed: "We couldn't save that. Try again.",
|
|
2984
|
+
noCart: "Adding to the cart isn't available here.",
|
|
2985
|
+
soldOut: "That one is sold out.",
|
|
2986
|
+
network: "We couldn't reach the cart. Try again.",
|
|
2987
|
+
addFailed: "We couldn't add that to your cart."
|
|
2988
|
+
};
|
|
2989
|
+
var _PostcardGuidePage = class _PostcardGuidePage extends LitElement2 {
|
|
2990
|
+
constructor() {
|
|
2991
|
+
super();
|
|
2992
|
+
this._size = new ContainerSizeController(this);
|
|
2993
|
+
this._seen = null;
|
|
2994
|
+
this._words = new WordsController(this, {
|
|
2995
|
+
fetchFn: () => this.authedFetch,
|
|
2996
|
+
// The turn travels as the decorated `turn` param, like every guide call.
|
|
2997
|
+
request: (turnId) => guideRequest(WORDS_PATH, this._params(turnId)),
|
|
2998
|
+
width: () => this._width,
|
|
2999
|
+
announce: (text) => this._announce(text, true)
|
|
3000
|
+
});
|
|
3001
|
+
this._shownWidth = null;
|
|
3002
|
+
this._history = null;
|
|
3003
|
+
this._noElement = /* @__PURE__ */ new Set();
|
|
3004
|
+
this._onVisibility = () => {
|
|
3005
|
+
if (document.visibilityState === "hidden")
|
|
3006
|
+
this._seen?.flush();
|
|
3007
|
+
};
|
|
3008
|
+
this._onPageHide = () => this._seen?.flush();
|
|
3009
|
+
this._onTap = (event) => {
|
|
3010
|
+
void this._tap(event.detail, this._originOf(event));
|
|
3011
|
+
};
|
|
3012
|
+
this._onPick = (event) => {
|
|
3013
|
+
const { itemId, handle, variantId } = event.detail;
|
|
3014
|
+
void this._pick(itemId, handle, variantId ?? null, this._originOf(event));
|
|
3015
|
+
};
|
|
3016
|
+
this._onSubmit = (event) => {
|
|
3017
|
+
event.preventDefault();
|
|
3018
|
+
const input = this.renderRoot.querySelector("form.composer input");
|
|
3019
|
+
const text = input?.value.trim() ?? "";
|
|
3020
|
+
if (!input || !text || this._busy)
|
|
3021
|
+
return;
|
|
3022
|
+
void this._say(text, input);
|
|
3023
|
+
};
|
|
3024
|
+
this.authedFetch = null;
|
|
3025
|
+
this.getCart = getCart;
|
|
3026
|
+
this.hints = [];
|
|
3027
|
+
this.consent = () => false;
|
|
3028
|
+
this.guideHost = "store";
|
|
3029
|
+
this.brand = "";
|
|
3030
|
+
this.allowedTemplates = null;
|
|
3031
|
+
this._view = null;
|
|
3032
|
+
this._turns = [];
|
|
3033
|
+
this._status = "loading";
|
|
3034
|
+
this._busy = false;
|
|
3035
|
+
this._notice = null;
|
|
3036
|
+
this._heard = false;
|
|
3037
|
+
}
|
|
3038
|
+
connectedCallback() {
|
|
3039
|
+
super.connectedCallback();
|
|
3040
|
+
if (this.guideHost === "store") {
|
|
3041
|
+
this._history = new ProductDetailHistory(this, () => this.renderRoot);
|
|
3042
|
+
this._history.attach();
|
|
3043
|
+
}
|
|
3044
|
+
document.addEventListener("visibilitychange", this._onVisibility);
|
|
3045
|
+
window.addEventListener("pagehide", this._onPageHide);
|
|
3046
|
+
if (this._view)
|
|
3047
|
+
return;
|
|
3048
|
+
void this._load();
|
|
3049
|
+
}
|
|
3050
|
+
disconnectedCallback() {
|
|
3051
|
+
this._history?.detach();
|
|
3052
|
+
this._history = null;
|
|
3053
|
+
document.removeEventListener("visibilitychange", this._onVisibility);
|
|
3054
|
+
window.removeEventListener("pagehide", this._onPageHide);
|
|
3055
|
+
this._seen?.flush();
|
|
3056
|
+
this._seen?.disconnect();
|
|
3057
|
+
this._seen = null;
|
|
3058
|
+
super.disconnectedCallback();
|
|
3059
|
+
}
|
|
3060
|
+
willUpdate() {
|
|
3061
|
+
this.toggleAttribute("data-card", this.guideHost === "card");
|
|
3062
|
+
}
|
|
3063
|
+
updated() {
|
|
3064
|
+
if (this._shownWidth !== null && this._shownWidth !== this._width)
|
|
3065
|
+
this._words.relayout();
|
|
3066
|
+
this._shownWidth = this._width;
|
|
3067
|
+
}
|
|
3068
|
+
get _width() {
|
|
3069
|
+
return this._size.value === "expanded" ? "desktop" : "phone";
|
|
3070
|
+
}
|
|
3071
|
+
_params(turn, move) {
|
|
3072
|
+
return {
|
|
3073
|
+
host: this.guideHost,
|
|
3074
|
+
width: this._width,
|
|
3075
|
+
consent: this.consent(),
|
|
3076
|
+
turn: turn ?? null,
|
|
3077
|
+
move: move ?? null,
|
|
3078
|
+
pageSession: PAGE_SESSION,
|
|
3079
|
+
allowedTemplates: this.allowedTemplates
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
_latestTurn() {
|
|
3083
|
+
return this._turns.at(-1)?.turn_id ?? null;
|
|
3084
|
+
}
|
|
3085
|
+
/**
|
|
3086
|
+
* The one live region (WCAG: one per surface). A notice is shown; a line that is
|
|
3087
|
+
* already on screen (`heard`) is only spoken, so it takes no row and the snapped
|
|
3088
|
+
* feed never shifts under the shopper.
|
|
3089
|
+
*/
|
|
3090
|
+
_announce(text, heard = false) {
|
|
3091
|
+
this._notice = text;
|
|
3092
|
+
this._heard = heard;
|
|
3093
|
+
}
|
|
3094
|
+
async _load() {
|
|
3095
|
+
if (!this.authedFetch) {
|
|
3096
|
+
console.error("[postcard-guide] no authed fetch on the page; the guide cannot load");
|
|
3097
|
+
this._giveBack("guide-unavailable");
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
this._status = "loading";
|
|
3101
|
+
this._apply(await loadGuide(this.authedFetch, this._params(), this.hints), "arrival");
|
|
3102
|
+
}
|
|
3103
|
+
/** Render nothing and tell the host, which collapses the section so the theme's page shows. */
|
|
3104
|
+
_giveBack(event) {
|
|
3105
|
+
this._status = "gone";
|
|
3106
|
+
this.dispatchEvent(new CustomEvent(event, { bubbles: true, composed: true }));
|
|
3107
|
+
}
|
|
3108
|
+
_apply(result, cause) {
|
|
3109
|
+
if (result.kind === "view") {
|
|
3110
|
+
const turn = result.view.turn;
|
|
3111
|
+
if (!turn) {
|
|
3112
|
+
console.error("[postcard-guide] a guide answer came without a turn");
|
|
3113
|
+
if (this._view && cause !== "arrival")
|
|
3114
|
+
this._announce(COPY.saveFailed);
|
|
3115
|
+
else
|
|
3116
|
+
this._giveBack("guide-unavailable");
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
if (this.guideHost === "card")
|
|
3120
|
+
this._seen?.flush();
|
|
3121
|
+
const first = !this._view;
|
|
3122
|
+
this._view = result.view;
|
|
3123
|
+
this._turns = this.guideHost === "card" ? [turn] : [...this._turns, turn];
|
|
3124
|
+
this._status = "ready";
|
|
3125
|
+
if (first) {
|
|
3126
|
+
this.dispatchEvent(new CustomEvent("guide-ready", { bubbles: true, composed: true }));
|
|
3127
|
+
}
|
|
3128
|
+
void this._landed(turn, cause);
|
|
3129
|
+
} else if (result.kind === "unpublished") {
|
|
3130
|
+
this._giveBack("guide-unpublished");
|
|
3131
|
+
} else if (result.kind === "unavailable") {
|
|
3132
|
+
if (this._view && cause !== "arrival")
|
|
3133
|
+
this._announce(COPY.saveFailed);
|
|
3134
|
+
else
|
|
3135
|
+
this._giveBack("guide-unavailable");
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
/** After a turn renders: watch its sections, bring it into view on a tap, tell listeners. */
|
|
3139
|
+
async _landed(turn, cause) {
|
|
3140
|
+
await this.updateComplete;
|
|
3141
|
+
const feed = this.renderRoot.querySelector(".feed");
|
|
3142
|
+
if (!feed)
|
|
3143
|
+
return;
|
|
3144
|
+
this._seen ?? (this._seen = new SeenTracker(feed, (report, final) => {
|
|
3145
|
+
if (this.authedFetch)
|
|
3146
|
+
void sendSeen(this.authedFetch, report, this._params(), final);
|
|
3147
|
+
}));
|
|
3148
|
+
const all = [...feed.querySelectorAll("section.snap")];
|
|
3149
|
+
for (const section of all)
|
|
3150
|
+
this._seen.observe(section);
|
|
3151
|
+
const first = all.find((s4) => s4.dataset.turnId === turn.turn_id);
|
|
3152
|
+
if (first && cause !== "arrival" && this.guideHost === "store")
|
|
3153
|
+
feed.scrollTop = first.offsetTop;
|
|
3154
|
+
if (first && cause === "tap") {
|
|
3155
|
+
await first.firstElementChild?.focusHeading?.();
|
|
3156
|
+
}
|
|
3157
|
+
this.dispatchEvent(new CustomEvent(GUIDE_TURN_EVENT, {
|
|
3158
|
+
detail: { turnId: turn.turn_id },
|
|
3159
|
+
bubbles: true,
|
|
3160
|
+
composed: true
|
|
3161
|
+
}));
|
|
3162
|
+
}
|
|
3163
|
+
/** One tap. Returns whether the server answered with a view. */
|
|
3164
|
+
async _tap(tap, from) {
|
|
3165
|
+
if (this._busy || !this.authedFetch)
|
|
3166
|
+
return false;
|
|
3167
|
+
this._busy = true;
|
|
3168
|
+
this._notice = null;
|
|
3169
|
+
try {
|
|
3170
|
+
const sent = await sendTap(this.authedFetch, tap, this._params(from.turn, from.move));
|
|
3171
|
+
const result = sent.kind === "rejected" ? await loadGuide(this.authedFetch, this._params()) : sent;
|
|
3172
|
+
this._apply(result, "tap");
|
|
3173
|
+
return sent.kind === "view";
|
|
3174
|
+
} finally {
|
|
3175
|
+
this._busy = false;
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
/** The section an event came from: a tap answers the turn it was shown in, and names its move. */
|
|
3179
|
+
_originOf(event) {
|
|
3180
|
+
const section = event.target?.closest?.("section.snap");
|
|
3181
|
+
return {
|
|
3182
|
+
turn: section?.dataset.turnId ?? this._latestTurn(),
|
|
3183
|
+
move: section?.dataset.moveId ?? null
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
/** The frame's own controls (stamps, done) answer the latest turn and come from no move. */
|
|
3187
|
+
_frameOrigin() {
|
|
3188
|
+
return { turn: this._latestTurn(), move: null };
|
|
3189
|
+
}
|
|
3190
|
+
async _pick(itemId, handle, variantId, from) {
|
|
3191
|
+
if (this._busy)
|
|
3192
|
+
return;
|
|
3193
|
+
const cart = this.getCart();
|
|
3194
|
+
if (!cart) {
|
|
3195
|
+
this._announce(COPY.noCart);
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
this._busy = true;
|
|
3199
|
+
this._notice = null;
|
|
3200
|
+
let added;
|
|
3201
|
+
try {
|
|
3202
|
+
added = await cart.addToCart({ product_id: handle, variant_id: variantId, quantity: 1 });
|
|
3203
|
+
} catch {
|
|
3204
|
+
added = { ok: false, error: { code: "network", message: "" } };
|
|
3205
|
+
} finally {
|
|
3206
|
+
this._busy = false;
|
|
3207
|
+
}
|
|
3208
|
+
if (!added.ok) {
|
|
3209
|
+
const code = added.error?.code;
|
|
3210
|
+
this._announce(code === "oos" ? COPY.soldOut : code === "network" ? COPY.network : COPY.addFailed);
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
const saved = await this._tap({ kind: "pick", item_id: itemId, handle }, from);
|
|
3214
|
+
if (!saved)
|
|
3215
|
+
this._announce(copyOf(this._view).saved_failed);
|
|
3216
|
+
}
|
|
3217
|
+
async _say(text, input) {
|
|
3218
|
+
if (this._busy || !this.authedFetch)
|
|
3219
|
+
return;
|
|
3220
|
+
this._busy = true;
|
|
3221
|
+
this._notice = null;
|
|
3222
|
+
try {
|
|
3223
|
+
let result = await sendMessage(this.authedFetch, text, this._params(this._latestTurn()));
|
|
3224
|
+
if (result.kind === "rejected")
|
|
3225
|
+
result = await loadGuide(this.authedFetch, this._params());
|
|
3226
|
+
if (result.kind === "view" && result.view.heard === false) {
|
|
3227
|
+
this._announce(result.view.voice ?? COPY.saveFailed);
|
|
3228
|
+
return;
|
|
3229
|
+
}
|
|
3230
|
+
this._apply(result, "say");
|
|
3231
|
+
if (result.kind === "view")
|
|
3232
|
+
input.value = "";
|
|
3233
|
+
} finally {
|
|
3234
|
+
this._busy = false;
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
_busyAttr() {
|
|
3238
|
+
return this._busy ? "true" : "false";
|
|
3239
|
+
}
|
|
3240
|
+
_sections() {
|
|
3241
|
+
const turns = this.guideHost === "card" ? this._turns.slice(-1) : this._turns;
|
|
3242
|
+
const out = [];
|
|
3243
|
+
for (const turn of turns) {
|
|
3244
|
+
const drawable = [];
|
|
3245
|
+
for (const shown of [...turn.shown].sort((a, b) => a.position - b.position)) {
|
|
3246
|
+
const tag = templateElement(shown.template);
|
|
3247
|
+
if (tag)
|
|
3248
|
+
drawable.push({ turnId: turn.turn_id, shown, tag });
|
|
3249
|
+
else if (!this._noElement.has(shown.template)) {
|
|
3250
|
+
this._noElement.add(shown.template);
|
|
3251
|
+
console.warn(`[postcard-guide] no template element for ${shown.template}; it is not drawn`);
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
out.push(...this.guideHost === "card" ? drawable.slice(0, 1) : drawable);
|
|
3255
|
+
}
|
|
3256
|
+
return out;
|
|
3257
|
+
}
|
|
3258
|
+
render() {
|
|
3259
|
+
if (this._status === "gone")
|
|
3260
|
+
return nothing7;
|
|
3261
|
+
const view = this._view;
|
|
3262
|
+
if (this._status === "loading" || !view) {
|
|
3263
|
+
return html8`<div class="frame" aria-busy="true">${this._brandBar(null)}</div>`;
|
|
3264
|
+
}
|
|
3265
|
+
const copy = copyOf(view);
|
|
3266
|
+
return html8`<div class="frame" role="region" aria-label=${copy.guide_name}>
|
|
3267
|
+
${this._brandBar(view)} ${this._known(view)}
|
|
3268
|
+
<div class="feed" @guide-tap=${this._onTap} @guide-pick=${this._onPick}>
|
|
3269
|
+
${repeat(this._sections(), (s4) => `${s4.turnId}:${s4.shown.move_id}`, (s4) => this._section(s4))}
|
|
3270
|
+
</div>
|
|
3271
|
+
<p class=${this._heard ? "notice heard" : "notice"} role="status">${this._notice ?? nothing7}</p>
|
|
3272
|
+
<form class="composer" @submit=${this._onSubmit}>
|
|
3273
|
+
<input
|
|
3274
|
+
type="text"
|
|
3275
|
+
maxlength="300"
|
|
3276
|
+
autocomplete="off"
|
|
3277
|
+
enterkeyhint="send"
|
|
3278
|
+
aria-label=${copy.ask_placeholder}
|
|
3279
|
+
placeholder=${copy.ask_placeholder}
|
|
3280
|
+
aria-disabled=${this._busyAttr()}
|
|
3281
|
+
/>
|
|
3282
|
+
<button type="submit" aria-disabled=${this._busyAttr()}>Send</button>
|
|
3283
|
+
</form>
|
|
3284
|
+
</div>`;
|
|
3285
|
+
}
|
|
3286
|
+
_brandBar(view) {
|
|
3287
|
+
const cart = this.getCart();
|
|
3288
|
+
const count = view?.pick_count ?? 0;
|
|
3289
|
+
return html8`<div class="brand">
|
|
3290
|
+
<span class="name">${this.brand}</span>
|
|
3291
|
+
${cart ? html8`<button
|
|
3292
|
+
class="cart"
|
|
3293
|
+
aria-label=${count ? `Cart, ${count} added here` : "Cart"}
|
|
3294
|
+
@click=${() => void cart.openCart()}
|
|
3295
|
+
>
|
|
3296
|
+
Cart${count ? html8`<span aria-hidden="true"> · ${count}</span>` : nothing7}
|
|
3297
|
+
</button>` : nothing7}
|
|
3298
|
+
</div>`;
|
|
3299
|
+
}
|
|
3300
|
+
_known(view) {
|
|
3301
|
+
const latest = this._frameOrigin();
|
|
3302
|
+
return html8`<div class="known">
|
|
3303
|
+
<p class="known-label">${copyOf(view).known_label}</p>
|
|
3304
|
+
<ul class="stamps">
|
|
3305
|
+
${view.stamps.map((s4) => html8`<li>
|
|
3306
|
+
<button
|
|
3307
|
+
class="stamp"
|
|
3308
|
+
aria-disabled=${this._busyAttr()}
|
|
3309
|
+
aria-label=${`Change ${s4.label}`}
|
|
3310
|
+
@click=${() => void this._tap({ kind: "stamp", fact_id: s4.fact_id }, latest)}
|
|
3311
|
+
>
|
|
3312
|
+
<img src=${s4.image} alt=${s4.label} />
|
|
3313
|
+
</button>
|
|
3314
|
+
<span class="words" aria-hidden="true">${s4.label}</span>
|
|
3315
|
+
</li>`)}
|
|
3316
|
+
${view.missing.map((m) => html8`<li>
|
|
3317
|
+
<span class="stamp empty" role="img" aria-label=${`${m.label}, not chosen yet`}></span>
|
|
3318
|
+
<span class="words unknown" aria-hidden="true">${m.label}</span>
|
|
3319
|
+
</li>`)}
|
|
3320
|
+
</ul>
|
|
3321
|
+
${view.stamps.length && view.stage === "postcard" ? html8`<button
|
|
3322
|
+
class="done"
|
|
3323
|
+
aria-disabled=${this._busyAttr()}
|
|
3324
|
+
@click=${() => void this._tap({ kind: "done" }, latest)}
|
|
3325
|
+
>
|
|
3326
|
+
${copyOf(view).done_label}
|
|
3327
|
+
</button>` : nothing7}
|
|
3328
|
+
</div>`;
|
|
3329
|
+
}
|
|
3330
|
+
_section(s4) {
|
|
3331
|
+
const tag = unsafeStatic(s4.tag);
|
|
3332
|
+
return html8`<section
|
|
3333
|
+
class="snap"
|
|
3334
|
+
data-turn-id=${s4.turnId}
|
|
3335
|
+
data-move-id=${s4.shown.move_id}
|
|
3336
|
+
data-template=${s4.shown.template}
|
|
3337
|
+
>
|
|
3338
|
+
${staticHtml`<${tag}
|
|
3339
|
+
.view=${s4.shown.view}
|
|
3340
|
+
.turnId=${s4.turnId}
|
|
3341
|
+
.moveId=${s4.shown.move_id}
|
|
3342
|
+
.cart=${this.getCart}
|
|
3343
|
+
.busy=${this._busy}
|
|
3344
|
+
.copy=${copyOf(this._view)}
|
|
3345
|
+
></${tag}>`}
|
|
3346
|
+
</section>`;
|
|
3347
|
+
}
|
|
3348
|
+
};
|
|
3349
|
+
_PostcardGuidePage.styles = pageStyles;
|
|
3350
|
+
_PostcardGuidePage.properties = {
|
|
3351
|
+
authedFetch: { attribute: false },
|
|
3352
|
+
getCart: { attribute: false },
|
|
3353
|
+
hints: { attribute: false },
|
|
3354
|
+
consent: { attribute: false },
|
|
3355
|
+
guideHost: { attribute: false },
|
|
3356
|
+
brand: { attribute: false },
|
|
3357
|
+
allowedTemplates: { attribute: false },
|
|
3358
|
+
_view: { state: true },
|
|
3359
|
+
_turns: { state: true },
|
|
3360
|
+
_status: { state: true },
|
|
3361
|
+
_busy: { state: true },
|
|
3362
|
+
_notice: { state: true },
|
|
3363
|
+
_heard: { state: true }
|
|
3364
|
+
};
|
|
3365
|
+
var PostcardGuidePage = _PostcardGuidePage;
|
|
3366
|
+
if (!customElements.get("postcard-guide-page")) {
|
|
3367
|
+
customElements.define("postcard-guide-page", PostcardGuidePage);
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
// src/guide-widget.ts
|
|
3371
|
+
import { css as css10, html as html9, LitElement as LitElement3, nothing as nothing8 } from "lit";
|
|
3372
|
+
var GUIDE_PAGE_TAG = "syntro-guide-page";
|
|
3373
|
+
var GuidePageWidget = class extends LitElement3 {
|
|
3374
|
+
constructor() {
|
|
3375
|
+
super();
|
|
3376
|
+
this._transport = null;
|
|
3377
|
+
this._hints = [];
|
|
3378
|
+
this._brand = "";
|
|
3379
|
+
/** Nothing to draw for the life of this tile: unpublished, down, or no way to load it. */
|
|
3380
|
+
this._gone = false;
|
|
3381
|
+
/** The first view is painted: the section takes its height now, never before. */
|
|
3382
|
+
this._show = () => {
|
|
3383
|
+
this.removeAttribute("data-pending");
|
|
3384
|
+
};
|
|
3385
|
+
/** Draw nothing and take no room, so the store's page reads as if the tile were not there. */
|
|
3386
|
+
this._collapse = () => {
|
|
3387
|
+
this._gone = true;
|
|
3388
|
+
this.hidden = true;
|
|
3389
|
+
this.removeAttribute("data-pending");
|
|
3390
|
+
this.requestUpdate();
|
|
3391
|
+
};
|
|
3392
|
+
this.templates = null;
|
|
3393
|
+
}
|
|
3394
|
+
connectedCallback() {
|
|
3395
|
+
super.connectedCallback();
|
|
3396
|
+
if (this._transport || this._gone) return;
|
|
3397
|
+
const transport = window.SynOS?.authedFetch ?? null;
|
|
3398
|
+
if (!transport) {
|
|
3399
|
+
console.error("[adaptive-guide] no authed fetch on the page; the guide cannot load");
|
|
3400
|
+
this._collapse();
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
if (this.templates && !this.templates.some((name) => Object.keys(TEMPLATE_ELEMENTS).includes(name))) {
|
|
3404
|
+
console.error(
|
|
3405
|
+
`[adaptive-guide] none of the templates ${JSON.stringify(this.templates)} can be drawn; the guide is not shown`
|
|
3406
|
+
);
|
|
3407
|
+
this._collapse();
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
this._transport = transport;
|
|
3411
|
+
this.setAttribute("data-pending", "");
|
|
3412
|
+
this._hints = landingHints();
|
|
3413
|
+
this._brand = storeName();
|
|
3414
|
+
if (!this._brand) {
|
|
3415
|
+
console.warn(
|
|
3416
|
+
"[adaptive-guide] the store page has no og:site_name; the brand bar shows no name"
|
|
3417
|
+
);
|
|
3418
|
+
}
|
|
3419
|
+
this.requestUpdate();
|
|
3420
|
+
}
|
|
3421
|
+
render() {
|
|
3422
|
+
if (this._gone || !this._transport) return nothing8;
|
|
3423
|
+
return html9`<postcard-guide-page
|
|
3424
|
+
.authedFetch=${this._transport}
|
|
3425
|
+
.getCart=${getCart}
|
|
3426
|
+
.hints=${this._hints}
|
|
3427
|
+
.consent=${consentGranted}
|
|
3428
|
+
.brand=${this._brand}
|
|
3429
|
+
.guideHost=${"store"}
|
|
3430
|
+
.allowedTemplates=${this.templates}
|
|
3431
|
+
@guide-ready=${this._show}
|
|
3432
|
+
@guide-unpublished=${this._collapse}
|
|
3433
|
+
@guide-unavailable=${this._collapse}
|
|
3434
|
+
></postcard-guide-page>`;
|
|
3435
|
+
}
|
|
3436
|
+
};
|
|
3437
|
+
GuidePageWidget.styles = css10`
|
|
3438
|
+
:host {
|
|
3439
|
+
display: block;
|
|
3440
|
+
width: 100%;
|
|
3441
|
+
}
|
|
3442
|
+
:host([hidden]) {
|
|
3443
|
+
display: none;
|
|
3444
|
+
}
|
|
3445
|
+
/* Loading the first view: laid out, so the page measures its width, but no height and
|
|
3446
|
+
unseen, so the store page does not move until there is something to show. */
|
|
3447
|
+
:host([data-pending]) postcard-guide-page {
|
|
3448
|
+
height: 0;
|
|
3449
|
+
overflow: hidden;
|
|
3450
|
+
visibility: hidden;
|
|
3451
|
+
}
|
|
3452
|
+
postcard-guide-page {
|
|
3453
|
+
display: block;
|
|
3454
|
+
/* At most a viewport tall, and under the tile card's cap (less the card's top
|
|
3455
|
+
gap) so the card body never scrolls: the guide's own feed does. */
|
|
3456
|
+
height: min(
|
|
3457
|
+
100svh,
|
|
3458
|
+
calc(var(--sc-card-max-height, 100svh) - var(--sc-tile-gap, 0.25rem))
|
|
3459
|
+
);
|
|
3460
|
+
}
|
|
3461
|
+
`;
|
|
3462
|
+
GuidePageWidget.properties = {
|
|
3463
|
+
templates: { attribute: false }
|
|
3464
|
+
};
|
|
3465
|
+
function registerGuidePageElement() {
|
|
3466
|
+
if (typeof customElements !== "undefined" && !customElements.get(GUIDE_PAGE_TAG)) {
|
|
3467
|
+
customElements.define(GUIDE_PAGE_TAG, GuidePageWidget);
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
registerGuidePageElement();
|
|
3471
|
+
|
|
3472
|
+
// src/runtime.ts
|
|
3473
|
+
var GuidePageMountable = {
|
|
3474
|
+
mount(container, config) {
|
|
3475
|
+
const props = stripMountPlumbing(config ?? null);
|
|
3476
|
+
const element = document.createElement(GUIDE_PAGE_TAG);
|
|
3477
|
+
element.templates = props.templates ?? null;
|
|
3478
|
+
container.style.minHeight = "0px";
|
|
3479
|
+
container.appendChild(element);
|
|
3480
|
+
return () => element.remove();
|
|
3481
|
+
},
|
|
3482
|
+
/** A refetch or republish keeps the same page, so the shopper keeps their place in it. */
|
|
3483
|
+
update(container, config) {
|
|
3484
|
+
const element = container.querySelector(GUIDE_PAGE_TAG);
|
|
3485
|
+
if (!element) return;
|
|
3486
|
+
const templates = stripMountPlumbing(config ?? null).templates ?? null;
|
|
3487
|
+
if (JSON.stringify(templates) !== JSON.stringify(element.templates))
|
|
3488
|
+
element.templates = templates;
|
|
3489
|
+
}
|
|
3490
|
+
};
|
|
3491
|
+
var runtime = {
|
|
3492
|
+
id: "adaptive-guide",
|
|
3493
|
+
version: "1.0.0",
|
|
3494
|
+
name: "Guided page",
|
|
3495
|
+
description: "The store's guided page, drawn as a tile in an inline slot.",
|
|
3496
|
+
/** No DOM-mutation executors: this tile renders only. */
|
|
3497
|
+
executors: [],
|
|
3498
|
+
widgets: [
|
|
3499
|
+
{
|
|
3500
|
+
id: "adaptive-guide:page",
|
|
3501
|
+
component: GuidePageMountable,
|
|
3502
|
+
metadata: {
|
|
3503
|
+
name: "Guided page",
|
|
3504
|
+
description: "The store's published guide, a section of the page after its anchor",
|
|
3505
|
+
icon: "\u{1F9ED}"
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
]
|
|
3509
|
+
};
|
|
3510
|
+
var runtime_default = runtime;
|
|
3511
|
+
|
|
3512
|
+
export {
|
|
3513
|
+
GuidePageMountable,
|
|
3514
|
+
runtime,
|
|
3515
|
+
runtime_default
|
|
3516
|
+
};
|
|
3517
|
+
/*! Bundled license information:
|
|
3518
|
+
|
|
3519
|
+
@lit/context/lib/context-request-event.js:
|
|
3520
|
+
@lit/context/lib/create-context.js:
|
|
3521
|
+
@lit/context/lib/controllers/context-consumer.js:
|
|
3522
|
+
@lit/context/lib/value-notifier.js:
|
|
3523
|
+
@lit/context/lib/controllers/context-provider.js:
|
|
3524
|
+
@lit/context/lib/context-root.js:
|
|
3525
|
+
(**
|
|
3526
|
+
* @license
|
|
3527
|
+
* Copyright 2021 Google LLC
|
|
3528
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
3529
|
+
*)
|
|
3530
|
+
|
|
3531
|
+
@lit/context/lib/decorators/provide.js:
|
|
3532
|
+
(**
|
|
3533
|
+
* @license
|
|
3534
|
+
* Copyright 2017 Google LLC
|
|
3535
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
3536
|
+
*)
|
|
3537
|
+
|
|
3538
|
+
@lit/context/lib/decorators/consume.js:
|
|
3539
|
+
(**
|
|
3540
|
+
* @license
|
|
3541
|
+
* Copyright 2022 Google LLC
|
|
3542
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
3543
|
+
*)
|
|
3544
|
+
*/
|
|
3545
|
+
//# sourceMappingURL=chunk-BVOACKL5.js.map
|