@composy/layout-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.
- package/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/index.css +2383 -0
- package/dist/index.js +2665 -0
- package/dist/index.js.map +1 -0
- package/dist/index.min.css +1 -0
- package/dist/index.min.js +1 -0
- package/es/components/LGrid.d.ts +3 -0
- package/es/components/LGrid.d.ts.map +1 -0
- package/es/components/LGrid.js +91 -0
- package/es/components/LGrid.js.map +1 -0
- package/es/components/LGridItem.d.ts +52 -0
- package/es/components/LGridItem.d.ts.map +1 -0
- package/es/components/LGridItem.js +52 -0
- package/es/components/LGridItem.js.map +1 -0
- package/es/components/LLayoutEditor.d.ts +49 -0
- package/es/components/LLayoutEditor.d.ts.map +1 -0
- package/es/components/LLayoutEditor.js +81 -0
- package/es/components/LLayoutEditor.js.map +1 -0
- package/es/components/LLayoutWorkspace.js +17 -0
- package/es/components/LLayoutWorkspace.js.map +1 -0
- package/es/components/LLayoutWorkspace.vue_vue_type_script_setup_true_lang.js +2217 -0
- package/es/components/LLayoutWorkspace.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/es/components/LMaterialDragSource.d.ts +55 -0
- package/es/components/LMaterialDragSource.d.ts.map +1 -0
- package/es/components/LMaterialDragSource.js +92 -0
- package/es/components/LMaterialDragSource.js.map +1 -0
- package/es/components/LMaterialRenderer.d.ts +63 -0
- package/es/components/LMaterialRenderer.d.ts.map +1 -0
- package/es/components/LMaterialRenderer.js +48 -0
- package/es/components/LMaterialRenderer.js.map +1 -0
- package/es/components/LMetricCard.d.ts +29 -0
- package/es/components/LMetricCard.d.ts.map +1 -0
- package/es/components/LMetricCard.js +39 -0
- package/es/components/LMetricCard.js.map +1 -0
- package/es/components/LTrendCard.d.ts +17 -0
- package/es/components/LTrendCard.d.ts.map +1 -0
- package/es/components/LTrendCard.js +36 -0
- package/es/components/LTrendCard.js.map +1 -0
- package/es/components/index.d.ts +10 -0
- package/es/components/index.d.ts.map +1 -0
- package/es/components/index.js +19 -0
- package/es/components/material-renderer.d.ts +26 -0
- package/es/components/material-renderer.d.ts.map +1 -0
- package/es/components/material-renderer.js +96 -0
- package/es/components/material-renderer.js.map +1 -0
- package/es/composables/index.d.ts +2 -0
- package/es/composables/index.d.ts.map +1 -0
- package/es/composables/index.js +12 -0
- package/es/composables/useLayoutEditor.d.ts +3 -0
- package/es/composables/useLayoutEditor.d.ts.map +1 -0
- package/es/composables/useLayoutEditor.js +22 -0
- package/es/composables/useLayoutEditor.js.map +1 -0
- package/es/index.d.ts +7 -0
- package/es/index.d.ts.map +1 -0
- package/es/index.js +22 -0
- package/es/style/css.js +1 -0
- package/es/style/index.js +1 -0
- package/es/style/index.less +1 -0
- package/es/style/workspace.css +2370 -0
- package/es/types.d.ts +26 -0
- package/es/types.d.ts.map +1 -0
- package/es/types.js +10 -0
- package/lib/components/LGrid.cjs +92 -0
- package/lib/components/LGrid.cjs.map +1 -0
- package/lib/components/LGrid.d.ts +3 -0
- package/lib/components/LGridItem.cjs +53 -0
- package/lib/components/LGridItem.cjs.map +1 -0
- package/lib/components/LGridItem.d.ts +52 -0
- package/lib/components/LLayoutEditor.cjs +82 -0
- package/lib/components/LLayoutEditor.cjs.map +1 -0
- package/lib/components/LLayoutEditor.d.ts +49 -0
- package/lib/components/LLayoutWorkspace.cjs +21 -0
- package/lib/components/LLayoutWorkspace.cjs.map +1 -0
- package/lib/components/LLayoutWorkspace.vue_vue_type_script_setup_true_lang.cjs +2217 -0
- package/lib/components/LLayoutWorkspace.vue_vue_type_script_setup_true_lang.cjs.map +1 -0
- package/lib/components/LMaterialDragSource.cjs +93 -0
- package/lib/components/LMaterialDragSource.cjs.map +1 -0
- package/lib/components/LMaterialDragSource.d.ts +55 -0
- package/lib/components/LMaterialRenderer.cjs +49 -0
- package/lib/components/LMaterialRenderer.cjs.map +1 -0
- package/lib/components/LMaterialRenderer.d.ts +63 -0
- package/lib/components/LMetricCard.cjs +40 -0
- package/lib/components/LMetricCard.cjs.map +1 -0
- package/lib/components/LMetricCard.d.ts +29 -0
- package/lib/components/LTrendCard.cjs +37 -0
- package/lib/components/LTrendCard.cjs.map +1 -0
- package/lib/components/LTrendCard.d.ts +17 -0
- package/lib/components/index.cjs +27 -0
- package/lib/components/index.d.ts +10 -0
- package/lib/components/material-renderer.cjs +99 -0
- package/lib/components/material-renderer.cjs.map +1 -0
- package/lib/components/material-renderer.d.ts +26 -0
- package/lib/composables/index.cjs +13 -0
- package/lib/composables/index.d.ts +2 -0
- package/lib/composables/useLayoutEditor.cjs +23 -0
- package/lib/composables/useLayoutEditor.cjs.map +1 -0
- package/lib/composables/useLayoutEditor.d.ts +3 -0
- package/lib/index.cjs +29 -0
- package/lib/index.d.ts +7 -0
- package/lib/package.json +3 -0
- package/lib/style/css.js +1 -0
- package/lib/style/index.css +2 -0
- package/lib/style/index.js +1 -0
- package/lib/style/workspace.css +2378 -0
- package/lib/types.cjs +11 -0
- package/lib/types.d.ts +26 -0
- package/package.json +117 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* ***********************************
|
|
3
|
+
* @composy/layout-vue v0.0.1 *
|
|
4
|
+
* Built with rollup *
|
|
5
|
+
* Build time: 2024-08-17 11:47:09 *
|
|
6
|
+
* Build mode: production *
|
|
7
|
+
* Minified: No *
|
|
8
|
+
* ***********************************
|
|
9
|
+
*/
|
|
10
|
+
import { defineComponent, h, onBeforeUnmount, onMounted, shallowRef, watch } from "vue";
|
|
11
|
+
import { createLayoutEditor } from "@composy/layout-editor";
|
|
12
|
+
//#region src/components/LLayoutEditor.ts
|
|
13
|
+
const LLayoutEditor = defineComponent({
|
|
14
|
+
name: "LLayoutEditor",
|
|
15
|
+
props: {
|
|
16
|
+
document: {
|
|
17
|
+
type: Object,
|
|
18
|
+
required: true
|
|
19
|
+
},
|
|
20
|
+
engine: {
|
|
21
|
+
type: Object,
|
|
22
|
+
required: true
|
|
23
|
+
},
|
|
24
|
+
mode: {
|
|
25
|
+
type: String,
|
|
26
|
+
default: "edit"
|
|
27
|
+
},
|
|
28
|
+
preview: Object,
|
|
29
|
+
materials: {
|
|
30
|
+
type: Object,
|
|
31
|
+
default: void 0
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
emits: [
|
|
35
|
+
"update:document",
|
|
36
|
+
"change",
|
|
37
|
+
"ready"
|
|
38
|
+
],
|
|
39
|
+
setup(props, { slots, emit, expose }) {
|
|
40
|
+
const editor = shallowRef(createLayoutEditor({
|
|
41
|
+
document: props.document,
|
|
42
|
+
engine: props.engine,
|
|
43
|
+
materials: props.materials,
|
|
44
|
+
preview: props.preview
|
|
45
|
+
}));
|
|
46
|
+
let lastEmittedDocument = JSON.stringify(props.document);
|
|
47
|
+
onMounted(() => {
|
|
48
|
+
editor.value.on("document:change", (event) => {
|
|
49
|
+
lastEmittedDocument = JSON.stringify(event.document);
|
|
50
|
+
emit("update:document", event.document);
|
|
51
|
+
emit("change", event);
|
|
52
|
+
});
|
|
53
|
+
editor.value.mount();
|
|
54
|
+
editor.value.setMode(props.mode);
|
|
55
|
+
emit("ready", editor.value);
|
|
56
|
+
});
|
|
57
|
+
watch(() => props.document, (value) => {
|
|
58
|
+
const next = JSON.stringify(value);
|
|
59
|
+
if (next !== lastEmittedDocument) {
|
|
60
|
+
editor.value.load(value);
|
|
61
|
+
lastEmittedDocument = next;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
watch(() => props.mode, (value) => editor.value.setMode(value));
|
|
65
|
+
const onKeydown = (event) => {
|
|
66
|
+
editor.value.handleKeyboard(event);
|
|
67
|
+
};
|
|
68
|
+
onBeforeUnmount(() => editor.value.destroy());
|
|
69
|
+
expose({ editor });
|
|
70
|
+
return () => h("div", {
|
|
71
|
+
class: "ldesign-layout-editor",
|
|
72
|
+
"data-layout-mode": props.mode,
|
|
73
|
+
"aria-keyshortcuts": "ArrowLeft ArrowRight ArrowUp ArrowDown Delete Control+C Control+V Control+X Control+D",
|
|
74
|
+
onKeydown
|
|
75
|
+
}, slots.default?.({ editor: editor.value }));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
//#endregion
|
|
79
|
+
export { LLayoutEditor };
|
|
80
|
+
/*! End of @composy/layout-vue | Powered by @composy/builder */
|
|
81
|
+
//# sourceMappingURL=LLayoutEditor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LLayoutEditor.js","names":[],"sources":["../../src/components/LLayoutEditor.ts"],"sourcesContent":["import {\n defineComponent,\n h,\n onBeforeUnmount,\n onMounted,\n shallowRef,\n watch,\n} from 'vue'\nimport { createLayoutEditor } from '@composy/layout-editor'\nimport type {\n LayoutDocument,\n MaterialDefinition,\n MaterialRegistry,\n} from '@composy/layout-core'\nimport type {\n LayoutEditorEngine,\n LayoutEditorOptions,\n} from '@composy/layout-editor'\n\nexport const LLayoutEditor = defineComponent({\n name: 'LLayoutEditor',\n props: {\n document: { type: Object as () => LayoutDocument, required: true },\n engine: { type: Object as () => LayoutEditorEngine, required: true },\n mode: {\n type: String as () => 'edit' | 'preview' | 'readonly',\n default: 'edit',\n },\n preview: Object as () => LayoutEditorOptions['preview'],\n materials: { type: Object, default: undefined },\n },\n emits: ['update:document', 'change', 'ready'],\n setup(props, { slots, emit, expose }) {\n const editor = shallowRef(\n createLayoutEditor({\n document: props.document,\n engine: props.engine,\n materials: props.materials as\n MaterialRegistry | readonly MaterialDefinition[] | undefined,\n preview: props.preview,\n })\n )\n let lastEmittedDocument = JSON.stringify(props.document)\n onMounted(() => {\n editor.value.on('document:change', event => {\n lastEmittedDocument = JSON.stringify(event.document)\n emit('update:document', event.document)\n emit('change', event)\n })\n editor.value.mount()\n editor.value.setMode(props.mode)\n emit('ready', editor.value)\n })\n watch(\n () => props.document,\n value => {\n const next = JSON.stringify(value)\n if (next !== lastEmittedDocument) {\n editor.value.load(value)\n lastEmittedDocument = next\n }\n }\n )\n watch(\n () => props.mode,\n value => editor.value.setMode(value)\n )\n const onKeydown = (event: KeyboardEvent) => {\n editor.value.handleKeyboard(event)\n }\n onBeforeUnmount(() => editor.value.destroy())\n expose({ editor })\n return () =>\n h(\n 'div',\n {\n class: 'ldesign-layout-editor',\n 'data-layout-mode': props.mode,\n 'aria-keyshortcuts':\n 'ArrowLeft ArrowRight ArrowUp ArrowDown Delete Control+C Control+V Control+X Control+D',\n onKeydown,\n },\n slots.default?.({ editor: editor.value })\n )\n },\n})\n"],"mappings":";;;;;;;;;;;;AAmBO,MAAM,gBAAgB,gBAAgB;CAC3C,MAAM;CACN,OAAO;EACL,UAAU;GAAE,MAAM;GAAgC,UAAU;EAAK;EACjE,QAAQ;GAAE,MAAM;GAAoC,UAAU;EAAK;EACnE,MAAM;GACJ,MAAM;GACN,SAAS;EACX;EACA,SAAS;EACT,WAAW;GAAE,MAAM;GAAQ,SAAS,KAAA;EAAU;CAChD;CACA,OAAO;EAAC;EAAmB;EAAU;CAAO;CAC5C,MAAM,OAAO,EAAE,OAAO,MAAM,UAAU;EACpC,MAAM,SAAS,WACb,mBAAmB;GACjB,UAAU,MAAM;GAChB,QAAQ,MAAM;GACd,WAAW,MAAM;GAEjB,SAAS,MAAM;EACjB,CAAC,CACH;EACA,IAAI,sBAAsB,KAAK,UAAU,MAAM,QAAQ;EACvD,gBAAgB;GACd,OAAO,MAAM,GAAG,oBAAmB,UAAS;IAC1C,sBAAsB,KAAK,UAAU,MAAM,QAAQ;IACnD,KAAK,mBAAmB,MAAM,QAAQ;IACtC,KAAK,UAAU,KAAK;GACtB,CAAC;GACD,OAAO,MAAM,MAAM;GACnB,OAAO,MAAM,QAAQ,MAAM,IAAI;GAC/B,KAAK,SAAS,OAAO,KAAK;EAC5B,CAAC;EACD,YACQ,MAAM,WACZ,UAAS;GACP,MAAM,OAAO,KAAK,UAAU,KAAK;GACjC,IAAI,SAAS,qBAAqB;IAChC,OAAO,MAAM,KAAK,KAAK;IACvB,sBAAsB;GACxB;EACF,CACF;EACA,YACQ,MAAM,OACZ,UAAS,OAAO,MAAM,QAAQ,KAAK,CACrC;EACA,MAAM,aAAa,UAAyB;GAC1C,OAAO,MAAM,eAAe,KAAK;EACnC;EACA,sBAAsB,OAAO,MAAM,QAAQ,CAAC;EAC5C,OAAO,EAAE,OAAO,CAAC;EACjB,aACE,EACE,OACA;GACE,OAAO;GACP,oBAAoB,MAAM;GAC1B,qBACE;GACF;EACF,GACA,MAAM,UAAU,EAAE,QAAQ,OAAO,MAAM,CAAC,CAC1C;CACJ;AACF,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* ***********************************
|
|
3
|
+
* @composy/layout-vue v0.0.1 *
|
|
4
|
+
* Built with rollup *
|
|
5
|
+
* Build time: 2024-08-17 11:47:09 *
|
|
6
|
+
* Build mode: production *
|
|
7
|
+
* Minified: No *
|
|
8
|
+
* ***********************************
|
|
9
|
+
*/
|
|
10
|
+
import LLayoutWorkspace_vue_vue_type_script_setup_true_lang_default from "./LLayoutWorkspace.vue_vue_type_script_setup_true_lang.js";
|
|
11
|
+
//#region src/components/LLayoutWorkspace.vue
|
|
12
|
+
LLayoutWorkspace_vue_vue_type_script_setup_true_lang_default.__file = "src/components/LLayoutWorkspace.vue";
|
|
13
|
+
var LLayoutWorkspace_default = LLayoutWorkspace_vue_vue_type_script_setup_true_lang_default;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { LLayoutWorkspace_default as default };
|
|
16
|
+
/*! End of @composy/layout-vue | Powered by @composy/builder */
|
|
17
|
+
//# sourceMappingURL=LLayoutWorkspace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LLayoutWorkspace.js","names":[],"sources":["../../src/components/LLayoutWorkspace.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { GridStack } from 'gridstack'\nimport 'gridstack/dist/gridstack.min.css'\nimport '../style/workspace.css'\nimport { createElement as createLucideElement, icons } from 'lucide'\nimport {\n ChartNoAxesColumnIncreasing,\n ChartSpline,\n ChevronDown,\n Copy,\n CopyCheck,\n Code2,\n Download,\n Eye,\n Filter,\n Hand,\n Grid2X2,\n Lock,\n Maximize2,\n Moon,\n Monitor,\n Move,\n MousePointer2,\n PanelsTopLeft,\n Pencil,\n Plus,\n ArrowLeftRight,\n ArrowUpDown,\n Redo2,\n RotateCcw,\n Scan,\n Search,\n Smartphone,\n Sun,\n Tablet,\n Trash2,\n Undo2,\n Unlock,\n X,\n ZoomIn,\n ZoomOut,\n} from 'lucide-vue-next'\nimport {\n createLayoutDocument,\n createMaterialRegistry,\n responsiveWebBreakpoints,\n} from '@composy/layout-core'\nimport type {\n LayoutDocument,\n LayoutNode,\n MaterialDefinition,\n MaterialRegistry,\n} from '@composy/layout-core'\nimport { createLayoutEditor } from '@composy/layout-editor'\nimport type { LayoutEditor } from '@composy/layout-editor'\nimport { compileLayout } from '@composy/layout-compiler'\nimport { createGridStackEditorEngine } from '@composy/layout-gridstack'\nimport { createLayoutRuntime } from '@composy/layout-runtime'\nimport type { LayoutRuntime } from '@composy/layout-runtime'\nimport { LGrid } from './LGrid'\nimport { LMaterialRenderer } from './LMaterialRenderer'\nimport { LMetricCard } from './LMetricCard'\nimport { LTrendCard } from './LTrendCard'\n\ntype DemoRoute = 'editor' | 'vanilla' | 'runtime' | 'vue'\ntype DevelopmentFramework = 'webcomponent' | 'vue'\ntype CanvasOrientation = 'portrait' | 'landscape'\n\ninterface LayoutWorkspaceProps {\n document?: LayoutDocument\n materials?: MaterialRegistry | readonly MaterialDefinition[]\n theme?: 'light' | 'dark'\n}\n\nconst workspaceProps = defineProps<LayoutWorkspaceProps>()\nconst workspaceEmit = defineEmits<{\n (event: 'update:document', document: LayoutDocument): void\n (event: 'change', document: LayoutDocument): void\n}>()\n\nconst routes: Array<{ id: DemoRoute; label: string; description: string }> = [\n { id: 'editor', label: '编辑', description: '可视化编辑工作区' },\n { id: 'vanilla', label: '原生', description: '原生运行时渲染' },\n {\n id: 'runtime',\n label: '预览',\n description: '网格布局运行时预览',\n },\n { id: 'vue', label: '组件', description: '组件适配器' },\n]\n\nconst breakpointIcons = {\n desktop: Monitor,\n tablet: Tablet,\n mobile: Smartphone,\n}\n\nconst routeIcons = {\n editor: Grid2X2,\n vanilla: Code2,\n runtime: Eye,\n vue: PanelsTopLeft,\n}\n\nconst developmentFrameworks: Array<{\n id: DevelopmentFramework\n label: string\n description: string\n icon: typeof Code2\n}> = [\n {\n id: 'webcomponent',\n label: '原生',\n description: '原生 Web Component 物料',\n icon: Code2,\n },\n {\n id: 'vue',\n label: 'Vue 3',\n description: 'Vue 3 组件物料',\n icon: PanelsTopLeft,\n },\n]\n\nfunction developmentFrameworkLabel(framework: DevelopmentFramework): string {\n if (framework === 'webcomponent') return '原生'\n return 'Vue 3'\n}\n\nfunction escapeHtml(value: string): string {\n return value.replace(\n /[&<>'\"]/g,\n character =>\n ({\n '&': '&',\n '<': '<',\n '>': '>',\n \"'\": ''',\n '\"': '"',\n })[character] ?? character\n )\n}\n\nfunction itemTitle(item: LayoutNode): string {\n return String(item.props?.title ?? item.id)\n}\n\nfunction materialLabel(type: string): string {\n return materials.get(type)?.title ?? materials.get(type)?.label ?? type\n}\n\nfunction formatMarkup(value: string): string {\n const tokens = value.replace(/></g, '>\\n<').split('\\n')\n let depth = 0\n return tokens\n .map(token => {\n const line = token.trim()\n if (!line) return ''\n if (line.startsWith('</')) depth = Math.max(0, depth - 1)\n const formatted = `${' '.repeat(depth)}${line}`\n if (\n line.startsWith('<') &&\n !line.startsWith('</') &&\n !line.startsWith('<!') &&\n !line.startsWith('<?') &&\n !line.endsWith('/>') &&\n !/^<(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)\\b/i.test(\n line\n )\n )\n depth += 1\n return formatted\n })\n .filter(Boolean)\n .join('\\n')\n}\n\nfunction formatCss(value: string): string {\n let depth = 0\n return value\n .replace(/\\s*\\{\\s*/g, ' {\\n')\n .replace(/\\s*;\\s*/g, ';\\n')\n .replace(/\\s*}\\s*/g, '\\n}\\n')\n .split('\\n')\n .map(line => line.trim())\n .filter(Boolean)\n .map(line => {\n if (line.startsWith('}')) depth = Math.max(0, depth - 1)\n const formatted = `${' '.repeat(depth)}${line}`\n if (line.endsWith('{')) depth += 1\n return formatted\n })\n .join('\\n')\n}\n\nfunction breakpointLabel(id: string): string {\n return id === 'desktop' ? '桌面' : id === 'tablet' ? '平板' : '手机'\n}\n\nfunction lucideMarkup(name: keyof typeof icons): string {\n if (typeof document === 'undefined') return ''\n const element = createLucideElement(icons[name])\n element.setAttribute('width', '13')\n element.setAttribute('height', '13')\n element.setAttribute('stroke-width', '1.9')\n element.setAttribute('aria-hidden', 'true')\n return element.outerHTML\n}\n\nfunction renderCardMarkup(\n item: LayoutNode,\n options: { interactive?: boolean } = {}\n): string {\n const type = materialLabel(item.type)\n const title = escapeHtml(itemTitle(item))\n const value = item.props?.value\n const valueMarkup =\n value === undefined\n ? `<span class=\"layout-card__chart\" aria-hidden=\"true\"><i></i><i></i><i></i><i></i><i></i></span>`\n : `<strong class=\"layout-card__value\">${escapeHtml(new Intl.NumberFormat('zh-CN').format(Number(value)))}</strong>`\n return `<article class=\"layout-card\" data-layout-card data-layout-type=\"${escapeHtml(item.type)}\">\n ${\n options.interactive\n ? `<div class=\"layout-card__actions\" data-layout-actions>\n <button class=\"layout-card__action\" type=\"button\" data-layout-action=\"copy\" aria-disabled=\"false\" aria-label=\"复制物料\" title=\"复制物料\">${lucideMarkup('Copy')}</button>\n <button class=\"layout-card__action\" type=\"button\" data-layout-action=\"delete\" aria-disabled=\"false\" aria-label=\"删除物料\" title=\"删除物料\">${lucideMarkup('Trash2')}</button>\n <button class=\"layout-card__action\" type=\"button\" data-layout-action=\"lock\" aria-disabled=\"false\" aria-label=\"${item.state?.locked ? '解锁物料' : '锁定物料'}\" title=\"${item.state?.locked ? '解锁物料' : '锁定物料'}\">${lucideMarkup(item.state?.locked ? 'Unlock' : 'Lock')}</button>\n </div>`\n : ''\n }\n <div class=\"layout-card__meta\"><span class=\"layout-card__type\">${escapeHtml(type)}</span><span class=\"layout-card__status\">已配置</span></div>\n <h3 class=\"layout-card__title\">${title}</h3>\n ${valueMarkup}\n <div class=\"layout-card__footer\"><span>实时数据</span><span class=\"layout-card__spark\">趋势稳定</span></div>\n </article>`\n}\n\nfunction renderExportMaterialMarkup(item: LayoutNode): string {\n const descriptor = materials.get(item.type)?.renderers?.webcomponent\n const tagName = descriptor?.tagName\n if (!tagName || !/^[a-z][a-z0-9.-]*$/i.test(tagName))\n return renderCardMarkup(item)\n const attributes = Object.entries(item.props ?? {})\n .filter(([, value]) => value !== undefined && value !== null)\n .filter(\n ([, value]) => typeof value !== 'object' && typeof value !== 'function'\n )\n .map(\n ([key, value]) =>\n `${key.replace(/[^a-zA-Z0-9:_.-]/g, '-')}=\"${escapeHtml(String(value))}\"`\n )\n .join(' ')\n return `<${tagName}${attributes ? ` ${attributes}` : ''}></${tagName}>`\n}\n\ntype WorkspaceMaterialElement = HTMLElement & {\n materialProps?: Record<string, unknown>\n}\n\nfunction ensureWorkspaceMaterialElement(tagName: string, type: string): void {\n if (\n typeof customElements === 'undefined' ||\n !/^[a-z][a-z0-9.-]*$/i.test(tagName) ||\n customElements.get(tagName)\n )\n return\n\n class LayoutMaterialElement extends HTMLElement {\n private props: Record<string, unknown> = {}\n\n static get observedAttributes(): string[] {\n return ['title', 'value']\n }\n\n set materialProps(value: Record<string, unknown> | undefined) {\n this.props = { ...(value ?? {}) }\n this.render()\n }\n\n connectedCallback(): void {\n this.render()\n }\n\n attributeChangedCallback(): void {\n if (!Object.keys(this.props).length) this.render()\n }\n\n private attributeProps(): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n for (const attribute of Array.from(this.attributes)) {\n if (attribute.name === 'class' || attribute.name === 'style') continue\n result[attribute.name] =\n attribute.name === 'value' &&\n attribute.value !== '' &&\n !Number.isNaN(Number(attribute.value))\n ? Number(attribute.value)\n : attribute.value\n }\n return result\n }\n\n private render(): void {\n const props = Object.keys(this.props).length\n ? this.props\n : this.attributeProps()\n this.style.display = 'block'\n this.style.width = '100%'\n this.style.height = '100%'\n this.innerHTML = renderCardMarkup({\n id: `native-${type}`,\n type,\n layout: {},\n props: props as LayoutNode['props'],\n })\n }\n }\n\n customElements.define(tagName, LayoutMaterialElement)\n}\n\nfunction setNativeRuntimeCard(shell: HTMLElement, item: LayoutNode): void {\n const descriptor = materials.get(item.type)?.renderers?.webcomponent\n const tagName = descriptor?.tagName\n shell.className = 'layout-item-shell'\n shell.dataset.layoutType = item.type\n shell.dataset.materialRenderer = 'webcomponent'\n if (!tagName || !/^[a-z][a-z0-9.-]*$/i.test(tagName)) {\n shell.innerHTML = renderCardMarkup(item)\n return\n }\n ensureWorkspaceMaterialElement(tagName, item.type)\n const element = shell.ownerDocument.createElement(\n tagName\n ) as WorkspaceMaterialElement\n element.materialProps = { ...(item.props ?? {}) }\n shell.replaceChildren(element)\n}\n\nfunction setRuntimeCard(shell: HTMLElement, item: LayoutNode): void {\n if (activeDevelopmentFramework.value === 'webcomponent') {\n setNativeRuntimeCard(shell, item)\n return\n }\n shell.className = 'layout-item-shell'\n shell.dataset.layoutType = item.type\n shell.dataset.materialRenderer = 'vue'\n shell.innerHTML = renderCardMarkup(item)\n}\n\nfunction parseRoute(value: string): DemoRoute {\n const id = value.replace(/^#\\/?/, '') as DemoRoute\n return routes.some(route => route.id === id) ? id : 'editor'\n}\n\nconst builtinMaterials: MaterialDefinition[] = [\n {\n type: 'metric',\n order: 1,\n title: '数据指标',\n category: 'data',\n categoryLabel: '数据组件',\n description: '数值与摘要',\n keywords: ['指标', '数值', 'KPI'],\n tags: ['数据', '卡片'],\n defaultLayout: { w: 4, h: 8 },\n size: {\n default: { w: 4, h: 8 },\n min: { w: 2, h: 4 },\n max: { w: 12, h: 20 },\n step: { w: 1, h: 1 },\n },\n constraints: { movable: true, resizable: true },\n defaults: { title: '新建指标', value: 0 },\n renderers: {\n html: { kind: 'html', tagName: 'article' },\n webcomponent: { kind: 'webcomponent', tagName: 'ldesign-metric-card' },\n vue: {\n kind: 'vue',\n package: '@composy/layout-vue',\n exportName: 'LMetricCard',\n component: LMetricCard,\n },\n },\n },\n {\n type: 'chart',\n order: 2,\n title: '趋势图表',\n category: 'visual',\n categoryLabel: '可视化组件',\n description: '变化与走向',\n keywords: ['趋势', '图表', '分析'],\n tags: ['可视化', '卡片'],\n defaultLayout: { w: 4, h: 10 },\n size: {\n default: { w: 4, h: 10 },\n min: { w: 3, h: 6 },\n max: { w: 12, h: 24 },\n step: { w: 1, h: 1 },\n },\n constraints: { movable: true, resizable: true },\n defaults: { title: '营收趋势' },\n renderers: {\n html: { kind: 'html', tagName: 'article' },\n webcomponent: { kind: 'webcomponent', tagName: 'ldesign-trend-card' },\n vue: {\n kind: 'vue',\n package: '@composy/layout-vue',\n exportName: 'LTrendCard',\n component: LTrendCard,\n },\n },\n },\n]\nfunction resolveWorkspaceMaterials(\n input: MaterialRegistry | readonly MaterialDefinition[] | undefined\n): MaterialRegistry {\n if (!input) return createMaterialRegistry(builtinMaterials)\n if (!Array.isArray(input)) return input as MaterialRegistry\n return createMaterialRegistry(input as readonly MaterialDefinition[])\n}\n\nconst materials = resolveWorkspaceMaterials(workspaceProps.materials)\nconst initial =\n workspaceProps.document ??\n createLayoutDocument({\n id: 'layout-alpha-demo',\n breakpoints: responsiveWebBreakpoints.map(item => ({ ...item })),\n defaultBreakpointId: 'desktop',\n items: [\n {\n id: 'sales',\n type: 'metric',\n props: { title: '月度销售额', value: 12840 },\n layout: {\n desktop: { x: 0, y: 0, w: 4, h: 10 },\n tablet: { x: 0, y: 0, w: 4, h: 10 },\n mobile: { x: 0, y: 0, w: 4, h: 8 },\n },\n },\n {\n id: 'orders',\n type: 'metric',\n props: { title: '待处理订单', value: 86 },\n layout: {\n desktop: { x: 4, y: 0, w: 4, h: 10 },\n tablet: { x: 4, y: 0, w: 4, h: 10 },\n mobile: { x: 0, y: 8, w: 4, h: 8 },\n },\n },\n {\n id: 'revenue',\n type: 'chart',\n props: { title: '营收预测' },\n layout: {\n desktop: { x: 8, y: 0, w: 4, h: 18 },\n tablet: { x: 0, y: 10, w: 8, h: 14 },\n mobile: { x: 0, y: 16, w: 4, h: 12 },\n },\n },\n ],\n })\nconst editorGrid = ref<HTMLElement>()\nconst canvasViewport = ref<HTMLElement>()\nconst runtimePreview = ref<HTMLElement>()\nconst vanillaRuntime = ref<HTMLElement>()\nconst previewDialogRuntime = ref<HTMLElement>()\nconst materialQuery = ref('')\nconst activeMaterialCategory = ref<string>()\nconst activeDevelopmentFramework = ref<DevelopmentFramework>('vue')\nconst materialElements = new Map<string, HTMLElement>()\nconst materialSourceDisposers = new Map<string, () => void>()\nconst materialCategories = computed(() => materials.getCategories())\nconst activeMaterialCategoryLabel = computed(() => {\n if (!activeMaterialCategory.value) return '全部分类'\n return (\n materialCategories.value.find(\n category => category.id === activeMaterialCategory.value\n )?.label ?? '全部分类'\n )\n})\nconst activeDevelopmentFrameworkLabel = computed(() =>\n developmentFrameworkLabel(activeDevelopmentFramework.value)\n)\nconst materialCategoryFilter = computed({\n get: () => activeMaterialCategory.value ?? '',\n set: value => {\n activeMaterialCategory.value = value || undefined\n },\n})\nconst hasMaterialFilters = computed(\n () =>\n Boolean(materialQuery.value.trim()) || Boolean(activeMaterialCategory.value)\n)\nconst visibleMaterials = computed(() =>\n materials.search({\n query: materialQuery.value,\n category: activeMaterialCategory.value,\n framework: activeDevelopmentFramework.value,\n })\n)\nconst layout = ref(structuredClone(initial) as LayoutDocument)\nconst selected = ref<string>()\nconst selectedIds = ref<string[]>([])\nconst activeBreakpoint = ref('desktop')\nconst canvasOrientation = ref<CanvasOrientation>('landscape')\nconst deviceMenuOpen = ref(false)\nconst categoryMenuOpen = ref(false)\nconst developmentModeMenuOpen = ref(false)\nconst copied = ref(false)\nconst previewMode = ref(false)\nconst previewDialogOpen = ref(false)\nconst materialCodeDialog = ref<MaterialDefinition>()\nconst materialCodeCopied = ref(false)\nconst activeRoute = ref<DemoRoute>('editor')\nconst theme = ref<'light' | 'dark'>(workspaceProps.theme ?? 'light')\nconst canvasZoom = ref(1)\nconst canvasPan = ref({ x: 0, y: 0 })\nconst canvasTool = ref<'select' | 'pan'>('select')\nconst canvasPanning = ref(false)\nconst canvasSpacePressed = ref(false)\nconst canvasPanOrigin = ref({ x: 0, y: 0, panX: 0, panY: 0 })\nlet editor: LayoutEditor | undefined\nlet vanillaRuntimeInstance: LayoutRuntime | undefined\nlet previewDialogRuntimeInstance: LayoutRuntime | undefined\nlet previousGridStackRenderCallback: typeof GridStack.renderCB | undefined\nlet interactionPreviewFrame: number | undefined\nconst disposers: Array<() => void> = []\n// The playground snapshot is intentionally reactive: committed editor changes\n// arrive through `document:change`, while transient drag/resize previews arrive\n// through `syncInteractionPreview`. Reading the non-reactive editor variable\n// here would let the computed stop tracking `layout` after the first commit.\nconst currentDocument = computed(() => layout.value)\nconst canvasSize = computed(() => {\n const dimensions: Record<\n string,\n {\n landscape: { width: number; height: number }\n portrait: { width: number; height: number }\n }\n > = {\n desktop: {\n landscape: { width: 1200, height: 760 },\n portrait: { width: 760, height: 1200 },\n },\n tablet: {\n landscape: { width: 1024, height: 768 },\n portrait: { width: 768, height: 1024 },\n },\n mobile: {\n landscape: { width: 844, height: 390 },\n portrait: { width: 390, height: 844 },\n },\n }\n return (\n dimensions[activeBreakpoint.value]?.[canvasOrientation.value] ?? {\n width: 1200,\n height: 760,\n }\n )\n})\nconst activeBreakpointIcon = computed(\n () =>\n breakpointIcons[activeBreakpoint.value as keyof typeof breakpointIcons] ??\n Monitor\n)\nconst exported = computed(() => JSON.stringify(currentDocument.value, null, 2))\nconst compiledResult = computed(() =>\n compileLayout(currentDocument.value, {\n target: 'html-css',\n breakpointId: activeBreakpoint.value,\n materials,\n materialCompiler: {\n supports: () => true,\n compile: item => renderExportMaterialMarkup(item),\n },\n })\n)\nconst compiled = computed(() => formatCss(compiledResult.value.css ?? ''))\nconst htmlSource = computed(() => {\n const result = compiledResult.value\n return `<style>\\n${compiled.value}\\n</style>\\n\\n${formatMarkup(result.html ?? '')}`\n})\nconst legacyVueSource = computed(\n () => `<script setup lang=\"ts\">\nimport { LGrid } from '@composy/layout-vue'\nimport type { LayoutDocument } from '@composy/layout-core'\n\nconst layoutDocument: LayoutDocument = ${JSON.stringify(currentDocument.value, null, 2)}\n<${'/'}script>\n\n<template>\n <LGrid :document=\"layoutDocument\" breakpoint=\"${activeBreakpoint.value}\">\n <template #item=\"{ item }\">\n <article class=\"layout-card\">\n <div class=\"layout-card__meta\">\n <span class=\"layout-card__type\">{{ item.type === 'metric' ? '数据指标' : '趋势图表' }}</span>\n <span class=\"layout-card__status\">已配置</span>\n </div>\n <h3 class=\"layout-card__title\">{{ item.props.title }}</h3>\n <strong v-if=\"item.props.value !== undefined\" class=\"layout-card__value\">\n {{ Number(item.props.value).toLocaleString('zh-CN') }}\n </strong>\n <span v-else class=\"layout-card__chart\" aria-hidden=\"true\">\n\n <i v-for=\"bar in 5\" :key=\"bar\" />\n </span>\n <div class=\"layout-card__footer\"><span>实时数据</span><span class=\"layout-card__spark\">趋势稳定</span></div>\n </article>\n </template>\n </LGrid>\n</template>`\n)\nconst legacyReactSource = computed(\n () => `import { Grid } from '@composy/layout-react'\nimport type { LayoutDocument } from '@composy/layout-core'\n\nconst layoutDocument: LayoutDocument = ${JSON.stringify(currentDocument.value, null, 2)}\n\nexport function LayoutPreview() {\n return (\n <Grid document={layoutDocument} breakpoint=\"${activeBreakpoint.value}\">\n {({ item }) => (\n <article className=\"layout-card\">\n <div className=\"layout-card__meta\">\n <span className=\"layout-card__type\">{item.type === 'metric' ? '数据指标' : '趋势图表'}</span>\n <span className=\"layout-card__status\">已配置</span>\n </div>\n <h3 className=\"layout-card__title\">{String(item.props.title)}</h3>\n {item.props.value !== undefined ? (\n <strong className=\"layout-card__value\">{Number(item.props.value).toLocaleString('zh-CN')}</strong>\n ) : (\n <span className=\"layout-card__chart\" aria-hidden=\"true\">{[1, 2, 3, 4, 5].map(bar => <i key={bar} />)}</span>\n )}\n <div className=\"layout-card__footer\"><span>实时数据</span><span className=\"layout-card__spark\">趋势稳定</span></div>\n </article>\n )}\n </Grid>\n )\n}`\n)\nconst generatedVueSource = computed(() => {\n const imports = new Map<string, string[]>()\n const bindings: string[] = []\n const usedLocals = new Set<string>(['LGrid'])\n for (const material of materials.getAll()) {\n const renderer = material.renderers?.vue\n if (!renderer?.package || !renderer.exportName) continue\n const exportName = renderer.exportName.trim()\n if (!/^[A-Za-z_$][\\w$]*$/.test(exportName)) continue\n let localName = exportName\n let suffix = 2\n while (usedLocals.has(localName)) localName = `${exportName}${suffix++}`\n usedLocals.add(localName)\n const packageImports = imports.get(renderer.package) ?? []\n packageImports.push(\n localName === exportName ? exportName : `${exportName} as ${localName}`\n )\n imports.set(renderer.package, packageImports)\n bindings.push(` ${JSON.stringify(material.type)}: ${localName}`)\n }\n const importLines = [\n \"import { LGrid } from '@composy/layout-vue'\",\n ...[...imports.entries()].map(\n ([packageName, exports]) =>\n `import { ${exports.join(', ')} } from '${packageName}'`\n ),\n ].join('\\n')\n const componentMap = bindings.length\n ? `const materialComponents = {\\n${bindings.join(',\\n')}\\n} as const`\n : 'const materialComponents: Record<string, never> = {}'\n return `<script setup lang=\"ts\">\n${importLines}\nimport type { LayoutDocument } from '@composy/layout-core'\n\nconst layoutDocument: LayoutDocument = ${JSON.stringify(currentDocument.value, null, 2)}\nconst breakpoint = '${activeBreakpoint.value}'\n${componentMap}\n<${'/'}script>\n\n<template>\n <LGrid :document=\"layoutDocument\" :breakpoint=\"breakpoint\">\n <template #item=\"{ item }\">\n <component\n :is=\"materialComponents[item.type as keyof typeof materialComponents]\"\n v-bind=\"item.props\"\n />\n </template>\n </LGrid>\n</template>\n\n<style scoped>\n/* LGrid renders the ldesign-layout-grid CSS Grid container. */\n${compiled.value}\n</style>`\n})\nconst generatedReactSource = computed(() =>\n `import type { LayoutDocument, LayoutNode } from '@composy/layout-core'\n\nconst layoutDocument: LayoutDocument = ${JSON.stringify(currentDocument.value, null, 2)}\nconst breakpoint = '${activeBreakpoint.value}'\nconst itemClass = (item: LayoutNode) => \\`ldesign-layout-item-\\${item.id.replace(/[^a-zA-Z0-9_-]/g, '-') }\\`\n\nexport function LayoutPreview() {\n return (\n <section className=\"ldesign-layout-grid\">\n {layoutDocument.items.map(item => (\n <article key={item.id} className={\\`\\${itemClass(item)} layout-card\\`}>\n <div className=\"layout-card__meta\"><span className=\"layout-card__type\">{item.type === 'metric' ? '数据指标' : '趋势图表'}</span><span className=\"layout-card__status\">已配置</span></div>\n <h3 className=\"layout-card__title\">{String(item.props.title)}</h3>\n {item.props.value !== undefined ? <strong className=\"layout-card__value\">{Number(item.props.value).toLocaleString('zh-CN')}</strong> : <span className=\"layout-card__chart\" aria-hidden=\"true\">{[1, 2, 3, 4, 5].map(bar => <i key={bar} />)}</span>}\n <div className=\"layout-card__footer\"><span>实时数据</span><span className=\"layout-card__spark\">趋势稳定</span></div>\n </article>\n ))}\n </section>\n )\n}\n\n/* CSS Grid styles are emitted separately by the compiler. */`\n .replace(\n /<span className=\"layout-card__type\">[\\s\\S]*?<\\/span>/g,\n '<span className=\"layout-card__type\">{item.type}</span>'\n )\n .replace(\n /<span className=\"layout-card__status\">[\\s\\S]*?<\\/span>/g,\n '<span className=\"layout-card__status\">已配置</span>'\n )\n .replace(\n /<div className=\"layout-card__footer\">[\\s\\S]*?<\\/div>/g,\n '<div className=\"layout-card__footer\"><span>实时数据</span><span className=\"layout-card__spark\">稳定</span></div>'\n )\n)\nconst activeCode = computed(() =>\n activeDevelopmentFramework.value === 'vue'\n ? generatedVueSource.value\n : htmlSource.value\n)\nconst activeCodeLabel = computed(() =>\n activeDevelopmentFramework.value === 'vue' ? 'Vue组件代码' : '原生代码'\n)\nconst activeCodeExtension = computed(() =>\n activeDevelopmentFramework.value === 'vue' ? 'vue' : 'html'\n)\n\nfunction materialCodeProps(\n material: MaterialDefinition\n): Record<string, unknown> {\n return {\n ...(material.defaults ?? {}),\n }\n}\n\nfunction materialVueCode(material: MaterialDefinition): string {\n const renderer = material.renderers?.vue\n const exportedName = renderer?.exportName?.trim()\n const componentName =\n (exportedName && /^[A-Za-z_$][\\w$]*$/.test(exportedName)\n ? exportedName\n : undefined) ??\n (renderer?.component as { name?: string } | undefined)?.name ??\n material.type.replace(/[^A-Za-z0-9_$]/g, '')\n const importLine =\n renderer?.package && exportedName && /^[A-Za-z_$][\\w$]*$/.test(exportedName)\n ? `import { ${componentName} } from '${renderer.package}'`\n : `// Vue 组件由物料注册表提供:${componentName}`\n const props = materialCodeProps(material)\n const bindings = Object.entries(props)\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([key, value]) => ` :${key}='${JSON.stringify(value)}'`)\n .join('\\n')\n return `<script setup lang=\"ts\">\n${importLine}\n<\\/script>\n\n<!-- 注册表物料:${material.type} -->\n<template>\n <${componentName}${bindings ? `\\n${bindings}\\n ` : ''}/>\n</template>`\n}\n\nfunction materialWebComponentCode(material: MaterialDefinition): string {\n const renderer = material.renderers?.webcomponent\n const tagName =\n renderer?.tagName ?? `ldesign-${material.type.replace(/[^a-z0-9-]/gi, '-')}`\n const attributes = Object.entries(materialCodeProps(material))\n .filter(([, value]) => value !== undefined && value !== null)\n .map(([key, value]) => `${key}=\"${String(value).replace(/\"/g, '"')}\"`)\n .join('\\n ')\n return `<!-- 注册表物料:${material.type} -->\n<!-- Web Component 标签:${tagName} -->\n<${tagName}${attributes ? `\\n ${attributes}` : ''}></${tagName}>`\n}\n\nconst materialCode = computed(() => {\n const material = materialCodeDialog.value\n if (!material) return ''\n return activeDevelopmentFramework.value === 'vue'\n ? materialVueCode(material)\n : materialWebComponentCode(material)\n})\nconst highlightedMaterialCode = computed(() =>\n highlightCode(materialCode.value)\n)\nconst materialCodeLineCount = computed(\n () => materialCode.value.split('\\n').length\n)\n\nfunction highlightCode(value: string): string {\n const fragments: string[] = []\n const stash = (value: string, className: string) => {\n const index =\n fragments.push(`<span class=\"code-token-${className}\">${value}</span>`) -\n 1\n return `__LDS_TOKEN_${index}__`\n }\n let code = escapeHtml(value)\n code = code.replace(/(".*?"|'.*?'|`.*?`)/g, match =>\n stash(match, 'string')\n )\n code = code.replace(/(\\/\\/[^\\n]*|\\/\\*[\\s\\S]*?\\*\\/)/g, match =>\n stash(match, 'comment')\n )\n code = code.replace(\n /(^|[;{\\n]\\s*)([A-Za-z_$][\\w$-]*)(?=\\s*:)/g,\n (_, prefix, name) => `${prefix}${stash(name, 'property')}`\n )\n code = code.replace(\n /\\b(import|from|const|let|function|return|export|as|type|class|extends|new|true|false|null|undefined)\\b/g,\n match =>\n stash(\n match,\n /^(true|false|null|undefined)$/.test(match) ? 'constant' : 'keyword'\n )\n )\n code = code.replace(/(<\\/?[A-Za-z][^&]*?>)/g, match =>\n stash(match, 'tag')\n )\n let previous = ''\n while (previous !== code && /__LDS_TOKEN_\\d+__/.test(code)) {\n previous = code\n code = code.replace(\n /__LDS_TOKEN_(\\d+)__/g,\n (_, index) => fragments[Number(index)] ?? ''\n )\n }\n return code\n}\nconst highlightedCode = computed(() => highlightCode(activeCode.value))\nconst activeCodeLineCount = computed(() => activeCode.value.split('\\n').length)\nconst canvasSceneStyle = computed(() => ({\n width: `${canvasSize.value.width}px`,\n minHeight: `${canvasSize.value.height}px`,\n '--canvas-height': `${canvasSize.value.height}px`,\n '--canvas-grid-height': `${Math.max(180, canvasSize.value.height)}px`,\n transform: `translate3d(${canvasPan.value.x}px, ${canvasPan.value.y}px, 0) scale(${canvasZoom.value})`,\n}))\nconst canvasZoomLabel = computed(() => `${Math.round(canvasZoom.value * 100)}%`)\nconst selectedItem = computed(() =>\n layout.value.items.find(item => item.id === selected.value)\n)\nconst selectedLocked = computed(() =>\n Boolean(selectedItem.value?.state?.locked)\n)\nconst selectedMovable = computed(\n () => selectedItem.value?.constraints?.movable !== false\n)\nconst selectedResizable = computed(\n () => selectedItem.value?.constraints?.resizable !== false\n)\n\nfunction add(type: string) {\n if (!editor) return\n const node = editor.addItem({ type })\n editor.updateItem(node.id, item => {\n for (const bp of Object.values(item.layout)) bp.y = 30\n return item\n })\n editor.select(node.id)\n}\n\nfunction materialProps(type: string): string {\n return JSON.stringify(materials.get(type)?.defaults ?? {})\n}\n\nfunction openMaterialCode(type: string): void {\n const material = materials.get(type)\n if (!material) return\n materialCodeDialog.value = material\n materialCodeCopied.value = false\n closeFilterMenus()\n}\n\nfunction closeMaterialCode(): void {\n materialCodeDialog.value = undefined\n materialCodeCopied.value = false\n}\n\nasync function copyMaterialCode(): Promise<void> {\n if (!materialCode.value || typeof navigator === 'undefined') return\n try {\n await navigator.clipboard.writeText(materialCode.value)\n materialCodeCopied.value = true\n window.setTimeout(() => {\n materialCodeCopied.value = false\n }, 1500)\n } catch {\n materialCodeCopied.value = false\n }\n}\n\nfunction handleMaterialClick(type: string): void {\n add(type)\n}\n\nfunction toggleFilterMenu(menu: 'category' | 'device'): void {\n categoryMenuOpen.value = menu === 'category' ? !categoryMenuOpen.value : false\n deviceMenuOpen.value = menu === 'device' ? !deviceMenuOpen.value : false\n developmentModeMenuOpen.value = false\n}\n\nfunction toggleDevelopmentModeMenu(): void {\n developmentModeMenuOpen.value = !developmentModeMenuOpen.value\n categoryMenuOpen.value = false\n deviceMenuOpen.value = false\n}\n\nfunction closeFilterMenus(): void {\n categoryMenuOpen.value = false\n deviceMenuOpen.value = false\n developmentModeMenuOpen.value = false\n}\n\nfunction handleFilterMenuKeydown(event: KeyboardEvent): void {\n if (event.key !== 'Escape') return\n if (\n !categoryMenuOpen.value &&\n !developmentModeMenuOpen.value &&\n !deviceMenuOpen.value\n )\n return\n closeFilterMenus()\n ;(event.target as HTMLElement | null)?.blur()\n}\n\nfunction handleFilterOutsidePointerdown(event: PointerEvent): void {\n const target = event.target\n if (!(target instanceof Element)) return\n if (target.closest('[data-layout-filter-menu]')) return\n closeFilterMenus()\n}\n\nfunction selectMaterialCategory(category: string): void {\n materialCategoryFilter.value = category\n closeFilterMenus()\n}\n\nfunction selectDevelopmentFramework(framework: DevelopmentFramework): void {\n activeDevelopmentFramework.value = framework\n if (typeof window !== 'undefined')\n window.localStorage.setItem('layout-development-framework', framework)\n closeFilterMenus()\n nextTick(() => {\n syncVanillaRuntime()\n syncPreviewDialogRuntime()\n if (previewMode.value && editor) {\n editor.setMode('edit')\n nextTick(() => editor?.setMode('preview'))\n }\n })\n}\n\nfunction resetMaterialFilters(): void {\n materialQuery.value = ''\n activeMaterialCategory.value = undefined\n closeFilterMenus()\n}\n\nfunction materialIcon(type: string) {\n return type === 'chart' ? ChartSpline : ChartNoAxesColumnIncreasing\n}\n\nfunction setMaterialElement(type: string, value: unknown): void {\n if (value instanceof HTMLElement) materialElements.set(type, value)\n else materialElements.delete(type)\n}\n\nfunction syncMaterialSources(): void {\n if (!editor) return\n const visibleTypes = new Set(\n visibleMaterials.value.map(material => material.type)\n )\n for (const material of visibleMaterials.value) {\n const element = materialElements.get(material.type)\n if (!element || materialSourceDisposers.has(material.type)) continue\n const size = material.defaultLayout ?? material.size?.default\n const dispose = editor.registerExternalDragSource({\n element,\n type: material.type,\n props: { ...(material.defaults ?? {}) },\n placement: { x: 0, y: 0, w: size?.w ?? 1, h: size?.h ?? 1 },\n })\n materialSourceDisposers.set(material.type, dispose)\n }\n\n for (const [type, dispose] of materialSourceDisposers) {\n if (!visibleTypes.has(type)) {\n dispose()\n materialSourceDisposers.delete(type)\n }\n }\n}\n\nwatch([materialQuery, activeMaterialCategory, activeDevelopmentFramework], () =>\n nextTick(syncMaterialSources)\n)\nfunction removeSelected() {\n editor?.handleKeyboard({ key: 'Delete' })\n}\nfunction toggleSelectedLock() {\n if (!editor || !selected.value) return\n if (selectedLocked.value) editor.unlockItem(selected.value)\n else editor.lockItem(selected.value)\n}\nfunction toggleSelectedConstraint(key: 'movable' | 'resizable') {\n if (!editor || !selected.value) return\n editor.updateItem(selected.value, item => {\n item.constraints = {\n ...(item.constraints ?? {}),\n [key]: item.constraints?.[key] === false,\n }\n return item\n })\n}\nfunction undo() {\n editor?.undo()\n}\nfunction redo() {\n editor?.redo()\n}\nfunction togglePreview() {\n const nextMode = previewMode.value ? 'edit' : 'preview'\n editor?.setMode(nextMode)\n // Keep the address bar in sync with the visible mode. This also makes a\n // later hash assignment to the same route observable to the router.\n navigate(nextMode === 'preview' ? 'runtime' : 'editor')\n}\n\nfunction openPreviewDialog(): void {\n previewDialogOpen.value = true\n nextTick(syncPreviewDialogRuntime)\n}\n\nfunction closePreviewDialog(): void {\n previewDialogOpen.value = false\n previewDialogRuntimeInstance?.destroy()\n previewDialogRuntimeInstance = undefined\n}\n\nfunction syncPreviewDialogRuntime(): void {\n if (\n activeDevelopmentFramework.value !== 'webcomponent' ||\n !previewDialogOpen.value ||\n !previewDialogRuntime.value\n ) {\n previewDialogRuntimeInstance?.destroy()\n previewDialogRuntimeInstance = undefined\n return\n }\n const runtimeDocument = editor?.exportRuntimeDocument() ?? layout.value\n if (!previewDialogRuntimeInstance) {\n previewDialogRuntimeInstance = createLayoutRuntime({\n container: previewDialogRuntime.value,\n document: runtimeDocument,\n materials,\n observeResize: false,\n renderItem: ({ shell, item }) => {\n const render = () => setNativeRuntimeCard(shell, item)\n render()\n return { update: render }\n },\n })\n previewDialogRuntimeInstance.mount()\n } else previewDialogRuntimeInstance.update(runtimeDocument)\n previewDialogRuntimeInstance.setBreakpoint(activeBreakpoint.value)\n}\n\nwatch(\n [previewDialogOpen, activeDevelopmentFramework, activeBreakpoint, layout],\n () => nextTick(syncPreviewDialogRuntime)\n)\n\nfunction syncVanillaRuntime() {\n if (\n activeDevelopmentFramework.value !== 'webcomponent' ||\n activeRoute.value !== 'vanilla' ||\n !vanillaRuntime.value ||\n !editor\n ) {\n vanillaRuntimeInstance?.destroy()\n vanillaRuntimeInstance = undefined\n return\n }\n const document = editor.exportRuntimeDocument()\n if (!vanillaRuntimeInstance) {\n vanillaRuntimeInstance = createLayoutRuntime({\n container: vanillaRuntime.value,\n document,\n materials,\n observeResize: false,\n renderItem: ({ shell, item }) => {\n const render = () => {\n setRuntimeCard(shell, item)\n }\n render()\n return { update: render }\n },\n })\n vanillaRuntimeInstance.mount()\n } else vanillaRuntimeInstance.update(document)\n vanillaRuntimeInstance.setBreakpoint(activeBreakpoint.value)\n}\n\nfunction navigate(route: DemoRoute) {\n activeRoute.value = route\n if (typeof window !== 'undefined') window.location.hash = `/${route}`\n if (route === 'runtime') {\n // Re-enter preview through edit when necessary so a stale GridStack DOM\n // cannot remain mounted after switching routes repeatedly.\n if (editor?.getMode() === 'preview') editor.setMode('edit')\n editor?.setMode('preview')\n const clearEditorWidgets = () => {\n if (activeRoute.value !== 'runtime') return\n editorGrid.value\n ?.querySelectorAll<HTMLElement>('.grid-stack-item')\n .forEach(element => element.remove())\n }\n clearEditorWidgets()\n nextTick(clearEditorWidgets)\n } else if (previewMode.value) editor?.setMode('edit')\n nextTick(syncVanillaRuntime)\n}\nfunction select(id: string) {\n editor?.select(id)\n}\nfunction changeBreakpoint(id: string) {\n activeBreakpoint.value = id\n canvasOrientation.value = id === 'mobile' ? 'portrait' : 'landscape'\n editor?.setBreakpoint(id)\n vanillaRuntimeInstance?.setBreakpoint(id)\n deviceMenuOpen.value = false\n}\n\nfunction selectBreakpoint(id: string): void {\n changeBreakpoint(id)\n}\n\nfunction toggleCanvasOrientation(): void {\n canvasOrientation.value =\n canvasOrientation.value === 'portrait' ? 'landscape' : 'portrait'\n}\n\nfunction clampCanvasZoom(value: number): number {\n return Math.min(2, Math.max(0.5, Math.round(value * 20) / 20))\n}\n\nfunction setCanvasZoom(value: number, anchor?: { x: number; y: number }) {\n const viewport = canvasViewport.value\n const previousZoom = canvasZoom.value\n const nextZoom = clampCanvasZoom(value)\n if (!viewport || previousZoom === nextZoom) return\n const rect = viewport.getBoundingClientRect()\n const anchorX = anchor?.x ?? rect.left + rect.width / 2\n const anchorY = anchor?.y ?? rect.top + rect.height / 2\n const localX = (anchorX - rect.left - canvasPan.value.x) / previousZoom\n const localY = (anchorY - rect.top - canvasPan.value.y) / previousZoom\n canvasZoom.value = nextZoom\n canvasPan.value = {\n x: anchorX - rect.left - localX * nextZoom,\n y: anchorY - rect.top - localY * nextZoom,\n }\n}\n\nfunction zoomCanvas(direction: 1 | -1) {\n setCanvasZoom(canvasZoom.value + direction * 0.1)\n}\n\nfunction resetCanvasView() {\n canvasZoom.value = 1\n canvasPan.value = { x: 0, y: 0 }\n}\n\nfunction resetCanvasPan() {\n canvasPan.value = { x: 0, y: 0 }\n}\n\nfunction toggleTheme() {\n theme.value = theme.value === 'light' ? 'dark' : 'light'\n if (typeof window !== 'undefined')\n window.localStorage.setItem('layout-theme', theme.value)\n}\n\nfunction handleCanvasWheel(event: WheelEvent) {\n event.preventDefault()\n const direction = event.deltaY < 0 ? 1 : -1\n setCanvasZoom(canvasZoom.value + direction * 0.08, {\n x: event.clientX,\n y: event.clientY,\n })\n}\n\nfunction handleCanvasPointerDown(event: PointerEvent) {\n const shouldPan =\n canvasTool.value === 'pan' || canvasSpacePressed.value || event.button === 1\n if (!shouldPan || event.button === 2) return\n event.preventDefault()\n event.stopPropagation()\n const target = event.currentTarget as HTMLElement\n target.setPointerCapture?.(event.pointerId)\n canvasPanning.value = true\n canvasPanOrigin.value = {\n x: event.clientX,\n y: event.clientY,\n panX: canvasPan.value.x,\n panY: canvasPan.value.y,\n }\n}\n\nfunction handleCanvasPointerMove(event: PointerEvent) {\n if (!canvasPanning.value) return\n const origin = canvasPanOrigin.value\n canvasPan.value = {\n x: origin.panX + event.clientX - origin.x,\n y: origin.panY + event.clientY - origin.y,\n }\n}\n\nfunction finishCanvasPan(event?: PointerEvent) {\n if (!canvasPanning.value) return\n canvasPanning.value = false\n if (event) {\n const target = event.currentTarget as HTMLElement\n target.releasePointerCapture?.(event.pointerId)\n }\n}\n\nfunction handleCanvasKeydown(event: KeyboardEvent) {\n if (event.code === 'Space') canvasSpacePressed.value = true\n}\n\nfunction handleCanvasKeyup(event: KeyboardEvent) {\n if (event.code === 'Space') canvasSpacePressed.value = false\n}\n\nfunction handleWorkspaceKeydown(event: KeyboardEvent): void {\n if (!editor || event.defaultPrevented) return\n const root = editorGrid.value?.closest<HTMLElement>('[data-layout-workspace]')\n if (!root || !(event.target instanceof Node) || !root.contains(event.target))\n return\n const key = event.key.toLowerCase()\n const isLayoutShortcut =\n (event.ctrlKey || event.metaKey) && ['c', 'v', 'x', 'd'].includes(key)\n if (!isLayoutShortcut && !['delete', 'backspace'].includes(key)) return\n editor.handleKeyboard({\n key: event.key,\n shiftKey: event.shiftKey,\n ctrlKey: event.ctrlKey,\n metaKey: event.metaKey,\n altKey: event.altKey,\n preventDefault: () => event.preventDefault(),\n })\n}\n\nasync function copyActiveCode() {\n const value = activeCode.value\n try {\n if (navigator.clipboard?.writeText)\n await navigator.clipboard.writeText(value)\n else {\n const textarea = document.createElement('textarea')\n\n textarea.value = value\n textarea.style.position = 'fixed'\n textarea.style.opacity = '0'\n document.body.appendChild(textarea)\n textarea.select()\n document.execCommand('copy')\n textarea.remove()\n }\n copied.value = true\n window.setTimeout(() => {\n copied.value = false\n }, 1600)\n } catch {\n copied.value = false\n }\n}\n\nfunction downloadActiveCode() {\n const blob = new Blob([activeCode.value], {\n type: 'text/plain;charset=utf-8',\n })\n const url = URL.createObjectURL(blob)\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = `layout-${activeDevelopmentFramework.value}.${activeCodeExtension.value}`\n anchor.click()\n URL.revokeObjectURL(url)\n}\n\nfunction handleEditorGridAction(event: MouseEvent | PointerEvent): void {\n const target = event.target\n if (!(target instanceof Element)) return\n const action = target.closest<HTMLElement>('[data-layout-action]')\n const widget = target.closest<HTMLElement>('.grid-stack-item')\n if (!action || !widget || action.dataset.layoutActionPointer === 'true') {\n if (action?.dataset.layoutActionPointer === 'true')\n delete action.dataset.layoutActionPointer\n return\n }\n if (event.type === 'pointerdown') {\n action.dataset.layoutActionPointer = 'true'\n event.preventDefault()\n }\n const id =\n widget.getAttribute('gs-id') ?? widget.getAttribute('data-layout-item-id')\n if (!id || !editor) return\n editor.select(id)\n const kind = action.dataset.layoutAction\n if (kind === 'copy') editor.copySelection()\n else if (kind === 'delete') editor.removeItem(id)\n else if (kind === 'lock') {\n if (layout.value.items.find(item => item.id === id)?.state?.locked)\n editor.unlockItem(id)\n else editor.lockItem(id)\n }\n}\n\nfunction syncSelection() {\n const root = editorGrid.value\n if (!root) return\n root.querySelectorAll<HTMLElement>('.grid-stack-item').forEach(item => {\n const id =\n item.getAttribute('gs-id') ??\n item.getAttribute('data-layout-item-id') ??\n ''\n const node = layout.value.items.find(candidate => candidate.id === id)\n const active = selectedIds.value.includes(id)\n item.classList.toggle('is-selected', active)\n item.tabIndex = 0\n item.setAttribute('role', 'button')\n if (node)\n item.setAttribute(\n 'aria-label',\n `${itemTitle(node)},${materialLabel(node.type)}`\n )\n item.setAttribute('aria-pressed', String(active))\n item.setAttribute('aria-selected', String(active))\n item.setAttribute('aria-disabled', String(Boolean(node?.state?.locked)))\n })\n}\n\nfunction syncInteractionPreview(): void {\n if (interactionPreviewFrame !== undefined) return\n const flush = () => {\n interactionPreviewFrame = undefined\n if (!editor) return\n layout.value = editor.exportRuntimeDocument()\n queueMicrotask(syncSelection)\n }\n if (typeof window === 'undefined') flush()\n else interactionPreviewFrame = window.requestAnimationFrame(flush)\n}\n\nonMounted(() => {\n if (typeof window !== 'undefined') {\n const savedTheme = window.localStorage.getItem('layout-theme')\n if (savedTheme === 'light' || savedTheme === 'dark')\n theme.value = savedTheme\n const savedFramework = window.localStorage.getItem(\n 'layout-development-framework'\n )\n if (savedFramework === 'webcomponent' || savedFramework === 'vue') {\n activeDevelopmentFramework.value = savedFramework\n }\n }\n if (!editorGrid.value) return\n previousGridStackRenderCallback = GridStack.renderCB\n GridStack.renderCB = (element, widget) => {\n if (element && widget?.content !== undefined)\n element.innerHTML = String(widget.content)\n }\n const engine = createGridStackEditorEngine({\n container: editorGrid.value,\n gridOptions: { acceptWidgets: true, animate: false },\n createGrid: options => GridStack.init(options, editorGrid.value!)!,\n renderItem: ({ item }) => renderCardMarkup(item, { interactive: true }),\n })\n editor = createLayoutEditor({\n document: initial,\n engine,\n materials,\n preview: runtimePreview.value\n ? {\n container: runtimePreview.value,\n renderItem: ({ shell, item }) => {\n const render = () => {\n setRuntimeCard(shell, item)\n }\n render()\n return { update: render }\n },\n }\n : undefined,\n })\n editor.on('document:change', event => {\n layout.value = event.document\n workspaceEmit('update:document', event.document)\n workspaceEmit('change', event.document)\n queueMicrotask(syncSelection)\n syncVanillaRuntime()\n })\n editor.on('interaction:move', syncInteractionPreview)\n editor.on('interaction:resize', syncInteractionPreview)\n editor.on('selection:change', event => {\n selected.value = event.primaryId\n selectedIds.value = [...event.selectedIds]\n syncSelection()\n })\n editor.on('mode:change', event => {\n previewMode.value = event.mode === 'preview'\n if (event.mode === 'preview' && activeRoute.value !== 'runtime')\n activeRoute.value = 'runtime'\n if (event.mode === 'edit' && activeRoute.value === 'runtime')\n activeRoute.value = 'editor'\n nextTick(syncVanillaRuntime)\n })\n editor.mount()\n editorGrid.value.addEventListener('pointerdown', handleEditorGridAction, true)\n editorGrid.value.addEventListener('click', handleEditorGridAction, true)\n disposers.push(() =>\n editorGrid.value?.removeEventListener(\n 'pointerdown',\n handleEditorGridAction,\n true\n )\n )\n disposers.push(() =>\n editorGrid.value?.removeEventListener('click', handleEditorGridAction, true)\n )\n nextTick(syncMaterialSources)\n syncSelection()\n if (typeof window !== 'undefined') {\n activeRoute.value = parseRoute(window.location.hash)\n const onHashChange = () => {\n const next = parseRoute(window.location.hash)\n if (next !== activeRoute.value) navigate(next)\n else if (next === 'runtime' && editor?.getMode() !== 'preview')\n editor.setMode('preview')\n else if (next !== 'runtime' && editor?.getMode() === 'preview')\n editor.setMode('edit')\n }\n window.addEventListener('hashchange', onHashChange)\n disposers.push(() => window.removeEventListener('hashchange', onHashChange))\n window.addEventListener('keydown', handleCanvasKeydown)\n window.addEventListener('keyup', handleCanvasKeyup)\n window.addEventListener('keydown', handleWorkspaceKeydown)\n document.addEventListener('pointerdown', handleFilterOutsidePointerdown)\n document.addEventListener('keydown', handleFilterMenuKeydown)\n disposers.push(() =>\n window.removeEventListener('keydown', handleCanvasKeydown)\n )\n disposers.push(() => window.removeEventListener('keyup', handleCanvasKeyup))\n disposers.push(() =>\n window.removeEventListener('keydown', handleWorkspaceKeydown)\n )\n disposers.push(() =>\n document.removeEventListener(\n 'pointerdown',\n handleFilterOutsidePointerdown\n )\n )\n disposers.push(() =>\n document.removeEventListener('keydown', handleFilterMenuKeydown)\n )\n }\n if (activeRoute.value === 'runtime') editor.setMode('preview')\n syncVanillaRuntime()\n})\n\nonBeforeUnmount(() => {\n if (interactionPreviewFrame !== undefined && typeof window !== 'undefined')\n window.cancelAnimationFrame(interactionPreviewFrame)\n interactionPreviewFrame = undefined\n materialSourceDisposers.forEach(dispose => dispose())\n materialSourceDisposers.clear()\n disposers.splice(0).forEach(dispose => dispose())\n vanillaRuntimeInstance?.destroy()\n vanillaRuntimeInstance = undefined\n previewDialogRuntimeInstance?.destroy()\n previewDialogRuntimeInstance = undefined\n editor?.destroy()\n if (previousGridStackRenderCallback) {\n GridStack.renderCB = previousGridStackRenderCallback\n previousGridStackRenderCallback = undefined\n }\n})\n</script>\n\n<template>\n <main\n class=\"layout-demo\"\n data-layout-workspace\n :data-theme=\"theme\"\n :data-development-framework=\"activeDevelopmentFramework\"\n >\n <header class=\"topbar\">\n <div class=\"brand\">\n <span class=\"brand-mark\" aria-hidden=\"true\">\n <Grid2X2 :size=\"18\" :stroke-width=\"1.8\" />\n </span>\n <div class=\"brand-copy\">\n <strong>布局工作台</strong><span>所见即所得 · 网格布局</span>\n </div>\n <div\n class=\"development-mode custom-select\"\n :class=\"{ 'is-open': developmentModeMenuOpen }\"\n data-layout-filter-menu\n @pointerdown.stop\n >\n <button\n class=\"custom-select__trigger\"\n type=\"button\"\n aria-haspopup=\"listbox\"\n :aria-expanded=\"developmentModeMenuOpen\"\n aria-label=\"开发框架\"\n :title=\"`当前开发框架:${activeDevelopmentFrameworkLabel}`\"\n @click.stop=\"toggleDevelopmentModeMenu\"\n >\n <component\n :is=\"\n developmentFrameworks.find(\n framework => framework.id === activeDevelopmentFramework\n )?.icon ?? PanelsTopLeft\n \"\n :size=\"14\"\n :stroke-width=\"1.9\"\n aria-hidden=\"true\"\n />\n <span>{{ activeDevelopmentFrameworkLabel }}</span>\n <ChevronDown :size=\"12\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n <div\n v-if=\"developmentModeMenuOpen\"\n class=\"custom-select__menu development-mode__menu\"\n role=\"listbox\"\n >\n <button\n v-for=\"framework in developmentFrameworks\"\n :key=\"framework.id\"\n class=\"custom-select__option\"\n type=\"button\"\n :aria-selected=\"activeDevelopmentFramework === framework.id\"\n @click=\"selectDevelopmentFramework(framework.id)\"\n >\n <component\n :is=\"framework.icon\"\n :size=\"14\"\n :stroke-width=\"1.8\"\n aria-hidden=\"true\"\n />\n <span>\n <strong>{{ framework.label }}</strong>\n <small>{{ framework.description }}</small>\n </span>\n </button>\n </div>\n </div>\n </div>\n <div class=\"topbar-center\" aria-hidden=\"true\" style=\"display: none\">\n <nav class=\"route-tabs\" aria-label=\"工作区模式\">\n <button\n v-for=\"demoRoute in routes\"\n :key=\"demoRoute.id\"\n type=\"button\"\n :aria-current=\"activeRoute === demoRoute.id ? 'page' : undefined\"\n :aria-label=\"`${demoRoute.label}模式`\"\n :title=\"demoRoute.description\"\n @click=\"navigate(demoRoute.id)\"\n >\n <component\n :is=\"routeIcons[demoRoute.id]\"\n class=\"route-icon\"\n :size=\"15\"\n :stroke-width=\"1.8\"\n aria-hidden=\"true\"\n />\n <span>{{ demoRoute.label }}</span>\n </button>\n </nav>\n <div class=\"breakpoint-tabs\" aria-label=\"画布尺寸\">\n <button\n v-for=\"breakpoint in layout.breakpoints\"\n :key=\"breakpoint.id\"\n class=\"breakpoint-tab\"\n type=\"button\"\n :aria-pressed=\"activeBreakpoint === breakpoint.id\"\n :aria-label=\"`切换到${breakpointLabel(breakpoint.id)}画布`\"\n :title=\"`${breakpointLabel(breakpoint.id)}画布`\"\n @click=\"changeBreakpoint(breakpoint.id)\"\n >\n <component\n :is=\"\n breakpointIcons[\n breakpoint.id as keyof typeof breakpointIcons\n ] ?? Monitor\n \"\n :size=\"14\"\n :stroke-width=\"1.8\"\n aria-hidden=\"true\"\n />\n <span>{{ breakpointLabel(breakpoint.id) }}</span>\n </button>\n <button\n class=\"breakpoint-download-button\"\n v-if=\"false\"\n type=\"button\"\n aria-label=\"涓嬭浇褰撳墠浠g爜\"\n title=\"涓嬭浇褰撳墠浠g爜\"\n @click=\"downloadActiveCode\"\n >\n <Download :size=\"15\" :stroke-width=\"1.8\" aria-hidden=\"true\" />\n </button>\n </div>\n </div>\n <div class=\"actions\">\n <button\n class=\"icon-button\"\n type=\"button\"\n aria-label=\"撤销\"\n title=\"撤销\"\n :disabled=\"!editor?.canUndo()\"\n @click=\"undo\"\n >\n <Undo2 :size=\"16\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n <button\n class=\"icon-button\"\n type=\"button\"\n aria-label=\"重做\"\n title=\"重做\"\n :disabled=\"!editor?.canRedo()\"\n @click=\"redo\"\n >\n <Redo2 :size=\"16\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n <button\n class=\"icon-button\"\n type=\"button\"\n aria-label=\"删除选中组件\"\n title=\"删除选中组件\"\n :disabled=\"!selected\"\n @click=\"removeSelected\"\n >\n <Trash2 :size=\"16\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n <button\n class=\"icon-button\"\n type=\"button\"\n :aria-label=\"selectedLocked ? '解锁选中组件' : '锁定选中组件'\"\n :title=\"selectedLocked ? '解锁选中组件' : '锁定选中组件'\"\n :disabled=\"!selected\"\n @click=\"toggleSelectedLock\"\n >\n <component\n :is=\"selectedLocked ? Unlock : Lock\"\n :size=\"16\"\n :stroke-width=\"1.9\"\n aria-hidden=\"true\"\n />\n </button>\n <button\n class=\"icon-button\"\n type=\"button\"\n :aria-label=\"selectedMovable ? '禁止移动' : '允许移动'\"\n :title=\"selectedMovable ? '禁止移动' : '允许移动'\"\n :disabled=\"!selected || selectedLocked\"\n @click=\"toggleSelectedConstraint('movable')\"\n >\n <Move :size=\"16\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n <button\n class=\"icon-button\"\n type=\"button\"\n :aria-label=\"selectedResizable ? '禁止调整大小' : '允许调整大小'\"\n :title=\"selectedResizable ? '禁止调整大小' : '允许调整大小'\"\n :disabled=\"!selected || selectedLocked\"\n @click=\"toggleSelectedConstraint('resizable')\"\n >\n <Maximize2 :size=\"16\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n <button\n class=\"icon-button theme-button\"\n type=\"button\"\n :aria-label=\"theme === 'dark' ? '切换到亮色主题' : '切换到暗色主题'\"\n :title=\"theme === 'dark' ? '切换到亮色主题' : '切换到暗色主题'\"\n @click=\"toggleTheme\"\n >\n <component\n :is=\"theme === 'dark' ? Sun : Moon\"\n :size=\"16\"\n :stroke-width=\"1.9\"\n aria-hidden=\"true\"\n />\n </button>\n <button\n class=\"preview-open-button\"\n type=\"button\"\n aria-label=\"打开实时预览\"\n title=\"打开实时预览\"\n @click=\"openPreviewDialog\"\n >\n <Eye :size=\"15\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n <span>实时预览</span>\n </button>\n <button\n class=\"mode-button\"\n type=\"button\"\n :aria-pressed=\"previewMode\"\n :aria-label=\"previewMode ? '返回编辑模式' : '进入预览模式'\"\n :title=\"previewMode ? '返回编辑模式' : '进入预览模式'\"\n @click=\"togglePreview\"\n >\n <component\n :is=\"previewMode ? Pencil : Eye\"\n :size=\"16\"\n :stroke-width=\"1.9\"\n aria-hidden=\"true\"\n />\n <span>{{ previewMode ? '编辑' : '预览' }}</span>\n </button>\n </div>\n </header>\n <section class=\"workspace\">\n <aside\n class=\"materials panel-scroll\"\n data-layout-panel=\"materials\"\n aria-label=\"物料面板\"\n >\n <div class=\"panel-heading\">\n <div>\n <span class=\"eyebrow\">工作区</span>\n <h2>物料</h2>\n </div>\n <span class=\"panel-count\">{{ materials.getAll().length }}</span>\n </div>\n <div class=\"material-toolbar\">\n <label class=\"material-search\">\n <Search :size=\"15\" :stroke-width=\"1.8\" aria-hidden=\"true\" />\n <input\n v-model=\"materialQuery\"\n type=\"search\"\n placeholder=\"搜索物料\"\n aria-label=\"搜索物料\"\n />\n </label>\n <div\n class=\"material-filter-row\"\n data-layout-filter-menu\n @keydown.esc.stop.prevent=\"handleFilterMenuKeydown\"\n >\n <div\n class=\"material-select custom-select\"\n :class=\"{ 'is-open': categoryMenuOpen }\"\n @pointerdown.stop\n >\n <button\n class=\"custom-select__trigger\"\n type=\"button\"\n aria-haspopup=\"listbox\"\n :aria-expanded=\"categoryMenuOpen\"\n aria-label=\"物料分类\"\n :title=\"`物料分类:${activeMaterialCategoryLabel}`\"\n @click.stop=\"toggleFilterMenu('category')\"\n >\n <Filter :size=\"13\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n <span>{{ activeMaterialCategoryLabel }}</span>\n <ChevronDown\n :size=\"12\"\n :stroke-width=\"1.9\"\n aria-hidden=\"true\"\n />\n </button>\n <div\n v-if=\"categoryMenuOpen\"\n class=\"custom-select__menu\"\n role=\"listbox\"\n >\n <button\n class=\"custom-select__option\"\n type=\"button\"\n :aria-selected=\"!materialCategoryFilter\"\n @click=\"selectMaterialCategory('')\"\n >\n 全部分类\n </button>\n <button\n v-for=\"category in materialCategories\"\n :key=\"category.id\"\n class=\"custom-select__option\"\n type=\"button\"\n :aria-selected=\"materialCategoryFilter === category.id\"\n @click=\"selectMaterialCategory(category.id)\"\n >\n {{ category.label }}\n </button>\n </div>\n </div>\n <button\n v-if=\"hasMaterialFilters\"\n class=\"material-filter-reset\"\n type=\"button\"\n aria-label=\"清除物料筛选\"\n title=\"清除筛选\"\n @click=\"resetMaterialFilters\"\n >\n <X :size=\"13\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n </div>\n <div class=\"material-result-summary\">\n <span\n >{{ visibleMaterials.length }} /\n {{ materials.getAll().length }} 个物料</span\n >\n <span v-if=\"hasMaterialFilters\">已筛选</span>\n </div>\n </div>\n <div v-if=\"!visibleMaterials.length\" class=\"material-empty\">\n <Search :size=\"17\" :stroke-width=\"1.7\" aria-hidden=\"true\" />\n <strong>没有匹配的物料</strong>\n <span>试试更换关键词或分类</span>\n <button\n v-if=\"hasMaterialFilters\"\n type=\"button\"\n @click=\"resetMaterialFilters\"\n >\n 清除筛选\n </button>\n </div>\n <div\n v-for=\"material in visibleMaterials\"\n :key=\"material.type\"\n :ref=\"element => setMaterialElement(material.type, element)\"\n class=\"material-source grid-stack-item\"\n role=\"button\"\n tabindex=\"0\"\n :data-material-type=\"material.type\"\n :data-layout-material=\"material.type\"\n :data-layout-props=\"materialProps(material.type)\"\n :aria-label=\"`添加${material.title ?? material.label ?? material.type}`\"\n @click=\"handleMaterialClick(material.type)\"\n @keydown.enter=\"add(material.type)\"\n @keydown.space.prevent=\"add(material.type)\"\n >\n <span class=\"material-icon material-icon--metric\" aria-hidden=\"true\">\n <component\n :is=\"materialIcon(material.type)\"\n :size=\"18\"\n :stroke-width=\"1.8\"\n />\n </span>\n <span class=\"material-copy\">\n <strong>{{\n material.title ?? material.label ?? material.type\n }}</strong>\n <small>{{\n material.description ?? material.categoryLabel ?? '布局组件'\n }}</small>\n </span>\n <span class=\"material-card-meta\">\n <span class=\"material-size\"\n >{{ material.defaultLayout?.w ?? material.size?.default.w ?? 1 }}\n ×\n {{\n material.defaultLayout?.h ?? material.size?.default.h ?? 1\n }}</span\n >\n <span class=\"material-renderer-badges\">\n <span\n class=\"material-renderer-badge\"\n :class=\"{\n 'material-renderer-badge--vue':\n activeDevelopmentFramework === 'vue',\n }\"\n :title=\"`当前开发目标:${activeDevelopmentFrameworkLabel}`\"\n >\n <component\n :is=\"\n activeDevelopmentFramework === 'vue' ? PanelsTopLeft : Code2\n \"\n :size=\"11\"\n :stroke-width=\"1.8\"\n aria-hidden=\"true\"\n />\n <span>{{ activeDevelopmentFrameworkLabel }}</span>\n </span>\n </span>\n </span>\n <span class=\"material-card-actions\">\n <button\n class=\"material-code-button\"\n type=\"button\"\n aria-label=\"查看当前物料代码\"\n title=\"查看当前物料代码\"\n @pointerdown.stop\n @click.stop=\"openMaterialCode(material.type)\"\n >\n <Code2 :size=\"14\" :stroke-width=\"1.8\" aria-hidden=\"true\" />\n </button>\n <span class=\"material-add\" aria-hidden=\"true\">\n <Plus :size=\"16\" :stroke-width=\"1.8\" />\n </span>\n </span>\n </div>\n <div\n v-if=\"false\"\n data-legacy-source=\"metric\"\n class=\"material-source grid-stack-item\"\n role=\"button\"\n tabindex=\"0\"\n aria-label=\"添加数据指标\"\n @click=\"handleMaterialClick('metric')\"\n @keydown.enter=\"add('metric')\"\n @keydown.space.prevent=\"add('metric')\"\n >\n <span class=\"material-icon material-icon--metric\" aria-hidden=\"true\">\n <ChartNoAxesColumnIncreasing :size=\"18\" :stroke-width=\"1.8\" />\n </span>\n <span><strong>数据指标</strong><small>数值与摘要</small></span>\n <span class=\"material-add\" aria-hidden=\"true\">\n <Plus :size=\"16\" :stroke-width=\"1.8\" />\n </span>\n </div>\n <div\n v-if=\"false\"\n data-legacy-source=\"chart\"\n class=\"material-source grid-stack-item\"\n role=\"button\"\n tabindex=\"0\"\n aria-label=\"添加趋势图表\"\n @click=\"handleMaterialClick('chart')\"\n @keydown.enter=\"add('chart')\"\n @keydown.space.prevent=\"add('chart')\"\n >\n <span class=\"material-icon material-icon--chart\" aria-hidden=\"true\">\n <ChartSpline :size=\"18\" :stroke-width=\"1.8\" />\n </span>\n <span><strong>趋势图表</strong><small>变化与走向</small></span>\n <span class=\"material-add\" aria-hidden=\"true\">\n <Plus :size=\"16\" :stroke-width=\"1.8\" />\n </span>\n </div>\n </aside>\n <section\n class=\"canvas panel-scroll\"\n data-layout-panel=\"canvas\"\n aria-label=\"布局画布\"\n >\n <div class=\"canvas-toolbar\" aria-label=\"画布操作\">\n <div\n class=\"canvas-device-control custom-select\"\n :class=\"{ 'is-open': deviceMenuOpen }\"\n @pointerdown.stop\n >\n <button\n class=\"custom-select__trigger\"\n type=\"button\"\n aria-haspopup=\"listbox\"\n :aria-expanded=\"deviceMenuOpen\"\n aria-label=\"设备画布\"\n @click.stop=\"toggleFilterMenu('device')\"\n >\n <component\n :is=\"activeBreakpointIcon\"\n :size=\"13\"\n :stroke-width=\"1.9\"\n aria-hidden=\"true\"\n />\n <span>{{ breakpointLabel(activeBreakpoint) }}</span>\n <ChevronDown :size=\"12\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n <div\n v-if=\"deviceMenuOpen\"\n class=\"custom-select__menu\"\n role=\"listbox\"\n >\n <button\n v-for=\"breakpoint in layout.breakpoints\"\n :key=\"breakpoint.id\"\n class=\"custom-select__option\"\n type=\"button\"\n :aria-selected=\"activeBreakpoint === breakpoint.id\"\n :aria-label=\"\n breakpointLabel(breakpoint.id) +\n ' / ' +\n breakpoint.columns +\n ' 列'\n \"\n @click=\"selectBreakpoint(breakpoint.id)\"\n >\n <component\n :is=\"\n breakpointIcons[\n breakpoint.id as keyof typeof breakpointIcons\n ] ?? Monitor\n \"\n :size=\"13\"\n :stroke-width=\"1.8\"\n aria-hidden=\"true\"\n />\n <span>{{ breakpointLabel(breakpoint.id) }}</span>\n <span class=\"custom-select__option-meta\"\n >{{ breakpoint.columns }} 列</span\n >\n </button>\n </div>\n </div>\n <button\n class=\"orientation-button\"\n type=\"button\"\n :aria-label=\"\n canvasOrientation === 'portrait' ? '切换为横屏' : '切换为竖屏'\n \"\n :title=\"\n canvasOrientation === 'portrait' ? '切换为横屏' : '切换为竖屏'\n \"\n @click=\"toggleCanvasOrientation\"\n >\n <component\n :is=\"\n canvasOrientation === 'portrait' ? ArrowLeftRight : ArrowUpDown\n \"\n :size=\"14\"\n :stroke-width=\"1.9\"\n aria-hidden=\"true\"\n />\n </button>\n <span class=\"canvas-toolbar__device-divider\" aria-hidden=\"true\" />\n <div class=\"canvas-toolbar__group\">\n <button\n class=\"canvas-tool-button\"\n type=\"button\"\n :class=\"{ active: canvasTool === 'select' }\"\n aria-label=\"选择组件\"\n title=\"选择组件\"\n @click=\"canvasTool = 'select'\"\n >\n <MousePointer2\n :size=\"15\"\n :stroke-width=\"1.9\"\n aria-hidden=\"true\"\n />\n </button>\n <button\n class=\"canvas-tool-button\"\n type=\"button\"\n :class=\"{ active: canvasTool === 'pan' }\"\n aria-label=\"移动画布\"\n title=\"移动画布(也可按住空格拖动)\"\n @click=\"canvasTool = canvasTool === 'pan' ? 'select' : 'pan'\"\n >\n <Hand :size=\"15\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n </div>\n <div class=\"canvas-toolbar__divider\" aria-hidden=\"true\" />\n <button\n class=\"canvas-tool-button\"\n type=\"button\"\n aria-label=\"缩小画布\"\n title=\"缩小画布\"\n @click=\"zoomCanvas(-1)\"\n >\n <ZoomOut :size=\"15\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n <span class=\"canvas-zoom-label\" aria-live=\"polite\">{{\n canvasZoomLabel\n }}</span>\n <button\n class=\"canvas-tool-button\"\n type=\"button\"\n aria-label=\"放大画布\"\n title=\"放大画布\"\n @click=\"zoomCanvas(1)\"\n >\n <ZoomIn :size=\"15\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n <button\n class=\"canvas-tool-button\"\n type=\"button\"\n aria-label=\"适配画布\"\n title=\"适配画布\"\n @click=\"resetCanvasView\"\n >\n <Scan :size=\"15\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n <button\n class=\"canvas-tool-button\"\n type=\"button\"\n aria-label=\"重置画布位置\"\n title=\"重置画布位置\"\n @click=\"resetCanvasPan\"\n >\n <RotateCcw :size=\"15\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n </div>\n <div\n ref=\"canvasViewport\"\n class=\"canvas-viewport\"\n :class=\"{\n 'is-panning': canvasPanning,\n 'is-pan-tool': canvasTool === 'pan',\n }\"\n @wheel=\"handleCanvasWheel\"\n @pointerdown.capture=\"handleCanvasPointerDown\"\n @pointermove=\"handleCanvasPointerMove\"\n @pointerup=\"finishCanvasPan\"\n @pointercancel=\"finishCanvasPan\"\n >\n <div\n class=\"canvas-scene\"\n :class=\"`is-${canvasOrientation}`\"\n :data-canvas-width=\"canvasSize.width\"\n :data-canvas-height=\"canvasSize.height\"\n :data-canvas-orientation=\"canvasOrientation\"\n :style=\"canvasSceneStyle\"\n >\n <section\n class=\"editor-stage\"\n aria-label=\"可视化编辑画布\"\n v-show=\"activeRoute === 'editor' && !previewMode\"\n >\n <header class=\"stage-header\">\n <div>\n <span class=\"stage-kicker\">编辑模式</span>\n <h2>拖拽布局</h2>\n </div>\n <span class=\"stage-badge\"\n ><i aria-hidden=\"true\" />{{\n breakpointLabel(activeBreakpoint)\n }}</span\n >\n </header>\n <div\n ref=\"editorGrid\"\n class=\"grid-stack layout-editor-grid\"\n role=\"grid\"\n aria-label=\"网格布局编辑器\"\n aria-multiselectable=\"true\"\n />\n </section>\n <section\n class=\"preview-stage\"\n aria-label=\"组件预览画布\"\n v-show=\"activeRoute === 'vue'\"\n >\n <header class=\"stage-header\">\n <div>\n <span class=\"stage-kicker\">预览模式</span>\n <h2>组件渲染</h2>\n </div>\n <span class=\"stage-badge\"\n ><i aria-hidden=\"true\" />同步布局</span\n >\n </header>\n <LGrid\n :document=\"layout\"\n :breakpoint=\"activeBreakpoint\"\n :materials=\"materials\"\n :target=\"activeDevelopmentFramework\"\n >\n <template #item=\"{ item }\">\n <button\n class=\"widget layout-item-shell layout-item-shell--interactive\"\n type=\"button\"\n :aria-label=\"`${itemTitle(item)},${materialLabel(item.type)}`\"\n :class=\"{ selected: selected === item.id }\"\n @click=\"select(item.id)\"\n >\n <LMaterialRenderer\n :item=\"item\"\n :materials=\"materials\"\n :target=\"activeDevelopmentFramework\"\n v-bind=\"item.props\"\n />\n </button>\n </template>\n </LGrid>\n </section>\n <section\n class=\"runtime-preview-stage\"\n :data-preview-framework=\"activeDevelopmentFramework\"\n aria-label=\"运行时预览画布\"\n v-show=\"activeRoute === 'runtime' && previewMode\"\n >\n <header class=\"stage-header\">\n <div>\n <span class=\"stage-kicker\">预览模式</span>\n <h2>运行时预览</h2>\n </div>\n <span class=\"stage-badge\"\n ><i aria-hidden=\"true\" />{{\n activeDevelopmentFrameworkLabel\n }}\n · 只读</span\n >\n </header>\n <LGrid\n v-if=\"activeDevelopmentFramework === 'vue'\"\n :document=\"layout\"\n :breakpoint=\"activeBreakpoint\"\n :materials=\"materials\"\n :target=\"activeDevelopmentFramework\"\n >\n <template #item=\"{ item }\">\n <LMaterialRenderer\n :item=\"item\"\n :materials=\"materials\"\n :target=\"activeDevelopmentFramework\"\n class=\"preview-dialog__card\"\n v-bind=\"item.props\"\n />\n </template>\n </LGrid>\n <div\n ref=\"runtimePreview\"\n class=\"runtime-preview-grid\"\n v-show=\"activeDevelopmentFramework === 'webcomponent'\"\n data-preview-renderer=\"webcomponent\"\n />\n </section>\n <section\n class=\"vanilla-runtime-stage\"\n :data-preview-framework=\"activeDevelopmentFramework\"\n aria-label=\"原生运行时画布\"\n v-show=\"activeRoute === 'vanilla'\"\n >\n <header class=\"stage-header\">\n <div>\n <span class=\"stage-kicker\">预览模式</span>\n <h2>原生运行时</h2>\n </div>\n <span class=\"stage-badge\"\n ><i aria-hidden=\"true\" />{{\n activeDevelopmentFrameworkLabel\n }}\n · 只读</span\n >\n </header>\n <LGrid\n v-if=\"activeDevelopmentFramework === 'vue'\"\n :document=\"layout\"\n :breakpoint=\"activeBreakpoint\"\n :materials=\"materials\"\n :target=\"activeDevelopmentFramework\"\n >\n <template #item=\"{ item }\">\n <LMaterialRenderer\n :item=\"item\"\n :materials=\"materials\"\n :target=\"activeDevelopmentFramework\"\n class=\"preview-dialog__card\"\n v-bind=\"item.props\"\n />\n </template>\n </LGrid>\n <div\n ref=\"vanillaRuntime\"\n class=\"runtime-preview-grid\"\n v-show=\"activeDevelopmentFramework === 'webcomponent'\"\n data-preview-renderer=\"webcomponent\"\n />\n </section>\n </div>\n </div>\n </section>\n <aside\n class=\"inspectors panel-scroll\"\n data-layout-panel=\"inspector\"\n aria-label=\"代码检查面板\"\n >\n <div class=\"inspector-heading\">\n <div>\n <span class=\"eyebrow\">实时输出</span>\n <h2>{{ activeCodeLabel }}</h2>\n </div>\n <button\n class=\"copy-code-button\"\n type=\"button\"\n :aria-label=\"copied ? '代码已复制' : '复制当前代码'\"\n :title=\"copied ? '代码已复制' : '复制当前代码'\"\n @click=\"copyActiveCode\"\n >\n <component\n :is=\"copied ? CopyCheck : Copy\"\n :size=\"15\"\n :stroke-width=\"1.8\"\n aria-hidden=\"true\"\n />\n </button>\n <button\n class=\"download-code-button\"\n type=\"button\"\n aria-label=\"下载当前代码\"\n title=\"下载当前代码\"\n @click=\"downloadActiveCode\"\n >\n <Download :size=\"15\" :stroke-width=\"1.8\" aria-hidden=\"true\" />\n </button>\n </div>\n <pre\n class=\"code-block\"\n data-code-output\n ><code v-html=\"highlightedCode\" /></pre>\n <div class=\"code-status\">\n <span class=\"code-status__dot\" aria-hidden=\"true\" />\n <span\n >已根据当前布局、{{ breakpointLabel(activeBreakpoint) }}断点实时生成\n · 共 {{ activeCodeLineCount }} 行,可滚动查看完整输出</span\n >\n </div>\n </aside>\n </section>\n <Transition name=\"preview-dialog\">\n <div\n v-if=\"previewDialogOpen\"\n class=\"preview-dialog\"\n :data-preview-framework=\"activeDevelopmentFramework\"\n :data-theme=\"theme\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"layout-preview-title\"\n >\n <button\n class=\"preview-dialog__backdrop\"\n type=\"button\"\n aria-label=\"关闭实时预览\"\n @click=\"closePreviewDialog\"\n />\n <section class=\"preview-dialog__panel\">\n <header class=\"preview-dialog__header\">\n <div>\n <span class=\"stage-kicker\">真实渲染</span>\n <h2 id=\"layout-preview-title\">实时预览</h2>\n </div>\n <div class=\"preview-dialog__header-meta\">\n <span class=\"stage-badge\"\n ><i aria-hidden=\"true\" />{{\n breakpointLabel(activeBreakpoint)\n }}</span\n >\n <button\n class=\"icon-button\"\n type=\"button\"\n aria-label=\"关闭实时预览\"\n title=\"关闭实时预览\"\n @click=\"closePreviewDialog\"\n >\n <X :size=\"16\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n </div>\n </header>\n <div class=\"preview-dialog__body\">\n <div class=\"preview-dialog__canvas\">\n <LGrid\n v-if=\"activeDevelopmentFramework === 'vue'\"\n :document=\"layout\"\n :breakpoint=\"activeBreakpoint\"\n :materials=\"materials\"\n :target=\"activeDevelopmentFramework\"\n >\n <template #item=\"{ item }\">\n <LMaterialRenderer\n :item=\"item\"\n :materials=\"materials\"\n :target=\"activeDevelopmentFramework\"\n class=\"preview-dialog__card\"\n v-bind=\"item.props\"\n />\n </template>\n </LGrid>\n <div\n v-else\n ref=\"previewDialogRuntime\"\n class=\"runtime-preview-grid preview-dialog__runtime-grid\"\n data-preview-renderer=\"webcomponent\"\n />\n </div>\n </div>\n <footer class=\"preview-dialog__footer\">\n <span>此处展示当前布局的真实 CSS Grid 渲染结果</span>\n <button\n class=\"preview-dialog__close-button\"\n type=\"button\"\n @click=\"closePreviewDialog\"\n >\n 关闭预览\n </button>\n </footer>\n </section>\n </div>\n </Transition>\n <Transition name=\"preview-dialog\">\n <div\n v-if=\"materialCodeDialog\"\n class=\"preview-dialog material-code-dialog\"\n data-material-code-dialog=\"true\"\n :data-material-code-framework=\"activeDevelopmentFramework\"\n :data-material-code-type=\"materialCodeDialog.type\"\n :data-theme=\"theme\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"material-code-title\"\n >\n <button\n class=\"preview-dialog__backdrop\"\n type=\"button\"\n aria-label=\"关闭物料代码\"\n @click=\"closeMaterialCode\"\n />\n <section class=\"preview-dialog__panel material-code-dialog__panel\">\n <header class=\"preview-dialog__header\">\n <div>\n <span class=\"stage-kicker\">注册表物料</span>\n <h2 id=\"material-code-title\">\n {{\n materialCodeDialog.title ??\n materialCodeDialog.label ??\n materialCodeDialog.type\n }}\n </h2>\n <p class=\"material-code-dialog__subtitle\">\n {{\n activeDevelopmentFramework === 'vue'\n ? 'Vue 3 真实组件用法'\n : '原生 Web Component 标签用法'\n }}\n </p>\n </div>\n <div class=\"preview-dialog__header-meta\">\n <span class=\"stage-badge\"\n ><i aria-hidden=\"true\" />{{\n activeDevelopmentFrameworkLabel\n }}</span\n >\n <button\n class=\"icon-button\"\n type=\"button\"\n aria-label=\"关闭物料代码\"\n title=\"关闭物料代码\"\n @click=\"closeMaterialCode\"\n >\n <X :size=\"16\" :stroke-width=\"1.9\" aria-hidden=\"true\" />\n </button>\n </div>\n </header>\n <div class=\"preview-dialog__body material-code-dialog__body\">\n <pre\n class=\"code-block material-code-dialog__code\"\n ><code v-html=\"highlightedMaterialCode\" /></pre>\n </div>\n <footer class=\"preview-dialog__footer\">\n <span\n >来自当前注册表的\n {{\n activeDevelopmentFramework === 'vue'\n ? '组件导出'\n : '自定义元素标签'\n }},共 {{ materialCodeLineCount }} 行</span\n >\n <div class=\"material-code-dialog__actions\">\n <button\n class=\"copy-code-button\"\n type=\"button\"\n :aria-label=\"materialCodeCopied ? '代码已复制' : '复制物料代码'\"\n :title=\"materialCodeCopied ? '代码已复制' : '复制物料代码'\"\n @click=\"copyMaterialCode\"\n >\n <component\n :is=\"materialCodeCopied ? CopyCheck : Copy\"\n :size=\"15\"\n :stroke-width=\"1.8\"\n aria-hidden=\"true\"\n />\n </button>\n <button\n class=\"preview-dialog__close-button\"\n type=\"button\"\n @click=\"closeMaterialCode\"\n >\n 关闭\n </button>\n </div>\n </footer>\n </section>\n </div>\n </Transition>\n </main>\n</template>\n"],"mappings":""}
|