@gemboss/gem-framework 0.19.0 → 0.20.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/index.js CHANGED
@@ -435,7 +435,7 @@ function gemButtonGradientEndCss(accent = "var(--gem-color-accent)", accentText
435
435
  }
436
436
 
437
437
  // ../../packages/gem-framework/src/tokens.ts
438
- var gemFrameworkVersion = "0.19.0";
438
+ var gemFrameworkVersion = "0.20.0";
439
439
  var GEM_FOCUS_RING_CSS_VAR = "--gem-focus-ring: 2px solid var(--gem-color-accent);";
440
440
  var GEM_TAP_MIN_CSS_VAR = "--gem-tap-min: 44px;";
441
441
  var GEM_FOCUS_OFFSET_CSS_VAR = "--gem-focus-offset: 3px;";
@@ -2092,10 +2092,10 @@ function createPanel(parent, ctx, shared) {
2092
2092
  ctx.on(prev, "click", () => step(-1));
2093
2093
  ctx.on(next, "click", () => step(1));
2094
2094
  ctx.on(add, "click", addToCart);
2095
- for (const [group, attr] of [[swatches, "data-gem-quick-view-color"], [sizes, "data-gem-quick-view-size"]]) {
2095
+ for (const [group, attr5] of [[swatches, "data-gem-quick-view-color"], [sizes, "data-gem-quick-view-size"]]) {
2096
2096
  ctx.on(group, "click", (event) => {
2097
- const b = event.target.closest && event.target.closest(`[${attr}]`);
2098
- if (b && !b.disabled) pick(group, Number(b.getAttribute(attr)));
2097
+ const b = event.target.closest && event.target.closest(`[${attr5}]`);
2098
+ if (b && !b.disabled) pick(group, Number(b.getAttribute(attr5)));
2099
2099
  });
2100
2100
  }
2101
2101
  ctx.on(dialog, "cancel", (event) => {
@@ -2456,6 +2456,787 @@ function gemMotionChunks(legacyQuickView) {
2456
2456
  };
2457
2457
  }
2458
2458
 
2459
+ // ../../packages/gem-framework/src/pod/core.js
2460
+ var FIELD = "[data-gem-personalize]";
2461
+ var SCOPES = ["[data-gem-personalize-scope]", '[data-gem-element="product"]', 'form[action*="/cart/add"]', "[data-gem-section-id]"];
2462
+ function scopeOf(el) {
2463
+ for (const selector of SCOPES) {
2464
+ const found = el && el.closest ? el.closest(selector) : null;
2465
+ if (found) return found;
2466
+ }
2467
+ return el && el.ownerDocument ? el.ownerDocument.documentElement : null;
2468
+ }
2469
+ var uploads = /* @__PURE__ */ new WeakMap();
2470
+ var attr = (el, name) => el && el.getAttribute ? el.getAttribute(name) : null;
2471
+ var trim = (value) => typeof value === "string" ? value.trim() : "";
2472
+ function fieldName(field) {
2473
+ const own = trim(attr(field, "data-gem-personalize-name"));
2474
+ if (own) return own;
2475
+ const control = field.querySelector && field.querySelector("input, select, textarea");
2476
+ return control ? trim(control.getAttribute("name")) : "";
2477
+ }
2478
+ function fieldType(field) {
2479
+ const own = trim(attr(field, "data-gem-personalize-type"));
2480
+ if (own) return own;
2481
+ const control = field.querySelector && field.querySelector("input, select, textarea");
2482
+ if (!control) return "text";
2483
+ if (control.tagName === "SELECT") return "select";
2484
+ if (control.tagName === "TEXTAREA") return "textarea";
2485
+ return control.type === "file" ? "upload" : control.type === "checkbox" ? "checkbox" : control.type === "radio" ? "swatch" : "text";
2486
+ }
2487
+ var controlsOf = (field) => Array.prototype.slice.call(field.querySelectorAll ? field.querySelectorAll("input, select, textarea") : []).filter((c) => c.type !== "hidden");
2488
+ var HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
2489
+ var hexColor = (value) => HEX.test(trim(value)) ? trim(value) : "";
2490
+ function readField(field) {
2491
+ const name = fieldName(field);
2492
+ const type = fieldType(field);
2493
+ const out = { name, type, value: "", color: "", preview: "", key: "" };
2494
+ const controls = controlsOf(field);
2495
+ if (type === "upload") {
2496
+ const state = uploads.get(field);
2497
+ if (state) Object.assign(out, { value: state.url || "", preview: state.preview || state.url || "", key: state.key || "" });
2498
+ return out;
2499
+ }
2500
+ if (type === "checkbox") {
2501
+ const box2 = controls.find((c) => c.type === "checkbox" && c.checked);
2502
+ if (box2) out.value = box2.value && box2.value !== "on" ? box2.value : "Yes";
2503
+ return out;
2504
+ }
2505
+ if (type === "select") {
2506
+ const select = controls.find((c) => c.tagName === "SELECT");
2507
+ const option = select && select.selectedIndex > -1 ? select.options[select.selectedIndex] : null;
2508
+ out.value = trim(select ? select.value : "");
2509
+ out.color = hexColor(attr(option, "data-gem-personalize-color"));
2510
+ return out;
2511
+ }
2512
+ const radios = controls.filter((c) => c.type === "radio");
2513
+ if (radios.length) {
2514
+ const picked = radios.find((c) => c.checked);
2515
+ if (picked) {
2516
+ out.value = trim(picked.value);
2517
+ const label = picked.closest ? picked.closest("label") : null;
2518
+ out.color = hexColor(attr(picked, "data-gem-personalize-color") || attr(label, "data-gem-personalize-color"));
2519
+ }
2520
+ return out;
2521
+ }
2522
+ const box = controls[0];
2523
+ out.value = trim(box ? box.value : "");
2524
+ return out;
2525
+ }
2526
+ function readScope(scope) {
2527
+ const values = {};
2528
+ const fields = scope && scope.querySelectorAll ? scope.querySelectorAll(FIELD) : [];
2529
+ Array.prototype.forEach.call(fields, (field) => {
2530
+ const read = readField(field);
2531
+ if (read.name) values[read.name] = field.hidden ? { ...read, value: "", color: "", preview: "" } : read;
2532
+ });
2533
+ return values;
2534
+ }
2535
+ var adopted = /* @__PURE__ */ new WeakMap();
2536
+ function adoptCss(el, key, sentinel, css) {
2537
+ const doc = el.ownerDocument;
2538
+ const view = doc && doc.defaultView;
2539
+ if (!css || !view) return false;
2540
+ let keys = adopted.get(doc);
2541
+ if (!keys) adopted.set(doc, keys = /* @__PURE__ */ new Set());
2542
+ if (keys.has(key)) return false;
2543
+ keys.add(key);
2544
+ try {
2545
+ if (view.getComputedStyle(el).getPropertyValue(sentinel).trim()) return false;
2546
+ const sheet = new view.CSSStyleSheet();
2547
+ sheet.replaceSync(css);
2548
+ doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, sheet];
2549
+ return true;
2550
+ } catch {
2551
+ return false;
2552
+ }
2553
+ }
2554
+
2555
+ // ../../packages/gem-framework/src/pod/personalize.js
2556
+ var PERSONALIZE_MESSAGES = {
2557
+ required: "Fill this in to add to cart.",
2558
+ type: "This file type is not accepted.",
2559
+ size: "This file is over {mb} MB.",
2560
+ failed: "The upload failed. Try again.",
2561
+ uploading: "Wait for the upload to finish.",
2562
+ unavailable: "Photo upload is not available."
2563
+ };
2564
+ var WHEN = /^\s*(!)?\s*([^!=|]+?)\s*(?:(!=|==|=)\s*(.+?))?\s*$/;
2565
+ function parsePersonalizeWhen(text2) {
2566
+ if (typeof text2 !== "string" || !text2.trim()) return null;
2567
+ const m = WHEN.exec(text2);
2568
+ if (!m) return null;
2569
+ const [, not, name, op, rhs] = m;
2570
+ if (!op) return { name: name.trim(), op: not ? "unset" : "set", values: [] };
2571
+ const values = rhs.split("|").map((v) => v.trim().toLowerCase()).filter(Boolean);
2572
+ if (not || !values.length) return null;
2573
+ return { name: name.trim(), op: op === "!=" ? "not" : "is", values };
2574
+ }
2575
+ function personalizeWhenHolds(cond, values) {
2576
+ const key = Object.keys(values).find((k) => k.toLowerCase() === cond.name.toLowerCase());
2577
+ const value = key ? String(values[key].value || "").trim().toLowerCase() : "";
2578
+ if (cond.op === "set") return value !== "";
2579
+ if (cond.op === "unset") return value === "";
2580
+ return cond.op === "is" ? cond.values.includes(value) : !cond.values.includes(value);
2581
+ }
2582
+ function fileAccepted(file, accept) {
2583
+ const tokens = String(accept || "").split(",").map((t) => t.trim().toLowerCase()).filter(Boolean);
2584
+ if (!tokens.length) return true;
2585
+ const type = String(file.type || "").toLowerCase();
2586
+ const name = String(file.name || "").toLowerCase();
2587
+ return tokens.some((t) => t.startsWith(".") ? name.endsWith(t) : t.endsWith("/*") ? type.startsWith(t.slice(0, -1)) : type === t);
2588
+ }
2589
+ var attr2 = (el, name) => el && el.getAttribute ? el.getAttribute(name) : null;
2590
+ var tail = (id) => String(id).split("/").pop();
2591
+ var LISTS = '[data-gem-element="product-related"], [data-gem-element="product-list"]';
2592
+ function mountPersonalize(env, onUnmount) {
2593
+ const { window: win, document: doc, on, mark, api } = env;
2594
+ const owned = /* @__PURE__ */ new Set();
2595
+ const objectUrls = /* @__PURE__ */ new Set();
2596
+ const aborts = /* @__PURE__ */ new Set();
2597
+ let styled = false;
2598
+ let alive = true;
2599
+ const scopeFields = (scope) => [...owned].filter((f) => f.isConnected && scopeOf(f) === scope);
2600
+ const isRequired = (field) => attr2(field, "data-gem-personalize-required") === "true" || controlsOf(field).some((c) => c.required);
2601
+ const message = (field, reason, fill = {}) => {
2602
+ const own = attr2(field, `data-gem-personalize-message-${reason}`) || attr2(scopeOf(field), `data-gem-personalize-message-${reason}`);
2603
+ return (own || PERSONALIZE_MESSAGES[reason] || "").replace(/\{(\w+)\}/g, (_, k) => k in fill ? String(fill[k]) : "");
2604
+ };
2605
+ const errorBox = (field) => {
2606
+ let box = field.querySelector("[data-gem-personalize-error]");
2607
+ if (!box) {
2608
+ box = doc.createElement("p");
2609
+ box.className = "gem-el-product-personalization__error";
2610
+ box.setAttribute("data-gem-personalize-error", "");
2611
+ box.setAttribute("role", "alert");
2612
+ field.appendChild(box);
2613
+ }
2614
+ if (!box.id) box.id = `gem-personalize-error-${Math.random().toString(36).slice(2, 9)}`;
2615
+ return box;
2616
+ };
2617
+ const showProblem = (field, reason, fill) => {
2618
+ const box = errorBox(field);
2619
+ box.textContent = message(field, reason, fill);
2620
+ box.hidden = false;
2621
+ field.setAttribute("data-gem-personalize-invalid", reason);
2622
+ controlsOf(field).forEach((c) => {
2623
+ c.setAttribute("aria-invalid", "true");
2624
+ const ids = (attr2(c, "aria-describedby") || "").split(/\s+/).filter(Boolean);
2625
+ if (!ids.includes(box.id)) c.setAttribute("aria-describedby", [...ids, box.id].join(" "));
2626
+ });
2627
+ };
2628
+ const clearProblem = (field) => {
2629
+ if (!field.hasAttribute("data-gem-personalize-invalid")) return;
2630
+ field.removeAttribute("data-gem-personalize-invalid");
2631
+ const box = field.querySelector("[data-gem-personalize-error]");
2632
+ if (box) {
2633
+ box.textContent = "";
2634
+ box.hidden = true;
2635
+ }
2636
+ controlsOf(field).forEach((c) => c.removeAttribute("aria-invalid"));
2637
+ };
2638
+ const emitChange = (field) => {
2639
+ const { name, value, type, color: color2, preview } = readField(field);
2640
+ field.dispatchEvent(new win.CustomEvent("gem:personalize:change", { bubbles: true, detail: { name, value, type, color: color2, preview } }));
2641
+ };
2642
+ const count = (field) => {
2643
+ const out = field.querySelector("[data-gem-personalize-count]");
2644
+ const box = controlsOf(field).find((c) => c.tagName === "TEXTAREA" || c.type === "text" || c.type === "");
2645
+ const max = Number(attr2(field, "data-gem-personalize-max")) || (box && box.maxLength > 0 ? box.maxLength : 0);
2646
+ if (!out || !box || !max) return;
2647
+ out.textContent = `${box.value.length}/${max}`;
2648
+ if (box.value.length >= max) out.setAttribute("data-gem-personalize-full", "");
2649
+ else out.removeAttribute("data-gem-personalize-full");
2650
+ };
2651
+ const applyConditions = (scope) => {
2652
+ const fields = scopeFields(scope).filter((f) => attr2(f, "data-gem-personalize-when"));
2653
+ for (let pass = 0; pass < 4; pass += 1) {
2654
+ const values = readScope(scope);
2655
+ let changed = false;
2656
+ for (const field of fields) {
2657
+ if (attr2(field, "data-gem-personalize-state") === "unavailable") continue;
2658
+ const cond = parsePersonalizeWhen(attr2(field, "data-gem-personalize-when"));
2659
+ if (!cond) {
2660
+ field.setAttribute("data-gem-personalize-when-invalid", "");
2661
+ continue;
2662
+ }
2663
+ const show = personalizeWhenHolds(cond, values);
2664
+ if (field.hidden === !show) continue;
2665
+ field.hidden = !show;
2666
+ controlsOf(field).forEach((c) => {
2667
+ c.disabled = !show;
2668
+ });
2669
+ if (!show) clearProblem(field);
2670
+ changed = true;
2671
+ }
2672
+ if (!changed) break;
2673
+ }
2674
+ };
2675
+ const claim = (field) => {
2676
+ if (!mark(field, "personalize")) return false;
2677
+ owned.add(field);
2678
+ if (!styled && env.css) {
2679
+ adoptCss(field, "personalize", "--gem-personalize-css", env.css);
2680
+ styled = true;
2681
+ }
2682
+ const max = Number(attr2(field, "data-gem-personalize-max"));
2683
+ controlsOf(field).forEach((c) => {
2684
+ if (max > 0 && (c.tagName === "TEXTAREA" || c.type === "text") && !(c.maxLength > 0)) c.maxLength = max;
2685
+ });
2686
+ count(field);
2687
+ if (fieldType(field) === "upload" && !endpointOf(field)) unavailable(field);
2688
+ return true;
2689
+ };
2690
+ const claimAll = () => {
2691
+ const fresh = Array.prototype.filter.call(doc.querySelectorAll(FIELD), claim);
2692
+ new Set(fresh.map(scopeOf)).forEach(applyConditions);
2693
+ return fresh.length;
2694
+ };
2695
+ const forget = (field) => {
2696
+ const state = uploads.get(field);
2697
+ if (state && state.preview && objectUrls.delete(state.preview) && win.URL && win.URL.revokeObjectURL) win.URL.revokeObjectURL(state.preview);
2698
+ uploads.delete(field);
2699
+ field.removeAttribute("data-gem-personalize-state");
2700
+ const thumb = field.querySelector("[data-gem-personalize-thumb]");
2701
+ if (thumb) {
2702
+ thumb.removeAttribute("src");
2703
+ thumb.hidden = true;
2704
+ }
2705
+ };
2706
+ const endpointOf = (field) => {
2707
+ const gem = win.GemTheme || {};
2708
+ const url = attr2(field, "data-gem-personalize-endpoint") || attr2(scopeOf(field), "data-gem-personalize-endpoint") || api && api.uploadEndpoint || gem.uploadEndpoint;
2709
+ return typeof url === "string" && url.trim() ? url.trim() : "";
2710
+ };
2711
+ const unavailable = (field) => {
2712
+ forget(field);
2713
+ clearProblem(field);
2714
+ field.setAttribute("data-gem-personalize-state", "unavailable");
2715
+ field.hidden = true;
2716
+ controlsOf(field).forEach((c) => {
2717
+ c.disabled = true;
2718
+ });
2719
+ emitChange(field);
2720
+ };
2721
+ const json = (res) => res && typeof res.json === "function" ? res.json().catch(() => ({})) : Promise.resolve({});
2722
+ const failure = (reason, extra = {}) => Object.assign(new Error(reason), { reason, ...extra });
2723
+ const send = (endpoint, field, file, mb, signal) => {
2724
+ const fetch = (url, init) => win.fetch(url, { ...init, ...signal ? { signal } : {} });
2725
+ return fetch(endpoint, { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json", accept: "application/json" }, body: JSON.stringify({ type: file.type, size: file.size, max_mb: mb }) }).then((res) => json(res).then((answer) => {
2726
+ if (res.status === 404) throw failure("unavailable");
2727
+ if (res.status === 415) throw failure("type");
2728
+ if (res.status === 413) throw failure("size", { mb: Number(answer.max_mb) || mb });
2729
+ if (!res.ok) throw failure("failed");
2730
+ const put = answer && answer.upload;
2731
+ if (put && typeof put.url === "string") {
2732
+ const headers = put.headers && typeof put.headers === "object" ? put.headers : {};
2733
+ return fetch(put.url, { method: typeof put.method === "string" ? put.method : "PUT", headers, body: file, credentials: "omit" }).then((done) => {
2734
+ if (!done || !done.ok) throw failure("failed");
2735
+ return answer;
2736
+ });
2737
+ }
2738
+ const body = new win.FormData();
2739
+ body.append("file", file, file.name);
2740
+ body.append("name", fieldName(field));
2741
+ return fetch(endpoint, { method: "POST", body, credentials: "same-origin" }).then((res2) => {
2742
+ if (!res2 || !res2.ok) throw failure(res2 && res2.status === 404 ? "unavailable" : "failed");
2743
+ return json(res2);
2744
+ });
2745
+ }));
2746
+ };
2747
+ const upload = (field, input) => {
2748
+ forget(field);
2749
+ clearProblem(field);
2750
+ const file = input.files && input.files[0];
2751
+ if (!file) {
2752
+ emitChange(field);
2753
+ return;
2754
+ }
2755
+ const accept = attr2(field, "data-gem-personalize-accept") || attr2(input, "accept") || "image/*";
2756
+ const mb = Number(attr2(field, "data-gem-personalize-max-mb")) || 10;
2757
+ const endpoint = endpointOf(field);
2758
+ const refuse = (reason, fill = { mb }) => {
2759
+ input.value = "";
2760
+ showProblem(field, reason, fill);
2761
+ emitChange(field);
2762
+ };
2763
+ if (!fileAccepted(file, accept)) return refuse("type");
2764
+ if (file.size > mb * 1048576) return refuse("size");
2765
+ if (!endpoint || typeof win.fetch !== "function") return unavailable(field);
2766
+ const preview = win.URL && win.URL.createObjectURL ? win.URL.createObjectURL(file) : "";
2767
+ if (preview) objectUrls.add(preview);
2768
+ const state = { uploading: true, preview, url: "", key: "" };
2769
+ uploads.set(field, state);
2770
+ field.setAttribute("data-gem-personalize-state", "uploading");
2771
+ const thumb = field.querySelector("[data-gem-personalize-thumb]");
2772
+ if (thumb && preview) {
2773
+ thumb.src = preview;
2774
+ thumb.hidden = false;
2775
+ }
2776
+ emitChange(field);
2777
+ const abort = win.AbortController ? new win.AbortController() : null;
2778
+ if (abort) aborts.add(abort);
2779
+ send(endpoint, field, file, mb, abort && abort.signal).then((answer) => {
2780
+ const url = answer && typeof answer.url === "string" && /^https?:\/\//i.test(answer.url) ? answer.url : "";
2781
+ if (!url) throw failure("failed");
2782
+ if (uploads.get(field) !== state) return;
2783
+ Object.assign(state, { uploading: false, url, key: typeof answer.key === "string" ? answer.key : "" });
2784
+ field.setAttribute("data-gem-personalize-state", "done");
2785
+ clearProblem(field);
2786
+ emitChange(field);
2787
+ }).catch((error) => {
2788
+ if (uploads.get(field) !== state || abort && abort.signal.aborted) return;
2789
+ const reason = error && error.reason;
2790
+ if (reason === "unavailable") return unavailable(field);
2791
+ forget(field);
2792
+ refuse(reason === "type" || reason === "size" ? reason : "failed", { mb: error && error.mb || mb });
2793
+ }).finally(() => {
2794
+ if (abort) aborts.delete(abort);
2795
+ });
2796
+ };
2797
+ const onEdit = (event) => {
2798
+ const field = event.target && event.target.closest ? event.target.closest(FIELD) : null;
2799
+ if (!field || !owned.has(field)) return;
2800
+ const type = fieldType(field);
2801
+ const typing = type === "text" || type === "textarea";
2802
+ if (event.type === "input" ? !typing : typing) return;
2803
+ if (type === "upload") {
2804
+ if (event.target.type === "file") upload(field, event.target);
2805
+ return;
2806
+ }
2807
+ count(field);
2808
+ if (readField(field).value) clearProblem(field);
2809
+ applyConditions(scopeOf(field));
2810
+ emitChange(field);
2811
+ };
2812
+ const problems = (scope) => scopeFields(scope).filter((f) => !f.hidden).flatMap((field) => {
2813
+ const state = uploads.get(field);
2814
+ if (state && state.uploading) return [[field, "uploading"]];
2815
+ return isRequired(field) && !readField(field).value ? [[field, "required"]] : [];
2816
+ });
2817
+ const allowed = (scope) => {
2818
+ applyConditions(scope);
2819
+ const found = problems(scope);
2820
+ scopeFields(scope).forEach((f) => {
2821
+ if (!found.some(([p]) => p === f)) clearProblem(f);
2822
+ });
2823
+ if (!found.length) return true;
2824
+ found.forEach(([field, reason]) => showProblem(field, reason));
2825
+ const [first] = found[0];
2826
+ const control = controlsOf(first)[0];
2827
+ if (control && control.focus) control.focus({ preventScroll: true });
2828
+ if (first.scrollIntoView) first.scrollIntoView({ block: "center" });
2829
+ scope.dispatchEvent(new win.CustomEvent("gem:personalize:blocked", { bubbles: true, detail: { names: found.map(([f]) => fieldName(f)), reasons: found.map(([, r]) => r) } }));
2830
+ return false;
2831
+ };
2832
+ const properties = (scope) => scopeFields(scope).filter((f) => !f.hidden).map(readField).filter((r) => r.name && r.value).flatMap((r) => [
2833
+ { key: r.name, value: r.value },
2834
+ ...r.type === "upload" && r.key ? [{ key: `_${r.name}_upload`, value: r.key }] : []
2835
+ ]);
2836
+ const merge = (list, add) => [...Array.isArray(list) ? list.filter((p) => !add.some((q) => p && q.key === p.key)) : [], ...add];
2837
+ const variantOf = (scope) => [scope, ...scope.querySelectorAll("[data-gem-variant-id]")].filter((el) => el === scope || scopeOf(el) === scope && !el.closest(LISTS)).map((el) => attr2(el, "data-gem-variant-id")).filter(Boolean).map(tail);
2838
+ const scopeOfAdd = (event) => {
2839
+ const t = event.target;
2840
+ if (t && t.nodeType === 1) {
2841
+ const scope = scopeOf(t);
2842
+ if (scopeFields(scope).length) return scope;
2843
+ }
2844
+ const detail = event.detail || {};
2845
+ const ids = [detail.variantId, ...Array.isArray(detail.lines) ? detail.lines.map((l) => l && l.variantId) : []].filter(Boolean).map(tail);
2846
+ if (!ids.length) return null;
2847
+ const scopes = [...new Set([...owned].filter((f) => f.isConnected).map(scopeOf))];
2848
+ return scopes.find((scope) => variantOf(scope).some((id) => ids.includes(id))) || null;
2849
+ };
2850
+ const onAdd = (event) => {
2851
+ const scope = scopeOfAdd(event);
2852
+ if (!scope) return;
2853
+ const detail = event.detail && typeof event.detail === "object" ? event.detail : null;
2854
+ if (!allowed(scope)) {
2855
+ event.stopImmediatePropagation();
2856
+ if (event.cancelable) event.preventDefault();
2857
+ win.setTimeout(() => win.dispatchEvent(new win.CustomEvent("gem:cart:add-failed", { detail: { variantId: detail && detail.variantId, reason: "personalization" } })), 0);
2858
+ return;
2859
+ }
2860
+ const add = properties(scope);
2861
+ if (!detail || !add.length) return;
2862
+ if (event.type === "gem:cart:add") {
2863
+ detail.properties = merge(detail.properties, add);
2864
+ return;
2865
+ }
2866
+ const lines = Array.isArray(detail.lines) ? detail.lines.filter((l) => l && typeof l === "object") : [];
2867
+ const ids = variantOf(scope);
2868
+ const mine = lines.filter((l) => ids.includes(tail(l.variantId)));
2869
+ (mine.length ? mine : lines).forEach((line) => {
2870
+ line.properties = merge(line.properties, add);
2871
+ });
2872
+ };
2873
+ const scopeOfForm = (form) => [...new Set([...owned].filter((f) => f.isConnected).map(scopeOf))].filter((s) => s.contains(form)).sort((a, b) => a.contains(b) ? 1 : -1)[0] || null;
2874
+ const onSubmit = (event) => {
2875
+ const form = event.target;
2876
+ if (!form || form.tagName !== "FORM") return;
2877
+ const scope = scopeOfForm(form);
2878
+ if (!scope) return;
2879
+ if (!allowed(scope)) {
2880
+ event.preventDefault();
2881
+ event.stopImmediatePropagation();
2882
+ return;
2883
+ }
2884
+ if (!form.hasAttribute("action")) return;
2885
+ form.querySelectorAll("[data-gem-personalize-hidden]").forEach((el) => el.remove());
2886
+ properties(scope).forEach(({ key, value }) => {
2887
+ const input = doc.createElement("input");
2888
+ Object.assign(input, { type: "hidden", name: `properties[${key}]`, value });
2889
+ input.setAttribute("data-gem-personalize-hidden", "");
2890
+ form.appendChild(input);
2891
+ });
2892
+ };
2893
+ const bound = claimAll();
2894
+ on(doc, "input", onEdit);
2895
+ on(doc, "change", onEdit);
2896
+ on(doc, "submit", onSubmit, true);
2897
+ on(win, "gem:cart:add", onAdd, true);
2898
+ on(win, "gem:cart:add-many", onAdd, true);
2899
+ if (typeof win.MutationObserver === "function") {
2900
+ let queued = false;
2901
+ const watch = new win.MutationObserver((records) => {
2902
+ if (queued || !records.some((r) => Array.prototype.some.call(r.addedNodes, (n) => n.nodeType === 1 && (n.matches(FIELD) || n.querySelector(FIELD))))) return;
2903
+ queued = true;
2904
+ Promise.resolve().then(() => {
2905
+ queued = false;
2906
+ if (alive) claimAll();
2907
+ });
2908
+ });
2909
+ watch.observe(doc.documentElement, { childList: true, subtree: true });
2910
+ onUnmount(() => watch.disconnect());
2911
+ }
2912
+ onUnmount(() => {
2913
+ alive = false;
2914
+ aborts.forEach((a) => a.abort());
2915
+ owned.forEach((field) => {
2916
+ if (field.hidden && (attr2(field, "data-gem-personalize-when") || attr2(field, "data-gem-personalize-state") === "unavailable")) {
2917
+ field.hidden = false;
2918
+ controlsOf(field).forEach((c) => {
2919
+ c.disabled = false;
2920
+ });
2921
+ }
2922
+ forget(field);
2923
+ clearProblem(field);
2924
+ });
2925
+ owned.clear();
2926
+ });
2927
+ return bound;
2928
+ }
2929
+
2930
+ // ../../packages/gem-framework/src/pod/print-preview.js
2931
+ var HOOK = "[data-gem-print-preview]";
2932
+ var attr3 = (el, name) => el && el.getAttribute ? el.getAttribute(name) || "" : "";
2933
+ function printPreviewContent(root, values) {
2934
+ const pick = (name) => {
2935
+ const key = name && Object.keys(values).find((k) => k.toLowerCase() === name.trim().toLowerCase());
2936
+ return key ? values[key] : null;
2937
+ };
2938
+ const text2 = pick(attr3(root, "data-gem-print-preview-text"));
2939
+ const photo = pick(attr3(root, "data-gem-print-preview-photo"));
2940
+ const spec = attr3(root, "data-gem-print-preview-color").trim();
2941
+ const from = /^from:/i.test(spec) ? pick(spec.slice(5)) : null;
2942
+ const photoUrl = photo ? photo.preview || photo.value : "";
2943
+ return {
2944
+ text: text2 ? String(text2.value || "") : "",
2945
+ photo: /^(?:https?:|blob:|data:image\/)/i.test(photoUrl) ? photoUrl : "",
2946
+ color: from ? from.color || hexColor(from.value) : hexColor(spec)
2947
+ };
2948
+ }
2949
+ function mountPrintPreview(env, onUnmount) {
2950
+ const { window: win, document: doc, on, mark } = env;
2951
+ let count = 0;
2952
+ doc.querySelectorAll(HOOK).forEach((root) => {
2953
+ if (!mark(root, "print-preview")) return;
2954
+ count += 1;
2955
+ if (env.css) adoptCss(root, "print-preview", "--gem-print-preview-css", env.css);
2956
+ const area = root.querySelector("[data-gem-print-preview-area]");
2957
+ if (!area) return;
2958
+ const scope = scopeOf(root);
2959
+ let textEl = null;
2960
+ let photoEl = null;
2961
+ const fit = () => {
2962
+ if (!textEl) return;
2963
+ textEl.style.transform = "";
2964
+ const room = area.clientWidth;
2965
+ const need = textEl.scrollWidth;
2966
+ if (room > 0 && need > room) textEl.style.transform = `scale(${Math.max(0.3, room / need).toFixed(3)})`;
2967
+ };
2968
+ const draw = () => {
2969
+ const { text: text2, photo, color: color2 } = printPreviewContent(root, readScope(scope));
2970
+ if (photo) {
2971
+ if (!photoEl) {
2972
+ photoEl = doc.createElement("img");
2973
+ photoEl.className = "gem-el-product-print-preview__photo";
2974
+ photoEl.alt = "";
2975
+ area.prepend(photoEl);
2976
+ }
2977
+ if (photoEl.getAttribute("src") !== photo) photoEl.src = photo;
2978
+ } else if (photoEl) {
2979
+ photoEl.remove();
2980
+ photoEl = null;
2981
+ }
2982
+ if (text2) {
2983
+ if (!textEl) {
2984
+ textEl = doc.createElement("p");
2985
+ textEl.className = "gem-el-product-print-preview__text";
2986
+ area.append(textEl);
2987
+ }
2988
+ textEl.textContent = text2;
2989
+ fit();
2990
+ } else if (textEl) {
2991
+ textEl.remove();
2992
+ textEl = null;
2993
+ }
2994
+ if (color2) root.style.setProperty("--gem-print-preview-color", color2);
2995
+ else root.style.removeProperty("--gem-print-preview-color");
2996
+ if (text2 || photo) root.setAttribute("data-gem-print-preview-state", "drawn");
2997
+ else root.removeAttribute("data-gem-print-preview-state");
2998
+ };
2999
+ on(scope, "gem:personalize:change", draw);
3000
+ if (typeof win.ResizeObserver === "function") new win.ResizeObserver(fit).observe(area);
3001
+ draw();
3002
+ onUnmount(() => {
3003
+ if (photoEl) photoEl.remove();
3004
+ if (textEl) textEl.remove();
3005
+ root.removeAttribute("data-gem-print-preview-state");
3006
+ root.style.removeProperty("--gem-print-preview-color");
3007
+ });
3008
+ });
3009
+ return count;
3010
+ }
3011
+
3012
+ // ../../packages/gem-framework/src/pod/delivery-estimate.js
3013
+ var DAY = 864e5;
3014
+ function parseDayRange(value) {
3015
+ if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? [Math.floor(value), Math.floor(value)] : null;
3016
+ const m = /^\s*(\d{1,3})\s*(?:[-–—]\s*(\d{1,3}))?\s*$/.exec(String(value == null ? "" : value));
3017
+ if (!m) return null;
3018
+ const min = Number(m[1]);
3019
+ const max = m[2] === void 0 ? min : Number(m[2]);
3020
+ return max >= min ? [min, max] : null;
3021
+ }
3022
+ var localDay = (now) => Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
3023
+ var weekend = (day) => {
3024
+ const d = new Date(day).getUTCDay();
3025
+ return d === 0 || d === 6;
3026
+ };
3027
+ var iso = (day) => new Date(day).toISOString().slice(0, 10);
3028
+ function addDays(day, n, business) {
3029
+ let at = day;
3030
+ let left = n;
3031
+ while (left > 0) {
3032
+ at += DAY;
3033
+ if (!business || !weekend(at)) left -= 1;
3034
+ }
3035
+ return at;
3036
+ }
3037
+ function storeHour(now, timeZone) {
3038
+ if (timeZone) {
3039
+ try {
3040
+ const part = new Intl.DateTimeFormat("en-US", { timeZone, hour: "numeric", hourCycle: "h23" }).formatToParts(now).find((p) => p.type === "hour");
3041
+ if (part) return Number(part.value) % 24;
3042
+ } catch {
3043
+ }
3044
+ }
3045
+ return now.getHours();
3046
+ }
3047
+ function deliveryEstimate(now, spec) {
3048
+ const production = parseDayRange(spec.production);
3049
+ const shipping = parseDayRange(spec.shipping);
3050
+ if (!production || !shipping || !(now instanceof Date) || Number.isNaN(now.getTime())) return null;
3051
+ const business = spec.businessDays !== false;
3052
+ const today = localDay(now);
3053
+ let start = today;
3054
+ const cutoff = Number(spec.cutoffHour);
3055
+ if (spec.cutoffHour !== void 0 && spec.cutoffHour !== null && spec.cutoffHour !== "" && Number.isInteger(cutoff) && cutoff >= 0 && cutoff <= 23 && storeHour(now, spec.timeZone) >= cutoff) start += DAY;
3056
+ while (business && weekend(start)) start += DAY;
3057
+ const ships = [addDays(start, production[0], business), addDays(start, production[1], business)];
3058
+ const arrives = [addDays(ships[0], shipping[0], business), addDays(ships[1], shipping[1], business)];
3059
+ return { ordered: iso(today), start: iso(start), ships: ships.map(iso), arrives: arrives.map(iso) };
3060
+ }
3061
+ function formatDeliveryRange(from, to, locale) {
3062
+ const at = (d) => /* @__PURE__ */ new Date(`${d}T00:00:00Z`);
3063
+ let fmt;
3064
+ try {
3065
+ fmt = new Intl.DateTimeFormat(locale || void 0, { month: "short", day: "numeric", timeZone: "UTC" });
3066
+ } catch {
3067
+ fmt = new Intl.DateTimeFormat(void 0, { month: "short", day: "numeric", timeZone: "UTC" });
3068
+ }
3069
+ if (!to || to === from) return fmt.format(at(from));
3070
+ return typeof fmt.formatRange === "function" ? fmt.formatRange(at(from), at(to)) : `${fmt.format(at(from))} \u2013 ${fmt.format(at(to))}`;
3071
+ }
3072
+ var attr4 = (el, name) => (el.getAttribute(name) || "").trim();
3073
+ function mountDeliveryEstimate(env) {
3074
+ const { document: doc, mark } = env;
3075
+ let count = 0;
3076
+ doc.querySelectorAll("[data-gem-delivery-estimate]").forEach((root) => {
3077
+ if (!mark(root, "delivery-estimate")) return;
3078
+ const estimate = deliveryEstimate(/* @__PURE__ */ new Date(), {
3079
+ production: attr4(root, "data-gem-delivery-production"),
3080
+ shipping: attr4(root, "data-gem-delivery-shipping"),
3081
+ businessDays: attr4(root, "data-gem-delivery-business-days") !== "false",
3082
+ cutoffHour: attr4(root, "data-gem-delivery-cutoff-hour"),
3083
+ timeZone: attr4(root, "data-gem-delivery-timezone")
3084
+ });
3085
+ if (!estimate) return;
3086
+ const locale = attr4(root, "data-gem-delivery-locale") || doc.documentElement && doc.documentElement.lang || void 0;
3087
+ const ranges = { ordered: [estimate.ordered, estimate.ordered], ships: estimate.ships, arrives: estimate.arrives };
3088
+ root.querySelectorAll("[data-gem-delivery-date]").forEach((out) => {
3089
+ const range = ranges[attr4(out, "data-gem-delivery-date")];
3090
+ if (!range) return;
3091
+ out.textContent = formatDeliveryRange(range[0], range[1], locale);
3092
+ out.setAttribute("data-gem-delivery-from", range[0]);
3093
+ out.setAttribute("data-gem-delivery-to", range[1]);
3094
+ });
3095
+ root.setAttribute("data-gem-delivery-state", "local");
3096
+ count += 1;
3097
+ });
3098
+ return count;
3099
+ }
3100
+
3101
+ // ../../packages/gem-framework/src/pod-engine.ts
3102
+ var P = ".gem-el-product-personalization";
3103
+ var V = ".gem-el-product-print-preview";
3104
+ var D = ".gem-el-product-delivery-estimate";
3105
+ var S = ".gem-el-product-design-switcher";
3106
+ var GEM_POD_BUYBOX_CSS_LINES = [
3107
+ // product-personalization: one field per element. The wrapper is the hook; a field whose condition fails is [hidden].
3108
+ `${P} { display: grid; gap: var(--gem-space-2xs); min-width: 0; }`,
3109
+ `${P}[hidden] { display: none; }`,
3110
+ `${P}__label { display: grid; gap: var(--gem-space-2xs); }`,
3111
+ `${P}__title { font-size: var(--gem-text-sm); font-weight: 600; }`,
3112
+ `${P}__required { color: var(--gem-color-danger); }`,
3113
+ `${P}__input { box-sizing: border-box; width: 100%; min-height: var(--gem-tap-min); padding: var(--gem-space-xs) var(--gem-space-sm); font: inherit; color: var(--gem-color-text); background: var(--gem-color-background); border: var(--gem-stroke, 1px) solid var(--gem-color-border); border-radius: var(--gem-radius-md); }`,
3114
+ `textarea${P}__input { min-height: calc(var(--gem-tap-min) * 2); resize: vertical; }`,
3115
+ `${P}__meta { display: flex; gap: var(--gem-space-xs); align-items: baseline; }`,
3116
+ `${P}__help { margin: 0; color: var(--gem-color-muted); font-size: var(--gem-text-xs); }`,
3117
+ `${P}__count { margin-left: auto; color: var(--gem-color-muted); font-size: var(--gem-text-xs); font-variant-numeric: tabular-nums; }`,
3118
+ // swatch and image-choice: radios inside labels; the radio stays focusable, its picture shows the state.
3119
+ `${P}__options { display: flex; flex-wrap: wrap; gap: var(--gem-space-xs); margin: 0; padding: 0; border: 0; min-width: 0; }`,
3120
+ `${P}__option { position: relative; display: inline-grid; justify-items: center; gap: var(--gem-space-2xs); font-size: var(--gem-text-xs); cursor: pointer; }`,
3121
+ `${P}__option input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }`,
3122
+ `${P}__swatch { display: block; width: var(--gem-tap-min); height: var(--gem-tap-min); border-radius: var(--gem-radius-pill); background: var(--gem-personalize-swatch, var(--gem-color-surface)); border: var(--gem-stroke, 1px) solid var(--gem-color-border); }`,
3123
+ `${P}__choice-image { display: block; width: calc(var(--gem-tap-min) * 1.75); aspect-ratio: 1; object-fit: cover; border-radius: var(--gem-radius-md); border: var(--gem-stroke, 1px) solid var(--gem-color-border); }`,
3124
+ `${P}__option input:checked ~ ${P}__swatch, ${P}__option input:checked ~ ${P}__choice-image { box-shadow: 0 0 0 var(--gem-stroke-thick, 2px) var(--gem-color-background), 0 0 0 calc(var(--gem-stroke-thick, 2px) * 2) var(--gem-color-text); }`,
3125
+ `${P}__option input:focus-visible ~ ${P}__swatch, ${P}__option input:focus-visible ~ ${P}__choice-image { outline: var(--gem-focus-ring); outline-offset: calc(var(--gem-focus-offset) + var(--gem-stroke-thick, 2px) * 2); }`,
3126
+ `${P}__checkbox { display: inline-flex; align-items: center; gap: var(--gem-space-xs); min-height: var(--gem-tap-min); cursor: pointer; }`,
3127
+ // upload: a dashed drop box around a visually hidden (still focusable) file input, a thumbnail once a photo is picked.
3128
+ `${P}__upload { position: relative; display: grid; justify-items: center; gap: var(--gem-space-2xs); padding: var(--gem-space-md); text-align: center; background: var(--gem-color-surface); border: var(--gem-stroke, 1px) dashed var(--gem-color-border); border-radius: var(--gem-radius-md); cursor: pointer; }`,
3129
+ `${P}__upload:focus-within { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); }`,
3130
+ `${P}__file { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }`,
3131
+ `${P}__thumb { display: block; width: calc(var(--gem-tap-min) * 2); aspect-ratio: 1; object-fit: cover; border-radius: var(--gem-radius-sm); }`,
3132
+ `${P}__thumb[hidden], ${P}__thumb:not([src]) { display: none; }`,
3133
+ `${P}__privacy { margin: 0; color: var(--gem-color-muted); font-size: var(--gem-text-xs); }`,
3134
+ `${P}__error { margin: 0; color: var(--gem-color-danger); font-size: var(--gem-text-xs); }`,
3135
+ `${P}__error:empty, ${P}__error[hidden] { display: none; }`,
3136
+ // product-print-preview: the product's current image, and the print area on it (percent of the image, from the element's
3137
+ // area_* props or the gemboss.print_area metafield). Empty until the shopper types or uploads.
3138
+ `${V} { position: relative; display: block; overflow: hidden; border-radius: var(--gem-media-radius, var(--gem-radius-md)); }`,
3139
+ `${V}__media { display: block; width: 100%; height: auto; }`,
3140
+ `${V}__area { position: absolute; left: calc(var(--gem-print-preview-x, 20) * 1%); top: calc(var(--gem-print-preview-y, 35) * 1%); width: calc(var(--gem-print-preview-w, 60) * 1%); height: calc(var(--gem-print-preview-h, 20) * 1%); display: grid; place-items: center; overflow: hidden; pointer-events: none; container-type: size; }`,
3141
+ `${V}[data-gem-print-preview-shape="circle"] ${V}__area { border-radius: 50%; }`,
3142
+ `${V}[data-gem-print-preview-font="body"] { --gem-print-preview-font: var(--gem-font-body); }`,
3143
+ `${V}[data-gem-print-preview-font="accent"] { --gem-print-preview-font: var(--gem-font-accent, var(--gem-font-heading)); }`,
3144
+ // product-delivery-estimate: three steps on a line (timeline), or one sentence (line). Dates are the server's for its today
3145
+ // until the plugin rewrites them in the shopper's.
3146
+ `${D} { display: grid; gap: var(--gem-space-xs); font-size: var(--gem-text-sm); }`,
3147
+ `${D}__steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--gem-space-sm); margin: 0; padding: 0; list-style: none; }`,
3148
+ `${D}__step { position: relative; display: grid; justify-items: center; gap: var(--gem-space-2xs); text-align: center; }`,
3149
+ `${D}__step::before { content: ""; width: 0.75em; height: 0.75em; border-radius: var(--gem-radius-pill); background: var(--gem-color-accent); }`,
3150
+ `${D}__step + ${D}__step::after { content: ""; position: absolute; top: 0.375em; right: calc(50% + 0.75em); width: calc(100% - 1.5em + var(--gem-space-sm)); height: var(--gem-stroke, 1px); background: var(--gem-color-border); }`,
3151
+ `${D}__label { color: var(--gem-color-muted); font-size: var(--gem-text-xs); }`,
3152
+ `${D}__date { font-weight: 600; font-variant-numeric: tabular-nums; }`,
3153
+ `${D}__note { margin: 0; color: var(--gem-color-muted); font-size: var(--gem-text-xs); }`,
3154
+ `${D}[data-gem-delivery-style="line"] ${D}__steps { display: flex; flex-wrap: wrap; gap: var(--gem-space-2xs) var(--gem-space-sm); }`,
3155
+ `${D}[data-gem-delivery-style="line"] ${D}__step { display: inline-flex; gap: var(--gem-space-2xs); text-align: start; }`,
3156
+ `${D}[data-gem-delivery-style="line"] ${D}__step::before, ${D}[data-gem-delivery-style="line"] ${D}__step::after { content: none; }`,
3157
+ // product-design-switcher: one link per product that shares the design (pills, or cards with the sibling's photo).
3158
+ `${S} { display: grid; gap: var(--gem-space-xs); }`,
3159
+ `${S}__list { display: flex; flex-wrap: wrap; gap: var(--gem-space-xs); margin: 0; padding: 0; list-style: none; }`,
3160
+ `${S}__item { display: inline-flex; align-items: center; gap: var(--gem-space-2xs); min-height: var(--gem-tap-min); padding: var(--gem-space-2xs) var(--gem-space-sm); font-size: var(--gem-text-sm); color: var(--gem-color-text); text-decoration: none; background: var(--gem-color-background); border: var(--gem-stroke, 1px) solid var(--gem-color-border); border-radius: var(--gem-radius-pill); }`,
3161
+ `${S}__item:hover { border-color: var(--gem-color-text); }`,
3162
+ `${S}__item:focus-visible { outline: var(--gem-focus-ring); outline-offset: var(--gem-focus-offset); }`,
3163
+ `${S}__item[aria-current="page"] { border-color: var(--gem-color-text); box-shadow: inset 0 0 0 var(--gem-stroke, 1px) var(--gem-color-text); }`,
3164
+ `${S}__price { color: var(--gem-color-muted); font-variant-numeric: tabular-nums; }`,
3165
+ `${S}__image { display: none; }`,
3166
+ `${S}[data-gem-design-switcher-style="cards"] ${S}__list { display: grid; grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); }`,
3167
+ `${S}[data-gem-design-switcher-style="cards"] ${S}__item { flex-direction: column; align-items: stretch; padding: var(--gem-space-xs); border-radius: var(--gem-radius-md); }`,
3168
+ `${S}[data-gem-design-switcher-style="cards"] ${S}__image { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: var(--gem-radius-sm); }`,
3169
+ `${S}[data-gem-design-switcher-style="cards"] ${S}__label { font-weight: 600; }`
3170
+ ];
3171
+ var GEM_PERSONALIZE_RUNTIME_CSS_LINES = [
3172
+ `${P}[data-gem-personalize-invalid] ${P}__input, ${P}[data-gem-personalize-invalid] ${P}__upload, ${P}[data-gem-personalize-invalid] ${P}__swatch, ${P}[data-gem-personalize-invalid] ${P}__choice-image { border-color: var(--gem-color-danger); }`,
3173
+ `${P}[data-gem-personalize-state="uploading"] ${P}__upload { cursor: progress; opacity: 0.7; }`,
3174
+ `${P}__count[data-gem-personalize-full] { color: var(--gem-color-text); font-weight: 600; }`
3175
+ ];
3176
+ var GEM_PRINT_PREVIEW_RUNTIME_CSS_LINES = [
3177
+ // The words: the theme font the element names, the colour it follows, as many lines as it allows, sized by the area's
3178
+ // height (container units); a word wider than the area is scaled down by the plugin with a transform, never re-laid out.
3179
+ `${V}__text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: var(--gem-print-preview-lines, 2); max-width: 100%; margin: 0; overflow: hidden; overflow-wrap: anywhere; font-family: var(--gem-print-preview-font, var(--gem-font-heading)); font-size: calc(82cqh / var(--gem-print-preview-lines, 2)); line-height: 1.1; color: var(--gem-print-preview-color, var(--gem-color-text)); text-align: center; transform-origin: center; }`,
3180
+ `${V}[data-gem-print-preview-align="left"] ${V}__area { place-items: center start; }`,
3181
+ `${V}[data-gem-print-preview-align="left"] ${V}__text { text-align: start; transform-origin: left center; }`,
3182
+ `${V}[data-gem-print-preview-align="right"] ${V}__area { place-items: center end; }`,
3183
+ `${V}[data-gem-print-preview-align="right"] ${V}__text { text-align: end; transform-origin: right center; }`,
3184
+ `${V}__photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }`
3185
+ ];
3186
+ var GEM_POD_BUYBOX_CSS = GEM_POD_BUYBOX_CSS_LINES.join("\n") + "\n";
3187
+ var GEM_PERSONALIZE_RUNTIME_CSS = GEM_PERSONALIZE_RUNTIME_CSS_LINES.join("\n") + "\n";
3188
+ var GEM_PRINT_PREVIEW_RUNTIME_CSS = GEM_PRINT_PREVIEW_RUNTIME_CSS_LINES.join("\n") + "\n";
3189
+ var GEM_POD_CLASSES = [
3190
+ ...["", "__label", "__title", "__required", "__input", "__meta", "__help", "__count", "__options", "__option", "__swatch", "__choice-image", "__checkbox", "__upload", "__file", "__thumb", "__privacy", "__error"].map((part) => `gem-el-product-personalization${part}`),
3191
+ ...["", "__media", "__area", "__text", "__photo"].map((part) => `gem-el-product-print-preview${part}`),
3192
+ ...["", "__steps", "__step", "__label", "__date", "__note"].map((part) => `gem-el-product-delivery-estimate${part}`),
3193
+ ...["", "__list", "__item", "__label", "__price", "__image"].map((part) => `gem-el-product-design-switcher${part}`)
3194
+ ];
3195
+ var GEM_POD_CSS_VARIABLES = [
3196
+ "--gem-personalize-css",
3197
+ "--gem-personalize-swatch",
3198
+ "--gem-print-preview-css",
3199
+ "--gem-print-preview-x",
3200
+ "--gem-print-preview-y",
3201
+ "--gem-print-preview-w",
3202
+ "--gem-print-preview-h",
3203
+ "--gem-print-preview-lines",
3204
+ "--gem-print-preview-color",
3205
+ "--gem-print-preview-font",
3206
+ // No accent font token in gem-framework yet: a skin may set it, the preview falls back to the heading font.
3207
+ "--gem-font-accent"
3208
+ ];
3209
+ var GEM_POD_RUNTIME_FILES = ["pod/core.js", "pod/personalize.js", "pod/print-preview.js", "pod/delivery-estimate.js"];
3210
+ var GEM_POD_PLUGINS = [
3211
+ { key: "personalize", hook: "[data-gem-personalize]", when: "viewport", budget: { gzip: 6144 } },
3212
+ { key: "print-preview", hook: "[data-gem-print-preview]", when: "interaction", budget: { gzip: 6144 } },
3213
+ { key: "delivery-estimate", hook: "[data-gem-delivery-estimate]", when: "viewport", budget: { gzip: 6144 } }
3214
+ ];
3215
+ var MOUNTS = {
3216
+ personalize: { name: "mountPersonalize", file: "personalize", css: GEM_PERSONALIZE_RUNTIME_CSS },
3217
+ "print-preview": { name: "mountPrintPreview", file: "print-preview", css: GEM_PRINT_PREVIEW_RUNTIME_CSS },
3218
+ "delivery-estimate": { name: "mountDeliveryEstimate", file: "delivery-estimate", css: "" }
3219
+ };
3220
+ function gemPodChunks() {
3221
+ const chunks = {};
3222
+ for (const { key, hook, when } of GEM_POD_PLUGINS) {
3223
+ const { name, file, css } = MOUNTS[key];
3224
+ chunks[key] = {
3225
+ hook,
3226
+ when,
3227
+ imports: [`import { ${name} } from "../pod/${file}.js";`],
3228
+ body: [
3229
+ ` registerPlugin('${key}', ({ api }) => {`,
3230
+ " const on = (el, type, fn, opt) => { if (canListen(el)) el.addEventListener(type, fn, opt); };",
3231
+ ` const count = ${name}({ window, document, on, mark, api${css ? `, css: ${JSON.stringify(compactCss(css))}` : ""} }, onUnmount);`,
3232
+ ` api.emit('gem:${key}:ready', { source: 'gem-runtime', count });`,
3233
+ " });"
3234
+ ]
3235
+ };
3236
+ }
3237
+ return chunks;
3238
+ }
3239
+
2459
3240
  // ../../packages/gem-framework/src/manifest.ts
2460
3241
  var gemFrameworkPrimitiveGroups = [
2461
3242
  {
@@ -2657,6 +3438,13 @@ var gemFrameworkPrimitiveGroups = [
2657
3438
  group: "motion",
2658
3439
  classes: [...GEM_MOTION_CLASSES]
2659
3440
  },
3441
+ {
3442
+ // The Boss Theme POD buy box (pod-engine.ts, 0.20.0): the four elements gemsite renders (personalization v2, print
3443
+ // preview, delivery estimate, design switcher) and what their plugins draw. Styled by dist/pod-buybox.css and the
3444
+ // personalize / print-preview chunks' own sheets, never by gem-framework.css.
3445
+ group: "pod",
3446
+ classes: [...GEM_POD_CLASSES]
3447
+ },
2660
3448
  {
2661
3449
  group: "sections",
2662
3450
  classes: [
@@ -2899,6 +3687,25 @@ var gemFrameworkRuntimePlugins = [
2899
3687
  key: "rail",
2900
3688
  load_strategy: "idle",
2901
3689
  events: ["gem:rail:ready", "gem:motion:front", "gem:motion:fallback"]
3690
+ },
3691
+ // The Boss Theme POD buy box (0.20.0, pod-engine.ts): lazy chunks like the motion effects. idle schedules gem-runtime.js's
3692
+ // stub, which imports the chunk when its hook comes near (personalize, delivery-estimate) or is first touched (print-preview),
3693
+ // never on a page without one. personalize holds the product's add (gem:cart:add / add-many, a form submit) until every
3694
+ // required field is filled, and answers a held add with gem:cart:add-failed.
3695
+ {
3696
+ key: "personalize",
3697
+ load_strategy: "idle",
3698
+ events: ["gem:personalize:ready", "gem:personalize:change", "gem:personalize:blocked", "gem:cart:add-failed"]
3699
+ },
3700
+ {
3701
+ key: "print-preview",
3702
+ load_strategy: "idle",
3703
+ events: ["gem:print-preview:ready"]
3704
+ },
3705
+ {
3706
+ key: "delivery-estimate",
3707
+ load_strategy: "idle",
3708
+ events: ["gem:delivery-estimate:ready"]
2902
3709
  }
2903
3710
  ];
2904
3711
  var gemFrameworkValidationRules = [
@@ -3072,7 +3879,7 @@ var GEM_POPUP_SHELL_CSS = GEM_POPUP_SHELL_CSS_LINES.join("\n");
3072
3879
 
3073
3880
  // ../../packages/gem-framework/src/primitives.ts
3074
3881
  var dividerMask = (path) => `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 10' preserveAspectRatio='none'%3E%3Cpath fill='%23fff' d='${path}'/%3E%3C/svg%3E")`;
3075
- var knownValues = (attr, values) => `:where(${values.map((v) => `[${attr}='${v}']`).join(", ")})`;
3882
+ var knownValues = (attr5, values) => `:where(${values.map((v) => `[${attr5}='${v}']`).join(", ")})`;
3076
3883
  var DIVIDER_SHAPES = Object.keys(GEM_SECTION_DIVIDER_PATHS);
3077
3884
  var GEM_DIVIDER_CSS_LINES = [
3078
3885
  // The clip is WIDENED, not removed. `.gem-section` sets `overflow: clip` and the wedge is drawn
@@ -7444,7 +8251,10 @@ var runtimePluginReadyEvents = {
7444
8251
  bundle: "gem:bundle:ready",
7445
8252
  "size-guide": "gem:size-guide:ready",
7446
8253
  orbit: "gem:orbit:ready",
7447
- rail: "gem:rail:ready"
8254
+ rail: "gem:rail:ready",
8255
+ personalize: "gem:personalize:ready",
8256
+ "print-preview": "gem:print-preview:ready",
8257
+ "delivery-estimate": "gem:delivery-estimate:ready"
7448
8258
  };
7449
8259
  var cartDrawerPluginRegistrationLines = [
7450
8260
  " registerPlugin('cart-drawer', ({ api }) => {",
@@ -7992,11 +8802,14 @@ var legacyQuickViewLines = [
7992
8802
  // page's api.quickView.
7993
8803
  " if (surface || !api.quickView) api.quickView = { open: (detail = {}) => openSurface({ ...detail }), close };"
7994
8804
  ];
7995
- var runtimeChunks = gemMotionChunks({
7996
- hook: '[data-gem-quick-view], [data-gem-action="quick-view"]',
7997
- lines: legacyQuickViewLines,
7998
- readyDetail: "count: bound + count, hasSurface: Boolean(surface)"
7999
- });
8805
+ var runtimeChunks = {
8806
+ ...gemMotionChunks({
8807
+ hook: '[data-gem-quick-view], [data-gem-action="quick-view"]',
8808
+ lines: legacyQuickViewLines,
8809
+ readyDetail: "count: bound + count, hasSurface: Boolean(surface)"
8810
+ }),
8811
+ ...gemPodChunks()
8812
+ };
8000
8813
  var promoPopupPluginRegistrationLines = [
8001
8814
  " registerPlugin('promo-popup', ({ api }) => {",
8002
8815
  // Only the explicit hook opts in. Every promo-popup section carries the archetype class .gem-promo-popup, and
@@ -8586,7 +9399,10 @@ var runtimePluginSourceLines = [
8586
9399
  ["bundle", bundlePluginRegistrationLines],
8587
9400
  ["size-guide", sizeGuidePluginRegistrationLines],
8588
9401
  ["orbit", runtimeChunks.orbit.body],
8589
- ["rail", runtimeChunks.rail.body]
9402
+ ["rail", runtimeChunks.rail.body],
9403
+ ["personalize", runtimeChunks.personalize.body],
9404
+ ["print-preview", runtimeChunks["print-preview"].body],
9405
+ ["delivery-estimate", runtimeChunks["delivery-estimate"].body]
8590
9406
  ];
8591
9407
  var noopPluginRegistrationLine = (key, readyEvent) => ` registerNoopPlugin(${JSON.stringify(key)}, ${JSON.stringify(readyEvent)});`;
8592
9408
  var chunkStubLine = (key, chunk) => ` registerPlugin(${JSON.stringify(key)}, ({ api }) => loadChunk(${JSON.stringify(key)}, ${JSON.stringify(chunk.hook)}, ${JSON.stringify(chunk.when)}, api));`;
@@ -8981,7 +9797,10 @@ var canonicalCssVariableNames = /* @__PURE__ */ new Set([
8981
9797
  // internals (--gem-orbit-a, --gem-rail-t) included, and sets --gem-orbit-i / -n, the settings and --gem-motion-tint.
8982
9798
  ...new Set([GEM_ORBIT_BASE_CSS, GEM_RAIL_BASE_CSS, GEM_ORBIT_RUNTIME_CSS, GEM_RAIL_RUNTIME_CSS, GEM_QUICK_VIEW_CSS].join("\n").match(/--gem-[a-z0-9-]+/g) ?? []),
8983
9799
  // …and every setting an effect reads from a CSS variable (`css: true`): a section sets it in its stylesheet, per breakpoint.
8984
- ...gemMotionEffectList.flatMap((effect) => Object.entries(effect.settings).filter(([, setting]) => setting.css).map(([name]) => `--gem-${effect.key}-${name}`))
9800
+ ...gemMotionEffectList.flatMap((effect) => Object.entries(effect.settings).filter(([, setting]) => setting.css).map(([name]) => `--gem-${effect.key}-${name}`)),
9801
+ // The POD buy box's (pod-engine.ts, 0.20.0): the print area and lines gemsite sets on a preview, the swatch colour of an
9802
+ // option, the chunks' sentinels, and --gem-font-accent, the accent font a skin may set (the preview falls back to heading).
9803
+ ...GEM_POD_CSS_VARIABLES
8985
9804
  ]);
8986
9805
  var TOKENIZED_PROPERTIES = [
8987
9806
  "background",
@@ -10353,7 +11172,14 @@ export {
10353
11172
  GEM_ORBIT_BASE_CSS,
10354
11173
  GEM_ORBIT_RUNTIME_CSS,
10355
11174
  GEM_PART_CLASS,
11175
+ GEM_PERSONALIZE_RUNTIME_CSS,
11176
+ GEM_POD_BUYBOX_CSS,
11177
+ GEM_POD_CLASSES,
11178
+ GEM_POD_CSS_VARIABLES,
11179
+ GEM_POD_PLUGINS,
11180
+ GEM_POD_RUNTIME_FILES,
10356
11181
  GEM_POPUP_SHELL_CSS,
11182
+ GEM_PRINT_PREVIEW_RUNTIME_CSS,
10357
11183
  GEM_QUICK_VIEW_CSS,
10358
11184
  GEM_RAIL_BASE_CSS,
10359
11185
  GEM_RAIL_RUNTIME_CSS,
@@ -10370,6 +11196,7 @@ export {
10370
11196
  MODERN_MINERAL_HOMEPAGE_LINEAGE_SECTIONS,
10371
11197
  MODERN_MINERAL_HOMEPAGE_PACK_ID,
10372
11198
  MODERN_MINERAL_STYLE_PACK_ID,
11199
+ PERSONALIZE_MESSAGES,
10373
11200
  aaLabelColor,
10374
11201
  aaTextColor,
10375
11202
  auditGemFrameworkClassName,
@@ -10397,8 +11224,11 @@ export {
10397
11224
  defaultGemTokens,
10398
11225
  defaultSelection,
10399
11226
  defineEffect,
11227
+ deliveryEstimate,
10400
11228
  deriveFormSetId,
11229
+ fileAccepted,
10401
11230
  follow,
11231
+ formatDeliveryRange,
10402
11232
  gemBandSchemeColors,
10403
11233
  gemBandSchemeCssVars,
10404
11234
  gemBandSchemeDeclarations,
@@ -10428,6 +11258,7 @@ export {
10428
11258
  gemPhoneTokenTarget,
10429
11259
  gemPlanFormSet,
10430
11260
  gemPlanShapeKnobs,
11261
+ gemPodChunks,
10431
11262
  gemRailBaseCss,
10432
11263
  gemRuntimePluginModuleKeys,
10433
11264
  gemRuntimePluginSources,
@@ -10444,14 +11275,23 @@ export {
10444
11275
  modernMineralHomepageLineageSections,
10445
11276
  motionCssVarAliases,
10446
11277
  motionCssVarName,
11278
+ mountDeliveryEstimate,
11279
+ mountPersonalize,
11280
+ mountPrintPreview,
10447
11281
  normalizeGemTokens,
10448
11282
  normalizeHex,
10449
11283
  offerEngineRuntimeScript,
10450
11284
  optionalGemTokenKeys,
10451
11285
  orbitEffect,
11286
+ parseDayRange,
11287
+ parsePersonalizeWhen,
11288
+ personalizeWhenHolds,
10452
11289
  planToRuntimeBootstrapConfig,
11290
+ printPreviewContent,
10453
11291
  quickViewEffect,
10454
11292
  railEffect,
11293
+ readField,
11294
+ readScope,
10455
11295
  renderGemFrameworkManifest,
10456
11296
  renderGemPreviewDocument,
10457
11297
  renderGemSectionMarkup,
@@ -10466,9 +11306,11 @@ export {
10466
11306
  ringDelta,
10467
11307
  ringPose,
10468
11308
  scopeMotionCss,
11309
+ scopeOf,
10469
11310
  scrollProgress,
10470
11311
  slugify,
10471
11312
  spring,
11313
+ storeHour,
10472
11314
  throwTarget,
10473
11315
  toCssVars,
10474
11316
  toMotionCssVars,