@ptengine/lp-editor-core 1.0.0-alpha.2 → 1.0.0-alpha.4

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
@@ -121,9 +121,9 @@ function canonicalTagName(el) {
121
121
  }
122
122
  function sortedAttributes(el) {
123
123
  const attrs = [];
124
- const list = el.attributes;
125
- for (let i = 0; i < list.length; i++) {
126
- const a = list[i];
124
+ const list2 = el.attributes;
125
+ for (let i = 0; i < list2.length; i++) {
126
+ const a = list2[i];
127
127
  const raw = a.value === null || a.value === void 0 ? "" : a.value;
128
128
  attrs.push([a.name, a.name === "class" ? normalizeClassValue(raw) : raw]);
129
129
  }
@@ -251,6 +251,9 @@ var THEME_PT_ID = "theme";
251
251
  var ROOT_PT_ID = "root";
252
252
  var FORMAT_META_NAME = "lpx-format";
253
253
  var FORMAT_VERSION = "4";
254
+ var WRAP_ATTR = "data-pt-wrap";
255
+ var WRAP_SECTION_PT_ID = "ptwrap-section";
256
+ var WRAP_CONTAINER_PT_ID = "ptwrap-container";
254
257
  var SWITCHES = [
255
258
  {
256
259
  key: "hide",
@@ -352,6 +355,9 @@ var rules = {
352
355
  rootPtId: ROOT_PT_ID,
353
356
  formatMetaName: FORMAT_META_NAME,
354
357
  formatVersion: FORMAT_VERSION,
358
+ wrapAttr: WRAP_ATTR,
359
+ wrapSectionPtId: WRAP_SECTION_PT_ID,
360
+ wrapContainerPtId: WRAP_CONTAINER_PT_ID,
355
361
  switches: SWITCHES,
356
362
  crossDeviceConsistentProperties: CROSS_DEVICE_CONSISTENT_PROPERTIES,
357
363
  opaqueModes: OPAQUE_MODES,
@@ -362,44 +368,9 @@ var rules = {
362
368
  arbitraryValueAllowedProperties: ARBITRARY_VALUE_ALLOWED_PROPERTIES
363
369
  };
364
370
 
365
- // src/kinds.ts
366
- var KIND_PROFILES = {
367
- page: {
368
- root: "document",
369
- layers: ["section", "container", "leaf"],
370
- compile: "document",
371
- leafTypes: ["text", "media", "button", "shape"],
372
- components: {
373
- carousel: "self",
374
- faq: "self",
375
- tabs: "self",
376
- "sticky-cta": "self",
377
- form: "self"
378
- },
379
- units: { vh: true },
380
- customCode: true
381
- },
382
- popup: {
383
- root: "fragment",
384
- // 弹窗无 section 层:root › children。
385
- layers: ["container", "leaf"],
386
- compile: "shadow-px",
387
- leafTypes: ["text", "media", "button", "shape"],
388
- components: {
389
- form: "engage-sdk",
390
- countdown: "engage-sdk",
391
- carousel: "self",
392
- faq: "self",
393
- tabs: "self"
394
- },
395
- units: { vh: false },
396
- customCode: false
397
- }
398
- };
399
-
400
371
  // src/version.ts
401
- var VERSION = "1.0.0-alpha.2";
402
- var COMPILER_VERSION = "1";
372
+ var VERSION = "1.0.0-alpha.4";
373
+ var COMPILER_VERSION = "2";
403
374
 
404
375
  // src/validate/tree.ts
405
376
  var INLINE_SET = new Set(INLINE_NO_ID_TAGS);
@@ -560,8 +531,165 @@ function nextImageSibling(from) {
560
531
  }
561
532
  function* walkElements(root) {
562
533
  if ("tagName" in root) yield root;
563
- const list = root.querySelectorAll("*");
564
- for (let i = 0; i < list.length; i++) yield list[i];
534
+ const list2 = root.querySelectorAll("*");
535
+ for (let i = 0; i < list2.length; i++) yield list2[i];
536
+ }
537
+
538
+ // src/wrap.ts
539
+ function isWrapLayer(el) {
540
+ return el.hasAttribute(WRAP_ATTR);
541
+ }
542
+ var SKELETON = `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="${FORMAT_META_NAME}" content="${FORMAT_VERSION}"></head><body></body></html>`;
543
+ function createWrap(parse) {
544
+ return function wrap(fragmentHtml) {
545
+ if (typeof fragmentHtml !== "string") {
546
+ throw new TypeError("wrap(): fragmentHtml \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
547
+ }
548
+ const doc = parse(SKELETON);
549
+ const frag = parseFragment(doc, fragmentHtml);
550
+ const { theme, root, before, after } = checkFragmentShape(frag);
551
+ checkReservedLiterals(frag, root, theme);
552
+ const body = doc.body;
553
+ const attrs = root.attributes;
554
+ for (let i = 0; i < attrs.length; i++) {
555
+ body.setAttribute(attrs[i].name, attrs[i].value);
556
+ }
557
+ if (theme) doc.head.appendChild(theme);
558
+ const section = doc.createElement("section");
559
+ section.setAttribute(WRAP_ATTR, "");
560
+ section.setAttribute("data-pt-id", WRAP_SECTION_PT_ID);
561
+ const container = doc.createElement("div");
562
+ container.setAttribute(WRAP_ATTR, "");
563
+ container.setAttribute("data-pt-id", WRAP_CONTAINER_PT_ID);
564
+ while (root.firstChild) container.appendChild(root.firstChild);
565
+ section.appendChild(container);
566
+ for (const node of before) body.appendChild(node);
567
+ body.appendChild(section);
568
+ for (const node of after) body.appendChild(node);
569
+ return doc;
570
+ };
571
+ }
572
+ function checkFragmentShape(frag) {
573
+ const elements = [];
574
+ const nonElements = [];
575
+ let child = frag.firstChild;
576
+ while (child) {
577
+ const next = child.nextSibling;
578
+ if (child.nodeType === 1) {
579
+ elements.push(child);
580
+ } else if (child.nodeType === 3 && child.data.trim() === "") {
581
+ nonElements.push({ node: child, afterElements: elements.length });
582
+ } else {
583
+ throw new TypeError(
584
+ `wrap(): \u7247\u6BB5\u9876\u5C42\u53EA\u5141\u8BB8 theme \u5757\u3001\u5355\u4E00\u6839\u5143\u7D20\u4E0E\u7A7A\u767D \u2014\u2014 \u51FA\u73B0\u4E86 ${describeNode(child)}\u3002\u91CE\u751F\u5F62\u72B6\u8BF7\u5728\u8C03\u7528\u65B9\u7684 adapter \u91CC\u5148\u89C4\u6574\u3002`
585
+ );
586
+ }
587
+ child = next;
588
+ }
589
+ let theme = null;
590
+ let root;
591
+ if (elements.length === 2) {
592
+ if (!isThemeStyle(elements[0])) {
593
+ throw new TypeError(
594
+ 'wrap(): \u7247\u6BB5\u9876\u5C42\u6709\u4E24\u4E2A\u5143\u7D20\u65F6\uFF0C\u7B2C\u4E00\u4E2A\u5FC5\u987B\u662F\u4E3B\u9898\u5757 <style type="text/tailwindcss">'
595
+ );
596
+ }
597
+ theme = elements[0];
598
+ root = elements[1];
599
+ } else if (elements.length === 1) {
600
+ if (isThemeStyle(elements[0])) {
601
+ throw new TypeError("wrap(): \u7247\u6BB5\u53EA\u6709\u4E3B\u9898\u5757\u3001\u6CA1\u6709\u6839\u5143\u7D20");
602
+ }
603
+ root = elements[0];
604
+ } else {
605
+ throw new TypeError(
606
+ `wrap(): \u7247\u6BB5\u9876\u5C42\u5FC5\u987B\u6070\u6709\u4E00\u4E2A\u6839\u5143\u7D20\uFF08\u53EF\u9009\u4E00\u4E2A\u4E3B\u9898\u5757\uFF09\uFF0C\u5B9E\u9645\u6709 ${elements.length} \u4E2A\u5143\u7D20`
607
+ );
608
+ }
609
+ if (tag(root) !== "div") {
610
+ throw new TypeError(`wrap(): \u7247\u6BB5\u6839\u5143\u7D20\u5FC5\u987B\u662F <div>\uFF0C\u5B9E\u9645\u662F <${tag(root)}>`);
611
+ }
612
+ if (root.getAttribute("data-pt-id") !== ROOT_PT_ID) {
613
+ throw new TypeError(
614
+ `wrap(): \u7247\u6BB5\u6839\u5143\u7D20\u7684 data-pt-id \u5FC5\u987B\u662F ${ROOT_PT_ID}\uFF0C\u5B9E\u9645\u662F ${JSON.stringify(root.getAttribute("data-pt-id"))}\u3002\u5B83\u4F1A\u88AB\u8BA4\u9886\u6210\u6587\u6863\u7684 <body>\uFF0C\u6539\u540D\u4F1A\u8BA9\u57CB\u70B9\u951A\u70B9\u65AD\u94FE\u3002`
615
+ );
616
+ }
617
+ const rootIndex = elements.indexOf(root);
618
+ const before = [];
619
+ const after = [];
620
+ for (const { node, afterElements } of nonElements) {
621
+ if (afterElements <= rootIndex) before.push(node);
622
+ else after.push(node);
623
+ }
624
+ return { theme, root, before, after };
625
+ }
626
+ function checkReservedLiterals(frag, root, theme) {
627
+ const all = frag.querySelectorAll("*");
628
+ for (let i = 0; i < all.length; i++) {
629
+ const el = all[i];
630
+ if (el.hasAttribute(WRAP_ATTR)) {
631
+ throw new TypeError(
632
+ `wrap(): \u7247\u6BB5\u91CC\u5DF2\u7ECF\u5E26\u7740\u5408\u6210\u5C42\u6807\u8BB0 ${WRAP_ATTR} \u2014\u2014 \u591A\u534A\u662F\u4E0A\u4E00\u8F6E\u6F0F\u5265\u6216\u56DE\u704C\uFF0C\u5148\u67E5\u4E0A\u6E38\uFF0C\u4E0D\u8981\u91CD\u590D\u5305\u88F9\u3002`
633
+ );
634
+ }
635
+ const id = el.getAttribute("data-pt-id");
636
+ if (id === null) continue;
637
+ if (id === WRAP_SECTION_PT_ID || id === WRAP_CONTAINER_PT_ID) {
638
+ throw new TypeError(`wrap(): \u7247\u6BB5\u91CC\u5DF2\u7ECF\u5360\u7528\u4E86\u5408\u6210\u5C42\u7684\u4FDD\u7559 id ${id}`);
639
+ }
640
+ if (id === ROOT_PT_ID && el !== root) {
641
+ throw new TypeError(
642
+ `wrap(): \u4FDD\u7559 id ${ROOT_PT_ID} \u53EA\u8BB8\u51FA\u73B0\u5728\u7247\u6BB5\u6839\u5143\u7D20\u4E0A\uFF0C\u8FD9\u91CC\u51FA\u73B0\u5728 <${tag(el)}> \u4E0A \u2014\u2014 \u5305\u88F9\u540E\u4F1A\u53D8\u6210\u91CD\u590D id`
643
+ );
644
+ }
645
+ if (id === THEME_PT_ID && el !== theme) {
646
+ throw new TypeError(
647
+ `wrap(): \u4FDD\u7559 id ${THEME_PT_ID} \u53EA\u8BB8\u51FA\u73B0\u5728\u4E3B\u9898\u5757\u4E0A\uFF0C\u8FD9\u91CC\u51FA\u73B0\u5728 <${tag(el)}> \u4E0A`
648
+ );
649
+ }
650
+ }
651
+ }
652
+ function unwrap(doc) {
653
+ if (!doc || doc.nodeType !== 9) {
654
+ throw new TypeError("unwrap(): \u5165\u53C2\u5FC5\u987B\u662F Document");
655
+ }
656
+ const clone = doc.cloneNode(true);
657
+ const body = clone.body;
658
+ if (!body) throw new TypeError("unwrap(): \u6587\u6863\u6CA1\u6709 <body>");
659
+ const section = onlyWrapChild(body, "body");
660
+ const container = onlyWrapChild(section, `<${tag(section)} ${WRAP_ATTR}>`);
661
+ const div = clone.createElement("div");
662
+ const attrs = body.attributes;
663
+ for (let i = 0; i < attrs.length; i++) div.setAttribute(attrs[i].name, attrs[i].value);
664
+ while (container.firstChild) div.appendChild(container.firstChild);
665
+ const out = clone.createDocumentFragment();
666
+ const theme = clone.head?.querySelector('style[type="text/tailwindcss"]') ?? null;
667
+ if (theme) out.appendChild(theme);
668
+ const children = Array.from(body.childNodes);
669
+ for (const node of children) {
670
+ out.appendChild(node === section ? div : node);
671
+ }
672
+ return out;
673
+ }
674
+ function onlyWrapChild(parent, label) {
675
+ const kids = parent.children;
676
+ if (kids.length !== 1) {
677
+ throw new TypeError(
678
+ `unwrap(): ${label} \u4E0B\u5FC5\u987B\u6070\u6709\u4E00\u4E2A\u5408\u6210\u5C42\u5B50\u5143\u7D20\uFF0C\u5B9E\u9645\u6709 ${kids.length} \u4E2A \u2014\u2014 \u4E0D\u731C\u4E00\u4E2A\u51FA\u6765\u3002\u8FD9\u4EFD\u6587\u6863\u4E0D\u662F wrap() \u7684\u4EA7\u7269\uFF0C\u6216\u8005\u88AB\u6539\u6210\u4E86\u522B\u7684\u7ED3\u6784\u3002`
679
+ );
680
+ }
681
+ const only = kids[0];
682
+ if (!only.hasAttribute(WRAP_ATTR)) {
683
+ throw new TypeError(
684
+ `unwrap(): ${label} \u4E0B\u552F\u4E00\u7684\u5B50\u5143\u7D20\u6CA1\u6709 ${WRAP_ATTR} \u6807\u8BB0 \u2014\u2014 \u8FD9\u4EFD\u6587\u6863\u6CA1\u6709\u88AB wrap() \u5305\u88F9\u8FC7\u3002`
685
+ );
686
+ }
687
+ return only;
688
+ }
689
+ function describeNode(node) {
690
+ if (node.nodeType === 8) return "\u6CE8\u91CA\u8282\u70B9";
691
+ if (node.nodeType === 3) return `\u975E\u7A7A\u767D\u6587\u672C ${JSON.stringify(node.data.trim())}`;
692
+ return `nodeType ${node.nodeType}`;
565
693
  }
566
694
 
567
695
  // src/validate/variants.ts
@@ -629,16 +757,354 @@ function isNativePaletteClass(className) {
629
757
  return SHADE.test(shade) && PALETTE.has(name);
630
758
  }
631
759
 
760
+ // src/components/contracts.ts
761
+ var carouselContract = {
762
+ type: "carousel",
763
+ fields: [
764
+ // image 档不渲染标题/副标题/按钮 —— 是结构差异,不是外观档位。
765
+ { id: "layout", type: "enum", values: ["hero", "image"], default: "hero" },
766
+ { id: "autoplay", type: "boolean", default: true },
767
+ { id: "interval", type: "number", default: 5e3 },
768
+ { id: "showDots", type: "boolean", default: true },
769
+ { id: "showArrows", type: "boolean", default: true }
770
+ ],
771
+ blocks: [
772
+ {
773
+ type: "slide",
774
+ fields: [
775
+ { id: "image", type: "image", default: "" },
776
+ { id: "title", type: "text", default: "" },
777
+ { id: "subtitle", type: "text", default: "" },
778
+ { id: "buttonText", type: "text", default: "" },
779
+ { id: "buttonUrl", type: "url", default: "#" }
780
+ ]
781
+ }
782
+ ]
783
+ };
784
+ var faqContract = {
785
+ type: "faq",
786
+ fields: [
787
+ { id: "title", type: "text", default: "" },
788
+ // list 档不出 <details>、不出脚本 —— 两套结构。
789
+ { id: "style", type: "enum", values: ["accordion", "list"], default: "accordion" },
790
+ { id: "iconStyle", type: "enum", values: ["chevron", "plus"], default: "chevron" }
791
+ ],
792
+ blocks: [
793
+ {
794
+ type: "item",
795
+ fields: [
796
+ { id: "question", type: "text", default: "" },
797
+ { id: "answer", type: "richtext", default: "" },
798
+ { id: "defaultOpen", type: "boolean", default: false }
799
+ ]
800
+ }
801
+ ]
802
+ };
803
+ var tabsContract = {
804
+ type: "tabs",
805
+ fields: [
806
+ { id: "title", type: "text", default: "" },
807
+ { id: "tabStyle", type: "enum", values: ["underline", "pill", "boxed"], default: "underline" },
808
+ { id: "defaultTab", type: "number", default: 0 }
809
+ ],
810
+ blocks: [
811
+ {
812
+ type: "tab",
813
+ fields: [
814
+ { id: "title", type: "text", default: "" },
815
+ { id: "content", type: "richtext", default: "" },
816
+ { id: "icon", type: "text", default: "" }
817
+ ]
818
+ }
819
+ ]
820
+ };
821
+ var formContract = {
822
+ type: "form",
823
+ fields: [
824
+ // 外部提交按钮按它定位表单 —— 判据②。
825
+ { id: "formName", type: "text", default: "" },
826
+ { id: "showSubmit", type: "boolean", default: true },
827
+ { id: "submitText", type: "text", default: "" },
828
+ // 四档各一套 glyph markup;空串档 = 不出 glyph。
829
+ { id: "submitIcon", type: "enum", values: ["", "email", "arrow", "check"], default: "" },
830
+ // richtext 而非 text:v2 原样插入它(`componentRenderers.ts:303` 无 esc),
831
+ // 而页脚正是隐私政策链接的所在。定成 text 会把 <a> 渲成字面字符。
832
+ // 紧邻的 successMessage 是转义的(同文件 :225)—— 这两个字段在 v2 里行为本就
833
+ // 不同,schema 却都标成 textarea,又一处只活在渲染器里的区别。
834
+ { id: "footerText", type: "richtext", default: "" },
835
+ { id: "successMessage", type: "text", default: "" },
836
+ { id: "multiStep", type: "boolean", default: false },
837
+ // inline 是另一套 markup(label 在旁 vs 在上),不是对齐取值。
838
+ { id: "labelPosition", type: "enum", values: ["top", "inline"], default: "top" },
839
+ { id: "requiredStyle", type: "enum", values: ["asterisk", "badge"], default: "asterisk" },
840
+ { id: "requiredBadgeText", type: "text", default: "" },
841
+ // 决定 <form action method> —— 判据②。
842
+ { id: "formAction", type: "url", default: "" }
843
+ ],
844
+ blocks: [
845
+ {
846
+ type: "field",
847
+ fields: [
848
+ { id: "label", type: "text", default: "" },
849
+ // 提交数据的键名。v2 默认 'field',不照抄:多个字段同键会静默产出错数据。
850
+ // 空串**也不是**可见的错 —— HTML 的表单提交算法会跳过 name 为空的控件,
851
+ // 那一栏的数据就这么没了,页面上一点痕迹都没有。选空串是因为它是唯一
852
+ // **可辨识**的「作者没填」:'field' 与作者真的填了 'field' 无法区分。
853
+ // 由此 2.6 的渲染器负责把它变成可见的错:控件型 fieldType(label / step
854
+ // 之外)拿到空 name 即渲染失败抛错(D4)—— 画布上是占位、导出直接失败,
855
+ // 而不是发布之后才发现表单少收一栏。
856
+ { id: "name", type: "text", default: "" },
857
+ {
858
+ id: "fieldType",
859
+ type: "enum",
860
+ values: [
861
+ "text",
862
+ "email",
863
+ "tel",
864
+ "number",
865
+ "textarea",
866
+ "select",
867
+ "checkbox",
868
+ "radio",
869
+ "date",
870
+ "label",
871
+ "step"
872
+ ],
873
+ default: "text"
874
+ },
875
+ { id: "placeholder", type: "text", default: "" },
876
+ // select / checkbox / radio 的选项表。v2 存成换行分隔的字符串,渲染器要用
877
+ // /\r?\n|\\n/ 去拆 —— 那个 \\n 分支的存在本身就说明 AI 写出过字面量反斜杠 n。
878
+ { id: "options", type: "list", default: [] },
879
+ { id: "required", type: "boolean", default: false },
880
+ { id: "width", type: "enum", values: ["full", "half"], default: "full" }
881
+ ]
882
+ }
883
+ ]
884
+ };
885
+ var stickyCtaContract = {
886
+ type: "sticky-cta",
887
+ fields: [
888
+ { id: "buttonText", type: "text", default: "" },
889
+ { id: "buttonUrl", type: "url", default: "#" },
890
+ // 按钮左侧的说明文字。
891
+ { id: "text", type: "text", default: "" },
892
+ // 滚动多少 px 之后出现 —— 判据③,脚本读。
893
+ { id: "showAfter", type: "number", default: 0 },
894
+ { id: "showOn", type: "enum", values: ["mobile", "all"], default: "mobile" },
895
+ // 条内排布模式(justify-content)而非纯对齐取值 —— 根元素的 text-* 顶不上去。
896
+ { id: "align", type: "enum", values: ["center", "between"], default: "center" }
897
+ ]
898
+ };
899
+ var COMPONENT_CONTRACTS = {
900
+ [carouselContract.type]: carouselContract,
901
+ [faqContract.type]: faqContract,
902
+ [tabsContract.type]: tabsContract,
903
+ [formContract.type]: formContract,
904
+ [stickyCtaContract.type]: stickyCtaContract
905
+ };
906
+ var COMPONENT_TYPES = Object.keys(COMPONENT_CONTRACTS);
907
+
908
+ // src/components/settings.ts
909
+ var BLOCKS_KEY = "_blocks";
910
+
911
+ // src/components/resolve.ts
912
+ function matches(spec, value) {
913
+ switch (spec.type) {
914
+ case "text":
915
+ case "richtext":
916
+ case "url":
917
+ case "image":
918
+ return typeof value === "string";
919
+ case "number":
920
+ return typeof value === "number" && Number.isFinite(value);
921
+ case "boolean":
922
+ return typeof value === "boolean";
923
+ case "enum":
924
+ return typeof value === "string" && spec.values.includes(value);
925
+ case "list":
926
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
927
+ default: {
928
+ const never = spec;
929
+ throw new Error(`resolveComponent: \u672A\u5904\u7406\u7684\u53D6\u503C\u7C7B\u578B ${JSON.stringify(never)}`);
930
+ }
931
+ }
932
+ }
933
+ function resolveFields(fields, raw) {
934
+ const out = {};
935
+ for (const spec of fields) {
936
+ const given = raw?.[spec.id];
937
+ out[spec.id] = given !== void 0 && matches(spec, given) ? given : spec.default;
938
+ }
939
+ return out;
940
+ }
941
+ function rawBlocks(raw) {
942
+ const value = raw[BLOCKS_KEY];
943
+ if (!Array.isArray(value)) return [];
944
+ return value.filter(
945
+ (b) => typeof b === "object" && b !== null
946
+ );
947
+ }
948
+ function blockSpecOf(blocks, type) {
949
+ if (typeof type !== "string") return void 0;
950
+ return blocks?.find((b) => b.type === type);
951
+ }
952
+ function resolveComponent(contract, raw = {}) {
953
+ const blocks = [];
954
+ for (const block of rawBlocks(raw)) {
955
+ const spec = blockSpecOf(contract.blocks, block.type);
956
+ if (!spec) continue;
957
+ const settings = typeof block.settings === "object" && block.settings !== null ? block.settings : void 0;
958
+ blocks.push({ type: spec.type, settings: resolveFields(spec.fields, settings) });
959
+ }
960
+ return {
961
+ type: contract.type,
962
+ settings: resolveFields(contract.fields, raw),
963
+ blocks
964
+ };
965
+ }
966
+
967
+ // src/validate/components.ts
968
+ var SETTINGS_ATTR = "data-pt-component-settings";
969
+ var TYPE_ATTR = "data-pt-component";
970
+ function brief(value) {
971
+ if (typeof value === "string") return value.length > 40 ? `${value.slice(0, 40)}\u2026` : value;
972
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
973
+ if (value === null) return "null";
974
+ if (Array.isArray(value)) return "array";
975
+ return typeof value;
976
+ }
977
+ function checkFields(fields, settings, base, ptId, issues) {
978
+ const known = new Map(fields.map((spec) => [spec.id, spec]));
979
+ for (const key of Object.keys(settings)) {
980
+ if (key === BLOCKS_KEY) continue;
981
+ const spec = known.get(key);
982
+ if (!spec) {
983
+ issues.push({ ptId, rule: "COMPONENT_SETTING_UNKNOWN", params: { ...base, field: key } });
984
+ continue;
985
+ }
986
+ const value = settings[key];
987
+ if (value === void 0) continue;
988
+ if (matches(spec, value)) continue;
989
+ issues.push({
990
+ ptId,
991
+ rule: "COMPONENT_SETTING_INVALID",
992
+ params: {
993
+ ...base,
994
+ field: key,
995
+ // 枚举越界与类型不符在这里分得开:前者类型对、只是取值不在集合里。
996
+ reason: spec.type === "enum" && typeof value === "string" ? "enum" : "type",
997
+ expected: spec.type,
998
+ value: brief(value)
999
+ }
1000
+ });
1001
+ }
1002
+ }
1003
+ function checkBlocks(specs, raw, component, ptId, issues) {
1004
+ if (raw === void 0) return;
1005
+ if (!Array.isArray(raw)) {
1006
+ issues.push({
1007
+ ptId,
1008
+ rule: "COMPONENT_SETTING_INVALID",
1009
+ params: { component, field: BLOCKS_KEY, reason: "type", expected: "array", value: brief(raw) }
1010
+ });
1011
+ return;
1012
+ }
1013
+ for (const [index, block] of raw.entries()) {
1014
+ if (typeof block !== "object" || block === null) {
1015
+ issues.push({
1016
+ ptId,
1017
+ rule: "COMPONENT_SETTING_INVALID",
1018
+ params: { component, field: BLOCKS_KEY, index, reason: "type", expected: "object", value: brief(block) }
1019
+ });
1020
+ continue;
1021
+ }
1022
+ const type = block.type;
1023
+ const spec = specs?.find((b) => b.type === type);
1024
+ if (!spec) {
1025
+ issues.push({
1026
+ ptId,
1027
+ rule: "COMPONENT_SETTING_UNKNOWN",
1028
+ params: { component, field: BLOCKS_KEY, index, block: brief(type) }
1029
+ });
1030
+ continue;
1031
+ }
1032
+ const settings = block.settings;
1033
+ if (settings === void 0) continue;
1034
+ if (typeof settings !== "object" || settings === null) {
1035
+ issues.push({
1036
+ ptId,
1037
+ rule: "COMPONENT_SETTING_INVALID",
1038
+ params: { component, field: BLOCKS_KEY, index, reason: "type", expected: "object", value: brief(settings) }
1039
+ });
1040
+ continue;
1041
+ }
1042
+ checkFields(
1043
+ spec.fields,
1044
+ settings,
1045
+ { component, block: spec.type, index },
1046
+ ptId,
1047
+ issues
1048
+ );
1049
+ }
1050
+ }
1051
+ function checkComponentSettings(el, ptId, options, issues) {
1052
+ const component = el.getAttribute(TYPE_ATTR);
1053
+ if (component === null) return;
1054
+ const contract = COMPONENT_CONTRACTS[component];
1055
+ if (!contract) {
1056
+ if (options.knownComponentsOnly) {
1057
+ issues.push({ ptId, rule: "COMPONENT_SETTING_UNKNOWN", params: { component } });
1058
+ }
1059
+ return;
1060
+ }
1061
+ const rawText = el.getAttribute(SETTINGS_ATTR);
1062
+ if (rawText === null || rawText === "") return;
1063
+ let parsed;
1064
+ try {
1065
+ parsed = JSON.parse(rawText);
1066
+ } catch {
1067
+ issues.push({
1068
+ ptId,
1069
+ rule: "COMPONENT_SETTING_INVALID",
1070
+ params: { component, reason: "json" }
1071
+ });
1072
+ return;
1073
+ }
1074
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1075
+ issues.push({
1076
+ ptId,
1077
+ rule: "COMPONENT_SETTING_INVALID",
1078
+ params: { component, reason: "type", expected: "object", value: brief(parsed) }
1079
+ });
1080
+ return;
1081
+ }
1082
+ const settings = parsed;
1083
+ checkFields(contract.fields, settings, { component }, ptId, issues);
1084
+ checkBlocks(contract.blocks, settings[BLOCKS_KEY], component, ptId, issues);
1085
+ }
1086
+
632
1087
  // src/validate/index.ts
633
1088
  var EVENT_ATTR = /^on[a-z]/i;
634
- function validate(root, options) {
1089
+ function validate(root, options = {}) {
635
1090
  const issues = [];
636
- const isDocument = options.profile.root === "document" && isDoc(root);
637
- if (isDocument) checkFormatMeta(root, issues);
1091
+ if (isDoc(root)) checkFormatMeta(root, issues);
638
1092
  checkIds(root, issues);
639
1093
  checkElements(root, options, issues);
1094
+ if (options.singleSection) checkSingleSection(root, issues);
640
1095
  return { ok: issues.length === 0, issues };
641
1096
  }
1097
+ function checkSingleSection(root, issues) {
1098
+ const sections = root.querySelectorAll("section");
1099
+ if (sections.length <= 1) return;
1100
+ for (let i = 1; i < sections.length; i++) {
1101
+ issues.push({
1102
+ ptId: sections[i].getAttribute("data-pt-id") ?? void 0,
1103
+ rule: "STRUCTURE",
1104
+ params: { reason: "multiple-sections", count: sections.length }
1105
+ });
1106
+ }
1107
+ }
642
1108
  function isDoc(root) {
643
1109
  return root.nodeType === 9;
644
1110
  }
@@ -686,6 +1152,7 @@ function checkElements(root, options, issues) {
686
1152
  }
687
1153
  checkAttributes(el, ptId, issues);
688
1154
  checkClasses(el, ptId, options, issues);
1155
+ checkComponentSettings(el, ptId, options, issues);
689
1156
  checkPlaceholder(el, ptId, issues);
690
1157
  }
691
1158
  }
@@ -723,11 +1190,27 @@ function checkClasses(el, ptId, options, issues) {
723
1190
  issues.push({ ptId, rule: "NATIVE_PALETTE", params: { className: cls } });
724
1191
  continue;
725
1192
  }
1193
+ if (options.allowVhUnits === false) {
1194
+ const unit = viewportUnitIn(cls);
1195
+ if (unit) {
1196
+ issues.push({ ptId, rule: "UNIT_FORBIDDEN", params: { className: cls, unit } });
1197
+ continue;
1198
+ }
1199
+ }
726
1200
  if (options.canCompileClass && !options.canCompileClass(cls)) {
727
1201
  issues.push({ ptId, rule: "CLASS_UNCOMPILABLE", params: { className: cls } });
728
1202
  }
729
1203
  }
730
1204
  }
1205
+ var ARBITRARY_VIEWPORT_UNIT = /\[[^\]]*?\d(?:\.\d+)?(l|s|d)?(vh|vw|vmin|vmax|vb|vi)\b/;
1206
+ var SCREEN_SCALE = /(^|:)(min-|max-)?[hw]-(screen|[lsd]vh|[lsd]vw|dvh|dvw|svh|svw|lvh|lvw)$/;
1207
+ function viewportUnitIn(cls) {
1208
+ const arbitrary = ARBITRARY_VIEWPORT_UNIT.exec(cls);
1209
+ if (arbitrary) return `${arbitrary[1] ?? ""}${arbitrary[2]}`;
1210
+ const scale = SCREEN_SCALE.exec(cls);
1211
+ if (scale) return scale[3];
1212
+ return null;
1213
+ }
731
1214
  function checkPlaceholder(el, ptId, issues) {
732
1215
  if (!el.hasAttribute("data-pt-placeholder")) return;
733
1216
  const cls = el.getAttribute("class") ?? "";
@@ -762,6 +1245,12 @@ var ERROR_CODES = [
762
1245
  "INLINE_STYLE",
763
1246
  /** 使用了 Tailwind 内置色板。它本身可编译,所以必须由独立规则拦。 */
764
1247
  "NATIVE_PALETTE",
1248
+ /**
1249
+ * 用了视口相对单位(vh / dvh / h-screen…),而当前调用方关掉了它
1250
+ * (`validate(doc, { allowVhUnits: false })`)。同 NATIVE_PALETTE:它可编译,
1251
+ * 拒绝它是产品决定,所以「可编译」判定拦不住,必须独立一条。
1252
+ */
1253
+ "UNIT_FORBIDDEN",
765
1254
  /** class 在当前 @theme 下编译不出 CSS。 */
766
1255
  "CLASS_UNCOMPILABLE",
767
1256
  /** 变体前缀不在允许集内,或组合顺序不是「设备在前、状态在后」。 */
@@ -777,7 +1266,27 @@ var ERROR_CODES = [
777
1266
  /** 片段 canonical 往返不一致,说明 HTML 畸形。 */
778
1267
  "FRAGMENT_NOT_CANONICAL",
779
1268
  /** 存在未落图的图片占位 —— 阻塞发布,不阻塞预览。 */
780
- "PLACEHOLDER_UNRESOLVED"
1269
+ "PLACEHOLDER_UNRESOLVED",
1270
+ /**
1271
+ * 渲染了一个注册表里没有的组件类型。
1272
+ *
1273
+ * 渲染**不返回降级标记**(v2 会返回一段「渲染失败」的灰色 div),所以这条必须抛。
1274
+ * 画布可以捕获后显示自己的占位(文案在宿主的三语字典里),成品拼装则应当整体失败 ——
1275
+ * 吞掉它就会静默产出一份缺了一块的成品,用户发布完才发现。
1276
+ */
1277
+ "COMPONENT_UNKNOWN",
1278
+ /** 渲染器自身抛错。同上,不降级、不吞。 */
1279
+ "COMPONENT_RENDER_FAILED",
1280
+ /**
1281
+ * 组件设置里出现了契约中不存在的字段。
1282
+ *
1283
+ * 这是**危险的那个方向**:写了一个渲染器不认的字段 → 渲染器按默认值渲染 →
1284
+ * 改了没反应、无报错(这个仓对弹窗内置件烧过同样的账)。反方向(契约有、面板
1285
+ * 没暴露)只是少个入口,无声无害,不报。
1286
+ */
1287
+ "COMPONENT_SETTING_UNKNOWN",
1288
+ /** 组件设置的取值类型不符、枚举越界,或设置 JSON 本身解析不出来。 */
1289
+ "COMPONENT_SETTING_INVALID"
781
1290
  ];
782
1291
  var ERROR_CODE_SET = new Set(ERROR_CODES);
783
1292
  function isErrorCode(value) {
@@ -795,13 +1304,13 @@ function editorError(code, extra) {
795
1304
 
796
1305
  // src/validate/structure.ts
797
1306
  var NOT_A_LAYER = /* @__PURE__ */ new Set(["html", "head", "body"]);
798
- function layerOf(el, profile) {
1307
+ var LEAF_TYPE_SET = new Set(LEAF_TYPES);
1308
+ function layerOf(el) {
799
1309
  const name = tag(el);
800
1310
  if (NOT_A_LAYER.has(name)) return null;
801
- const hasSectionLayer = profile.layers.includes("section");
802
- if (hasSectionLayer && name === "section") return "section";
1311
+ if (name === "section") return "section";
803
1312
  const ptType = el.getAttribute("data-pt-type");
804
- if (ptType && profile.leafTypes.includes(ptType)) return "leaf";
1313
+ if (ptType && LEAF_TYPE_SET.has(ptType)) return "leaf";
805
1314
  if (el.hasAttribute("data-pt-component")) return "leaf";
806
1315
  const opaque = closestOpaque(el);
807
1316
  if (opaque && opaque.host === el) return "leaf";
@@ -809,19 +1318,19 @@ function layerOf(el, profile) {
809
1318
  return null;
810
1319
  }
811
1320
  var MAX_CONTAINER_DEPTH = 2;
812
- function containerDepth(el, profile) {
1321
+ function containerDepth(el) {
813
1322
  let depth = 0;
814
1323
  let cur = el;
815
1324
  while (cur) {
816
- if (layerOf(cur, profile) === "container") depth += 1;
1325
+ if (layerOf(cur) === "container" && !isWrapLayer(cur)) depth += 1;
817
1326
  cur = cur.parentElement;
818
1327
  }
819
1328
  return depth;
820
1329
  }
821
- function canContain(parent, childLayer, profile) {
1330
+ function canContain(parent, childLayer) {
822
1331
  const opaque = closestOpaque(parent);
823
1332
  if (opaque) return "opaque-subtree";
824
- const parentLayer = layerOf(parent, profile);
1333
+ const parentLayer = layerOf(parent);
825
1334
  if (childLayer === "section") {
826
1335
  const isBody = tag(parent) === "body";
827
1336
  return isBody ? null : "section-not-body-child";
@@ -829,23 +1338,21 @@ function canContain(parent, childLayer, profile) {
829
1338
  if (childLayer === "container") {
830
1339
  if (parentLayer === "section") return null;
831
1340
  if (parentLayer === "container") {
832
- return containerDepth(parent, profile) + 1 > MAX_CONTAINER_DEPTH ? "container-too-deep" : null;
1341
+ return containerDepth(parent) + 1 > MAX_CONTAINER_DEPTH ? "container-too-deep" : null;
833
1342
  }
834
- if (!profile.layers.includes("section") && parentLayer === null) return null;
835
1343
  return parentLayer === "leaf" ? "leaf-cannot-contain" : "unknown-layer";
836
1344
  }
837
1345
  if (parentLayer === "container") return null;
838
1346
  if (parentLayer === "leaf") return "leaf-cannot-contain";
839
- if (!profile.layers.includes("section") && parentLayer === null) return null;
840
1347
  return "unknown-layer";
841
1348
  }
842
- function deleteTarget(el, profile) {
843
- const layer = layerOf(el, profile);
1349
+ function deleteTarget(el) {
1350
+ const layer = layerOf(el);
844
1351
  if (layer !== "container") return el;
845
1352
  const parent = el.parentElement;
846
- if (!parent || layerOf(parent, profile) !== "section") return el;
1353
+ if (!parent || layerOf(parent) !== "section") return el;
847
1354
  const siblingContainers = Array.from(parent.children).filter(
848
- (c) => c !== el && layerOf(c, profile) === "container"
1355
+ (c) => c !== el && layerOf(c) === "container"
849
1356
  );
850
1357
  return siblingContainers.length === 0 ? parent : el;
851
1358
  }
@@ -931,33 +1438,33 @@ function requireCanonicalFragment(doc, html) {
931
1438
 
932
1439
  // src/ops/apply.ts
933
1440
  var RAW_TEXT = /* @__PURE__ */ new Set(["style", "script", "xmp", "iframe", "noembed", "noframes", "plaintext"]);
934
- function apply(doc, ops, options) {
1441
+ function apply(doc, ops) {
935
1442
  if (!Array.isArray(ops)) throw new TypeError("apply(): ops \u5FC5\u987B\u662F\u6570\u7EC4");
936
1443
  if (ops.length === 0) return { ok: true, inverse: [] };
937
1444
  const rehearsal = doc.cloneNode(true);
938
- const dry = run(rehearsal, ops, options);
1445
+ const dry = run(rehearsal, ops);
939
1446
  if (!dry.ok) return dry;
940
- return run(doc, ops, options);
1447
+ return run(doc, ops);
941
1448
  }
942
- function run(doc, ops, options) {
1449
+ function run(doc, ops) {
943
1450
  const inverse = [];
944
1451
  for (let i = 0; i < ops.length; i++) {
945
- const result = applyOne(doc, ops[i], options);
1452
+ const result = applyOne(doc, ops[i]);
946
1453
  if (!result.ok) return { ok: false, error: result.error, failedIndex: i };
947
1454
  inverse.unshift(result.inverse);
948
1455
  }
949
1456
  return { ok: true, inverse };
950
1457
  }
951
- function applyOne(doc, op, options) {
1458
+ function applyOne(doc, op) {
952
1459
  switch (op.op) {
953
1460
  case "set":
954
1461
  return applySet(doc, op);
955
1462
  case "insert":
956
- return applyInsert(doc, op, options);
1463
+ return applyInsert(doc, op);
957
1464
  case "delete":
958
- return applyDelete(doc, op, options);
1465
+ return applyDelete(doc, op);
959
1466
  case "move":
960
- return applyMove(doc, op, options);
1467
+ return applyMove(doc, op);
961
1468
  case "replace":
962
1469
  return applyReplace(doc, op);
963
1470
  default:
@@ -969,10 +1476,15 @@ function applyOne(doc, op, options) {
969
1476
  };
970
1477
  }
971
1478
  }
1479
+ function rejectWrapTarget(el, ptId) {
1480
+ return isWrapLayer(el) ? editorError("STRUCTURE", { ptId, params: { reason: "wrap-layer-immutable" } }) : null;
1481
+ }
972
1482
  function applySet(doc, op) {
973
1483
  const miss = requireIdHit(doc, op.id);
974
1484
  if (miss) return { ok: false, error: miss };
975
1485
  const el = findById(doc, op.id);
1486
+ const wrapped = rejectWrapTarget(el, op.id);
1487
+ if (wrapped) return { ok: false, error: wrapped };
976
1488
  const opaque = closestOpaque(el);
977
1489
  if (opaque && opaque.host !== el && opaque.mode !== "text") {
978
1490
  return { ok: false, error: editorError("STRUCTURE", { ptId: op.id, params: { reason: "opaque-subtree" } }) };
@@ -1018,10 +1530,10 @@ function applySet(doc, op) {
1018
1530
  return { ok: true, inverse: inv };
1019
1531
  }
1020
1532
  function applyClasses(el, spec) {
1021
- const list = normalizeClassValue(el.getAttribute("class") ?? "").split(" ").filter(Boolean);
1022
- const set = new Set(list);
1533
+ const list2 = normalizeClassValue(el.getAttribute("class") ?? "").split(" ").filter(Boolean);
1534
+ const set = new Set(list2);
1023
1535
  for (const c of spec.remove ?? []) set.delete(c);
1024
- const out = list.filter((c) => set.has(c));
1536
+ const out = list2.filter((c) => set.has(c));
1025
1537
  for (const c of spec.add ?? []) {
1026
1538
  if (!set.has(c)) {
1027
1539
  set.add(c);
@@ -1065,7 +1577,7 @@ function setChildren(doc, el, text) {
1065
1577
  }
1066
1578
  el.appendChild(parseFragment(doc, text));
1067
1579
  }
1068
- function applyInsert(doc, op, options) {
1580
+ function applyInsert(doc, op) {
1069
1581
  const miss = requireIdHit(doc, op.parentId);
1070
1582
  if (miss) return { ok: false, error: miss };
1071
1583
  const parent = findById(doc, op.parentId);
@@ -1079,11 +1591,11 @@ function applyInsert(doc, op, options) {
1079
1591
  const idErr = requireFragmentIds(frag, collectDocIds(doc));
1080
1592
  if (idErr) return { ok: false, error: idErr };
1081
1593
  const root = fragmentElementRoots(frag)[0];
1082
- const childLayer = layerOf(root, options.profile);
1594
+ const childLayer = layerOf(root);
1083
1595
  if (!childLayer) {
1084
1596
  return { ok: false, error: editorError("STRUCTURE", { params: { reason: "unknown-layer" } }) };
1085
1597
  }
1086
- const rejection = canContain(parent, childLayer, options.profile);
1598
+ const rejection = canContain(parent, childLayer);
1087
1599
  if (rejection) {
1088
1600
  return { ok: false, error: editorError("STRUCTURE", { ptId: op.parentId, params: { reason: rejection } }) };
1089
1601
  }
@@ -1092,15 +1604,17 @@ function applyInsert(doc, op, options) {
1092
1604
  parent.insertBefore(frag, parent.childNodes[at] ?? null);
1093
1605
  return { ok: true, inverse: { op: "delete", id: newId } };
1094
1606
  }
1095
- function applyDelete(doc, op, options) {
1607
+ function applyDelete(doc, op) {
1096
1608
  const miss = requireIdHit(doc, op.id);
1097
1609
  if (miss) return { ok: false, error: miss };
1098
1610
  const el = findById(doc, op.id);
1611
+ const wrapped = rejectWrapTarget(el, op.id);
1612
+ if (wrapped) return { ok: false, error: wrapped };
1099
1613
  const opaque = closestOpaque(el);
1100
1614
  if (opaque && opaque.host !== el) {
1101
1615
  return { ok: false, error: editorError("STRUCTURE", { ptId: op.id, params: { reason: "opaque-subtree" } }) };
1102
1616
  }
1103
- const target = deleteTarget(el, options.profile);
1617
+ const target = deleteTarget(el);
1104
1618
  const parent = target.parentElement;
1105
1619
  if (!parent) {
1106
1620
  return { ok: false, error: editorError("STRUCTURE", { ptId: op.id, params: { reason: "no-parent" } }) };
@@ -1118,21 +1632,23 @@ function applyDelete(doc, op, options) {
1118
1632
  inverse: { op: "insert", parentId, index: 0, html, nodeIndex }
1119
1633
  };
1120
1634
  }
1121
- function applyMove(doc, op, options) {
1635
+ function applyMove(doc, op) {
1122
1636
  const missSelf = requireIdHit(doc, op.id);
1123
1637
  if (missSelf) return { ok: false, error: missSelf };
1124
1638
  const missParent = requireIdHit(doc, op.parentId);
1125
1639
  if (missParent) return { ok: false, error: missParent };
1126
1640
  const el = findById(doc, op.id);
1127
1641
  const parent = findById(doc, op.parentId);
1642
+ const wrapped = rejectWrapTarget(el, op.id);
1643
+ if (wrapped) return { ok: false, error: wrapped };
1128
1644
  if (el === parent || el.contains(parent)) {
1129
1645
  return { ok: false, error: editorError("STRUCTURE", { ptId: op.id, params: { reason: "move-into-self" } }) };
1130
1646
  }
1131
- const layer = layerOf(el, options.profile);
1647
+ const layer = layerOf(el);
1132
1648
  if (!layer) {
1133
1649
  return { ok: false, error: editorError("STRUCTURE", { ptId: op.id, params: { reason: "unknown-layer" } }) };
1134
1650
  }
1135
- const rejection = canContain(parent, layer, options.profile);
1651
+ const rejection = canContain(parent, layer);
1136
1652
  if (rejection) {
1137
1653
  return { ok: false, error: editorError("STRUCTURE", { ptId: op.parentId, params: { reason: rejection } }) };
1138
1654
  }
@@ -1153,6 +1669,8 @@ function applyReplace(doc, op) {
1153
1669
  const miss = requireIdHit(doc, op.id);
1154
1670
  if (miss) return { ok: false, error: miss };
1155
1671
  const el = findById(doc, op.id);
1672
+ const wrapped = rejectWrapTarget(el, op.id);
1673
+ if (wrapped) return { ok: false, error: wrapped };
1156
1674
  const canonical = requireCanonicalFragment(doc, op.html);
1157
1675
  if (canonical) return { ok: false, error: canonical };
1158
1676
  const frag = parseFragment(doc, op.html);
@@ -1382,6 +1900,450 @@ function clearCompileCache() {
1382
1900
  cache.clear();
1383
1901
  }
1384
1902
 
1903
+ // src/components/index.ts
1904
+ var components_exports = {};
1905
+ __export(components_exports, {
1906
+ BLOCKS_KEY: () => BLOCKS_KEY,
1907
+ COMPONENT_CONTRACTS: () => COMPONENT_CONTRACTS,
1908
+ COMPONENT_TYPES: () => COMPONENT_TYPES,
1909
+ SETTING_TYPES: () => SETTING_TYPES,
1910
+ carouselContract: () => carouselContract,
1911
+ faqContract: () => faqContract,
1912
+ formContract: () => formContract,
1913
+ renderCss: () => renderCss,
1914
+ renderHtml: () => renderHtml,
1915
+ renderJs: () => renderJs,
1916
+ resolveComponent: () => resolveComponent,
1917
+ stickyCtaContract: () => stickyCtaContract,
1918
+ tabsContract: () => tabsContract
1919
+ });
1920
+
1921
+ // src/components/renderers/shared.ts
1922
+ var esc = escapeText;
1923
+ var escAttr = escapeAttribute;
1924
+ function str(settings, id) {
1925
+ return settings[id];
1926
+ }
1927
+ function num(settings, id) {
1928
+ return settings[id];
1929
+ }
1930
+ function bool(settings, id) {
1931
+ return settings[id];
1932
+ }
1933
+ function list(settings, id) {
1934
+ return settings[id];
1935
+ }
1936
+ function componentScript(type, initBody) {
1937
+ if (!/^[a-z][a-z0-9-]*$/.test(type)) throw new TypeError(type);
1938
+ return `(function(){var T='[data-pt-component="${type}"]',F='__pt_${type.replace(/-/g, "_")}';function init(root){if(root[F])return;root[F]=1;${initBody}}function scan(){var l=document.querySelectorAll(T);for(var i=0;i<l.length;i++)init(l[i]);}if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',scan);else scan();})();`;
1939
+ }
1940
+
1941
+ // src/components/renderers/carousel.ts
1942
+ var carouselRenderer = {
1943
+ html(instance) {
1944
+ const layout = str(instance.settings, "layout");
1945
+ const imageOnly = layout === "image";
1946
+ const slides = instance.blocks.filter((b) => b.type === "slide");
1947
+ const many = slides.length > 1;
1948
+ const showDots = bool(instance.settings, "showDots") && many;
1949
+ const showArrows = bool(instance.settings, "showArrows") && many;
1950
+ const track = slides.map((slide) => {
1951
+ const image = str(slide.settings, "image");
1952
+ const title = str(slide.settings, "title");
1953
+ const subtitle = str(slide.settings, "subtitle");
1954
+ const buttonText = str(slide.settings, "buttonText");
1955
+ const img = image ? `<img class="ptc-carousel-img" src="${escAttr(image)}" alt="${escAttr(title)}" loading="lazy">` : "";
1956
+ const body = imageOnly ? "" : [
1957
+ title ? `<h3 class="ptc-carousel-title">${esc(title)}</h3>` : "",
1958
+ subtitle ? `<p class="ptc-carousel-sub">${esc(subtitle)}</p>` : "",
1959
+ buttonText ? `<a class="ptc-carousel-btn" href="${escAttr(str(slide.settings, "buttonUrl"))}">${esc(buttonText)}</a>` : ""
1960
+ ].join("");
1961
+ return '<div class="ptc-carousel-slide">' + img + (body ? `<div class="ptc-carousel-body">${body}</div>` : "") + "</div>";
1962
+ }).join("");
1963
+ const dots = showDots ? '<div class="ptc-carousel-dots">' + slides.map(
1964
+ (_, i) => `<button type="button" class="ptc-carousel-dot" data-go="${i}" aria-current="${i === 0 ? "true" : "false"}"></button>`
1965
+ ).join("") + "</div>" : "";
1966
+ const arrows = showArrows ? '<button type="button" class="ptc-carousel-arrow" data-go="prev">\u2039</button><button type="button" class="ptc-carousel-arrow" data-go="next">\u203A</button>' : "";
1967
+ return `<div class="ptc-carousel" data-carousel-layout="${escAttr(layout)}" data-autoplay="${bool(instance.settings, "autoplay") && many ? "true" : "false"}" data-interval="${num(instance.settings, "interval")}"><div class="ptc-carousel-track">${track}</div>` + dots + arrows + "</div>";
1968
+ },
1969
+ css() {
1970
+ return [
1971
+ ".ptc-carousel{position:relative;width:100%}",
1972
+ // 滑动本体:不跑脚本也能用手指滑。
1973
+ ".ptc-carousel-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;scroll-behavior:smooth;scrollbar-width:none}",
1974
+ ".ptc-carousel-track::-webkit-scrollbar{display:none}",
1975
+ ".ptc-carousel-slide{position:relative;flex:0 0 100%;scroll-snap-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:calc(var(--spacing)*2);aspect-ratio:16/9;overflow:hidden;background:var(--pt-carousel-slide-bg,color-mix(in oklab,currentColor 6%,transparent))}",
1976
+ ".ptc-carousel-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}",
1977
+ ".ptc-carousel-body{position:relative;padding:calc(var(--spacing)*6)}",
1978
+ ".ptc-carousel-title{margin:0;font-size:2em;font-weight:700}",
1979
+ ".ptc-carousel-sub{margin:calc(var(--spacing)*2) 0 0}",
1980
+ ".ptc-carousel-btn{display:inline-block;margin-top:calc(var(--spacing)*4);padding:calc(var(--spacing)*3) calc(var(--spacing)*7);border-radius:var(--radius-lg);text-decoration:none;background:var(--pt-carousel-btn-bg,currentColor);color:var(--pt-carousel-btn-fg,white)}",
1981
+ ".ptc-carousel-dots{position:absolute;left:0;right:0;bottom:calc(var(--spacing)*4);display:flex;justify-content:center;gap:calc(var(--spacing)*2)}",
1982
+ ".ptc-carousel-dot{width:calc(var(--spacing)*2.5);height:calc(var(--spacing)*2.5);padding:0;border:0;border-radius:var(--radius-4xl);cursor:pointer;background:var(--pt-carousel-dot-off,color-mix(in oklab,currentColor 25%,transparent))}",
1983
+ '.ptc-carousel-dot[aria-current="true"]{background:var(--pt-carousel-dot-on,currentColor)}',
1984
+ ".ptc-carousel-arrow{position:absolute;top:50%;translate:0 -50%;display:flex;align-items:center;justify-content:center;width:calc(var(--spacing)*9);height:calc(var(--spacing)*9);border:0;border-radius:var(--radius-4xl);cursor:pointer;font-size:1.25em;line-height:1;background:var(--pt-carousel-arrow-bg,color-mix(in oklab,currentColor 20%,transparent))}",
1985
+ '.ptc-carousel-arrow[data-go="prev"]{left:calc(var(--spacing)*3)}',
1986
+ '.ptc-carousel-arrow[data-go="next"]{right:calc(var(--spacing)*3)}',
1987
+ // image 档没有文案层,body 不渲染;这条只是把 padding 的余味也去掉。
1988
+ '[data-carousel-layout="image"] .ptc-carousel-slide{gap:0}',
1989
+ "@media (prefers-reduced-motion:reduce){.ptc-carousel-track{scroll-behavior:auto}}"
1990
+ ].join("");
1991
+ },
1992
+ js() {
1993
+ return componentScript(
1994
+ "carousel",
1995
+ `var tk=root.querySelector('.ptc-carousel-track');if(!tk)return;var dt=root.querySelectorAll('.ptc-carousel-dot');function at(){return tk.clientWidth?Math.round(tk.scrollLeft/tk.clientWidth):0;}function go(i){var n=tk.children.length;if(!n)return;var t=((i%n)+n)%n;tk.scrollTo({left:t*tk.clientWidth,behavior:"smooth"});}function sync(){var c=at();for(var i=0;i<dt.length;i++)dt[i].setAttribute('aria-current',i===c?'true':'false');}tk.addEventListener('scroll',sync);root.addEventListener('click',function(e){var t=e.target,el=t&&t.closest?t.closest("[data-go]"):null;if(!el||!root.contains(el))return;var v=el.getAttribute('data-go');if(v==='prev')go(at()-1);else if(v==='next')go(at()+1);else go(parseInt(v,10));});if(root.getAttribute('data-autoplay')==='true'){var ms=parseInt(root.getAttribute('data-interval'),10);if(ms>0)setInterval(function(){go(at()+1);},ms);}sync();`
1996
+ );
1997
+ }
1998
+ };
1999
+
2000
+ // src/components/renderers/faq.ts
2001
+ function icon(kind) {
2002
+ if (kind === "plus") {
2003
+ return '<span class="ptc-faq-ic" aria-hidden="true"><span class="ptc-faq-ic-p">+</span><span class="ptc-faq-ic-m">\u2212</span></span>';
2004
+ }
2005
+ return '<span class="ptc-faq-ic" aria-hidden="true">\u203A</span>';
2006
+ }
2007
+ function item(question, answer, open, listStyle, ic) {
2008
+ const q = esc(question);
2009
+ if (listStyle) {
2010
+ return `<div class="ptc-faq-item"><div class="ptc-faq-q">${q}</div><div class="ptc-faq-a">${answer}</div></div>`;
2011
+ }
2012
+ return `<details class="ptc-faq-item"${open ? " open" : ""}><summary class="ptc-faq-q"><span>${q}</span>${ic}</summary><div class="ptc-faq-a">${answer}</div></details>`;
2013
+ }
2014
+ var faqRenderer = {
2015
+ html(instance) {
2016
+ const style = str(instance.settings, "style");
2017
+ const iconStyle = str(instance.settings, "iconStyle");
2018
+ const title = str(instance.settings, "title");
2019
+ const listStyle = style === "list";
2020
+ const ic = icon(iconStyle);
2021
+ const head = title ? `<h3 class="ptc-faq-title">${esc(title)}</h3>` : "";
2022
+ const items = instance.blocks.filter((b) => b.type === "item").map(
2023
+ (b) => item(
2024
+ str(b.settings, "question"),
2025
+ // richtext:按契约原样插入,不转义。
2026
+ str(b.settings, "answer"),
2027
+ b.settings["defaultOpen"] === true,
2028
+ listStyle,
2029
+ ic
2030
+ )
2031
+ ).join("");
2032
+ return `<div class="ptc-faq" data-faq-style="${escAttr(style)}" data-faq-icon="${escAttr(iconStyle)}">` + head + `<div class="ptc-faq-items">${items}</div></div>`;
2033
+ },
2034
+ css() {
2035
+ return [
2036
+ ".ptc-faq{width:100%}",
2037
+ ".ptc-faq-title{margin:0 0 calc(var(--spacing)*10);font-size:1.5em;font-weight:700}",
2038
+ ".ptc-faq-items{display:flex;flex-direction:column;gap:calc(var(--spacing)*2)}",
2039
+ ".ptc-faq-item{border:1px solid var(--pt-faq-border,color-mix(in oklab,currentColor 15%,transparent));border-radius:var(--radius-lg);overflow:hidden}",
2040
+ ".ptc-faq-q{display:flex;align-items:center;justify-content:space-between;gap:calc(var(--spacing)*3);padding:calc(var(--spacing)*3) calc(var(--spacing)*4);font-weight:500}",
2041
+ ".ptc-faq-a{padding:calc(var(--spacing)*3) calc(var(--spacing)*4);line-height:1.6;color:var(--pt-faq-answer-fg,color-mix(in oklab,currentColor 70%,transparent))}",
2042
+ "details.ptc-faq-item>.ptc-faq-q{cursor:pointer;list-style:none;background:var(--pt-faq-summary-bg,color-mix(in oklab,currentColor 4%,transparent))}",
2043
+ "details.ptc-faq-item>.ptc-faq-q::-webkit-details-marker{display:none}",
2044
+ ".ptc-faq-ic{flex:none;font-size:1.2em;line-height:1}",
2045
+ // chevron:靠旋转表达开合,一个字形。
2046
+ '[data-faq-icon="chevron"] .ptc-faq-ic{transition:transform .2s}',
2047
+ '[data-faq-icon="chevron"] details[open]>.ptc-faq-q .ptc-faq-ic{transform:rotate(90deg)}',
2048
+ // plus:两个字形,显隐由 [open] 决定,同样不需要脚本。
2049
+ '[data-faq-icon="plus"] .ptc-faq-ic-m{display:none}',
2050
+ '[data-faq-icon="plus"] details[open]>.ptc-faq-q .ptc-faq-ic-p{display:none}',
2051
+ '[data-faq-icon="plus"] details[open]>.ptc-faq-q .ptc-faq-ic-m{display:inline}',
2052
+ // list 档:没有 <details>,去掉交互态的余味。
2053
+ '[data-faq-style="list"] .ptc-faq-item{border:0;border-radius:0}',
2054
+ '[data-faq-style="list"] .ptc-faq-q{padding-inline:0;background:none}',
2055
+ '[data-faq-style="list"] .ptc-faq-a{padding-inline:0}'
2056
+ ].join("");
2057
+ },
2058
+ js() {
2059
+ return componentScript(
2060
+ "faq",
2061
+ `root.addEventListener('click',function(e){var t=e.target,s=t&&t.closest?t.closest("summary"):null;if(!s||!root.contains(s))return;var d=s.parentNode;if(!d||d.tagName!=='DETAILS'||d.open)return;var l=root.querySelectorAll('details');for(var i=0;i<l.length;i++)if(l[i]!==d)l[i].removeAttribute('open');});`
2062
+ );
2063
+ }
2064
+ };
2065
+
2066
+ // src/components/renderers/form.ts
2067
+ var NOT_A_CONTROL = /* @__PURE__ */ new Set(["label", "step"]);
2068
+ var INPUT_TYPES = /* @__PURE__ */ new Set(["email", "tel", "number", "date"]);
2069
+ var SUBMIT_ICONS = {
2070
+ email: "\u2709\uFE0E ",
2071
+ arrow: "\u2192 ",
2072
+ check: "\u2713 "
2073
+ };
2074
+ function requiredMark(style, badgeText) {
2075
+ if (style === "badge") return `<span class="ptc-form-badge">${esc(badgeText)}</span>`;
2076
+ return '<span class="ptc-form-star" aria-hidden="true">*</span>';
2077
+ }
2078
+ function control(field2, id, name) {
2079
+ const type = str(field2.settings, "fieldType");
2080
+ const required = bool(field2.settings, "required") ? " required" : "";
2081
+ const placeholder = str(field2.settings, "placeholder");
2082
+ const ph = placeholder ? ` placeholder="${escAttr(placeholder)}"` : "";
2083
+ if (type === "textarea") {
2084
+ return `<textarea class="ptc-form-input" id="${id}" name="${name}"${ph}${required}></textarea>`;
2085
+ }
2086
+ if (type === "select") {
2087
+ const options = list(field2.settings, "options").map((o) => `<option>${esc(o)}</option>`).join("");
2088
+ return `<select class="ptc-form-input" id="${id}" name="${name}"${required}>${options}</select>`;
2089
+ }
2090
+ if (type === "checkbox" || type === "radio") {
2091
+ return list(field2.settings, "options").map(
2092
+ (o, i) => `<label class="ptc-form-choice"><input type="${type}" name="${name}" value="${escAttr(o)}"${i === 0 ? required : ""}><span>${esc(o)}</span></label>`
2093
+ ).join("");
2094
+ }
2095
+ const inputType = INPUT_TYPES.has(type) ? type : "text";
2096
+ return `<input class="ptc-form-input" type="${inputType}" id="${id}" name="${name}"${ph}${required}>`;
2097
+ }
2098
+ function field(block, index, instanceId, style, badge) {
2099
+ const type = str(block.settings, "fieldType");
2100
+ const label = str(block.settings, "label");
2101
+ if (NOT_A_CONTROL.has(type)) {
2102
+ return type === "label" ? `<div class="ptc-form-note">${esc(label)}</div>` : "";
2103
+ }
2104
+ const name = str(block.settings, "name");
2105
+ if (!name) {
2106
+ throw editorError("COMPONENT_RENDER_FAILED", {
2107
+ params: { type: "form", field: index, fieldType: type }
2108
+ });
2109
+ }
2110
+ const id = `${escAttr(instanceId)}-f${index}`;
2111
+ const mark = bool(block.settings, "required") ? requiredMark(style, badge) : "";
2112
+ const body = control(block, id, escAttr(name));
2113
+ const width = str(block.settings, "width");
2114
+ if (type === "checkbox" || type === "radio") {
2115
+ return `<fieldset class="ptc-form-field" data-width="${escAttr(width)}"><legend class="ptc-form-label">${esc(label)}${mark}</legend><div class="ptc-form-choices">${body}</div></fieldset>`;
2116
+ }
2117
+ return `<div class="ptc-form-field" data-width="${escAttr(width)}"><label class="ptc-form-label" for="${id}">${esc(label)}${mark}</label>${body}</div>`;
2118
+ }
2119
+ function intoSteps(blocks) {
2120
+ const steps = [];
2121
+ let current = { title: "", fields: [] };
2122
+ let started = false;
2123
+ for (const block of blocks) {
2124
+ if (str(block.settings, "fieldType") === "step") {
2125
+ if (started || current.fields.length > 0) steps.push(current);
2126
+ current = { title: str(block.settings, "label"), fields: [] };
2127
+ started = true;
2128
+ } else {
2129
+ current.fields.push(block);
2130
+ }
2131
+ }
2132
+ steps.push(current);
2133
+ return steps;
2134
+ }
2135
+ var formRenderer = {
2136
+ html(instance, instanceId) {
2137
+ const settings = instance.settings;
2138
+ const blocks = instance.blocks.filter((b) => b.type === "field");
2139
+ const requiredStyle = str(settings, "requiredStyle");
2140
+ const badge = str(settings, "requiredBadgeText");
2141
+ const id = escAttr(instanceId);
2142
+ let seq = 0;
2143
+ const render = (block) => field(block, seq++, instanceId, requiredStyle, badge);
2144
+ const steps = bool(settings, "multiStep") ? intoSteps(blocks) : [];
2145
+ const stepped = steps.length > 1;
2146
+ let nav = "";
2147
+ let body;
2148
+ if (stepped) {
2149
+ nav = '<div class="ptc-form-steps">' + steps.map(
2150
+ (step, i) => `<button type="button" class="ptc-form-step-tab" data-step-to="${i}" aria-selected="${i === 0 ? "true" : "false"}">${esc(step.title)}</button>`
2151
+ ).join("") + "</div>";
2152
+ body = steps.map(
2153
+ (step, i) => `<div class="ptc-form-step" data-step="${i}"${i === 0 ? "" : " hidden"}>` + step.fields.map(render).join("") + "</div>"
2154
+ ).join("");
2155
+ } else {
2156
+ body = blocks.map(render).join("");
2157
+ }
2158
+ const action = str(settings, "formAction");
2159
+ const formName = str(settings, "formName");
2160
+ const submit = bool(settings, "showSubmit") ? '<div class="ptc-form-actions"><button type="submit" class="ptc-form-submit">' + (SUBMIT_ICONS[str(settings, "submitIcon")] ?? "") + `${esc(str(settings, "submitText"))}</button></div>` : "";
2161
+ const footer = str(settings, "footerText");
2162
+ const success = str(settings, "successMessage");
2163
+ return `<div class="ptc-form" data-label-position="${escAttr(str(settings, "labelPosition"))}">` + nav + `<form class="ptc-form-body" id="${id}-form"` + (formName ? ` data-form-name="${escAttr(formName)}"` : "") + (action ? ` action="${escAttr(action)}" method="post"` : "") + `>${body}${submit}</form>` + // richtext:原样插入(页脚正是隐私政策链接的所在)。
2164
+ (footer ? `<div class="ptc-form-footer">${footer}</div>` : "") + `<div class="ptc-form-success" role="status" hidden>${esc(success)}</div></div>`;
2165
+ },
2166
+ css() {
2167
+ return [
2168
+ ".ptc-form{width:100%}",
2169
+ ".ptc-form-body{display:flex;flex-wrap:wrap;gap:calc(var(--spacing)*4)}",
2170
+ ".ptc-form-step{display:flex;flex-wrap:wrap;gap:calc(var(--spacing)*4);width:100%}",
2171
+ ".ptc-form-field{flex:1 1 100%;min-width:0;margin:0;padding:0;border:0}",
2172
+ '.ptc-form-field[data-width="half"]{flex:1 1 calc(50% - var(--spacing)*2)}',
2173
+ ".ptc-form-label{display:block;margin-bottom:calc(var(--spacing)*1.5);font-weight:500}",
2174
+ ".ptc-form-note{width:100%}",
2175
+ ".ptc-form-input{width:100%;box-sizing:border-box;font:inherit;padding:calc(var(--spacing)*2.5) calc(var(--spacing)*3.5);border:1px solid var(--pt-form-field-border,color-mix(in oklab,currentColor 25%,transparent));border-radius:var(--radius-md);background:var(--pt-form-field-bg,transparent);color:var(--pt-form-field-fg,inherit)}",
2176
+ "textarea.ptc-form-input{min-height:6em;resize:vertical}",
2177
+ ".ptc-form-choices{display:flex;flex-wrap:wrap;gap:calc(var(--spacing)*4)}",
2178
+ ".ptc-form-choice{display:inline-flex;align-items:center;gap:calc(var(--spacing)*1.5)}",
2179
+ ".ptc-form-star{color:var(--pt-form-required-fg,currentColor)}",
2180
+ ".ptc-form-badge{margin-left:calc(var(--spacing)*1.5);padding:0 calc(var(--spacing)*1.5);border-radius:var(--radius-xs);font-size:.75em;vertical-align:middle;background:var(--pt-form-required-fg,currentColor);color:var(--pt-form-on-accent,white)}",
2181
+ ".ptc-form-actions{width:100%}",
2182
+ // 手机满宽(拇指点击区),md 起自适应 —— submitFullWidth 那个开关的继任者。
2183
+ ".ptc-form-submit{width:100%;font:inherit;font-weight:600;cursor:pointer;padding:calc(var(--spacing)*3) calc(var(--spacing)*7);border:0;border-radius:var(--radius-md);background:var(--pt-form-accent,currentColor);color:var(--pt-form-on-accent,white)}",
2184
+ "@media (min-width:768px){.ptc-form-submit{width:auto}}",
2185
+ ".ptc-form-steps{display:flex;gap:calc(var(--spacing)*1.5);margin-bottom:calc(var(--spacing)*4)}",
2186
+ ".ptc-form-step-tab{flex:1;font:inherit;cursor:pointer;padding:calc(var(--spacing)*2) calc(var(--spacing)*1);border:0;border-radius:var(--radius-md);background:color-mix(in oklab,currentColor 10%,transparent)}",
2187
+ '.ptc-form-step-tab[aria-selected="true"]{background:var(--pt-form-accent,currentColor);color:var(--pt-form-on-accent,white)}',
2188
+ ".ptc-form-footer{margin-top:calc(var(--spacing)*3);font-size:.875em;color:color-mix(in oklab,currentColor 65%,transparent)}",
2189
+ ".ptc-form-success{margin-top:calc(var(--spacing)*3)}",
2190
+ // labelPosition=inline:标签与控件并排。
2191
+ '[data-label-position="inline"] .ptc-form-field{display:flex;align-items:center;gap:calc(var(--spacing)*3)}',
2192
+ '[data-label-position="inline"] .ptc-form-label{flex:0 0 auto;margin-bottom:0}'
2193
+ ].join("");
2194
+ },
2195
+ js() {
2196
+ return componentScript(
2197
+ "form",
2198
+ // 分步:与 tabs 同款,只翻 aria-selected / hidden。
2199
+ `root.addEventListener('click',function(e){var t=e.target,b=t&&t.closest?t.closest("[data-step-to]"):null;if(!b||!root.contains(b))return;var i=b.getAttribute('data-step-to');var tb=root.querySelectorAll("[data-step-to]"),pn=root.querySelectorAll("[data-step]");for(var k=0;k<tb.length;k++)tb[k].setAttribute('aria-selected',tb[k]===b?'true':'false');for(var j=0;j<pn.length;j++){if(pn[j].getAttribute('data-step')===i)pn[j].removeAttribute('hidden');else pn[j].setAttribute('hidden','');}});root.addEventListener('submit',function(e){var f=e.target;if(!f||f.tagName!=='FORM')return;var d={};new FormData(f).forEach(function(v,k){d[k]=v;});f.dispatchEvent(new CustomEvent('pt-form-submit',{detail:d,bubbles:true,composed:true}));if(!f.getAttribute('action')){e.preventDefault();var s=root.querySelector('.ptc-form-success');if(s)s.removeAttribute('hidden');}});`
2200
+ );
2201
+ }
2202
+ };
2203
+
2204
+ // src/components/renderers/stickyCta.ts
2205
+ var BAR_HEIGHT = "var(--pt-sticky-cta-h,3.5rem)";
2206
+ var RESERVE = `calc(${BAR_HEIGHT} + env(safe-area-inset-bottom,0px))`;
2207
+ var stickyCtaRenderer = {
2208
+ html(instance) {
2209
+ const settings = instance.settings;
2210
+ const text = str(settings, "text");
2211
+ const buttonText = str(settings, "buttonText");
2212
+ const showAfter = num(settings, "showAfter");
2213
+ return `<div class="ptc-sticky" data-sticky-on="${escAttr(str(settings, "showOn"))}" data-sticky-align="${escAttr(str(settings, "align"))}" data-show-after="${showAfter}"` + // armed = 交给脚本管可见性。只有 showAfter > 0 才有这回事。
2214
+ (showAfter > 0 ? " data-armed" : "") + ">" + (text ? `<span class="ptc-sticky-text">${esc(text)}</span>` : "") + (buttonText ? `<a class="ptc-sticky-btn" href="${escAttr(str(settings, "buttonUrl"))}">${esc(buttonText)}</a>` : "") + "</div>";
2215
+ },
2216
+ css() {
2217
+ return [
2218
+ `.ptc-sticky{position:fixed;left:0;right:0;bottom:0;z-index:50;box-sizing:border-box;display:flex;align-items:center;gap:calc(var(--spacing)*3);min-height:${BAR_HEIGHT};padding:calc(var(--spacing)*2) calc(var(--spacing)*4);padding-bottom:calc(var(--spacing)*2 + env(safe-area-inset-bottom,0px));background:var(--pt-sticky-cta-bg,canvas);color:var(--pt-sticky-cta-fg,canvastext);border-top:1px solid var(--pt-sticky-cta-border,color-mix(in oklab,currentColor 15%,transparent))}`,
2219
+ '.ptc-sticky[data-sticky-align="center"]{justify-content:center}',
2220
+ '.ptc-sticky[data-sticky-align="between"]{justify-content:space-between}',
2221
+ ".ptc-sticky-text{min-width:0}",
2222
+ ".ptc-sticky-btn{flex:none;display:inline-block;padding:calc(var(--spacing)*2.5) calc(var(--spacing)*6);border-radius:var(--radius-lg);text-decoration:none;font-weight:600;background:var(--pt-sticky-cta-accent,currentColor);color:var(--pt-sticky-cta-on-accent,canvas)}",
2223
+ // 滚够了才出现:初始 armed,脚本摘掉它。
2224
+ ".ptc-sticky[data-armed]{display:none}",
2225
+ // PC 隐藏是 showOn=mobile 的默认档,不是写死的行为。
2226
+ '@media (min-width:768px){.ptc-sticky[data-sticky-on="mobile"]{display:none}}',
2227
+ // 底部留白。见文件头:只能由 body 腾,且不写重置规则。
2228
+ `body:has(.ptc-sticky[data-sticky-on="all"]){padding-bottom:${RESERVE}}`,
2229
+ `@media (max-width:767.98px){body:has(.ptc-sticky[data-sticky-on="mobile"]){padding-bottom:${RESERVE}}}`
2230
+ ].join("");
2231
+ },
2232
+ js() {
2233
+ return componentScript(
2234
+ "sticky-cta",
2235
+ "var b=root.querySelector('.ptc-sticky');if(!b)return;var a=parseInt(b.getAttribute('data-show-after'),10);if(!(a>0))return;function u(){if((window.pageYOffset||0)>=a)b.removeAttribute('data-armed');else b.setAttribute('data-armed','');}window.addEventListener('scroll',u,{passive:true});u();"
2236
+ );
2237
+ }
2238
+ };
2239
+
2240
+ // src/components/renderers/tabs.ts
2241
+ var tabsRenderer = {
2242
+ html(instance, instanceId) {
2243
+ const style = str(instance.settings, "tabStyle");
2244
+ const title = str(instance.settings, "title");
2245
+ const tabs = instance.blocks.filter((b) => b.type === "tab");
2246
+ const wanted = num(instance.settings, "defaultTab");
2247
+ const active = Number.isInteger(wanted) && wanted >= 0 && wanted < tabs.length ? wanted : 0;
2248
+ const id = escAttr(instanceId);
2249
+ const nav = tabs.map((tab, i) => {
2250
+ const on = i === active;
2251
+ const icon2 = str(tab.settings, "icon");
2252
+ const label = (icon2 ? `${esc(icon2)} ` : "") + esc(str(tab.settings, "title"));
2253
+ return `<button type="button" class="ptc-tabs-tab" role="tab" id="${id}-t${i}" aria-controls="${id}-p${i}" aria-selected="${on ? "true" : "false"}" tabindex="${on ? "0" : "-1"}">${label}</button>`;
2254
+ }).join("");
2255
+ const panels = tabs.map(
2256
+ (tab, i) => `<div class="ptc-tabs-panel" role="tabpanel" id="${id}-p${i}" aria-labelledby="${id}-t${i}"${i === active ? "" : " hidden"}>${str(tab.settings, "content")}</div>`
2257
+ ).join("");
2258
+ const head = title ? `<h3 class="ptc-tabs-title">${esc(title)}</h3>` : "";
2259
+ return `<div class="ptc-tabs" data-tabs-style="${escAttr(style)}">` + head + `<div class="ptc-tabs-nav" role="tablist">${nav}</div><div class="ptc-tabs-panels">${panels}</div></div>`;
2260
+ },
2261
+ css() {
2262
+ return [
2263
+ ".ptc-tabs{width:100%}",
2264
+ ".ptc-tabs-title{margin:0 0 calc(var(--spacing)*4);font-size:1.25em;font-weight:700}",
2265
+ ".ptc-tabs-nav{display:flex;flex-wrap:wrap;gap:calc(var(--spacing)*1)}",
2266
+ ".ptc-tabs-tab{appearance:none;border:0;background:none;font:inherit;cursor:pointer;padding:calc(var(--spacing)*2) calc(var(--spacing)*4);color:color-mix(in oklab,currentColor 60%,transparent)}",
2267
+ // 激活态只由 aria-selected 决定 —— 脚本翻的就是它。
2268
+ '.ptc-tabs-tab[aria-selected="true"]{color:var(--pt-tabs-accent,currentColor)}',
2269
+ ".ptc-tabs-panel{padding-block:calc(var(--spacing)*5)}",
2270
+ // underline
2271
+ '[data-tabs-style="underline"] .ptc-tabs-nav{border-bottom:1px solid var(--pt-tabs-border,color-mix(in oklab,currentColor 15%,transparent))}',
2272
+ '[data-tabs-style="underline"] .ptc-tabs-tab{border-bottom:2px solid transparent;margin-bottom:-1px}',
2273
+ '[data-tabs-style="underline"] .ptc-tabs-tab[aria-selected="true"]{border-bottom-color:var(--pt-tabs-accent,currentColor)}',
2274
+ // pill
2275
+ '[data-tabs-style="pill"] .ptc-tabs-tab{border-radius:var(--radius-xl)}',
2276
+ '[data-tabs-style="pill"] .ptc-tabs-tab[aria-selected="true"]{background:var(--pt-tabs-accent,currentColor);color:var(--pt-tabs-on-accent,white)}',
2277
+ // boxed —— v2 的 'box' 是笔误,这一档在 v2 里根本进不去
2278
+ '[data-tabs-style="boxed"] .ptc-tabs-tab{border:1px solid transparent;border-radius:var(--radius-md)}',
2279
+ '[data-tabs-style="boxed"] .ptc-tabs-tab[aria-selected="true"]{border-color:var(--pt-tabs-border,color-mix(in oklab,currentColor 15%,transparent))}'
2280
+ ].join("");
2281
+ },
2282
+ js() {
2283
+ return componentScript(
2284
+ "tabs",
2285
+ `root.addEventListener('click',function(e){var t=e.target;var b=t&&t.closest?t.closest('[role="tab"]'):null;if(!b||!root.contains(b))return;var tb=root.querySelectorAll('[role="tab"]'),pn=root.querySelectorAll('[role="tabpanel"]');for(var i=0;i<tb.length;i++){var on=tb[i]===b;tb[i].setAttribute('aria-selected',on?'true':'false');tb[i].setAttribute('tabindex',on?'0':'-1');if(pn[i]){if(on)pn[i].removeAttribute('hidden');else pn[i].setAttribute('hidden','');}}});`
2286
+ );
2287
+ }
2288
+ };
2289
+
2290
+ // src/components/render.ts
2291
+ var RENDERERS = {
2292
+ carousel: carouselRenderer,
2293
+ faq: faqRenderer,
2294
+ form: formRenderer,
2295
+ tabs: tabsRenderer,
2296
+ "sticky-cta": stickyCtaRenderer
2297
+ };
2298
+ function rendererOf(type) {
2299
+ const renderer = RENDERERS[type];
2300
+ if (!renderer) throw editorError("COMPONENT_UNKNOWN", { params: { type } });
2301
+ return renderer;
2302
+ }
2303
+ function renderHtml(type, settings, instanceId) {
2304
+ const renderer = rendererOf(type);
2305
+ const contract = COMPONENT_CONTRACTS[type];
2306
+ try {
2307
+ return renderer.html(resolveComponent(contract, settings), instanceId);
2308
+ } catch (err) {
2309
+ asRenderError(type, err);
2310
+ }
2311
+ }
2312
+ function renderCss(type) {
2313
+ const renderer = rendererOf(type);
2314
+ try {
2315
+ return renderer.css();
2316
+ } catch (err) {
2317
+ asRenderError(type, err);
2318
+ }
2319
+ }
2320
+ function renderJs(type) {
2321
+ const renderer = rendererOf(type);
2322
+ try {
2323
+ return renderer.js();
2324
+ } catch (err) {
2325
+ asRenderError(type, err);
2326
+ }
2327
+ }
2328
+ function asRenderError(type, err) {
2329
+ if (typeof err === "object" && err !== null && isErrorCode(err.code)) {
2330
+ throw err;
2331
+ }
2332
+ throw editorError("COMPONENT_RENDER_FAILED", { params: { type } });
2333
+ }
2334
+
2335
+ // src/components/contract.ts
2336
+ var SETTING_TYPES = [
2337
+ "text",
2338
+ "richtext",
2339
+ "number",
2340
+ "boolean",
2341
+ "enum",
2342
+ "url",
2343
+ "image",
2344
+ "list"
2345
+ ];
2346
+
1385
2347
  // src/collectClasses.ts
1386
2348
  function collectClasses(root) {
1387
2349
  const out = /* @__PURE__ */ new Set();
@@ -1407,20 +2369,22 @@ function createCore(options) {
1407
2369
  return {
1408
2370
  parse,
1409
2371
  parseFragment,
2372
+ wrap: createWrap(parse),
2373
+ unwrap,
1410
2374
  serialize,
1411
2375
  normalize,
1412
2376
  validate,
1413
2377
  apply,
1414
2378
  guards: guards_exports,
1415
2379
  compile: compile_exports,
2380
+ components: components_exports,
1416
2381
  collectClasses,
1417
2382
  rules,
1418
- KIND_PROFILES,
1419
2383
  VERSION,
1420
2384
  COMPILER_VERSION
1421
2385
  };
1422
2386
  }
1423
2387
 
1424
- export { COMPILER_VERSION, EMPTY_THEME, ERROR_CODES, KIND_PROFILES, PUBLISH_BLOCKING_RULES, VERSION, apply, canContain, checkVariants, collectClasses, compile_exports as compile, createCore, deleteTarget, editorError, fragmentElementRoots, guards_exports as guards, isErrorCode, isNativePaletteClass, isPublishBlocking, layerOf, normalize, normalizeClassValue, parseFragment, rules, serialize, themeFromSource, themeOf, themeTokens, validate };
2388
+ export { COMPILER_VERSION, EMPTY_THEME, ERROR_CODES, MAX_CONTAINER_DEPTH, PUBLISH_BLOCKING_RULES, VERSION, apply, canContain, checkVariants, collectClasses, compile_exports as compile, components_exports as components, containerDepth, createCore, deleteTarget, editorError, fragmentElementRoots, guards_exports as guards, isErrorCode, isNativePaletteClass, isPublishBlocking, isWrapLayer, layerOf, normalize, normalizeClassValue, parseFragment, rules, serialize, themeFromSource, themeOf, themeTokens, unwrap, validate };
1425
2389
  //# sourceMappingURL=index.js.map
1426
2390
  //# sourceMappingURL=index.js.map