@syntrologie/adapt-product 2.41.3 → 2.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cdn.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  onActivate,
3
3
  runtime
4
- } from "./chunk-A6OGE4OG.js";
4
+ } from "./chunk-6DWXPP5X.js";
5
5
  import "./chunk-Y2QAYCSK.js";
6
6
 
7
7
  // src/cdn.ts
@@ -21,6 +21,21 @@ function n(n2) {
21
21
  // ../../sdk-contracts/dist/canvas-context.js
22
22
  var canvasRuntimeContext = n("syntrologie:canvas-runtime");
23
23
 
24
+ // ../../sdk-contracts/dist/detector-events.js
25
+ var DETECTOR_EVENT_NAMES = [
26
+ "ui.hover",
27
+ "ui.idle",
28
+ "ui.hesitate",
29
+ "ui.rage_click",
30
+ "ui.scroll_thrash",
31
+ "ui.focus_bounce"
32
+ ];
33
+ var CANONICAL_BUS_EVENT_NAMES = [
34
+ ...DETECTOR_EVENT_NAMES,
35
+ "nav.section_viewed",
36
+ "nav.scroll_depth"
37
+ ];
38
+
24
39
  // ../../sdk-contracts/dist/dive-deeper.js
25
40
  var DIVE_DEEPER_EVENT = "syntro:chat:dive-deeper";
26
41
  function dispatchDiveDeeper(detail) {
@@ -43,7 +58,103 @@ function stripMountPlumbing(config) {
43
58
  }
44
59
 
45
60
  // ../../sdk-contracts/dist/routes.js
61
+ var RESERVED_BYTES = /* @__PURE__ */ new Set([
62
+ 33,
63
+ // !
64
+ 35,
65
+ // #
66
+ 36,
67
+ // $
68
+ 38,
69
+ // &
70
+ 39,
71
+ // '
72
+ 40,
73
+ // (
74
+ 41,
75
+ // )
76
+ 42,
77
+ // *
78
+ 43,
79
+ // +
80
+ 44,
81
+ // ,
82
+ 47,
83
+ // /
84
+ 58,
85
+ // :
86
+ 59,
87
+ // ;
88
+ 61,
89
+ // =
90
+ 63,
91
+ // ?
92
+ 64,
93
+ // @
94
+ 91,
95
+ // [
96
+ 93
97
+ // ]
98
+ ]);
46
99
  var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
100
+ function decodeUnreservedOnly(input) {
101
+ let out = "";
102
+ let pending2 = [];
103
+ const flushPending = () => {
104
+ if (pending2.length === 0)
105
+ return;
106
+ const bytes = new Uint8Array(pending2);
107
+ out += utf8Decoder.decode(bytes);
108
+ pending2 = [];
109
+ };
110
+ let i2 = 0;
111
+ while (i2 < input.length) {
112
+ const ch = input[i2];
113
+ if (ch === "%" && i2 + 2 < input.length && isHex(input[i2 + 1]) && isHex(input[i2 + 2])) {
114
+ const byte = parseInt(input.slice(i2 + 1, i2 + 3), 16);
115
+ if (RESERVED_BYTES.has(byte)) {
116
+ flushPending();
117
+ out += `%${input.slice(i2 + 1, i2 + 3).toUpperCase()}`;
118
+ i2 += 3;
119
+ } else {
120
+ pending2.push(byte);
121
+ i2 += 3;
122
+ }
123
+ } else {
124
+ flushPending();
125
+ out += ch;
126
+ i2 += 1;
127
+ }
128
+ }
129
+ flushPending();
130
+ return out;
131
+ }
132
+ function isHex(c2) {
133
+ return c2 >= "0" && c2 <= "9" || c2 >= "a" && c2 <= "f" || c2 >= "A" && c2 <= "F";
134
+ }
135
+ function stripQueryAndHash(s4) {
136
+ const q = s4.indexOf("?");
137
+ if (q !== -1)
138
+ s4 = s4.slice(0, q);
139
+ const h = s4.indexOf("#");
140
+ if (h !== -1)
141
+ s4 = s4.slice(0, h);
142
+ return s4;
143
+ }
144
+ function normalizeRoute(path) {
145
+ if (typeof path !== "string" || path.length === 0) {
146
+ throw new TypeError("normalizeRoute: input must be a non-empty string");
147
+ }
148
+ if (!path.startsWith("/")) {
149
+ throw new TypeError(`normalizeRoute: input must be absolute (start with '/'); got ${JSON.stringify(path)}`);
150
+ }
151
+ let s4 = stripQueryAndHash(path);
152
+ s4 = decodeUnreservedOnly(s4);
153
+ s4 = s4.replace(/\/+/g, "/");
154
+ if (s4.length > 1 && s4.endsWith("/"))
155
+ s4 = s4.slice(0, -1);
156
+ return s4;
157
+ }
47
158
 
48
159
  // ../../sdk-contracts/dist/schemas.js
49
160
  import { z } from "zod";
@@ -52,7 +163,26 @@ var AnchorIdZ = z.object({
52
163
  selector: z.string().regex(NO_CSS_BREAKOUT_PATTERN, {
53
164
  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.'
54
165
  }).describe("CSS selector for the target element"),
55
- route: z.union([z.string(), z.array(z.string())]).describe("URL path(s) where this element exists")
166
+ route: z.union([z.string(), z.array(z.string())]).superRefine((value, ctx) => {
167
+ for (const route of Array.isArray(value) ? value : [value]) {
168
+ let canonical;
169
+ try {
170
+ canonical = normalizeRoute(route);
171
+ } catch (err) {
172
+ ctx.addIssue({
173
+ code: z.ZodIssueCode.custom,
174
+ message: `route must be an absolute path starting with "/" (got ${JSON.stringify(route)}): ${err instanceof Error ? err.message : String(err)}`
175
+ });
176
+ continue;
177
+ }
178
+ if (canonical !== route) {
179
+ ctx.addIssue({
180
+ code: z.ZodIssueCode.custom,
181
+ message: `route ${JSON.stringify(route)} is not canonical \u2014 use ${JSON.stringify(canonical)} (this must match what the backend's RouteCanonicalityCheck accepts)`
182
+ });
183
+ }
184
+ }
185
+ }).describe("URL path(s) where this element exists")
56
186
  }).strict().describe("DOM element target. selector = CSS selector, route = URL path(s) where the element exists.");
57
187
  var AuthoringFieldsZ = {
58
188
  id: z.string().optional().describe('Stable action identifier (e.g. "act_3db6a14d2ab0").'),
@@ -4248,6 +4378,14 @@ resolvePrice_fn = async function(productId, bind) {
4248
4378
  };
4249
4379
 
4250
4380
  // src/decision/card.ts
4381
+ var VARIANT_ERROR_COPY = {
4382
+ retryable: "Couldn't load options right now.",
4383
+ terminal: "Options aren't available for this item right now."
4384
+ };
4385
+ var NON_RETRYABLE_VARIANT_ERROR_NAME = "ProductNotFoundError";
4386
+ function isRetryableVariantError(errorName) {
4387
+ return errorName !== NON_RETRYABLE_VARIANT_ERROR_NAME;
4388
+ }
4251
4389
  function createInitialCardState(generation = 0) {
4252
4390
  return {
4253
4391
  generation,
@@ -4270,18 +4408,23 @@ function selectCardView(state) {
4270
4408
  mode: "add",
4271
4409
  disabled: state.connection !== "connected" || state.commerce !== "available" || state.product.status !== "valid"
4272
4410
  };
4273
- let panel = { status: "hidden", payload: null, error: null };
4411
+ let panel = { status: "hidden", payload: null, error: null, retryable: false };
4274
4412
  if (state.face === "back") {
4275
4413
  switch (state.variants.status) {
4276
4414
  case "ready":
4277
- panel = { status: "ready", payload: state.variants.payload, error: null };
4415
+ panel = { status: "ready", payload: state.variants.payload, error: null, retryable: false };
4278
4416
  break;
4279
4417
  case "error":
4280
- panel = { status: "error", payload: null, error: state.variants.message };
4418
+ panel = {
4419
+ status: "error",
4420
+ payload: null,
4421
+ error: state.variants.retryable ? VARIANT_ERROR_COPY.retryable : VARIANT_ERROR_COPY.terminal,
4422
+ retryable: state.variants.retryable
4423
+ };
4281
4424
  break;
4282
4425
  case "idle":
4283
4426
  case "loading":
4284
- panel = { status: "loading", payload: null, error: null };
4427
+ panel = { status: "loading", payload: null, error: null, retryable: false };
4285
4428
  break;
4286
4429
  }
4287
4430
  }
@@ -4300,7 +4443,7 @@ function replaceProps(state, event) {
4300
4443
  const sameProduct = event.valid && event.productId !== null && state.product.status === "valid" && state.product.id === event.productId;
4301
4444
  if (sameProduct && event.productId !== null) {
4302
4445
  const effects2 = [{ type: "bind-product", productId: event.productId }];
4303
- if (state.variants.status === "idle") {
4446
+ if (event.commerceCapable && state.variants.status === "idle") {
4304
4447
  effects2.push({
4305
4448
  type: "lookup-variants",
4306
4449
  generation: state.generation,
@@ -4322,15 +4465,15 @@ function replaceProps(state, event) {
4322
4465
  effects
4323
4466
  };
4324
4467
  }
4325
- effects.push(
4326
- { type: "bind-product", productId: event.productId },
4327
- { type: "lookup-variants", generation, productId: event.productId },
4328
- { type: "report-render-health" }
4329
- );
4468
+ effects.push({ type: "bind-product", productId: event.productId });
4469
+ if (event.commerceCapable) {
4470
+ effects.push({ type: "lookup-variants", generation, productId: event.productId });
4471
+ }
4472
+ effects.push({ type: "report-render-health" });
4330
4473
  return {
4331
4474
  state: {
4332
4475
  ...next,
4333
- product: { status: "valid", id: event.productId }
4476
+ product: { status: "valid", id: event.productId, commerceCapable: event.commerceCapable }
4334
4477
  },
4335
4478
  effects
4336
4479
  };
@@ -4339,7 +4482,12 @@ function publish(name, props) {
4339
4482
  return { type: "publish-event", name, props };
4340
4483
  }
4341
4484
  function activatePrimary(state, event) {
4342
- if (state.connection !== "connected" || state.commerce !== "available" || state.product.status !== "valid" || state.face !== "front") {
4485
+ if (state.connection !== "connected" || state.commerce !== "available" || state.product.status !== "valid" || // Defense in depth: the widget only wires this event to an
4486
+ // actionId==='add_to_cart' CTA click (a non-commerce/booking product's
4487
+ // CTA is a plain navigating <a>, never dispatched here) — but the state
4488
+ // machine itself must also refuse to start the Woo/Shopify variant-
4489
+ // lookup flow for a product that has none.
4490
+ !state.product.commerceCapable || state.face !== "front") {
4343
4491
  return unchanged2(state);
4344
4492
  }
4345
4493
  const productId = state.product.id;
@@ -4490,7 +4638,11 @@ function transitionCard(state, event) {
4490
4638
  });
4491
4639
  const next = {
4492
4640
  ...state,
4493
- variants: { status: "error", message: event.message }
4641
+ variants: {
4642
+ status: "error",
4643
+ message: event.message,
4644
+ retryable: isRetryableVariantError(event.errorName)
4645
+ }
4494
4646
  };
4495
4647
  if (state.activation.status !== "pending") {
4496
4648
  return { state: next, effects: [failureEffect] };
@@ -4524,7 +4676,10 @@ function transitionCard(state, event) {
4524
4676
  case "add-failed":
4525
4677
  return applyAddResult(state, event);
4526
4678
  case "retry-activated":
4527
- if (state.product.status !== "valid" || state.commerce !== "available" || state.variants.status !== "error") {
4679
+ if (state.product.status !== "valid" || state.commerce !== "available" || state.variants.status !== "error" || // The panel never OFFERS a retry affordance when the failure isn't
4680
+ // retryable (see selectCardView), but a stale/duplicate dispatch of
4681
+ // this event must still be a no-op — it can never succeed.
4682
+ !state.variants.retryable) {
4528
4683
  return unchanged2(state);
4529
4684
  }
4530
4685
  return {
@@ -4571,7 +4726,12 @@ function transitionCard(state, event) {
4571
4726
  state: { ...state, generation, connection: "connected" },
4572
4727
  effects: state.product.status === "valid" && state.commerce === "available" ? [
4573
4728
  { type: "bind-product", productId: state.product.id },
4574
- { type: "lookup-variants", generation, productId: state.product.id },
4729
+ // Same commerce gate as replaceProps — a booking-fulfilment
4730
+ // product never re-fires the Woo/Shopify variant lookup,
4731
+ // including on the reconnect prefetch.
4732
+ ...state.product.commerceCapable ? [
4733
+ { type: "lookup-variants", generation, productId: state.product.id }
4734
+ ] : [],
4575
4735
  { type: "report-render-health" }
4576
4736
  ] : []
4577
4737
  };
@@ -5241,7 +5401,17 @@ var _VariantPanelLit = class _VariantPanelLit extends LitElement7 {
5241
5401
  * used as the fallback when the selected variant carries no featured_image. */
5242
5402
  this.productImage = null;
5243
5403
  this.state = "idle";
5404
+ /** Already sanitized, visitor-safe copy by the time it reaches this
5405
+ * component — the decision core (`card.ts`'s `selectCardView`) NEVER
5406
+ * forwards the raw platform error text (which may name an internal
5407
+ * slug/handle, e.g. `WooCommerce product not found for slug "botox"`).
5408
+ * This component's only job is to render whatever generic string it's
5409
+ * given; it must never be handed a raw message to begin with. */
5244
5410
  this.error = null;
5411
+ /** Whether "Try again" is offered — false for a structurally dead-end
5412
+ * failure (retrying the identical query can never succeed) so the panel
5413
+ * never invites a retry that's already known to be pointless. */
5414
+ this.retryable = true;
5245
5415
  /** @internal */
5246
5416
  this._selection = {};
5247
5417
  /** @internal */
@@ -5335,8 +5505,10 @@ var _VariantPanelLit = class _VariantPanelLit extends LitElement7 {
5335
5505
  if (this.state === "error") {
5336
5506
  return html7`
5337
5507
  <div class="svp-error-block" data-error-block>
5338
- <div>Couldn't load options. ${this.error ?? ""}</div>
5339
- <button class="svp-retry-btn" data-retry-button @click=${this._onRetry}>Try again</button>
5508
+ <div>${this.error ?? "Couldn't load options."}</div>
5509
+ ${this.retryable ? html7`<button class="svp-retry-btn" data-retry-button @click=${this._onRetry}>
5510
+ Try again
5511
+ </button>` : nothing3}
5340
5512
  </div>
5341
5513
  `;
5342
5514
  }
@@ -5583,6 +5755,7 @@ _VariantPanelLit.properties = {
5583
5755
  productImage: { attribute: false },
5584
5756
  state: { type: String },
5585
5757
  error: { type: String },
5758
+ retryable: { type: Boolean },
5586
5759
  _selection: { state: true },
5587
5760
  _touched: { state: true },
5588
5761
  _expandedAxes: { state: true },
@@ -5821,6 +5994,7 @@ var ProductCardLit = class extends LitElement8 {
5821
5994
  .productImage=${product.image ? { src: product.image.src, alt: product.image.alt } : null}
5822
5995
  .state=${view.panel.status === "error" ? "error" : view.panel.status === "ready" ? "idle" : "loading"}
5823
5996
  .error=${view.panel.error}
5997
+ .retryable=${view.panel.retryable}
5824
5998
  @variant-panel-back=${this._onVariantBack}
5825
5999
  @variant-panel-add=${this._onVariantAdd}
5826
6000
  @variant-panel-retry=${this._onVariantRetry}
@@ -5961,7 +6135,14 @@ readProductObservation_fn = function() {
5961
6135
  const rawProductId = this.props?.product?.id;
5962
6136
  return {
5963
6137
  productId: parsed?.success ? parsed.data.product.id : typeof rawProductId === "string" ? rawProductId : null,
5964
- valid: parsed?.success === true
6138
+ valid: parsed?.success === true,
6139
+ // Does this product have a REAL Add-to-cart action? A booking-
6140
+ // fulfilment product (server-hydrated with a single "Book a consult"
6141
+ // href CTA — see product_hydration.build_product_from_hit) has none,
6142
+ // and its platform variant-lookup endpoint has nothing to find — the
6143
+ // decision core (card.ts) uses this to skip the Woo/Shopify variant
6144
+ // prefetch entirely for it, never just to disable a button.
6145
+ commerceCapable: parsed?.success === true && parsed.data.product.ctas.some((cta) => cta.actionId === "add_to_cart")
5965
6146
  };
5966
6147
  };
5967
6148
  syncDecisionInputsBeforeIntent_fn = function() {
@@ -5971,7 +6152,7 @@ syncDecisionInputsBeforeIntent_fn = function() {
5971
6152
  });
5972
6153
  const observation = __privateMethod(this, _ProductCardLit_instances, readProductObservation_fn).call(this);
5973
6154
  const product = this._decisionState.product;
5974
- const isCurrent = observation.valid === (product.status === "valid") && observation.productId === product.id;
6155
+ const isCurrent = observation.valid === (product.status === "valid") && observation.productId === product.id && (product.status !== "valid" || observation.commerceCapable === product.commerceCapable);
5975
6156
  if (!isCurrent) __privateMethod(this, _ProductCardLit_instances, dispatchCard_fn).call(this, { type: "props-replaced", ...observation });
5976
6157
  };
5977
6158
  ProductCardLit.properties = {
@@ -6398,9 +6579,16 @@ function renderProductCardInner(product, density, resolved, visibleFacts, ctas,
6398
6579
  style=${styleMap(ctaStyles)}
6399
6580
  @click=${disabled2 ? (e2) => {
6400
6581
  e2.preventDefault();
6401
- } : i2 === 0 && onPrimaryCtaClick ? onPrimaryCtaClick : c2.actionId === "add_to_cart" ? (e2) => {
6402
- e2.preventDefault();
6403
- } : void 0}
6582
+ } : (
6583
+ // The commerce (Add-to-cart / open-picker) intent wires ONLY to
6584
+ // an actual add_to_cart CTA. A plain navigating CTA — e.g. a
6585
+ // booking-fulfilment product's "Book a consult" href, which
6586
+ // sits at i===0 too — must fall through to its own <a href>
6587
+ // and never enter the Woo/Shopify variant-lookup flow.
6588
+ i2 === 0 && c2.actionId === "add_to_cart" && onPrimaryCtaClick ? onPrimaryCtaClick : c2.actionId === "add_to_cart" ? (e2) => {
6589
+ e2.preventDefault();
6590
+ } : void 0
6591
+ )}
6404
6592
  >${// Secondary "Product Page" link gets a trailing chevron (panel's
6405
6593
  // quiet-secondary affordance) unless the author already added one.
6406
6594
  i2 !== 0 && !/[›>→]\s*$/.test(ctaLabel) ? `${ctaLabel} \u203A` : ctaLabel}</a
@@ -11739,4 +11927,4 @@ export {
11739
11927
  * SPDX-License-Identifier: BSD-3-Clause
11740
11928
  *)
11741
11929
  */
11742
- //# sourceMappingURL=chunk-A6OGE4OG.js.map
11930
+ //# sourceMappingURL=chunk-6DWXPP5X.js.map