@ptengine/lp-editor-core 1.0.0-alpha.3 → 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
  }
@@ -369,8 +369,8 @@ var rules = {
369
369
  };
370
370
 
371
371
  // src/version.ts
372
- var VERSION = "1.0.0-alpha.3";
373
- var COMPILER_VERSION = "1";
372
+ var VERSION = "1.0.0-alpha.4";
373
+ var COMPILER_VERSION = "2";
374
374
 
375
375
  // src/validate/tree.ts
376
376
  var INLINE_SET = new Set(INLINE_NO_ID_TAGS);
@@ -531,8 +531,8 @@ function nextImageSibling(from) {
531
531
  }
532
532
  function* walkElements(root) {
533
533
  if ("tagName" in root) yield root;
534
- const list = root.querySelectorAll("*");
535
- 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
536
  }
537
537
 
538
538
  // src/wrap.ts
@@ -757,6 +757,333 @@ function isNativePaletteClass(className) {
757
757
  return SHADE.test(shade) && PALETTE.has(name);
758
758
  }
759
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
+
760
1087
  // src/validate/index.ts
761
1088
  var EVENT_ATTR = /^on[a-z]/i;
762
1089
  function validate(root, options = {}) {
@@ -825,6 +1152,7 @@ function checkElements(root, options, issues) {
825
1152
  }
826
1153
  checkAttributes(el, ptId, issues);
827
1154
  checkClasses(el, ptId, options, issues);
1155
+ checkComponentSettings(el, ptId, options, issues);
828
1156
  checkPlaceholder(el, ptId, issues);
829
1157
  }
830
1158
  }
@@ -938,7 +1266,27 @@ var ERROR_CODES = [
938
1266
  /** 片段 canonical 往返不一致,说明 HTML 畸形。 */
939
1267
  "FRAGMENT_NOT_CANONICAL",
940
1268
  /** 存在未落图的图片占位 —— 阻塞发布,不阻塞预览。 */
941
- "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"
942
1290
  ];
943
1291
  var ERROR_CODE_SET = new Set(ERROR_CODES);
944
1292
  function isErrorCode(value) {
@@ -1182,10 +1530,10 @@ function applySet(doc, op) {
1182
1530
  return { ok: true, inverse: inv };
1183
1531
  }
1184
1532
  function applyClasses(el, spec) {
1185
- const list = normalizeClassValue(el.getAttribute("class") ?? "").split(" ").filter(Boolean);
1186
- const set = new Set(list);
1533
+ const list2 = normalizeClassValue(el.getAttribute("class") ?? "").split(" ").filter(Boolean);
1534
+ const set = new Set(list2);
1187
1535
  for (const c of spec.remove ?? []) set.delete(c);
1188
- const out = list.filter((c) => set.has(c));
1536
+ const out = list2.filter((c) => set.has(c));
1189
1537
  for (const c of spec.add ?? []) {
1190
1538
  if (!set.has(c)) {
1191
1539
  set.add(c);
@@ -1552,6 +1900,450 @@ function clearCompileCache() {
1552
1900
  cache.clear();
1553
1901
  }
1554
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
+
1555
2347
  // src/collectClasses.ts
1556
2348
  function collectClasses(root) {
1557
2349
  const out = /* @__PURE__ */ new Set();
@@ -1585,6 +2377,7 @@ function createCore(options) {
1585
2377
  apply,
1586
2378
  guards: guards_exports,
1587
2379
  compile: compile_exports,
2380
+ components: components_exports,
1588
2381
  collectClasses,
1589
2382
  rules,
1590
2383
  VERSION,
@@ -1592,6 +2385,6 @@ function createCore(options) {
1592
2385
  };
1593
2386
  }
1594
2387
 
1595
- export { COMPILER_VERSION, EMPTY_THEME, ERROR_CODES, MAX_CONTAINER_DEPTH, PUBLISH_BLOCKING_RULES, VERSION, apply, canContain, checkVariants, collectClasses, compile_exports as compile, containerDepth, createCore, deleteTarget, editorError, fragmentElementRoots, guards_exports as guards, isErrorCode, isNativePaletteClass, isPublishBlocking, isWrapLayer, layerOf, normalize, normalizeClassValue, parseFragment, rules, serialize, themeFromSource, themeOf, themeTokens, unwrap, 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 };
1596
2389
  //# sourceMappingURL=index.js.map
1597
2390
  //# sourceMappingURL=index.js.map