mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-09-20 05:29:58 +08:00
feat(editor): 使用 floatbox 替换原抽屉栏
This commit is contained in:
parent
260286f9cf
commit
a035f02f83
@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<component
|
<MFormBox
|
||||||
:is="slideType === 'box' ? MFormBox : MFormDrawer"
|
|
||||||
class="m-editor-code-block-editor"
|
class="m-editor-code-block-editor"
|
||||||
ref="fomDrawer"
|
ref="fomDrawer"
|
||||||
label-width="80px"
|
label-width="80px"
|
||||||
@ -20,7 +19,7 @@
|
|||||||
<template #left>
|
<template #left>
|
||||||
<TMagicButton type="primary" link @click="difVisible = true">查看修改</TMagicButton>
|
<TMagicButton type="primary" link @click="difVisible = true">查看修改</TMagicButton>
|
||||||
</template>
|
</template>
|
||||||
</component>
|
</MFormBox>
|
||||||
|
|
||||||
<TMagicDialog v-model="difVisible" title="查看修改" fullscreen>
|
<TMagicDialog v-model="difVisible" title="查看修改" fullscreen>
|
||||||
<div style="display: flex; margin-bottom: 10px">
|
<div style="display: flex; margin-bottom: 10px">
|
||||||
@ -55,7 +54,7 @@ import { ColumnConfig, FormConfig, FormState, MFormBox, MFormDrawer } from '@tma
|
|||||||
import type { CodeBlockContent } from '@tmagic/schema';
|
import type { CodeBlockContent } from '@tmagic/schema';
|
||||||
|
|
||||||
import CodeEditor from '@editor/layouts/CodeEditor.vue';
|
import CodeEditor from '@editor/layouts/CodeEditor.vue';
|
||||||
import type { Services, SlideType } from '@editor/type';
|
import type { Services } from '@editor/type';
|
||||||
import { getConfig } from '@editor/utils/config';
|
import { getConfig } from '@editor/utils/config';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@ -67,7 +66,6 @@ const props = defineProps<{
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isDataSource?: boolean;
|
isDataSource?: boolean;
|
||||||
dataSourceType?: string;
|
dataSourceType?: string;
|
||||||
slideType?: SlideType;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body" v-if="visible">
|
<Teleport to="body" v-if="visible">
|
||||||
<div ref="target" class="m-editor-float-box" :style="style">
|
<div ref="target" class="m-editor-float-box" :style="style" @mousedown="nextZIndex">
|
||||||
<div ref="dragTarget" class="m-editor-float-box-title">
|
<div ref="dragTarget" class="m-editor-float-box-title">
|
||||||
<slot name="title">
|
<slot name="title">
|
||||||
<span>{{ title }}</span>
|
<span>{{ title }}</span>
|
||||||
@ -21,7 +21,7 @@ import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
|||||||
import { Close } from '@element-plus/icons-vue';
|
import { Close } from '@element-plus/icons-vue';
|
||||||
import VanillaMoveable from 'moveable';
|
import VanillaMoveable from 'moveable';
|
||||||
|
|
||||||
import { TMagicButton } from '@tmagic/design';
|
import { TMagicButton, useZIndex } from '@tmagic/design';
|
||||||
|
|
||||||
interface Position {
|
interface Position {
|
||||||
left: number;
|
left: number;
|
||||||
@ -47,11 +47,15 @@ const emit = defineEmits<{
|
|||||||
const target = ref<HTMLDivElement>();
|
const target = ref<HTMLDivElement>();
|
||||||
const dragTarget = ref<HTMLDivElement>();
|
const dragTarget = ref<HTMLDivElement>();
|
||||||
|
|
||||||
|
const zIndex = useZIndex();
|
||||||
|
const curZIndex = ref<number>(zIndex.nextZIndex());
|
||||||
|
|
||||||
const style = computed(() => ({
|
const style = computed(() => ({
|
||||||
left: `${props.position.left}px`,
|
left: `${props.position.left}px`,
|
||||||
top: `${props.position.top}px`,
|
top: `${props.position.top}px`,
|
||||||
width: typeof props.rect.width === 'string' ? props.rect.width : `${props.rect.width}px`,
|
width: typeof props.rect.width === 'string' ? props.rect.width : `${props.rect.width}px`,
|
||||||
height: typeof props.rect.height === 'string' ? props.rect.height : `${props.rect.height}px`,
|
height: typeof props.rect.height === 'string' ? props.rect.height : `${props.rect.height}px`,
|
||||||
|
zIndex: curZIndex.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let moveable: VanillaMoveable | null = null;
|
let moveable: VanillaMoveable | null = null;
|
||||||
@ -70,6 +74,7 @@ const initMoveable = () => {
|
|||||||
dragTargetSelf: false,
|
dragTargetSelf: false,
|
||||||
linePadding: 10,
|
linePadding: 10,
|
||||||
controlPadding: 10,
|
controlPadding: 10,
|
||||||
|
bounds: { left: 0, top: 0, right: 0, bottom: 0, position: 'css' },
|
||||||
});
|
});
|
||||||
|
|
||||||
moveable.on('drag', (e) => {
|
moveable.on('drag', (e) => {
|
||||||
@ -111,6 +116,10 @@ const closeHandler = () => {
|
|||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const nextZIndex = () => {
|
||||||
|
curZIndex.value = zIndex.nextZIndex();
|
||||||
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
target,
|
target,
|
||||||
});
|
});
|
||||||
|
@ -54,7 +54,6 @@ export const useCodeBlockEdit = (codeBlockService?: CodeBlockService) => {
|
|||||||
codeId.value = id;
|
codeId.value = id;
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
codeBlockEditor.value?.show();
|
codeBlockEditor.value?.show();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="m-editor-nav-menu" :style="{ height: `${height}px` }">
|
<div class="m-editor-nav-menu" :style="{ height: `${height}px` }" ref="navMenu">
|
||||||
<div v-for="key in keys" :class="`menu-${key}`" :key="key" :style="`width: ${columnWidth?.[key]}px`">
|
<div v-for="key in keys" :class="`menu-${key}`" :key="key" :style="`width: ${columnWidth?.[key]}px`">
|
||||||
<ToolButton :data="item" v-for="(item, index) in buttons[key]" :key="index"></ToolButton>
|
<ToolButton :data="item" v-for="(item, index) in buttons[key]" :key="index"></ToolButton>
|
||||||
</div>
|
</div>
|
||||||
@ -7,7 +7,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, markRaw } from 'vue';
|
import { computed, inject, markRaw, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import { Back, Delete, FullScreen, Grid, Memo, Right, ScaleToOriginal, ZoomIn, ZoomOut } from '@element-plus/icons-vue';
|
import { Back, Delete, FullScreen, Grid, Memo, Right, ScaleToOriginal, ZoomIn, ZoomOut } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
import { NodeType } from '@tmagic/schema';
|
import { NodeType } from '@tmagic/schema';
|
||||||
@ -178,4 +178,23 @@ const buttons = computed(() => {
|
|||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
const rect = navMenu.value?.getBoundingClientRect();
|
||||||
|
if (rect) {
|
||||||
|
uiService?.set('navMenuRect', {
|
||||||
|
left: rect.left,
|
||||||
|
top: rect.top,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const navMenu = ref<HTMLDivElement>();
|
||||||
|
onMounted(() => {
|
||||||
|
navMenu.value && resizeObserver.observe(navMenu.value);
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -204,6 +204,11 @@ const getItemConfig = (data: SideItem): SideComponent => {
|
|||||||
text: '代码编辑',
|
text: '代码编辑',
|
||||||
component: CodeBlockListPanel,
|
component: CodeBlockListPanel,
|
||||||
slots: {},
|
slots: {},
|
||||||
|
boxComponentConfig: {
|
||||||
|
props: {
|
||||||
|
slideType: 'box',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'data-source': {
|
'data-source': {
|
||||||
$key: 'data-source',
|
$key: 'data-source',
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<slot name="code-block-panel-header">
|
<slot name="code-block-panel-header">
|
||||||
<div class="search-wrapper">
|
<div class="search-wrapper">
|
||||||
<SearchInput @search="filterTextChangeHandler"></SearchInput>
|
<SearchInput @search="filterTextChangeHandler"></SearchInput>
|
||||||
<TMagicButton v-if="editable" class="create-code-button" type="primary" size="small" @click="createCodeBlock"
|
<TMagicButton v-if="editable" class="create-code-button" type="primary" size="small" @click="showCreate"
|
||||||
>新增</TMagicButton
|
>新增</TMagicButton
|
||||||
>
|
>
|
||||||
<slot name="code-block-panel-search"></slot>
|
<slot name="code-block-panel-search"></slot>
|
||||||
@ -11,7 +11,7 @@
|
|||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<!-- 代码块列表 -->
|
<!-- 代码块列表 -->
|
||||||
<CodeBlockList ref="codeBlockList" :custom-error="customError" @edit="editCode" @remove="deleteCode">
|
<CodeBlockList ref="codeBlockList" :custom-error="customError" @edit="showEdit" @remove="deleteCode">
|
||||||
<template #code-block-panel-tool="{ id, data }">
|
<template #code-block-panel-tool="{ id, data }">
|
||||||
<slot name="code-block-panel-tool" :id="id" :data="data"></slot>
|
<slot name="code-block-panel-tool" :id="id" :data="data"></slot>
|
||||||
</template>
|
</template>
|
||||||
@ -19,23 +19,31 @@
|
|||||||
</TMagicScrollbar>
|
</TMagicScrollbar>
|
||||||
|
|
||||||
<!-- 代码块编辑区 -->
|
<!-- 代码块编辑区 -->
|
||||||
<CodeBlockEditor
|
<FloatingBox v-model:visible="popVisible" title="代码编辑" :position="boxPosition">
|
||||||
v-if="codeConfig"
|
<template #body>
|
||||||
ref="codeBlockEditor"
|
<div ref="scrollBar"></div>
|
||||||
:disabled="!editable"
|
</template>
|
||||||
:content="codeConfig"
|
</FloatingBox>
|
||||||
:slideType="slideType"
|
|
||||||
@submit="submitCodeBlockHandler"
|
<Teleport :to="scrollBar" :disabled="slideType === 'box'" v-if="editVisible">
|
||||||
></CodeBlockEditor>
|
<CodeBlockEditor
|
||||||
|
v-if="codeConfig"
|
||||||
|
ref="codeBlockEditor"
|
||||||
|
:disabled="!editable"
|
||||||
|
:content="codeConfig"
|
||||||
|
@submit="submitCodeBlockHandler"
|
||||||
|
></CodeBlockEditor>
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, inject, ref } from 'vue';
|
import { computed, inject, nextTick, ref } from 'vue';
|
||||||
|
|
||||||
import { TMagicButton, TMagicScrollbar } from '@tmagic/design';
|
import { TMagicButton, TMagicScrollbar } from '@tmagic/design';
|
||||||
import type { Id } from '@tmagic/schema';
|
import type { Id } from '@tmagic/schema';
|
||||||
|
|
||||||
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
|
import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
|
||||||
|
import FloatingBox from '@editor/components/FloatingBox.vue';
|
||||||
import SearchInput from '@editor/components/SearchInput.vue';
|
import SearchInput from '@editor/components/SearchInput.vue';
|
||||||
import { useCodeBlockEdit } from '@editor/hooks/use-code-block-edit';
|
import { useCodeBlockEdit } from '@editor/hooks/use-code-block-edit';
|
||||||
import type { CodeBlockListPanelSlots, CodeDeleteErrorType, Services, SlideType } from '@editor/type';
|
import type { CodeBlockListPanelSlots, CodeDeleteErrorType, Services, SlideType } from '@editor/type';
|
||||||
@ -48,12 +56,12 @@ defineOptions({
|
|||||||
name: 'MEditorCodeBlockListPanel',
|
name: 'MEditorCodeBlockListPanel',
|
||||||
});
|
});
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
customError?: (id: Id, errorType: CodeDeleteErrorType) => any;
|
customError?: (id: Id, errorType: CodeDeleteErrorType) => any;
|
||||||
slideType?: SlideType;
|
slideType?: SlideType;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { codeBlockService } = inject<Services>('services') || {};
|
const { codeBlockService, uiService } = inject<Services>('services') || {};
|
||||||
|
|
||||||
const editable = computed(() => codeBlockService?.getEditStatus());
|
const editable = computed(() => codeBlockService?.getEditStatus());
|
||||||
|
|
||||||
@ -65,4 +73,35 @@ const codeBlockList = ref<InstanceType<typeof CodeBlockList>>();
|
|||||||
const filterTextChangeHandler = (val: string) => {
|
const filterTextChangeHandler = (val: string) => {
|
||||||
codeBlockList.value?.filter(val);
|
codeBlockList.value?.filter(val);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const boxPosition = computed(() => {
|
||||||
|
const columnWidth = uiService?.get('columnWidth');
|
||||||
|
const navMenuRect = uiService?.get('navMenuRect');
|
||||||
|
return {
|
||||||
|
left: columnWidth?.left ?? 0,
|
||||||
|
top: (navMenuRect?.top ?? 0) + (navMenuRect?.height ?? 0),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const scrollBar = ref<HTMLDivElement>();
|
||||||
|
const popVisible = ref<boolean>(false);
|
||||||
|
const editVisible = ref<boolean>(false);
|
||||||
|
|
||||||
|
const beforeShowEdit = async () => {
|
||||||
|
if (props.slideType !== 'box') {
|
||||||
|
popVisible.value = true;
|
||||||
|
}
|
||||||
|
await nextTick();
|
||||||
|
editVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showEdit = async (id: string) => {
|
||||||
|
await beforeShowEdit();
|
||||||
|
editCode(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showCreate = async () => {
|
||||||
|
await beforeShowEdit();
|
||||||
|
createCodeBlock();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -47,8 +47,13 @@ const state = reactive<UiState>({
|
|||||||
showRule: true,
|
showRule: true,
|
||||||
propsPanelSize: 'small',
|
propsPanelSize: 'small',
|
||||||
showAddPageButton: true,
|
showAddPageButton: true,
|
||||||
floatBox: new Map(),
|
|
||||||
hideSlideBar: false,
|
hideSlideBar: false,
|
||||||
|
navMenuRect: {
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const canUsePluginMethods = {
|
const canUsePluginMethods = {
|
||||||
@ -110,19 +115,6 @@ class Ui extends BaseService {
|
|||||||
return Math.min((width - 60) / stageWidth || 1, (height - 80) / stageHeight || 1);
|
return Math.min((width - 60) / stageWidth || 1, (height - 80) / stageHeight || 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setFloatBox(keys: string[]) {
|
|
||||||
const map = state.floatBox;
|
|
||||||
for (const key of keys) {
|
|
||||||
if (map.get(key)) continue;
|
|
||||||
map.set(key, {
|
|
||||||
status: false,
|
|
||||||
zIndex: 99,
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetState() {
|
public resetState() {
|
||||||
this.set('showSrc', false);
|
this.set('showSrc', false);
|
||||||
this.set('uiSelectMode', false);
|
this.set('uiSelectMode', false);
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
.m-container-vs-code {
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.magic-code-editor {
|
.magic-code-editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -72,12 +72,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.m-editor-slide-list-box {
|
.m-editor-slide-list-box {
|
||||||
min-width: 270px;
|
display: flex;
|
||||||
|
min-width: 240px;
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
max-height: 1024px;
|
|
||||||
> div {
|
> div {
|
||||||
&:first-child {
|
&:first-child {
|
||||||
width: 100%;
|
min-width: 240px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.m-form-box {
|
||||||
|
border-left: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,20 +236,16 @@ export interface UiState {
|
|||||||
propsPanelSize: 'large' | 'default' | 'small';
|
propsPanelSize: 'large' | 'default' | 'small';
|
||||||
/** 是否显示新增页面按钮 */
|
/** 是否显示新增页面按钮 */
|
||||||
showAddPageButton: boolean;
|
showAddPageButton: boolean;
|
||||||
|
|
||||||
/** slide 拖拽悬浮窗 state */
|
|
||||||
floatBox: Map<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
status: boolean;
|
|
||||||
zIndex: number;
|
|
||||||
top: number;
|
|
||||||
left: number;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
/** 是否隐藏侧边栏 */
|
/** 是否隐藏侧边栏 */
|
||||||
hideSlideBar: boolean;
|
hideSlideBar: boolean;
|
||||||
|
|
||||||
|
// navMenu 的宽高
|
||||||
|
navMenuRect: {
|
||||||
|
left: number;
|
||||||
|
top: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorNodeInfo {
|
export interface EditorNodeInfo {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
width: 100%;
|
max-width: 90%;
|
||||||
.el-box__header {
|
.el-box__header {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React, { constructor, useEffect, useMemo, useState } from 'react';
|
import React, { constructor, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import type { MComponent, MContainer, MNode, MPage, MPageFragment } from '@tmagic/schema';
|
import type { MComponent, MContainer, MNode, MPage, MPageFragment } from '@tmagic/schema';
|
||||||
@ -16,24 +15,24 @@ const PageFragmentContainer: React.FC<PageFragmentContainerProps> = ({ config })
|
|||||||
|
|
||||||
if (!app) return null;
|
if (!app) return null;
|
||||||
const MagicUiContainer = app.resolveComponent('container');
|
const MagicUiContainer = app.resolveComponent('container');
|
||||||
let containerConfig = {}
|
let containerConfig = {};
|
||||||
const fragment = app?.dsl?.items?.find((page) => page.id === config.pageFragmentId)
|
const fragment = app?.dsl?.items?.find((page) => page.id === config.pageFragmentId);
|
||||||
if(fragment) {
|
if (fragment) {
|
||||||
const { id, type, items, ...others } = fragment;
|
const { id, type, items, ...others } = fragment;
|
||||||
const itemsWithoutId = items.map((item: MNode) => {
|
const itemsWithoutId = items.map((item: MNode) => {
|
||||||
const { id, ...otherConfig } = item;
|
const { id, ...otherConfig } = item;
|
||||||
return otherConfig;
|
return otherConfig;
|
||||||
});
|
});
|
||||||
if (app?.platform === 'editor') {
|
if (app?.platform === 'editor') {
|
||||||
containerConfig ={
|
containerConfig = {
|
||||||
...others,
|
...others,
|
||||||
items: itemsWithoutId,
|
items: itemsWithoutId,
|
||||||
};
|
};
|
||||||
}else {
|
} else {
|
||||||
containerConfig = {
|
containerConfig = {
|
||||||
...others,
|
...others,
|
||||||
items
|
items,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,13 +42,11 @@ const PageFragmentContainer: React.FC<PageFragmentContainerProps> = ({ config })
|
|||||||
className="magic-ui-page-fragment-container"
|
className="magic-ui-page-fragment-container"
|
||||||
style={app.transformStyle(config.style || {})}
|
style={app.transformStyle(config.style || {})}
|
||||||
>
|
>
|
||||||
<MagicUiContainer
|
<MagicUiContainer config={containerConfig}></MagicUiContainer>
|
||||||
config={containerConfig}
|
|
||||||
></MagicUiContainer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
PageFragmentContainer.displayName = 'magic-ui-page-fragment-container';
|
PageFragmentContainer.displayName = 'magic-ui-page-fragment-container';
|
||||||
|
|
||||||
export default PageFragmentContainer;
|
export default PageFragmentContainer;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import type { MComponent, MContainer, MPageFragment } from '@tmagic/schema';
|
import type { MComponent, MContainer, MPageFragment } from '@tmagic/schema';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user