@syntrologie/adapt-product 2.8.0-canary.245
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/bind/controller.d.ts +12 -0
- package/dist/bind/controller.d.ts.map +1 -0
- package/dist/bind/dom-selector.d.ts +11 -0
- package/dist/bind/dom-selector.d.ts.map +1 -0
- package/dist/bind/index.d.ts +6 -0
- package/dist/bind/index.d.ts.map +1 -0
- package/dist/bind/jsonld.d.ts +11 -0
- package/dist/bind/jsonld.d.ts.map +1 -0
- package/dist/bind/opengraph.d.ts +7 -0
- package/dist/bind/opengraph.d.ts.map +1 -0
- package/dist/bind/shopify-ajax.d.ts +11 -0
- package/dist/bind/shopify-ajax.d.ts.map +1 -0
- package/dist/bind/types.d.ts +7 -0
- package/dist/bind/types.d.ts.map +1 -0
- package/dist/bind/woocommerce-store-api.d.ts +7 -0
- package/dist/bind/woocommerce-store-api.d.ts.map +1 -0
- package/dist/cdn.d.ts +30 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.js +34 -0
- package/dist/cdn.js.map +7 -0
- package/dist/chunk-E2IVTQMX.js +1100 -0
- package/dist/chunk-E2IVTQMX.js.map +7 -0
- package/dist/chunk-SM35E4R3.js +126 -0
- package/dist/chunk-SM35E4R3.js.map +7 -0
- package/dist/runtime.d.ts +567 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +18 -0
- package/dist/runtime.js.map +7 -0
- package/dist/sanitizer.d.ts +8 -0
- package/dist/sanitizer.d.ts.map +1 -0
- package/dist/schema-primitives.d.ts +414 -0
- package/dist/schema-primitives.d.ts.map +1 -0
- package/dist/schema.d.ts +3645 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +17 -0
- package/dist/schema.js.map +7 -0
- package/dist/theme.d.ts +9 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/widgets/ProductCardLit.d.ts +32 -0
- package/dist/widgets/ProductCardLit.d.ts.map +1 -0
- package/dist/widgets/ProductComparisonLit.d.ts +33 -0
- package/dist/widgets/ProductComparisonLit.d.ts.map +1 -0
- package/dist/widgets/ProductGridLit.d.ts +27 -0
- package/dist/widgets/ProductGridLit.d.ts.map +1 -0
- package/dist/widgets/ProductHeroLit.d.ts +27 -0
- package/dist/widgets/ProductHeroLit.d.ts.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__privateAdd,
|
|
3
|
+
__privateGet,
|
|
4
|
+
__privateMethod,
|
|
5
|
+
__privateSet,
|
|
6
|
+
cardSchema,
|
|
7
|
+
comparisonSchema,
|
|
8
|
+
gridSchema,
|
|
9
|
+
heroSchema
|
|
10
|
+
} from "./chunk-SM35E4R3.js";
|
|
11
|
+
|
|
12
|
+
// ../../sdk-contracts/dist/mount-plumbing.js
|
|
13
|
+
var MOUNT_PLUMBING_KEYS = ["instanceId", "runtime", "tileId"];
|
|
14
|
+
function stripMountPlumbing(config) {
|
|
15
|
+
if (!config || typeof config !== "object") {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
const out = { ...config };
|
|
19
|
+
for (const key of MOUNT_PLUMBING_KEYS) {
|
|
20
|
+
delete out[key];
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ../../sdk-contracts/dist/schemas.js
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
var AnchorIdZ = z.object({
|
|
28
|
+
selector: z.string(),
|
|
29
|
+
route: z.union([z.string(), z.array(z.string())])
|
|
30
|
+
}).strict();
|
|
31
|
+
var AuthoringFieldsZ = {
|
|
32
|
+
title: z.string().max(200).optional().describe("Authoring-only: short label shown on the action plan dashboard. Stripped before serving to the runtime SDK."),
|
|
33
|
+
description: z.string().max(1e3).optional().describe("Authoring-only: one-sentence explanation of what this action does and why. Stripped before serving to the runtime SDK."),
|
|
34
|
+
validation: z.array(z.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.")
|
|
35
|
+
};
|
|
36
|
+
var COUNTABLE_EVENTS = [
|
|
37
|
+
// User interactions (from PostHog autocapture normalization)
|
|
38
|
+
"ui.click",
|
|
39
|
+
"ui.scroll",
|
|
40
|
+
"ui.input",
|
|
41
|
+
"ui.change",
|
|
42
|
+
"ui.submit",
|
|
43
|
+
// Behavioral detectors (from event-processor)
|
|
44
|
+
"ui.hover",
|
|
45
|
+
"ui.idle",
|
|
46
|
+
"ui.scroll_thrash",
|
|
47
|
+
"ui.focus_bounce",
|
|
48
|
+
// Navigation
|
|
49
|
+
"nav.page_view",
|
|
50
|
+
"nav.page_leave",
|
|
51
|
+
// Derived behavioral signals
|
|
52
|
+
"behavior.rage_click",
|
|
53
|
+
"behavior.hesitation",
|
|
54
|
+
"behavior.confusion"
|
|
55
|
+
];
|
|
56
|
+
var CountableEventZ = z.enum(COUNTABLE_EVENTS).describe("Event name to count. ui.* = user interactions and behavioral detectors, nav.* = page navigation, behavior.* = derived behavioral signals.");
|
|
57
|
+
var SESSION_METRIC_KEYS = ["time_on_page", "page_views", "scroll_depth"];
|
|
58
|
+
var SessionMetricKeyZ = z.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.");
|
|
59
|
+
var PageUrlConditionZ = z.object({
|
|
60
|
+
type: z.literal("page_url"),
|
|
61
|
+
url: z.string().describe('URL path to match (e.g. "/pricing", "/dashboard")')
|
|
62
|
+
}).describe('Fires when the current page URL matches. Use for page-specific actions. Example: {"type": "page_url", "url": "/pricing"}');
|
|
63
|
+
var RouteConditionZ = z.object({
|
|
64
|
+
type: z.literal("route"),
|
|
65
|
+
routeId: z.string().describe("Named route ID from the route filter")
|
|
66
|
+
}).describe("Fires when the current route matches a named route ID.");
|
|
67
|
+
var AnchorVisibleConditionZ = z.object({
|
|
68
|
+
type: z.literal("anchor_visible"),
|
|
69
|
+
anchorId: z.string().describe("CSS selector of the anchor element"),
|
|
70
|
+
state: z.enum(["visible", "present", "absent"]).describe('"visible" = in viewport, "present" = in DOM, "absent" = not in DOM')
|
|
71
|
+
}).describe(`Fires based on a DOM element's visibility state. Example: {"type": "anchor_visible", "anchorId": "#cta-button", "state": "visible"}`);
|
|
72
|
+
var EventOccurredConditionZ = z.object({
|
|
73
|
+
type: z.literal("event_occurred"),
|
|
74
|
+
eventName: z.string().describe('Event name (e.g. "ui.click", "$pageview")'),
|
|
75
|
+
withinMs: z.number().optional().describe("Time window in ms. Omit = any time this session.")
|
|
76
|
+
}).describe('Fires when a specific event has occurred during this session. Example: {"type": "event_occurred", "eventName": "ui.click", "withinMs": 5000}');
|
|
77
|
+
var StateEqualsConditionZ = z.object({
|
|
78
|
+
type: z.literal("state_equals"),
|
|
79
|
+
key: z.string().describe("Key in the SDK persistent state store (localStorage). Only valid for keys the host app explicitly sets via syntro.state.set()."),
|
|
80
|
+
value: z.unknown().describe("Expected value to match against")
|
|
81
|
+
}).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.");
|
|
82
|
+
var ViewportConditionZ = z.object({
|
|
83
|
+
type: z.literal("viewport"),
|
|
84
|
+
minWidth: z.number().optional().describe("Minimum viewport width in pixels"),
|
|
85
|
+
maxWidth: z.number().optional().describe("Maximum viewport width in pixels"),
|
|
86
|
+
minHeight: z.number().optional().describe("Minimum viewport height in pixels"),
|
|
87
|
+
maxHeight: z.number().optional().describe("Maximum viewport height in pixels")
|
|
88
|
+
}).describe('Fires based on viewport (screen) size. Use for responsive behavior. Example: {"type": "viewport", "minWidth": 768} \u2014 fires on tablet and larger.');
|
|
89
|
+
var SessionMetricConditionZ = z.object({
|
|
90
|
+
type: z.literal("session_metric"),
|
|
91
|
+
key: SessionMetricKeyZ,
|
|
92
|
+
operator: z.enum(["gte", "lte", "eq", "gt", "lt"]),
|
|
93
|
+
threshold: z.number().describe("Numeric threshold to compare against")
|
|
94
|
+
}).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}');
|
|
95
|
+
var DismissedConditionZ = z.object({
|
|
96
|
+
type: z.literal("dismissed"),
|
|
97
|
+
key: z.string().describe("Dismissal key (usually a tile or action ID)"),
|
|
98
|
+
inverted: z.boolean().optional().describe("When true, fires if NOT dismissed (default behavior)")
|
|
99
|
+
}).describe("Checks if an item has been dismissed by the user. Use with inverted: true to show only if not dismissed.");
|
|
100
|
+
var CooldownActiveConditionZ = z.object({
|
|
101
|
+
type: z.literal("cooldown_active"),
|
|
102
|
+
key: z.string().describe("Cooldown key"),
|
|
103
|
+
inverted: z.boolean().optional().describe("When true, fires if cooldown is NOT active")
|
|
104
|
+
}).describe("Checks if a cooldown timer is currently active. Use to prevent showing the same intervention too frequently.");
|
|
105
|
+
var FrequencyLimitConditionZ = z.object({
|
|
106
|
+
type: z.literal("frequency_limit"),
|
|
107
|
+
key: z.string().describe("Frequency counter key"),
|
|
108
|
+
limit: z.number().describe("Maximum allowed count"),
|
|
109
|
+
inverted: z.boolean().optional().describe("When true, fires if limit NOT reached")
|
|
110
|
+
}).describe("Checks if a frequency limit has been reached. Use to cap how many times an action fires per session.");
|
|
111
|
+
var MatchOpZ = z.object({
|
|
112
|
+
equals: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
113
|
+
contains: z.string().optional()
|
|
114
|
+
}).describe("Match operator for counter filters. Exactly one of equals or contains must be specified.");
|
|
115
|
+
var CounterDefZ = z.object({
|
|
116
|
+
events: z.array(CountableEventZ).min(1).describe("Event names to count. Use values from the countable events enum."),
|
|
117
|
+
match: z.record(z.string(), MatchOpZ).optional().describe("Property filters. Keys are event prop names or element-chain fields (tag_name, $el_text, attr__*). All entries AND together.")
|
|
118
|
+
}).describe("Defines what events to count. Registered as an accumulator predicate at config-load time.");
|
|
119
|
+
var EventCountConditionZ = z.object({
|
|
120
|
+
type: z.literal("event_count"),
|
|
121
|
+
key: z.string().describe("Unique key for this counter (used for accumulator registration)"),
|
|
122
|
+
operator: z.enum(["gte", "lte", "eq", "gt", "lt"]),
|
|
123
|
+
count: z.number().int().min(0).describe("Target count threshold"),
|
|
124
|
+
withinMs: z.number().positive().optional().describe("Time window in ms. Omit = count across entire session."),
|
|
125
|
+
counter: CounterDefZ.optional().describe("Inline counter definition. Defines what events to count.")
|
|
126
|
+
}).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"}}}}');
|
|
127
|
+
var ConditionZ = z.discriminatedUnion("type", [
|
|
128
|
+
PageUrlConditionZ,
|
|
129
|
+
RouteConditionZ,
|
|
130
|
+
AnchorVisibleConditionZ,
|
|
131
|
+
EventOccurredConditionZ,
|
|
132
|
+
StateEqualsConditionZ,
|
|
133
|
+
ViewportConditionZ,
|
|
134
|
+
SessionMetricConditionZ,
|
|
135
|
+
DismissedConditionZ,
|
|
136
|
+
CooldownActiveConditionZ,
|
|
137
|
+
FrequencyLimitConditionZ,
|
|
138
|
+
EventCountConditionZ
|
|
139
|
+
]);
|
|
140
|
+
var RuleZ = z.object({
|
|
141
|
+
conditions: z.array(ConditionZ).describe("Array of conditions \u2014 ALL must match (AND logic) for this rule to fire."),
|
|
142
|
+
value: z.unknown().describe("Value returned when all conditions match. For triggerWhen: true = fire the action.")
|
|
143
|
+
}).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.");
|
|
144
|
+
var RuleStrategyZ = z.object({
|
|
145
|
+
type: z.literal("rules"),
|
|
146
|
+
rules: z.array(RuleZ).describe("Ordered list of rules. Evaluated top-to-bottom \u2014 first match wins."),
|
|
147
|
+
default: z.unknown().describe("Fallback value when no rule matches. For triggerWhen: false = do not fire by default.")
|
|
148
|
+
}).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.");
|
|
149
|
+
var ScoreStrategyZ = z.object({
|
|
150
|
+
type: z.literal("score"),
|
|
151
|
+
field: z.string(),
|
|
152
|
+
threshold: z.number(),
|
|
153
|
+
above: z.unknown(),
|
|
154
|
+
below: z.unknown()
|
|
155
|
+
}).describe("Score-based strategy. Compares a field value against a threshold.");
|
|
156
|
+
var ModelStrategyZ = z.object({
|
|
157
|
+
type: z.literal("model"),
|
|
158
|
+
modelId: z.string(),
|
|
159
|
+
inputs: z.array(z.string()),
|
|
160
|
+
outputMapping: z.record(z.string(), z.unknown()),
|
|
161
|
+
default: z.unknown()
|
|
162
|
+
}).describe("ML model strategy. Sends inputs to a model and maps outputs.");
|
|
163
|
+
var ExternalStrategyZ = z.object({
|
|
164
|
+
type: z.literal("external"),
|
|
165
|
+
endpoint: z.string(),
|
|
166
|
+
method: z.enum(["GET", "POST"]).optional(),
|
|
167
|
+
default: z.unknown(),
|
|
168
|
+
timeoutMs: z.number().optional()
|
|
169
|
+
}).describe("External API strategy. Calls an endpoint to determine the value.");
|
|
170
|
+
var DecisionStrategyZ = z.discriminatedUnion("type", [
|
|
171
|
+
RuleStrategyZ,
|
|
172
|
+
ScoreStrategyZ,
|
|
173
|
+
ModelStrategyZ,
|
|
174
|
+
ExternalStrategyZ
|
|
175
|
+
]);
|
|
176
|
+
var TriggerWhenZ = DecisionStrategyZ.nullable().optional();
|
|
177
|
+
var EventScopeZ = z.object({
|
|
178
|
+
events: z.array(z.string()),
|
|
179
|
+
urlContains: z.string().optional(),
|
|
180
|
+
props: z.record(z.union([z.string(), z.number(), z.boolean()])).optional()
|
|
181
|
+
});
|
|
182
|
+
var NotifyZ = z.object({
|
|
183
|
+
title: z.string().optional(),
|
|
184
|
+
body: z.string().optional(),
|
|
185
|
+
icon: z.string().optional()
|
|
186
|
+
}).nullable().optional();
|
|
187
|
+
|
|
188
|
+
// src/widgets/ProductCardLit.ts
|
|
189
|
+
import { html, LitElement, nothing } from "lit";
|
|
190
|
+
|
|
191
|
+
// src/bind/dom-selector.ts
|
|
192
|
+
async function resolveDomSelectorPrice(input, _signal) {
|
|
193
|
+
const el = document.querySelector(input.selector);
|
|
194
|
+
const text = el?.textContent?.trim();
|
|
195
|
+
return text && text.length > 0 ? text : null;
|
|
196
|
+
}
|
|
197
|
+
async function resolveDomSelectorImage(input, _signal) {
|
|
198
|
+
const el = document.querySelector(input.selector);
|
|
199
|
+
if (!el) return null;
|
|
200
|
+
if (el instanceof HTMLImageElement) return el.currentSrc || el.src || null;
|
|
201
|
+
const src = el.getAttribute("src");
|
|
202
|
+
return src && src.length > 0 ? src : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/bind/jsonld.ts
|
|
206
|
+
function isProduct(node) {
|
|
207
|
+
if (typeof node !== "object" || node === null) return false;
|
|
208
|
+
const t = node["@type"];
|
|
209
|
+
if (typeof t === "string") return t === "Product";
|
|
210
|
+
if (Array.isArray(t)) return t.includes("Product");
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
function parseScripts(scripts) {
|
|
214
|
+
for (const text of scripts) {
|
|
215
|
+
let parsed;
|
|
216
|
+
try {
|
|
217
|
+
parsed = JSON.parse(text);
|
|
218
|
+
} catch {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const stack = [parsed];
|
|
222
|
+
while (stack.length) {
|
|
223
|
+
const cur = stack.pop();
|
|
224
|
+
if (Array.isArray(cur)) {
|
|
225
|
+
for (const c of cur) stack.push(c);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (isProduct(cur)) return cur;
|
|
229
|
+
if (typeof cur === "object" && cur !== null) {
|
|
230
|
+
const graph = cur["@graph"];
|
|
231
|
+
if (Array.isArray(graph)) for (const g of graph) stack.push(g);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
function collectInlineScripts() {
|
|
238
|
+
const els = document.querySelectorAll('script[type="application/ld+json"]');
|
|
239
|
+
return Array.from(els, (el) => el.textContent ?? "");
|
|
240
|
+
}
|
|
241
|
+
async function collectScriptsFromUrl(url, signal) {
|
|
242
|
+
const res = await fetch(url, { signal });
|
|
243
|
+
if (!res.ok) return [];
|
|
244
|
+
const text = await res.text();
|
|
245
|
+
const doc = new DOMParser().parseFromString(text, "text/html");
|
|
246
|
+
return Array.from(
|
|
247
|
+
doc.querySelectorAll('script[type="application/ld+json"]'),
|
|
248
|
+
(el) => el.textContent ?? ""
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
function pickPrice(node) {
|
|
252
|
+
const offer = Array.isArray(node.offers) ? node.offers[0] : node.offers;
|
|
253
|
+
if (!offer) return null;
|
|
254
|
+
const raw = offer.price;
|
|
255
|
+
const num = typeof raw === "number" ? raw : typeof raw === "string" ? Number.parseFloat(raw) : Number.NaN;
|
|
256
|
+
if (!Number.isFinite(num)) return null;
|
|
257
|
+
return { amount: num, currency: offer.priceCurrency || "USD" };
|
|
258
|
+
}
|
|
259
|
+
function formatPrice(amount, currency) {
|
|
260
|
+
try {
|
|
261
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
console.warn("[adaptive-product.bind] jsonld: Intl.NumberFormat failed", { currency }, err);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
async function resolveJsonLdPrice(input, signal) {
|
|
268
|
+
const scripts = input.url ? await collectScriptsFromUrl(input.url, signal) : collectInlineScripts();
|
|
269
|
+
const product = parseScripts(scripts);
|
|
270
|
+
if (!product) return null;
|
|
271
|
+
const picked = pickPrice(product);
|
|
272
|
+
return picked ? formatPrice(picked.amount, picked.currency) : null;
|
|
273
|
+
}
|
|
274
|
+
async function resolveJsonLdImage(input, signal) {
|
|
275
|
+
const scripts = input.url ? await collectScriptsFromUrl(input.url, signal) : collectInlineScripts();
|
|
276
|
+
const product = parseScripts(scripts);
|
|
277
|
+
if (!product) return null;
|
|
278
|
+
const img = Array.isArray(product.image) ? product.image[0] : product.image;
|
|
279
|
+
return typeof img === "string" && img.length > 0 ? img : null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/bind/opengraph.ts
|
|
283
|
+
async function resolveOpenGraphImage(input, signal) {
|
|
284
|
+
const res = await fetch(input.url, { signal });
|
|
285
|
+
if (!res.ok) return null;
|
|
286
|
+
const text = await res.text();
|
|
287
|
+
const doc = new DOMParser().parseFromString(text, "text/html");
|
|
288
|
+
const meta = doc.querySelector('meta[property="og:image"]');
|
|
289
|
+
const raw = meta?.getAttribute("content");
|
|
290
|
+
if (!raw) return null;
|
|
291
|
+
try {
|
|
292
|
+
return new URL(raw, input.url).toString();
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/bind/shopify-ajax.ts
|
|
299
|
+
function originFor(base) {
|
|
300
|
+
if (base) return base.replace(/\/$/, "");
|
|
301
|
+
return typeof location !== "undefined" ? location.origin : "";
|
|
302
|
+
}
|
|
303
|
+
async function fetchProduct(handle, base, signal) {
|
|
304
|
+
const url = `${originFor(base)}/products/${encodeURIComponent(handle)}.js`;
|
|
305
|
+
const res = await fetch(url, { signal });
|
|
306
|
+
if (!res.ok) return null;
|
|
307
|
+
try {
|
|
308
|
+
return await res.json();
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (err instanceof Error && err.name === "AbortError") throw err;
|
|
311
|
+
console.warn("[adaptive-product.bind] shopify-ajax: json parse failed", err);
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async function resolveShopifyAjaxPrice(input, signal) {
|
|
316
|
+
const payload = await fetchProduct(input.handle, input.base, signal);
|
|
317
|
+
if (!payload || typeof payload.price !== "number") return null;
|
|
318
|
+
const currency = payload.currency || "USD";
|
|
319
|
+
try {
|
|
320
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(
|
|
321
|
+
payload.price / 100
|
|
322
|
+
);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.warn(
|
|
325
|
+
"[adaptive-product.bind] shopify-ajax/price: Intl.NumberFormat failed",
|
|
326
|
+
{ currency },
|
|
327
|
+
err
|
|
328
|
+
);
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function resolveShopifyAjaxImage(input, signal) {
|
|
333
|
+
const payload = await fetchProduct(input.handle, input.base, signal);
|
|
334
|
+
const first = payload?.images?.[0];
|
|
335
|
+
return typeof first === "string" && first.length > 0 ? first : null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/bind/woocommerce-store-api.ts
|
|
339
|
+
function originFor2(base) {
|
|
340
|
+
if (base) return base.replace(/\/$/, "");
|
|
341
|
+
return typeof location !== "undefined" ? location.origin : "";
|
|
342
|
+
}
|
|
343
|
+
async function resolveWooStoreApiPrice(input, signal) {
|
|
344
|
+
const url = `${originFor2(input.base)}/wp-json/wc/store/products?slug=${encodeURIComponent(input.slug)}`;
|
|
345
|
+
const res = await fetch(url, { signal });
|
|
346
|
+
if (!res.ok) return null;
|
|
347
|
+
let body;
|
|
348
|
+
try {
|
|
349
|
+
body = await res.json();
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if (err instanceof Error && err.name === "AbortError") throw err;
|
|
352
|
+
console.warn("[adaptive-product.bind] woocommerce-store-api: json parse failed", err);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
const product = Array.isArray(body) ? body.find((p) => p.slug === input.slug) : void 0;
|
|
356
|
+
const prices = product?.prices;
|
|
357
|
+
if (!prices || typeof prices.price !== "string") return null;
|
|
358
|
+
const minor = prices.currency_minor_unit ?? 2;
|
|
359
|
+
const raw = Number.parseInt(prices.price, 10);
|
|
360
|
+
if (!Number.isFinite(raw)) return null;
|
|
361
|
+
const amount = raw / 10 ** minor;
|
|
362
|
+
const currency = prices.currency_code || "USD";
|
|
363
|
+
try {
|
|
364
|
+
return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(amount);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.warn(
|
|
367
|
+
"[adaptive-product.bind] woocommerce-store-api: Intl.NumberFormat failed",
|
|
368
|
+
{ currency },
|
|
369
|
+
err
|
|
370
|
+
);
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/bind/index.ts
|
|
376
|
+
async function resolvePriceBind(input, signal) {
|
|
377
|
+
try {
|
|
378
|
+
switch (input.type) {
|
|
379
|
+
case "dom-selector":
|
|
380
|
+
return await resolveDomSelectorPrice(input, signal);
|
|
381
|
+
case "shopify-ajax":
|
|
382
|
+
return await resolveShopifyAjaxPrice(input, signal);
|
|
383
|
+
case "woocommerce-store-api":
|
|
384
|
+
return await resolveWooStoreApiPrice(input, signal);
|
|
385
|
+
case "jsonld":
|
|
386
|
+
return await resolveJsonLdPrice(input, signal);
|
|
387
|
+
default:
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
} catch (err) {
|
|
391
|
+
if (!(err instanceof Error && err.name === "AbortError")) {
|
|
392
|
+
console.warn(`[adaptive-product.bind] price/${input.type}:`, err);
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async function resolveImageBind(input, signal) {
|
|
398
|
+
try {
|
|
399
|
+
switch (input.type) {
|
|
400
|
+
case "dom-selector":
|
|
401
|
+
return await resolveDomSelectorImage(input, signal);
|
|
402
|
+
case "shopify-ajax":
|
|
403
|
+
return await resolveShopifyAjaxImage(input, signal);
|
|
404
|
+
case "opengraph":
|
|
405
|
+
return await resolveOpenGraphImage(input, signal);
|
|
406
|
+
case "jsonld":
|
|
407
|
+
return await resolveJsonLdImage(input, signal);
|
|
408
|
+
default:
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
} catch (err) {
|
|
412
|
+
if (!(err instanceof Error && err.name === "AbortError")) {
|
|
413
|
+
console.warn(`[adaptive-product.bind] image/${input.type}:`, err);
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/bind/controller.ts
|
|
420
|
+
var _abort, _pending, _destroyed, _BindController_instances, resolveImage_fn, resolvePrice_fn;
|
|
421
|
+
var BindController = class {
|
|
422
|
+
constructor(products, callback) {
|
|
423
|
+
this.products = products;
|
|
424
|
+
this.callback = callback;
|
|
425
|
+
__privateAdd(this, _BindController_instances);
|
|
426
|
+
__privateAdd(this, _abort, new AbortController());
|
|
427
|
+
__privateAdd(this, _pending, []);
|
|
428
|
+
__privateAdd(this, _destroyed, false);
|
|
429
|
+
}
|
|
430
|
+
start() {
|
|
431
|
+
for (const product of this.products) {
|
|
432
|
+
if (product.image.bind) {
|
|
433
|
+
__privateGet(this, _pending).push(__privateMethod(this, _BindController_instances, resolveImage_fn).call(this, product.id, product.image.bind));
|
|
434
|
+
}
|
|
435
|
+
if (product.price?.bind) {
|
|
436
|
+
__privateGet(this, _pending).push(__privateMethod(this, _BindController_instances, resolvePrice_fn).call(this, product.id, product.price.bind));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async allSettled() {
|
|
441
|
+
await Promise.allSettled(__privateGet(this, _pending));
|
|
442
|
+
}
|
|
443
|
+
destroy() {
|
|
444
|
+
if (__privateGet(this, _destroyed)) return;
|
|
445
|
+
__privateSet(this, _destroyed, true);
|
|
446
|
+
__privateGet(this, _abort).abort();
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
_abort = new WeakMap();
|
|
450
|
+
_pending = new WeakMap();
|
|
451
|
+
_destroyed = new WeakMap();
|
|
452
|
+
_BindController_instances = new WeakSet();
|
|
453
|
+
resolveImage_fn = async function(productId, bind) {
|
|
454
|
+
const value = await resolveImageBind(bind, __privateGet(this, _abort).signal);
|
|
455
|
+
if (__privateGet(this, _destroyed)) return;
|
|
456
|
+
this.callback(productId, "image", value);
|
|
457
|
+
};
|
|
458
|
+
resolvePrice_fn = async function(productId, bind) {
|
|
459
|
+
const value = await resolvePriceBind(bind, __privateGet(this, _abort).signal);
|
|
460
|
+
if (__privateGet(this, _destroyed)) return;
|
|
461
|
+
this.callback(productId, "price", value);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// src/sanitizer.ts
|
|
465
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
466
|
+
"b",
|
|
467
|
+
"strong",
|
|
468
|
+
"i",
|
|
469
|
+
"em",
|
|
470
|
+
"u",
|
|
471
|
+
"span",
|
|
472
|
+
"div",
|
|
473
|
+
"p",
|
|
474
|
+
"br",
|
|
475
|
+
"ul",
|
|
476
|
+
"ol",
|
|
477
|
+
"li",
|
|
478
|
+
"code",
|
|
479
|
+
"pre",
|
|
480
|
+
"small",
|
|
481
|
+
"sup",
|
|
482
|
+
"sub",
|
|
483
|
+
"a",
|
|
484
|
+
"button",
|
|
485
|
+
// SVG elements (for inline Lucide icons in config HTML)
|
|
486
|
+
"svg",
|
|
487
|
+
"path",
|
|
488
|
+
"circle",
|
|
489
|
+
"line",
|
|
490
|
+
"polyline",
|
|
491
|
+
"polygon",
|
|
492
|
+
"rect",
|
|
493
|
+
"g"
|
|
494
|
+
]);
|
|
495
|
+
function sanitizeHtml(html5) {
|
|
496
|
+
const hasNative = typeof window.Sanitizer === "function";
|
|
497
|
+
if (hasNative) {
|
|
498
|
+
try {
|
|
499
|
+
const s = new window.Sanitizer({});
|
|
500
|
+
const frag = s.sanitizeToFragment(html5);
|
|
501
|
+
const div = document.createElement("div");
|
|
502
|
+
div.append(frag);
|
|
503
|
+
return div.innerHTML;
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const tpl = document.createElement("template");
|
|
508
|
+
tpl.innerHTML = html5;
|
|
509
|
+
const root = tpl.content;
|
|
510
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null);
|
|
511
|
+
const toRemove = [];
|
|
512
|
+
while (walker.nextNode()) {
|
|
513
|
+
const el = walker.currentNode;
|
|
514
|
+
const tag = el.tagName.toLowerCase();
|
|
515
|
+
if (!ALLOWED_TAGS.has(tag)) {
|
|
516
|
+
toRemove.push(el);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
for (const attr of Array.from(el.attributes)) {
|
|
520
|
+
const name = attr.name.toLowerCase();
|
|
521
|
+
const value = attr.value.trim().toLowerCase();
|
|
522
|
+
const isEvent = name.startsWith("on");
|
|
523
|
+
const isJsUrl = (name === "href" || name === "src") && value.startsWith("javascript:");
|
|
524
|
+
if (isEvent || isJsUrl) {
|
|
525
|
+
el.removeAttribute(attr.name);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
for (const el of toRemove) {
|
|
530
|
+
while (el.firstChild) {
|
|
531
|
+
el.parentNode?.insertBefore(el.firstChild, el);
|
|
532
|
+
}
|
|
533
|
+
el.remove();
|
|
534
|
+
}
|
|
535
|
+
return tpl.innerHTML;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/widgets/ProductCardLit.ts
|
|
539
|
+
var _controller, _ProductCardLit_instances, startBind_fn;
|
|
540
|
+
var ProductCardLit = class extends LitElement {
|
|
541
|
+
constructor() {
|
|
542
|
+
super(...arguments);
|
|
543
|
+
__privateAdd(this, _ProductCardLit_instances);
|
|
544
|
+
this.props = void 0;
|
|
545
|
+
this.resolved = {};
|
|
546
|
+
this.validationError = false;
|
|
547
|
+
__privateAdd(this, _controller);
|
|
548
|
+
}
|
|
549
|
+
createRenderRoot() {
|
|
550
|
+
return this;
|
|
551
|
+
}
|
|
552
|
+
willUpdate(changed) {
|
|
553
|
+
if (changed.has("props")) {
|
|
554
|
+
__privateMethod(this, _ProductCardLit_instances, startBind_fn).call(this);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
disconnectedCallback() {
|
|
558
|
+
super.disconnectedCallback();
|
|
559
|
+
__privateGet(this, _controller)?.destroy();
|
|
560
|
+
}
|
|
561
|
+
render() {
|
|
562
|
+
if (this.validationError || !this.props) return html``;
|
|
563
|
+
const parsed = cardSchema.safeParse(this.props);
|
|
564
|
+
if (!parsed.success) return html``;
|
|
565
|
+
return renderProductCard(
|
|
566
|
+
parsed.data.product,
|
|
567
|
+
parsed.data.density,
|
|
568
|
+
this.resolved[parsed.data.product.id]
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
_controller = new WeakMap();
|
|
573
|
+
_ProductCardLit_instances = new WeakSet();
|
|
574
|
+
startBind_fn = function() {
|
|
575
|
+
__privateGet(this, _controller)?.destroy();
|
|
576
|
+
this.resolved = {};
|
|
577
|
+
if (!this.props) {
|
|
578
|
+
this.validationError = false;
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const result = cardSchema.safeParse(this.props);
|
|
582
|
+
if (!result.success) {
|
|
583
|
+
this.validationError = true;
|
|
584
|
+
console.warn("[adaptive-product] invalid card props:", result.error.issues);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
this.validationError = false;
|
|
588
|
+
const parsed = result.data;
|
|
589
|
+
__privateSet(this, _controller, new BindController([parsed.product], (productId, kind, value) => {
|
|
590
|
+
if (value == null) return;
|
|
591
|
+
this.resolved = {
|
|
592
|
+
...this.resolved,
|
|
593
|
+
[productId]: { ...this.resolved[productId], [kind]: value }
|
|
594
|
+
};
|
|
595
|
+
}));
|
|
596
|
+
__privateGet(this, _controller).start();
|
|
597
|
+
};
|
|
598
|
+
ProductCardLit.properties = {
|
|
599
|
+
props: { attribute: false },
|
|
600
|
+
resolved: { state: true },
|
|
601
|
+
validationError: { state: true }
|
|
602
|
+
};
|
|
603
|
+
function renderProductCard(product, density, resolved) {
|
|
604
|
+
const imgSrc = resolved?.image ?? product.image.src;
|
|
605
|
+
const priceText = resolved?.price ?? product.price?.amount;
|
|
606
|
+
const showFraming = density === "standard" && product.framing;
|
|
607
|
+
return html`
|
|
608
|
+
<article class="sc-product-card" data-density=${density}>
|
|
609
|
+
<img
|
|
610
|
+
class="sc-product-card__image"
|
|
611
|
+
data-product-image
|
|
612
|
+
src=${imgSrc}
|
|
613
|
+
alt=${product.image.alt}
|
|
614
|
+
loading="lazy"
|
|
615
|
+
@error=${onImageError}
|
|
616
|
+
/>
|
|
617
|
+
<header class="sc-product-card__header">
|
|
618
|
+
<h3 class="sc-product-card__name" data-product-name>${product.name}</h3>
|
|
619
|
+
${product.tagline ? html`<p class="sc-product-card__tagline" data-product-tagline>${product.tagline}</p>` : nothing}
|
|
620
|
+
</header>
|
|
621
|
+
${product.price ? html`<div class="sc-product-card__price" data-product-price>
|
|
622
|
+
<span class="sc-product-card__amount">${priceText}</span>
|
|
623
|
+
${product.price.cadence ? html`<span class="sc-product-card__cadence">${product.price.cadence}</span>` : nothing}
|
|
624
|
+
</div>` : nothing}
|
|
625
|
+
${product.badge ? html`<span
|
|
626
|
+
class="sc-product-card__badge"
|
|
627
|
+
data-product-badge
|
|
628
|
+
data-tone=${product.badge.tone}
|
|
629
|
+
>${product.badge.label}</span
|
|
630
|
+
>` : nothing}
|
|
631
|
+
${product.attributes.length > 0 ? html`<ul class="sc-product-card__attrs">
|
|
632
|
+
${product.attributes.map(
|
|
633
|
+
(a) => html`<li data-attribute-emphasis=${a.emphasis ? "true" : "false"}>
|
|
634
|
+
<span class="sc-product-card__attr-label">${a.label}</span>
|
|
635
|
+
<span class="sc-product-card__attr-value">${a.value}</span>
|
|
636
|
+
</li>`
|
|
637
|
+
)}
|
|
638
|
+
</ul>` : nothing}
|
|
639
|
+
${showFraming ? html`<p
|
|
640
|
+
class="sc-product-card__framing"
|
|
641
|
+
.innerHTML=${sanitizeHtml(product.framing ?? "")}
|
|
642
|
+
></p>` : nothing}
|
|
643
|
+
<div class="sc-product-card__ctas">
|
|
644
|
+
${product.ctas.map(
|
|
645
|
+
(c, i) => html`<a
|
|
646
|
+
class="sc-product-card__cta"
|
|
647
|
+
data-product-cta=${i === 0 ? "primary" : "secondary"}
|
|
648
|
+
data-variant=${c.variant}
|
|
649
|
+
href=${c.href ?? "#"}
|
|
650
|
+
target=${c.target}
|
|
651
|
+
rel=${c.target === "_blank" ? "noopener noreferrer" : ""}
|
|
652
|
+
data-action-id=${c.actionId ?? ""}
|
|
653
|
+
>${c.label}</a
|
|
654
|
+
>`
|
|
655
|
+
)}
|
|
656
|
+
</div>
|
|
657
|
+
</article>
|
|
658
|
+
`;
|
|
659
|
+
}
|
|
660
|
+
function onImageError(e) {
|
|
661
|
+
const img = e.currentTarget;
|
|
662
|
+
img.dataset.imageError = "true";
|
|
663
|
+
img.removeAttribute("src");
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/widgets/ProductComparisonLit.ts
|
|
667
|
+
import { html as html2, LitElement as LitElement2, nothing as nothing2 } from "lit";
|
|
668
|
+
var _controller2, _parsed, _ProductComparisonLit_instances, startBind_fn2, toggle_fn, onHeaderKey_fn, renderRow_fn;
|
|
669
|
+
var ProductComparisonLit = class extends LitElement2 {
|
|
670
|
+
constructor() {
|
|
671
|
+
super(...arguments);
|
|
672
|
+
__privateAdd(this, _ProductComparisonLit_instances);
|
|
673
|
+
this.props = void 0;
|
|
674
|
+
this.open = /* @__PURE__ */ new Set();
|
|
675
|
+
this.resolved = {};
|
|
676
|
+
this.validationError = false;
|
|
677
|
+
__privateAdd(this, _controller2);
|
|
678
|
+
__privateAdd(this, _parsed);
|
|
679
|
+
}
|
|
680
|
+
createRenderRoot() {
|
|
681
|
+
return this;
|
|
682
|
+
}
|
|
683
|
+
willUpdate(changed) {
|
|
684
|
+
if (changed.has("props")) {
|
|
685
|
+
__privateMethod(this, _ProductComparisonLit_instances, startBind_fn2).call(this);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
disconnectedCallback() {
|
|
689
|
+
super.disconnectedCallback();
|
|
690
|
+
__privateGet(this, _controller2)?.destroy();
|
|
691
|
+
}
|
|
692
|
+
render() {
|
|
693
|
+
if (this.validationError || !__privateGet(this, _parsed)) return html2``;
|
|
694
|
+
const parsed = __privateGet(this, _parsed);
|
|
695
|
+
return html2`
|
|
696
|
+
<section class="sc-product-comparison">
|
|
697
|
+
${parsed.heading ? html2`<h2
|
|
698
|
+
class="sc-product-comparison__heading"
|
|
699
|
+
data-comparison-heading
|
|
700
|
+
>${parsed.heading}</h2>` : nothing2}
|
|
701
|
+
<ul class="sc-product-comparison__rows">
|
|
702
|
+
${parsed.products.map((p) => __privateMethod(this, _ProductComparisonLit_instances, renderRow_fn).call(this, p))}
|
|
703
|
+
</ul>
|
|
704
|
+
</section>
|
|
705
|
+
`;
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
_controller2 = new WeakMap();
|
|
709
|
+
_parsed = new WeakMap();
|
|
710
|
+
_ProductComparisonLit_instances = new WeakSet();
|
|
711
|
+
startBind_fn2 = function() {
|
|
712
|
+
__privateGet(this, _controller2)?.destroy();
|
|
713
|
+
this.resolved = {};
|
|
714
|
+
if (!this.props) {
|
|
715
|
+
this.validationError = false;
|
|
716
|
+
__privateSet(this, _parsed, void 0);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const result = comparisonSchema.safeParse(this.props);
|
|
720
|
+
if (!result.success) {
|
|
721
|
+
this.validationError = true;
|
|
722
|
+
__privateSet(this, _parsed, void 0);
|
|
723
|
+
console.warn("[adaptive-product] invalid comparison props:", result.error.issues);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
this.validationError = false;
|
|
727
|
+
__privateSet(this, _parsed, result.data);
|
|
728
|
+
this.open = new Set(
|
|
729
|
+
typeof result.data.defaultExpandedIndex === "number" ? [result.data.products[result.data.defaultExpandedIndex]?.id].filter(Boolean) : []
|
|
730
|
+
);
|
|
731
|
+
__privateSet(this, _controller2, new BindController(result.data.products, (productId, kind, value) => {
|
|
732
|
+
if (value == null) return;
|
|
733
|
+
this.resolved = {
|
|
734
|
+
...this.resolved,
|
|
735
|
+
[productId]: { ...this.resolved[productId], [kind]: value }
|
|
736
|
+
};
|
|
737
|
+
}));
|
|
738
|
+
__privateGet(this, _controller2).start();
|
|
739
|
+
};
|
|
740
|
+
toggle_fn = function(id) {
|
|
741
|
+
if (!__privateGet(this, _parsed)) return;
|
|
742
|
+
const next = new Set(this.open);
|
|
743
|
+
if (next.has(id)) {
|
|
744
|
+
next.delete(id);
|
|
745
|
+
} else {
|
|
746
|
+
if (__privateGet(this, _parsed).expandBehavior === "accordion") next.clear();
|
|
747
|
+
next.add(id);
|
|
748
|
+
}
|
|
749
|
+
this.open = next;
|
|
750
|
+
};
|
|
751
|
+
onHeaderKey_fn = function(id, e) {
|
|
752
|
+
if (e.key === "Escape" && this.open.has(id)) {
|
|
753
|
+
const next = new Set(this.open);
|
|
754
|
+
next.delete(id);
|
|
755
|
+
this.open = next;
|
|
756
|
+
e.preventDefault();
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
renderRow_fn = function(product) {
|
|
760
|
+
const expanded = this.open.has(product.id);
|
|
761
|
+
const regionId = `sc-cmp-${product.id}-panel`;
|
|
762
|
+
const resolved = this.resolved[product.id];
|
|
763
|
+
const imgSrc = resolved?.image ?? product.image.src;
|
|
764
|
+
const priceText = resolved?.price ?? product.price?.amount;
|
|
765
|
+
const primary = product.ctas[0];
|
|
766
|
+
return html2`
|
|
767
|
+
<li
|
|
768
|
+
class="sc-product-comparison__row"
|
|
769
|
+
data-comparison-row
|
|
770
|
+
data-product-id=${product.id}
|
|
771
|
+
data-expanded=${expanded ? "true" : "false"}
|
|
772
|
+
>
|
|
773
|
+
<div class="sc-product-comparison__row-inner">
|
|
774
|
+
<button
|
|
775
|
+
class="sc-product-comparison__header"
|
|
776
|
+
data-comparison-header
|
|
777
|
+
type="button"
|
|
778
|
+
aria-expanded=${expanded ? "true" : "false"}
|
|
779
|
+
aria-controls=${regionId}
|
|
780
|
+
@click=${() => __privateMethod(this, _ProductComparisonLit_instances, toggle_fn).call(this, product.id)}
|
|
781
|
+
@keydown=${(e) => __privateMethod(this, _ProductComparisonLit_instances, onHeaderKey_fn).call(this, product.id, e)}
|
|
782
|
+
>
|
|
783
|
+
<img
|
|
784
|
+
class="sc-product-comparison__thumb"
|
|
785
|
+
src=${imgSrc}
|
|
786
|
+
alt=${product.image.alt}
|
|
787
|
+
loading="lazy"
|
|
788
|
+
/>
|
|
789
|
+
<span class="sc-product-comparison__name">${product.name}</span>
|
|
790
|
+
${product.price ? html2`<span class="sc-product-comparison__price"
|
|
791
|
+
>${priceText}${product.price.cadence ? html2` <small>${product.price.cadence}</small>` : nothing2}</span
|
|
792
|
+
>` : nothing2}
|
|
793
|
+
${product.framing ? html2`<span
|
|
794
|
+
class="sc-product-comparison__framing"
|
|
795
|
+
.innerHTML=${sanitizeHtml(product.framing)}
|
|
796
|
+
></span>` : nothing2}
|
|
797
|
+
</button>
|
|
798
|
+
<span class="sc-product-comparison__primary-cta" data-product-cta="primary">
|
|
799
|
+
<a
|
|
800
|
+
href=${primary.href ?? "#"}
|
|
801
|
+
target=${primary.target}
|
|
802
|
+
rel=${primary.target === "_blank" ? "noopener noreferrer" : ""}
|
|
803
|
+
data-action-id=${primary.actionId ?? ""}
|
|
804
|
+
data-variant=${primary.variant}
|
|
805
|
+
>${primary.label}</a
|
|
806
|
+
>
|
|
807
|
+
</span>
|
|
808
|
+
</div>
|
|
809
|
+
${expanded ? html2`<div class="sc-product-comparison__panel" id=${regionId} role="region">
|
|
810
|
+
${product.attributes.length > 0 ? html2`<ul class="sc-product-comparison__attrs">
|
|
811
|
+
${product.attributes.map(
|
|
812
|
+
(a) => html2`<li data-attribute-emphasis=${a.emphasis ? "true" : "false"}>
|
|
813
|
+
<span>${a.label}</span><span>${a.value}</span>
|
|
814
|
+
</li>`
|
|
815
|
+
)}
|
|
816
|
+
</ul>` : nothing2}
|
|
817
|
+
${product.ctas[1] ? html2`<a
|
|
818
|
+
class="sc-product-comparison__secondary-cta"
|
|
819
|
+
data-product-cta="secondary"
|
|
820
|
+
href=${product.ctas[1].href ?? "#"}
|
|
821
|
+
target=${product.ctas[1].target}
|
|
822
|
+
rel=${product.ctas[1].target === "_blank" ? "noopener noreferrer" : ""}
|
|
823
|
+
data-action-id=${product.ctas[1].actionId ?? ""}
|
|
824
|
+
data-variant=${product.ctas[1].variant}
|
|
825
|
+
>${product.ctas[1].label}</a
|
|
826
|
+
>` : nothing2}
|
|
827
|
+
</div>` : nothing2}
|
|
828
|
+
</li>
|
|
829
|
+
`;
|
|
830
|
+
};
|
|
831
|
+
ProductComparisonLit.properties = {
|
|
832
|
+
props: { attribute: false },
|
|
833
|
+
open: { state: true },
|
|
834
|
+
resolved: { state: true },
|
|
835
|
+
validationError: { state: true }
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
// src/widgets/ProductGridLit.ts
|
|
839
|
+
import { html as html3, LitElement as LitElement3, nothing as nothing3 } from "lit";
|
|
840
|
+
var _controller3, _ProductGridLit_instances, startBind_fn3;
|
|
841
|
+
var ProductGridLit = class extends LitElement3 {
|
|
842
|
+
constructor() {
|
|
843
|
+
super(...arguments);
|
|
844
|
+
__privateAdd(this, _ProductGridLit_instances);
|
|
845
|
+
this.props = void 0;
|
|
846
|
+
this.resolved = {};
|
|
847
|
+
this.validationError = false;
|
|
848
|
+
__privateAdd(this, _controller3);
|
|
849
|
+
}
|
|
850
|
+
createRenderRoot() {
|
|
851
|
+
return this;
|
|
852
|
+
}
|
|
853
|
+
willUpdate(changed) {
|
|
854
|
+
if (changed.has("props")) {
|
|
855
|
+
__privateMethod(this, _ProductGridLit_instances, startBind_fn3).call(this);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
disconnectedCallback() {
|
|
859
|
+
super.disconnectedCallback();
|
|
860
|
+
__privateGet(this, _controller3)?.destroy();
|
|
861
|
+
}
|
|
862
|
+
render() {
|
|
863
|
+
if (this.validationError || !this.props) return html3``;
|
|
864
|
+
const parsed = gridSchema.safeParse(this.props);
|
|
865
|
+
if (!parsed.success) return html3``;
|
|
866
|
+
const { products, desktopColumns, heading } = parsed.data;
|
|
867
|
+
return html3`
|
|
868
|
+
<section class="sc-product-grid">
|
|
869
|
+
${heading ? html3`<h2 data-grid-heading class="sc-product-grid__heading">${heading}</h2>` : nothing3}
|
|
870
|
+
<div
|
|
871
|
+
data-grid-root
|
|
872
|
+
class="sc-product-grid__cells"
|
|
873
|
+
style=${`--sc-product-grid-cols:${desktopColumns}`}
|
|
874
|
+
>
|
|
875
|
+
${products.map(
|
|
876
|
+
(p) => html3`<div data-grid-cell class="sc-product-grid__cell">
|
|
877
|
+
${renderProductCard(p, "standard", this.resolved[p.id])}
|
|
878
|
+
</div>`
|
|
879
|
+
)}
|
|
880
|
+
</div>
|
|
881
|
+
</section>
|
|
882
|
+
`;
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
_controller3 = new WeakMap();
|
|
886
|
+
_ProductGridLit_instances = new WeakSet();
|
|
887
|
+
startBind_fn3 = function() {
|
|
888
|
+
__privateGet(this, _controller3)?.destroy();
|
|
889
|
+
this.resolved = {};
|
|
890
|
+
if (!this.props) {
|
|
891
|
+
this.validationError = false;
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
const result = gridSchema.safeParse(this.props);
|
|
895
|
+
if (!result.success) {
|
|
896
|
+
this.validationError = true;
|
|
897
|
+
console.warn("[adaptive-product] invalid grid props:", result.error.issues);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
this.validationError = false;
|
|
901
|
+
__privateSet(this, _controller3, new BindController(result.data.products, (productId, kind, value) => {
|
|
902
|
+
if (value == null) return;
|
|
903
|
+
this.resolved = {
|
|
904
|
+
...this.resolved,
|
|
905
|
+
[productId]: { ...this.resolved[productId], [kind]: value }
|
|
906
|
+
};
|
|
907
|
+
}));
|
|
908
|
+
__privateGet(this, _controller3).start();
|
|
909
|
+
};
|
|
910
|
+
ProductGridLit.properties = {
|
|
911
|
+
props: { attribute: false },
|
|
912
|
+
resolved: { state: true },
|
|
913
|
+
validationError: { state: true }
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
// src/widgets/ProductHeroLit.ts
|
|
917
|
+
import { html as html4, LitElement as LitElement4, nothing as nothing4 } from "lit";
|
|
918
|
+
var _controller4, _ProductHeroLit_instances, startBind_fn4;
|
|
919
|
+
var ProductHeroLit = class extends LitElement4 {
|
|
920
|
+
constructor() {
|
|
921
|
+
super(...arguments);
|
|
922
|
+
__privateAdd(this, _ProductHeroLit_instances);
|
|
923
|
+
this.props = void 0;
|
|
924
|
+
this.resolved = {};
|
|
925
|
+
this.validationError = false;
|
|
926
|
+
__privateAdd(this, _controller4);
|
|
927
|
+
}
|
|
928
|
+
createRenderRoot() {
|
|
929
|
+
return this;
|
|
930
|
+
}
|
|
931
|
+
willUpdate(changed) {
|
|
932
|
+
if (changed.has("props")) {
|
|
933
|
+
__privateMethod(this, _ProductHeroLit_instances, startBind_fn4).call(this);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
disconnectedCallback() {
|
|
937
|
+
super.disconnectedCallback();
|
|
938
|
+
__privateGet(this, _controller4)?.destroy();
|
|
939
|
+
}
|
|
940
|
+
render() {
|
|
941
|
+
if (this.validationError || !this.props) return html4``;
|
|
942
|
+
const parsed = heroSchema.safeParse(this.props);
|
|
943
|
+
if (!parsed.success) return html4``;
|
|
944
|
+
const { product, layout, longCopy } = parsed.data;
|
|
945
|
+
const resolved = this.resolved[product.id];
|
|
946
|
+
const imgSrc = resolved?.image ?? product.image.src;
|
|
947
|
+
const priceText = resolved?.price ?? product.price?.amount;
|
|
948
|
+
return html4`
|
|
949
|
+
<section class="sc-product-hero" data-hero-layout=${layout}>
|
|
950
|
+
<img
|
|
951
|
+
class="sc-product-hero__image"
|
|
952
|
+
src=${imgSrc}
|
|
953
|
+
alt=${product.image.alt}
|
|
954
|
+
loading="lazy"
|
|
955
|
+
/>
|
|
956
|
+
<div class="sc-product-hero__content">
|
|
957
|
+
<h2 class="sc-product-hero__name">${product.name}</h2>
|
|
958
|
+
${product.tagline ? html4`<p class="sc-product-hero__tagline">${product.tagline}</p>` : nothing4}
|
|
959
|
+
${product.price ? html4`<p class="sc-product-hero__price">
|
|
960
|
+
${priceText}${product.price.cadence ? html4` <small>${product.price.cadence}</small>` : nothing4}
|
|
961
|
+
</p>` : nothing4}
|
|
962
|
+
${longCopy ? html4`<div
|
|
963
|
+
data-hero-longcopy
|
|
964
|
+
class="sc-product-hero__longcopy"
|
|
965
|
+
.innerHTML=${sanitizeHtml(longCopy)}
|
|
966
|
+
></div>` : nothing4}
|
|
967
|
+
<div class="sc-product-hero__ctas">
|
|
968
|
+
${product.ctas.map(
|
|
969
|
+
(c, i) => html4`<a
|
|
970
|
+
class="sc-product-hero__cta"
|
|
971
|
+
data-product-cta=${i === 0 ? "primary" : "secondary"}
|
|
972
|
+
data-variant=${c.variant}
|
|
973
|
+
href=${c.href ?? "#"}
|
|
974
|
+
target=${c.target}
|
|
975
|
+
rel=${c.target === "_blank" ? "noopener noreferrer" : ""}
|
|
976
|
+
>${c.label}</a
|
|
977
|
+
>`
|
|
978
|
+
)}
|
|
979
|
+
</div>
|
|
980
|
+
</div>
|
|
981
|
+
</section>
|
|
982
|
+
`;
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
_controller4 = new WeakMap();
|
|
986
|
+
_ProductHeroLit_instances = new WeakSet();
|
|
987
|
+
startBind_fn4 = function() {
|
|
988
|
+
__privateGet(this, _controller4)?.destroy();
|
|
989
|
+
this.resolved = {};
|
|
990
|
+
if (!this.props) {
|
|
991
|
+
this.validationError = false;
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const result = heroSchema.safeParse(this.props);
|
|
995
|
+
if (!result.success) {
|
|
996
|
+
this.validationError = true;
|
|
997
|
+
console.warn("[adaptive-product] invalid hero props:", result.error.issues);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
this.validationError = false;
|
|
1001
|
+
__privateSet(this, _controller4, new BindController([result.data.product], (productId, kind, value) => {
|
|
1002
|
+
if (value == null) return;
|
|
1003
|
+
this.resolved = {
|
|
1004
|
+
...this.resolved,
|
|
1005
|
+
[productId]: { ...this.resolved[productId], [kind]: value }
|
|
1006
|
+
};
|
|
1007
|
+
}));
|
|
1008
|
+
__privateGet(this, _controller4).start();
|
|
1009
|
+
};
|
|
1010
|
+
ProductHeroLit.properties = {
|
|
1011
|
+
props: { attribute: false },
|
|
1012
|
+
resolved: { state: true },
|
|
1013
|
+
validationError: { state: true }
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
// src/runtime.ts
|
|
1017
|
+
var TAGS = {
|
|
1018
|
+
card: "syntro-product-card",
|
|
1019
|
+
comparison: "syntro-product-comparison",
|
|
1020
|
+
hero: "syntro-product-hero",
|
|
1021
|
+
grid: "syntro-product-grid"
|
|
1022
|
+
};
|
|
1023
|
+
if (typeof customElements !== "undefined") {
|
|
1024
|
+
if (!customElements.get(TAGS.card)) customElements.define(TAGS.card, ProductCardLit);
|
|
1025
|
+
if (!customElements.get(TAGS.comparison))
|
|
1026
|
+
customElements.define(TAGS.comparison, ProductComparisonLit);
|
|
1027
|
+
if (!customElements.get(TAGS.hero)) customElements.define(TAGS.hero, ProductHeroLit);
|
|
1028
|
+
if (!customElements.get(TAGS.grid)) customElements.define(TAGS.grid, ProductGridLit);
|
|
1029
|
+
}
|
|
1030
|
+
function makeMountable(tag) {
|
|
1031
|
+
return {
|
|
1032
|
+
mount(container, config) {
|
|
1033
|
+
const widgetProps = stripMountPlumbing(config ?? null);
|
|
1034
|
+
const el = document.createElement(tag);
|
|
1035
|
+
el.props = widgetProps;
|
|
1036
|
+
container.appendChild(el);
|
|
1037
|
+
return () => el.remove();
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
var ProductCardMountable = makeMountable(TAGS.card);
|
|
1042
|
+
var ProductComparisonMountable = makeMountable(TAGS.comparison);
|
|
1043
|
+
var ProductHeroMountable = makeMountable(TAGS.hero);
|
|
1044
|
+
var ProductGridMountable = makeMountable(TAGS.grid);
|
|
1045
|
+
var runtime = {
|
|
1046
|
+
id: "adaptive-product",
|
|
1047
|
+
version: "1.0.0",
|
|
1048
|
+
name: "Product",
|
|
1049
|
+
description: "Product surfaces: cards, side-by-side comparisons, featured hero, and grid lists. Optional bind for price/image to host-page sources (DOM, Shopify, WooCommerce, JSON-LD, OpenGraph).",
|
|
1050
|
+
executors: [],
|
|
1051
|
+
widgets: [
|
|
1052
|
+
{
|
|
1053
|
+
id: "adaptive-product:card",
|
|
1054
|
+
component: ProductCardMountable,
|
|
1055
|
+
metadata: {
|
|
1056
|
+
name: "Product Card",
|
|
1057
|
+
description: "Single product card",
|
|
1058
|
+
icon: "\u{1F6CD}\uFE0F"
|
|
1059
|
+
}
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
id: "adaptive-product:comparison",
|
|
1063
|
+
component: ProductComparisonMountable,
|
|
1064
|
+
metadata: {
|
|
1065
|
+
name: "Product Comparison",
|
|
1066
|
+
description: "2\u20134 products, expandable rows",
|
|
1067
|
+
icon: "\u2696\uFE0F"
|
|
1068
|
+
}
|
|
1069
|
+
},
|
|
1070
|
+
{
|
|
1071
|
+
id: "adaptive-product:hero",
|
|
1072
|
+
component: ProductHeroMountable,
|
|
1073
|
+
metadata: {
|
|
1074
|
+
name: "Product Hero",
|
|
1075
|
+
description: "Featured product with long copy",
|
|
1076
|
+
icon: "\u2B50"
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
id: "adaptive-product:grid",
|
|
1081
|
+
component: ProductGridMountable,
|
|
1082
|
+
metadata: {
|
|
1083
|
+
name: "Product Grid",
|
|
1084
|
+
description: "2\u201312 products in a grid",
|
|
1085
|
+
icon: "\u{1F5C2}\uFE0F"
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
]
|
|
1089
|
+
};
|
|
1090
|
+
var runtime_default = runtime;
|
|
1091
|
+
|
|
1092
|
+
export {
|
|
1093
|
+
ProductCardMountable,
|
|
1094
|
+
ProductComparisonMountable,
|
|
1095
|
+
ProductHeroMountable,
|
|
1096
|
+
ProductGridMountable,
|
|
1097
|
+
runtime,
|
|
1098
|
+
runtime_default
|
|
1099
|
+
};
|
|
1100
|
+
//# sourceMappingURL=chunk-E2IVTQMX.js.map
|