@vigilkids/section-renderer-vue 0.0.1

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.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/dist/assets/images/article/arrow.svg +3 -0
  4. package/dist/assets/images/article/hot.svg +3 -0
  5. package/dist/assets/images/article/notice-info-icon.svg +5 -0
  6. package/dist/assets/images/article/notice-info.svg +3 -0
  7. package/dist/assets/images/article/notice-warning-icon.svg +5 -0
  8. package/dist/assets/images/article/notice-warning.svg +10 -0
  9. package/dist/assets/images/article/question.svg +10 -0
  10. package/dist/composables/useInlineEdit.d.ts +30 -0
  11. package/dist/composables/useInlineEdit.mjs +94 -0
  12. package/dist/composables/useLazyRender.d.ts +18 -0
  13. package/dist/composables/useLazyRender.mjs +33 -0
  14. package/dist/composables/useRegistry.d.ts +38 -0
  15. package/dist/composables/useRegistry.mjs +60 -0
  16. package/dist/composables/useSectionSEO.d.ts +26 -0
  17. package/dist/composables/useSectionSEO.mjs +122 -0
  18. package/dist/composables/useSectionStyle.d.ts +23 -0
  19. package/dist/composables/useSectionStyle.mjs +111 -0
  20. package/dist/editor.d.ts +4 -0
  21. package/dist/editor.mjs +9 -0
  22. package/dist/index.d.ts +18 -0
  23. package/dist/index.mjs +14 -0
  24. package/dist/plugin.d.ts +6 -0
  25. package/dist/plugin.mjs +14 -0
  26. package/dist/preview/createPreviewApp.d.ts +20 -0
  27. package/dist/preview/createPreviewApp.mjs +161 -0
  28. package/dist/renderer/FallbackSection.d.vue.ts +8 -0
  29. package/dist/renderer/FallbackSection.vue +17 -0
  30. package/dist/renderer/FallbackSection.vue.d.ts +8 -0
  31. package/dist/renderer/LazySection.d.vue.ts +60 -0
  32. package/dist/renderer/LazySection.vue +115 -0
  33. package/dist/renderer/LazySection.vue.d.ts +60 -0
  34. package/dist/renderer/SectionErrorBoundary.d.vue.ts +16 -0
  35. package/dist/renderer/SectionErrorBoundary.vue +38 -0
  36. package/dist/renderer/SectionErrorBoundary.vue.d.ts +16 -0
  37. package/dist/renderer/SectionRenderer.d.vue.ts +29 -0
  38. package/dist/renderer/SectionRenderer.vue +99 -0
  39. package/dist/renderer/SectionRenderer.vue.d.ts +29 -0
  40. package/dist/renderer/SectionWrapper.d.vue.ts +24 -0
  41. package/dist/renderer/SectionWrapper.vue +52 -0
  42. package/dist/renderer/SectionWrapper.vue.d.ts +24 -0
  43. package/dist/sections/RichTextSection.d.vue.ts +9 -0
  44. package/dist/sections/RichTextSection.vue +135 -0
  45. package/dist/sections/RichTextSection.vue.d.ts +9 -0
  46. package/dist/sections/article/index.d.ts +2 -0
  47. package/dist/sections/article/index.mjs +174 -0
  48. package/dist/sections/article/prosemirror.d.ts +2 -0
  49. package/dist/sections/article/prosemirror.mjs +65 -0
  50. package/dist/sections/article/shared/ArticleCustomHtml.d.vue.ts +9 -0
  51. package/dist/sections/article/shared/ArticleCustomHtml.vue +32 -0
  52. package/dist/sections/article/shared/ArticleCustomHtml.vue.d.ts +9 -0
  53. package/dist/sections/article/shared/ArticleImage.d.vue.ts +21 -0
  54. package/dist/sections/article/shared/ArticleImage.vue +53 -0
  55. package/dist/sections/article/shared/ArticleImage.vue.d.ts +21 -0
  56. package/dist/sections/article/vigilkids/ArticleBulletList.d.vue.ts +21 -0
  57. package/dist/sections/article/vigilkids/ArticleBulletList.vue +48 -0
  58. package/dist/sections/article/vigilkids/ArticleBulletList.vue.d.ts +21 -0
  59. package/dist/sections/article/vigilkids/ArticleCta.d.vue.ts +21 -0
  60. package/dist/sections/article/vigilkids/ArticleCta.vue +126 -0
  61. package/dist/sections/article/vigilkids/ArticleCta.vue.d.ts +21 -0
  62. package/dist/sections/article/vigilkids/ArticleFaq.d.vue.ts +21 -0
  63. package/dist/sections/article/vigilkids/ArticleFaq.vue +62 -0
  64. package/dist/sections/article/vigilkids/ArticleFaq.vue.d.ts +21 -0
  65. package/dist/sections/article/vigilkids/ArticleFaqItem.d.vue.ts +5 -0
  66. package/dist/sections/article/vigilkids/ArticleFaqItem.vue +24 -0
  67. package/dist/sections/article/vigilkids/ArticleFaqItem.vue.d.ts +5 -0
  68. package/dist/sections/article/vigilkids/ArticleFeature.d.vue.ts +21 -0
  69. package/dist/sections/article/vigilkids/ArticleFeature.vue +77 -0
  70. package/dist/sections/article/vigilkids/ArticleFeature.vue.d.ts +21 -0
  71. package/dist/sections/article/vigilkids/ArticleHeading.d.vue.ts +21 -0
  72. package/dist/sections/article/vigilkids/ArticleHeading.vue +53 -0
  73. package/dist/sections/article/vigilkids/ArticleHeading.vue.d.ts +21 -0
  74. package/dist/sections/article/vigilkids/ArticleNotice.d.vue.ts +21 -0
  75. package/dist/sections/article/vigilkids/ArticleNotice.vue +81 -0
  76. package/dist/sections/article/vigilkids/ArticleNotice.vue.d.ts +21 -0
  77. package/dist/sections/article/vigilkids/ArticleProsCons.d.vue.ts +21 -0
  78. package/dist/sections/article/vigilkids/ArticleProsCons.vue +74 -0
  79. package/dist/sections/article/vigilkids/ArticleProsCons.vue.d.ts +21 -0
  80. package/dist/sections/article/vigilkids/ArticleQuestion.d.vue.ts +21 -0
  81. package/dist/sections/article/vigilkids/ArticleQuestion.vue +58 -0
  82. package/dist/sections/article/vigilkids/ArticleQuestion.vue.d.ts +21 -0
  83. package/dist/sections/article/vigilkids/ArticleQuote.d.vue.ts +21 -0
  84. package/dist/sections/article/vigilkids/ArticleQuote.vue +50 -0
  85. package/dist/sections/article/vigilkids/ArticleQuote.vue.d.ts +21 -0
  86. package/dist/sections/article/vigilkids/ArticleStepList.d.vue.ts +21 -0
  87. package/dist/sections/article/vigilkids/ArticleStepList.vue +49 -0
  88. package/dist/sections/article/vigilkids/ArticleStepList.vue.d.ts +21 -0
  89. package/dist/sections/article/vigilkids/ArticleSubheading.d.vue.ts +21 -0
  90. package/dist/sections/article/vigilkids/ArticleSubheading.vue +56 -0
  91. package/dist/sections/article/vigilkids/ArticleSubheading.vue.d.ts +21 -0
  92. package/dist/sections/article/vigilkids/ArticleTable.d.vue.ts +9 -0
  93. package/dist/sections/article/vigilkids/ArticleTable.vue +75 -0
  94. package/dist/sections/article/vigilkids/ArticleTable.vue.d.ts +9 -0
  95. package/dist/sections/article/vigilkids/ArticleToc.d.vue.ts +21 -0
  96. package/dist/sections/article/vigilkids/ArticleToc.vue +102 -0
  97. package/dist/sections/article/vigilkids/ArticleToc.vue.d.ts +21 -0
  98. package/dist/sections/article/visiva/ArticleBulletList.d.vue.ts +21 -0
  99. package/dist/sections/article/visiva/ArticleBulletList.vue +48 -0
  100. package/dist/sections/article/visiva/ArticleBulletList.vue.d.ts +21 -0
  101. package/dist/sections/article/visiva/ArticleCta.d.vue.ts +21 -0
  102. package/dist/sections/article/visiva/ArticleCta.vue +148 -0
  103. package/dist/sections/article/visiva/ArticleCta.vue.d.ts +21 -0
  104. package/dist/sections/article/visiva/ArticleFaq.d.vue.ts +21 -0
  105. package/dist/sections/article/visiva/ArticleFaq.vue +76 -0
  106. package/dist/sections/article/visiva/ArticleFaq.vue.d.ts +21 -0
  107. package/dist/sections/article/visiva/ArticleFeature.d.vue.ts +21 -0
  108. package/dist/sections/article/visiva/ArticleFeature.vue +79 -0
  109. package/dist/sections/article/visiva/ArticleFeature.vue.d.ts +21 -0
  110. package/dist/sections/article/visiva/ArticleHeading.d.vue.ts +21 -0
  111. package/dist/sections/article/visiva/ArticleHeading.vue +61 -0
  112. package/dist/sections/article/visiva/ArticleHeading.vue.d.ts +21 -0
  113. package/dist/sections/article/visiva/ArticleNotice.d.vue.ts +21 -0
  114. package/dist/sections/article/visiva/ArticleNotice.vue +102 -0
  115. package/dist/sections/article/visiva/ArticleNotice.vue.d.ts +21 -0
  116. package/dist/sections/article/visiva/ArticleProsCons.d.vue.ts +21 -0
  117. package/dist/sections/article/visiva/ArticleProsCons.vue +98 -0
  118. package/dist/sections/article/visiva/ArticleProsCons.vue.d.ts +21 -0
  119. package/dist/sections/article/visiva/ArticleQuestion.d.vue.ts +21 -0
  120. package/dist/sections/article/visiva/ArticleQuestion.vue +80 -0
  121. package/dist/sections/article/visiva/ArticleQuestion.vue.d.ts +21 -0
  122. package/dist/sections/article/visiva/ArticleQuote.d.vue.ts +21 -0
  123. package/dist/sections/article/visiva/ArticleQuote.vue +50 -0
  124. package/dist/sections/article/visiva/ArticleQuote.vue.d.ts +21 -0
  125. package/dist/sections/article/visiva/ArticleStepList.d.vue.ts +21 -0
  126. package/dist/sections/article/visiva/ArticleStepList.vue +48 -0
  127. package/dist/sections/article/visiva/ArticleStepList.vue.d.ts +21 -0
  128. package/dist/sections/article/visiva/ArticleSubheading.d.vue.ts +21 -0
  129. package/dist/sections/article/visiva/ArticleSubheading.vue +91 -0
  130. package/dist/sections/article/visiva/ArticleSubheading.vue.d.ts +21 -0
  131. package/dist/sections/article/visiva/ArticleTable.d.vue.ts +9 -0
  132. package/dist/sections/article/visiva/ArticleTable.vue +140 -0
  133. package/dist/sections/article/visiva/ArticleTable.vue.d.ts +9 -0
  134. package/dist/sections/article/visiva/ArticleToc.d.vue.ts +21 -0
  135. package/dist/sections/article/visiva/ArticleToc.vue +116 -0
  136. package/dist/sections/article/visiva/ArticleToc.vue.d.ts +21 -0
  137. package/dist/shims-css.d.ts +4 -0
  138. package/dist/styles/products/vigilkids.css +1 -0
  139. package/dist/styles/products/visiva.css +1 -0
  140. package/package.json +43 -0
@@ -0,0 +1,111 @@
1
+ import { computed, toValue } from "vue";
2
+ const SAFE_COLOR_PATTERN = /^#[\da-fA-F]{6}$/;
3
+ const SPACING_PRESETS = {
4
+ compact: "py-8 sm:py-12",
5
+ standard: "py-12 sm:py-24",
6
+ spacious: "py-20 sm:py-32"
7
+ };
8
+ const ALIGN_CLASSES = {
9
+ left: "text-left",
10
+ center: "text-center",
11
+ right: "text-right"
12
+ };
13
+ const MAX_WIDTH_CLASSES = {
14
+ prose: "mx-auto max-w-prose",
15
+ wide: "mx-auto max-w-screen-lg",
16
+ full: "w-full"
17
+ };
18
+ const VISIBILITY_CLASSES = {
19
+ visible: "",
20
+ hidden: "hidden",
21
+ "mobile-only": "sm:hidden",
22
+ "desktop-only": "hidden sm:block"
23
+ };
24
+ function safeColor(value) {
25
+ const s = typeof value === "string" ? value : void 0;
26
+ if (s && SAFE_COLOR_PATTERN.test(s)) return s;
27
+ return void 0;
28
+ }
29
+ function safePixel(value) {
30
+ const n = typeof value === "number" ? value : Number(value);
31
+ if (!Number.isNaN(n) && n >= 0 && n <= 500) return n;
32
+ return void 0;
33
+ }
34
+ export function useSectionStyle(sectionId, settings) {
35
+ const style = computed(() => {
36
+ const id = toValue(sectionId);
37
+ const s = toValue(settings) ?? {};
38
+ const classes = [];
39
+ const styles = {};
40
+ const cssParts = [];
41
+ const spacingPreset = s.spacing_preset;
42
+ if (spacingPreset && spacingPreset !== "custom" && SPACING_PRESETS[spacingPreset]) {
43
+ classes.push(SPACING_PRESETS[spacingPreset]);
44
+ }
45
+ const textAlign = s.text_alignment;
46
+ if (textAlign && ALIGN_CLASSES[textAlign]) {
47
+ classes.push(ALIGN_CLASSES[textAlign]);
48
+ }
49
+ const maxWidth = s.max_width;
50
+ if (maxWidth && MAX_WIDTH_CLASSES[maxWidth]) {
51
+ classes.push(MAX_WIDTH_CLASSES[maxWidth]);
52
+ }
53
+ const visibility = s.visibility;
54
+ if (visibility && VISIBILITY_CLASSES[visibility]) {
55
+ classes.push(VISIBILITY_CLASSES[visibility]);
56
+ }
57
+ const bgColor = safeColor(s.background_color);
58
+ if (bgColor) {
59
+ styles["background-color"] = bgColor;
60
+ }
61
+ const textColor = safeColor(s.text_color);
62
+ if (textColor) {
63
+ styles.color = textColor;
64
+ }
65
+ const borderRadius = safePixel(s.border_radius);
66
+ if (borderRadius !== void 0 && borderRadius > 0) {
67
+ styles["border-radius"] = `${borderRadius}px`;
68
+ }
69
+ const marginTop = safePixel(s.margin_top);
70
+ if (marginTop !== void 0 && marginTop > 0) {
71
+ styles["margin-top"] = `${marginTop}px`;
72
+ }
73
+ const marginBottom = safePixel(s.margin_bottom);
74
+ if (marginBottom !== void 0 && marginBottom > 0) {
75
+ styles["margin-bottom"] = `${marginBottom}px`;
76
+ }
77
+ if (spacingPreset === "custom") {
78
+ const pt = safePixel(s.padding_top);
79
+ const pb = safePixel(s.padding_bottom);
80
+ if (pt !== void 0 || pb !== void 0) {
81
+ const mobileRules = [];
82
+ const desktopRules = [];
83
+ if (pt !== void 0) {
84
+ mobileRules.push(`padding-top: ${Math.round(pt * 0.75)}px`);
85
+ desktopRules.push(`padding-top: ${pt}px`);
86
+ }
87
+ if (pb !== void 0) {
88
+ mobileRules.push(`padding-bottom: ${Math.round(pb * 0.75)}px`);
89
+ desktopRules.push(`padding-bottom: ${pb}px`);
90
+ }
91
+ cssParts.push(`#section-${id} { ${mobileRules.join("; ")}; }`);
92
+ cssParts.push(`@media (min-width: 768px) { #section-${id} { ${desktopRules.join("; ")}; } }`);
93
+ }
94
+ }
95
+ const customCSS = typeof s.custom_css === "string" ? s.custom_css.trim() : "";
96
+ if (customCSS) {
97
+ const sanitized = customCSS.replace(/[<>]/g, "").slice(0, 2e3);
98
+ if (sanitized) {
99
+ cssParts.push(`#section-${id} { ${sanitized} }`);
100
+ }
101
+ }
102
+ const customClass = typeof s.custom_class === "string" ? s.custom_class.trim() : "";
103
+ return {
104
+ wrapperClasses: classes.join(" "),
105
+ wrapperStyles: styles,
106
+ scopedCSS: cssParts.join("\n"),
107
+ customClass
108
+ };
109
+ });
110
+ return { style };
111
+ }
@@ -0,0 +1,4 @@
1
+ export * from './index';
2
+ export { createPreviewApp } from './preview/createPreviewApp';
3
+ export type { EditorToPreviewMessage, PreviewToEditorMessage, } from '@vigilkids/section-core/bridge';
4
+ export { BRIDGE_PROTOCOL_VERSION, HEARTBEAT_INTERVAL_MS, HEARTBEAT_MAX_MISSES, PreviewBridge, PREVIEW_CHANNEL, } from '@vigilkids/section-core/bridge';
@@ -0,0 +1,9 @@
1
+ export * from "./index.mjs";
2
+ export { createPreviewApp } from "./preview/createPreviewApp.mjs";
3
+ export {
4
+ BRIDGE_PROTOCOL_VERSION,
5
+ HEARTBEAT_INTERVAL_MS,
6
+ HEARTBEAT_MAX_MISSES,
7
+ PreviewBridge,
8
+ PREVIEW_CHANNEL
9
+ } from "@vigilkids/section-core/bridge";
@@ -0,0 +1,18 @@
1
+ export type { BlockData, SectionDefinition, SectionInstance, SectionRegistration, SectionsData, } from '@vigilkids/section-core';
2
+ export { safeColor, safeUrl } from '@vigilkids/section-core';
3
+ export { default as FallbackSection } from './renderer/FallbackSection.vue';
4
+ export { default as LazySection } from './renderer/LazySection.vue';
5
+ export { default as SectionErrorBoundary } from './renderer/SectionErrorBoundary.vue';
6
+ export { default as SectionRenderer } from './renderer/SectionRenderer.vue';
7
+ export { default as SectionWrapper } from './renderer/SectionWrapper.vue';
8
+ export { registerSection, useRegistry } from './composables/useRegistry';
9
+ export { useLazyRender } from './composables/useLazyRender';
10
+ export { useSectionStyle } from './composables/useSectionStyle';
11
+ export type { SectionStyleResult } from './composables/useSectionStyle';
12
+ export { useInlineEdit } from './composables/useInlineEdit';
13
+ export type { UseInlineEditOptions } from './composables/useInlineEdit';
14
+ export { generateJsonLdScripts, generateSectionJsonLd } from './composables/useSectionSEO';
15
+ export type { SectionSEOOptions } from './composables/useSectionSEO';
16
+ export { SectionRendererPlugin } from './plugin';
17
+ export { default as RichTextSection } from './sections/RichTextSection.vue';
18
+ export { registerArticleSections } from './sections/article';
package/dist/index.mjs ADDED
@@ -0,0 +1,14 @@
1
+ export { safeColor, safeUrl } from "@vigilkids/section-core";
2
+ export { default as FallbackSection } from "./renderer/FallbackSection.vue";
3
+ export { default as LazySection } from "./renderer/LazySection.vue";
4
+ export { default as SectionErrorBoundary } from "./renderer/SectionErrorBoundary.vue";
5
+ export { default as SectionRenderer } from "./renderer/SectionRenderer.vue";
6
+ export { default as SectionWrapper } from "./renderer/SectionWrapper.vue";
7
+ export { registerSection, useRegistry } from "./composables/useRegistry.mjs";
8
+ export { useLazyRender } from "./composables/useLazyRender.mjs";
9
+ export { useSectionStyle } from "./composables/useSectionStyle.mjs";
10
+ export { useInlineEdit } from "./composables/useInlineEdit.mjs";
11
+ export { generateJsonLdScripts, generateSectionJsonLd } from "./composables/useSectionSEO.mjs";
12
+ export { SectionRendererPlugin } from "./plugin.mjs";
13
+ export { default as RichTextSection } from "./sections/RichTextSection.vue";
14
+ export { registerArticleSections } from "./sections/article/index.mjs";
@@ -0,0 +1,6 @@
1
+ import type { Plugin } from 'vue';
2
+ /**
3
+ * Vue 插件 — 一键安装 Section Renderer SDK
4
+ * 使用: app.use(SectionRendererPlugin)
5
+ */
6
+ export declare const SectionRendererPlugin: Plugin;
@@ -0,0 +1,14 @@
1
+ import { registerSection } from "./composables/useRegistry.mjs";
2
+ function registerBuiltinSections() {
3
+ registerSection({
4
+ name: "rich-text",
5
+ componentName: "RichTextSection",
6
+ component: () => import("./sections/RichTextSection.vue"),
7
+ supportsInlineEdit: true
8
+ });
9
+ }
10
+ export const SectionRendererPlugin = {
11
+ install() {
12
+ registerBuiltinSections();
13
+ }
14
+ };
@@ -0,0 +1,20 @@
1
+ /** Preview App 配置 */
2
+ interface PreviewAppOptions {
3
+ /** postMessage bridge 实例 */
4
+ bridge: {
5
+ send: (msg: Record<string, unknown>) => void;
6
+ on: (type: string, handler: (payload: unknown) => void) => () => void;
7
+ };
8
+ /** 点击 Section 事件 */
9
+ onSectionClick?: (sectionId: string) => void;
10
+ /** Hover Section 事件 */
11
+ onSectionHover?: (sectionId: string | null) => void;
12
+ /** 内容渲染完成 */
13
+ onContentReady?: () => void;
14
+ }
15
+ /**
16
+ * 创建并挂载 Preview App
17
+ * 在 preview-host 的 main.ts 中调用
18
+ */
19
+ export declare function createPreviewApp(selector: string, options: PreviewAppOptions): void;
20
+ export {};
@@ -0,0 +1,161 @@
1
+ import { detectUndoRedo, isEditableTarget, isModKey } from "@vigilkids/section-core";
2
+ import { createApp, h, provide, ref } from "vue";
3
+ import { SectionRendererPlugin } from "../plugin.mjs";
4
+ import SectionRenderer from "../renderer/SectionRenderer.vue";
5
+ const loadedProducts = /* @__PURE__ */ new Set();
6
+ const productCSSLoaders = {
7
+ vigilkids: () => import("../styles/products/vigilkids.css"),
8
+ visiva: () => import("../styles/products/visiva.css")
9
+ };
10
+ async function loadProductCSS(productCode) {
11
+ if (loadedProducts.has(productCode)) return;
12
+ const loader = productCSSLoaders[productCode];
13
+ if (loader) {
14
+ await loader();
15
+ loadedProducts.add(productCode);
16
+ }
17
+ }
18
+ export function createPreviewApp(selector, options) {
19
+ const sectionsData = ref({
20
+ sections: {},
21
+ section_order: []
22
+ });
23
+ const selectedSectionId = ref(null);
24
+ const allSectionsSelected = ref(false);
25
+ const rawHtmlContent = ref(null);
26
+ const currentProduct = ref("");
27
+ const inlineEditingField = ref(null);
28
+ const app = createApp({
29
+ setup() {
30
+ provide("currentProduct", currentProduct);
31
+ options.bridge.on("sections:update", (payload) => {
32
+ rawHtmlContent.value = null;
33
+ const data = payload;
34
+ if (inlineEditingField.value) {
35
+ const { sectionId, key } = inlineEditingField.value;
36
+ const currentSection = sectionsData.value.sections[sectionId];
37
+ if (currentSection && data.sections[sectionId]) {
38
+ data.sections[sectionId].settings[key] = currentSection.settings[key];
39
+ }
40
+ }
41
+ sectionsData.value = data;
42
+ });
43
+ options.bridge.on("html:preview", (payload) => {
44
+ const data = payload;
45
+ rawHtmlContent.value = data.html;
46
+ });
47
+ options.bridge.on("section:select", (payload) => {
48
+ const data = payload;
49
+ selectedSectionId.value = data.sectionId;
50
+ let attempts = 0;
51
+ const tryScroll = () => {
52
+ const el = document.getElementById(`section-${data.sectionId}`);
53
+ if (el) {
54
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
55
+ } else if (attempts++ < 30) {
56
+ requestAnimationFrame(tryScroll);
57
+ }
58
+ };
59
+ requestAnimationFrame(tryScroll);
60
+ });
61
+ options.bridge.on("section:deselect", () => {
62
+ selectedSectionId.value = null;
63
+ allSectionsSelected.value = false;
64
+ });
65
+ options.bridge.on("section:select-all", () => {
66
+ selectedSectionId.value = null;
67
+ allSectionsSelected.value = true;
68
+ });
69
+ options.bridge.on("preview:ping", () => {
70
+ options.bridge.send({ type: "preview:pong" });
71
+ });
72
+ options.bridge.on("theme:change", (payload) => {
73
+ const data = payload;
74
+ const root = document.documentElement;
75
+ for (const [key, value] of Object.entries(data.tokens)) {
76
+ root.style.setProperty(key, value);
77
+ }
78
+ if (data.productCode) {
79
+ currentProduct.value = data.productCode;
80
+ loadProductCSS(data.productCode);
81
+ }
82
+ });
83
+ function handleSectionClick(sectionId) {
84
+ options.onSectionClick?.(sectionId);
85
+ }
86
+ return () => rawHtmlContent.value !== null ? h("div", {
87
+ class: `article-content article-content--${currentProduct.value || "vigilkids"}`,
88
+ innerHTML: rawHtmlContent.value
89
+ }) : h(SectionRenderer, {
90
+ sectionsData: sectionsData.value,
91
+ editorMode: true,
92
+ selectedSectionId: selectedSectionId.value,
93
+ allSectionsSelected: allSectionsSelected.value,
94
+ productCode: currentProduct.value || void 0,
95
+ onSectionClick: handleSectionClick,
96
+ onSettingUpdate: (sectionId, key, value) => {
97
+ options.bridge.send({ type: "inline-edit:update", payload: { sectionId, key, value } });
98
+ },
99
+ onBlockSettingUpdate: (sectionId, blockId, key, value) => {
100
+ options.bridge.send({ type: "inline-edit:block-update", payload: { sectionId, blockId, key, value } });
101
+ },
102
+ onInlineEditStart: (sectionId, key) => {
103
+ inlineEditingField.value = { sectionId, key };
104
+ options.bridge.send({ type: "inline-edit:start", payload: { sectionId, key } });
105
+ },
106
+ onInlineEditEnd: () => {
107
+ inlineEditingField.value = null;
108
+ options.bridge.send({ type: "inline-edit:end" });
109
+ },
110
+ onInlineEditUndoRedo: (action) => {
111
+ options.bridge.send({ type: action === "undo" ? "shortcut:undo" : "shortcut:redo" });
112
+ }
113
+ });
114
+ }
115
+ });
116
+ app.use(SectionRendererPlugin);
117
+ app.mount(selector);
118
+ document.addEventListener("keydown", (e) => {
119
+ if (isEditableTarget(e)) return;
120
+ const undoRedoAction = detectUndoRedo(e);
121
+ if (undoRedoAction) {
122
+ e.preventDefault();
123
+ options.bridge.send({ type: undoRedoAction === "undo" ? "shortcut:undo" : "shortcut:redo" });
124
+ return;
125
+ }
126
+ if (isModKey(e)) {
127
+ const key = e.key.toLowerCase();
128
+ const modShortcuts = {
129
+ c: "shortcut:copy",
130
+ v: "shortcut:paste",
131
+ a: "shortcut:select-all"
132
+ };
133
+ const mapped = modShortcuts[key];
134
+ if (mapped) {
135
+ e.preventDefault();
136
+ options.bridge.send({ type: mapped });
137
+ return;
138
+ }
139
+ }
140
+ if (e.altKey && e.key === "ArrowUp") {
141
+ e.preventDefault();
142
+ options.bridge.send({ type: "shortcut:move-up" });
143
+ return;
144
+ }
145
+ if (e.altKey && e.key === "ArrowDown") {
146
+ e.preventDefault();
147
+ options.bridge.send({ type: "shortcut:move-down" });
148
+ return;
149
+ }
150
+ if (e.key === "Escape") {
151
+ options.bridge.send({ type: "shortcut:escape" });
152
+ return;
153
+ }
154
+ if (e.key === "Delete") {
155
+ options.bridge.send({ type: "shortcut:delete" });
156
+ }
157
+ });
158
+ options.onContentReady?.();
159
+ options.bridge.send({ type: "preview:ready" });
160
+ options.bridge.send({ type: "preview:styles-ready" });
161
+ }
@@ -0,0 +1,8 @@
1
+ type __VLS_Props = {
2
+ blockOrder?: string[];
3
+ blocks?: Record<string, unknown>;
4
+ editorMode?: boolean;
5
+ settings?: Record<string, unknown>;
6
+ };
7
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
+ export default _default;
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ blockOrder?: string[]
4
+ blocks?: Record<string, unknown>
5
+ editorMode?: boolean
6
+ settings?: Record<string, unknown>
7
+ }>()
8
+ </script>
9
+
10
+ <template>
11
+ <div class="flex items-center justify-center rounded border border-dashed border-gray-300 bg-gray-50 p-8 text-gray-400">
12
+ <div class="text-center">
13
+ <div class="mb-2 text-2xl">📦</div>
14
+ <p class="text-sm">组件加载中...</p>
15
+ </div>
16
+ </div>
17
+ </template>
@@ -0,0 +1,8 @@
1
+ type __VLS_Props = {
2
+ blockOrder?: string[];
3
+ blocks?: Record<string, unknown>;
4
+ editorMode?: boolean;
5
+ settings?: Record<string, unknown>;
6
+ };
7
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
8
+ export default _default;
@@ -0,0 +1,60 @@
1
+ declare const _default: import("vue").DefineComponent<{
2
+ /** Block 实例映射 */
3
+ blocks: Record<string, {
4
+ type: string;
5
+ settings: Record<string, unknown>;
6
+ }>;
7
+ /** Block 排序 */
8
+ blockOrder: string[];
9
+ /** 编辑器模式 */
10
+ editorMode?: boolean;
11
+ /** 是否选中 */
12
+ isSelected?: boolean;
13
+ /** 产品标识,用于解析产品级 Section 组件覆盖 */
14
+ productCode?: string;
15
+ /** IntersectionObserver rootMargin,控制提前加载距离 */
16
+ rootMargin?: string;
17
+ /** Section ID */
18
+ sectionId: string;
19
+ /** Section 类型 */
20
+ sectionType: string;
21
+ /** Section 设置 */
22
+ settings: Record<string, unknown>;
23
+ }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
24
+ inlineEditEnd: () => any;
25
+ sectionClick: (sectionId: string) => any;
26
+ inlineEditUndoRedo: (action: "redo" | "undo") => any;
27
+ inlineEditStart: (sectionId: string, key: string) => any;
28
+ blockSettingUpdate: (sectionId: string, blockId: string, key: string, value: unknown) => any;
29
+ settingUpdate: (sectionId: string, key: string, value: unknown) => any;
30
+ }, string, import("vue").PublicProps, Readonly<{
31
+ /** Block 实例映射 */
32
+ blocks: Record<string, {
33
+ type: string;
34
+ settings: Record<string, unknown>;
35
+ }>;
36
+ /** Block 排序 */
37
+ blockOrder: string[];
38
+ /** 编辑器模式 */
39
+ editorMode?: boolean;
40
+ /** 是否选中 */
41
+ isSelected?: boolean;
42
+ /** 产品标识,用于解析产品级 Section 组件覆盖 */
43
+ productCode?: string;
44
+ /** IntersectionObserver rootMargin,控制提前加载距离 */
45
+ rootMargin?: string;
46
+ /** Section ID */
47
+ sectionId: string;
48
+ /** Section 类型 */
49
+ sectionType: string;
50
+ /** Section 设置 */
51
+ settings: Record<string, unknown>;
52
+ }> & Readonly<{
53
+ onInlineEditEnd?: (() => any) | undefined;
54
+ onSectionClick?: ((sectionId: string) => any) | undefined;
55
+ onInlineEditUndoRedo?: ((action: "redo" | "undo") => any) | undefined;
56
+ onInlineEditStart?: ((sectionId: string, key: string) => any) | undefined;
57
+ onBlockSettingUpdate?: ((sectionId: string, blockId: string, key: string, value: unknown) => any) | undefined;
58
+ onSettingUpdate?: ((sectionId: string, key: string, value: unknown) => any) | undefined;
59
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
60
+ export default _default;
@@ -0,0 +1,115 @@
1
+ <script lang="ts">
2
+ /** 根据 Section 类型返回占位高度,防止 CLS 布局偏移 */
3
+ function getPlaceholderHeight(type: string): string {
4
+ const heights: Record<string, string> = {
5
+ 'hero-banner': '400px',
6
+ 'feature-grid': '300px',
7
+ 'text-with-image': '250px',
8
+ 'testimonials': '280px',
9
+ 'faq': '200px',
10
+ 'cta': '180px',
11
+ 'rich-text': '200px',
12
+ 'spacer': '40px',
13
+ }
14
+ return heights[type] ?? '200px'
15
+ }
16
+ </script>
17
+
18
+ <script setup lang="ts">
19
+ import { computed, ref } from 'vue'
20
+
21
+ import { useLazyRender } from '../composables/useLazyRender'
22
+ import { useRegistry } from '../composables/useRegistry'
23
+ import FallbackSection from './FallbackSection.vue'
24
+ import SectionErrorBoundary from './SectionErrorBoundary.vue'
25
+ import SectionWrapper from './SectionWrapper.vue'
26
+
27
+ const props = defineProps<{
28
+ /** Block 实例映射 */
29
+ blocks: Record<string, { type: string; settings: Record<string, unknown> }>
30
+ /** Block 排序 */
31
+ blockOrder: string[]
32
+ /** 编辑器模式 */
33
+ editorMode?: boolean
34
+ /** 是否选中 */
35
+ isSelected?: boolean
36
+ /** 产品标识,用于解析产品级 Section 组件覆盖 */
37
+ productCode?: string
38
+ /** IntersectionObserver rootMargin,控制提前加载距离 */
39
+ rootMargin?: string
40
+ /** Section ID */
41
+ sectionId: string
42
+ /** Section 类型 */
43
+ sectionType: string
44
+ /** Section 设置 */
45
+ settings: Record<string, unknown>
46
+ }>()
47
+
48
+ const emit = defineEmits<{
49
+ (e: 'sectionClick', sectionId: string): void
50
+ (e: 'settingUpdate', sectionId: string, key: string, value: unknown): void
51
+ (e: 'blockSettingUpdate', sectionId: string, blockId: string, key: string, value: unknown): void
52
+ (e: 'inlineEditStart', sectionId: string, key: string): void
53
+ (e: 'inlineEditEnd'): void
54
+ (e: 'inlineEditUndoRedo', action: 'redo' | 'undo'): void
55
+ }>()
56
+
57
+ const containerRef = ref<HTMLElement | null>(null)
58
+ const { isVisible } = useLazyRender(containerRef, {
59
+ rootMargin: props.rootMargin ?? '200px',
60
+ // SSR 输出完整 HTML,客户端 hydration 后再由 Observer 接管
61
+ ssrEager: true,
62
+ })
63
+
64
+ const registry = useRegistry()
65
+
66
+ // 解析注册表中的组件,未注册则降级到 FallbackSection
67
+ const resolvedComponent = computed(() =>
68
+ registry.resolve(props.sectionType, props.productCode) ?? FallbackSection,
69
+ )
70
+ </script>
71
+
72
+ <template>
73
+ <div ref="containerRef">
74
+ <template v-if="isVisible || isSelected">
75
+ <SectionErrorBoundary
76
+ :section-id="sectionId"
77
+ :section-type="sectionType"
78
+ >
79
+ <SectionWrapper
80
+ :section-id="sectionId"
81
+ :section-type="sectionType"
82
+ :editor-mode="editorMode"
83
+ :is-selected="isSelected"
84
+ :settings="settings"
85
+ @click="emit('sectionClick', sectionId)"
86
+ >
87
+ <component
88
+ :is="resolvedComponent"
89
+ :settings="settings"
90
+ :blocks="blocks"
91
+ :block-order="blockOrder"
92
+ :editor-mode="editorMode"
93
+ @update:setting="(key: string, value: unknown) => emit('settingUpdate', sectionId, key, value)"
94
+ @update:block-setting="(blockId: string, key: string, value: unknown) => emit('blockSettingUpdate', sectionId, blockId, key, value)"
95
+ @inline-edit-start="(key: string) => emit('inlineEditStart', sectionId, key)"
96
+ @inline-edit-end="emit('inlineEditEnd')"
97
+ @undo-redo="(action: 'redo' | 'undo') => emit('inlineEditUndoRedo', action)"
98
+ />
99
+ </SectionWrapper>
100
+ </SectionErrorBoundary>
101
+ </template>
102
+
103
+ <!-- CLS 占位:未加载时保持最小高度,防止内容加载后布局偏移 -->
104
+ <div
105
+ v-else
106
+ class="section-lazy-placeholder"
107
+ :data-section-type="sectionType"
108
+ :style="{ minHeight: getPlaceholderHeight(sectionType) }"
109
+ />
110
+ </div>
111
+ </template>
112
+
113
+ <style scoped>
114
+ .section-lazy-placeholder{animation:shimmer 1.5s infinite;background:linear-gradient(90deg,#f3f4f6 25%,#e5e7eb 50%,#f3f4f6 75%);background-size:200% 100%;border-radius:4px}@keyframes shimmer{0%{background-position:-200% 0}to{background-position:200% 0}}
115
+ </style>
@@ -0,0 +1,60 @@
1
+ declare const _default: import("vue").DefineComponent<{
2
+ /** Block 实例映射 */
3
+ blocks: Record<string, {
4
+ type: string;
5
+ settings: Record<string, unknown>;
6
+ }>;
7
+ /** Block 排序 */
8
+ blockOrder: string[];
9
+ /** 编辑器模式 */
10
+ editorMode?: boolean;
11
+ /** 是否选中 */
12
+ isSelected?: boolean;
13
+ /** 产品标识,用于解析产品级 Section 组件覆盖 */
14
+ productCode?: string;
15
+ /** IntersectionObserver rootMargin,控制提前加载距离 */
16
+ rootMargin?: string;
17
+ /** Section ID */
18
+ sectionId: string;
19
+ /** Section 类型 */
20
+ sectionType: string;
21
+ /** Section 设置 */
22
+ settings: Record<string, unknown>;
23
+ }, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
24
+ inlineEditEnd: () => any;
25
+ sectionClick: (sectionId: string) => any;
26
+ inlineEditUndoRedo: (action: "redo" | "undo") => any;
27
+ inlineEditStart: (sectionId: string, key: string) => any;
28
+ blockSettingUpdate: (sectionId: string, blockId: string, key: string, value: unknown) => any;
29
+ settingUpdate: (sectionId: string, key: string, value: unknown) => any;
30
+ }, string, import("vue").PublicProps, Readonly<{
31
+ /** Block 实例映射 */
32
+ blocks: Record<string, {
33
+ type: string;
34
+ settings: Record<string, unknown>;
35
+ }>;
36
+ /** Block 排序 */
37
+ blockOrder: string[];
38
+ /** 编辑器模式 */
39
+ editorMode?: boolean;
40
+ /** 是否选中 */
41
+ isSelected?: boolean;
42
+ /** 产品标识,用于解析产品级 Section 组件覆盖 */
43
+ productCode?: string;
44
+ /** IntersectionObserver rootMargin,控制提前加载距离 */
45
+ rootMargin?: string;
46
+ /** Section ID */
47
+ sectionId: string;
48
+ /** Section 类型 */
49
+ sectionType: string;
50
+ /** Section 设置 */
51
+ settings: Record<string, unknown>;
52
+ }> & Readonly<{
53
+ onInlineEditEnd?: (() => any) | undefined;
54
+ onSectionClick?: ((sectionId: string) => any) | undefined;
55
+ onInlineEditUndoRedo?: ((action: "redo" | "undo") => any) | undefined;
56
+ onInlineEditStart?: ((sectionId: string, key: string) => any) | undefined;
57
+ onBlockSettingUpdate?: ((sectionId: string, blockId: string, key: string, value: unknown) => any) | undefined;
58
+ onSettingUpdate?: ((sectionId: string, key: string, value: unknown) => any) | undefined;
59
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
60
+ export default _default;
@@ -0,0 +1,16 @@
1
+ type __VLS_Props = {
2
+ sectionId: string;
3
+ sectionType: string;
4
+ };
5
+ declare var __VLS_1: {};
6
+ type __VLS_Slots = {} & {
7
+ default?: (props: typeof __VLS_1) => any;
8
+ };
9
+ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
11
+ export default _default;
12
+ type __VLS_WithSlots<T, S> = T & {
13
+ new (): {
14
+ $slots: S;
15
+ };
16
+ };