@hy-bricks/editor
v0.6.6
Published
HyperCard 嵌入式编辑器 — Monaco 三栏 + 拖 tab 分栏 + 实时预览 + shadcn-vue 风格 UI
Maintainers
Readme
@hy-bricks/editor
嵌入式低代码组件编辑器:Monaco 三栏(html/js/css)+ 拖 tab 多分栏 + 实时预览 + 草稿持久化 + Monaco 类型注入。
📦 当前发布版 0.6.3 ── 与
@hy-bricks/core同步(fixed 组同版本号)。逐版变更(含历史 BREAKING)以 CHANGELOG 为准;注意 0.6.0 跟随 core 收口公开面(移除 test-only / 内部导出)。
┌─────────────────────────────────────────────────────────────────────┐
│ demo-component v3 · 已发布 ☐ 自动预览 ⚙ 保存草稿 推版本 │
├──────────────────────────────────┬──────────────────────────────────┤
│ html │ javascript │ + css │ 预览 ⧉ │
│ ─────────────────────────────── │ ─────────────────────────────── │
│ <div class="root"> │ │
│ <h2>{{ greeting }}</h2> │ Hello World ! │
│ <el-input v-model="msg" /> │ ┌───────────────┐ │
│ <el-button @click="add"> │ │ 试试输入 │ │
│ 点我 +1 │ └───────────────┘ │
│ </el-button> │ 你输入了: │
│ </div> │ │
│ │ [ 点我 +1 ] 点了 0 次 │
└──────────────────────────────────┴──────────────────────────────────┘
(拖 tab 头 → 多分栏)<HyperCardEditor> 是一个受控组件:你给一份 {html, js, css} 源码,它给你一个 IDE 风界面 + 一份能跑的实时预览。所有副作用(保存、推版本、开窗口、设置面板)都通过事件抛回宿主,SDK 不调任何 API、不依赖 vue-router、不强制宿主目录结构。
Quickstart(5 分钟,从空白 Vite + Vue 3 项目接进来)
1. 安装 SDK + 全部 peerDeps
reka-ui / cva / clsx / tailwind-merge / lucide-vue-next 在 SDK 构建时被 vite external,必须由宿主装(详见下面的 peerDependencies)。
pnpm add @hy-bricks/editor @hy-bricks/core \
vue monaco-editor \
reka-ui class-variance-authority clsx tailwind-merge lucide-vue-next \
tailwindcss tailwindcss-animate autoprefixer postcss2. tailwind.config.js —— 不要用 SDK 的 preset
包内
.vue用了大量 Tailwind utility,但 Tailwind v3 默认 ignorenode_modules,即便content写上node_modules/@hy-bricks/**也大概率扫不全(symlink、pnpm hoist 路径变化)。正确做法:本包发布时已经 self-compile 出
dist/style.css,把全部需要的 utility 都打进去了。宿主只要 import 这一个 css 就行,不需要让自己的 Tailwind 扫包内源码。
/** tailwind.config.js — 宿主只配自己的内容,不扫 SDK */
export default {
content: [
'./index.html',
'./src/**/*.{vue,ts,tsx,js,jsx}',
],
theme: { extend: {} },
plugins: [],
}3. src/style.css
@tailwind base;
@tailwind components;
@tailwind utilities;⚠ 不要在这个 css 里
@import '@hy-bricks/editor/style.css'。PostCSS 的@import走它自己的解析器,不识别 npm scope alias(@hy-bricks/...),通常会 silent 失败: 编译没报错,主题变量却丢失。唯一推荐:在main.ts里用 JSimport(下一步), 走 bundler 的 ESM 解析,稳定可靠。
4. src/main.ts
import { createApp } from 'vue'
import { createHyperCard } from '@hy-bricks/core'
// 通过 vite ESM resolve 直接拿包内 self-compiled 样式 — 这是引入主题 CSS 的
// **唯一推荐方式**(不要在 style.css 里 @import,见上一步说明)
import '@hy-bricks/editor/style.css'
import App from './App.vue'
import './style.css'
const app = createApp(App)
app.use(
createHyperCard({
libs: {
// 你提供给组件作者用的运行时库,组件代码里通过 __HYPERCARD__.libs.* 拿
http: /* axios 或自己封装的 fetch */ undefined,
ui: /* ElementPlus / antdv / 你自己的 UI 库 */ undefined,
},
}),
)
app.mount('#app')5. App.vue
<script setup lang="ts">
import { ref } from 'vue'
import { HyperCardEditor } from '@hy-bricks/editor'
const source = ref({ html: '', js: '', css: '' })
</script>
<template>
<div class="h-screen">
<HyperCardEditor v-model="source" draft-key="my-component" />
</div>
</template>跑 pnpm dev 就能看到完整 IDE。三栏 Monaco 都能编辑、改了立刻预览、Cmd+S 存草稿、刷新页面草稿还在。
peerDependencies(必装清单)
| 包 | 版本 | 必装 | 说明 |
|---|---|---|---|
| vue | ^3.5 | ✅ | Composition API + <script setup> |
| monaco-editor | ^0.55 | ✅ | 三栏代码编辑器内核 |
| tailwindcss | ^3.4 \|\| ^4 | ✅ | 宿主自己的 Tailwind(只扫自己,不扫 SDK) |
| @hy-bricks/core | 同 SDK 版本 | ✅ | 运行时(预览盒子、createHyperCard plugin) |
| reka-ui | ^1.0.0-alpha.8 | ✅ | shadcn-vue 的 headless 底座(Button 等用了) |
| class-variance-authority | ^0.7 | ✅ | buttonVariants 生成器 |
| clsx | ^2.1 | ✅ | className 合并 |
| tailwind-merge | ^2.5 | ✅ | dedupe Tailwind 冲突 class |
| lucide-vue-next | ^0.469 | ✅ | 顶栏图标(Settings / RefreshCw 等) |
| tailwindcss-animate | ^1.0 | 推荐 | shadcn-vue 默认动画(若你启用宿主侧 shadcn 组件) |
SDK
package.json里 reka-ui / cva / clsx / tailwind-merge / lucide-vue-next 已经移到peerDependencies(语义就是"宿主项目需要装"):宿主必须显式安装;SDK build 时把它们external,避免重复打包。
API
<HyperCardEditor> props
| prop | 类型 | 默认 | 说明 |
|---|---|---|---|
| modelValue | { html: string; js: string; css: string } | — | 必填。v-model 双向绑定;形状写死,宿主必须适配 |
| draftKey | string | undefined | 启用 localStorage 草稿;实际 key = hc:draft:<draftKey>。留空 = 不启用草稿 |
| componentId | string | 回退 draftKey,再回退实例级唯一 id(__hc_editor_<uid>__,用 getCurrentInstance().uid) | 给运行时实例的 id。默认回退已自动每实例唯一,不再撞;多实例仍建议显式传,方便 runtime / cssScope / 调试定位 |
| showBackButton | boolean | false | 顶栏左侧返回按钮。点击触发 go-back 事件,SDK 自己不路由 |
| title | string | undefined | 顶栏组件标题 |
| statusLabel | string | undefined | 顶栏右侧的版本号 / 状态文字(如 'v3 · 已发布') |
| autoPreview | boolean | true | 改源码后自动 debounce 渲染预览;false 时显示「重新渲染」按钮,需手点 |
| previewRatio | number | 0.62 | 编辑器与预览的初始宽度比 0–1 |
| extraLibs | ExtraLib[] | undefined | 给 Monaco 注入的额外类型 lib(自动补全 __HYPERCARD__.libs.*),详见 Monaco 类型注入 |
interface ExtraLib {
filePath: string // 必须形如 'file:///foo.d.ts'
content: string // ambient .d.ts 字符串(不能含 export {})
}emit 事件
| 事件 | payload | 触发时机 | 宿主该怎么处理 |
|---|---|---|---|
| update:modelValue | { html, js, css } | 任意一栏 Monaco 内容变化(throttle) | v-model 自动接,无需手写 |
| dirty | boolean | 用户改了源码 / 调用 applyDraft 后 → true;v-model 重置或保存草稿后 → false | 顶栏 dot、disable「推版本」按钮等 |
| save-draft | — | 用户点顶栏「保存草稿」或 Cmd+S(草稿已写入 localStorage) | toast「已保存」即可,无需自己再写 |
| push-version | — | 用户点顶栏「推版本」 | 弹自家 dialog 让填备注 → 调后端 API |
| open-preview-window | — | 用户点预览面板右上的「⧉ 独立窗口」 | window.open(routerResolve('/preview/...'), ...);开了之后 SDK 自动用 BroadcastChannel 同步源码 |
| open-settings | — | 用户点顶栏齿轮图标 | 弹自家组件元数据 drawer(name / desc / tags 等) |
| go-back | — | showBackButton=true 时点击返回按钮 | router.back() 之类 |
| error | { message, stack? } | 编译 / 运行时报错 | 写 console、上报 Sentry、Toast 等 |
expose(ref 拿到的 instance)
const editor = useTemplateRef<{
isDirty: Ref<boolean> // ← 注意是 Ref,不是裸 boolean
confirmLeave(): boolean
applyDraft(draft: ComponentDraft): void
manualPreview(): void
applyPreview(): void
}>('editor')| 方法 / 属性 | 签名 | 说明 |
|---|---|---|
| isDirty | Ref<boolean> | 是 ref 不是裸值,读用 editor.value.isDirty.value、模板里直接 editor?.isDirty |
| confirmLeave() | () => boolean | 同步弹 confirm,dirty 时提示「未保存,确定离开?」;返回 true 表示放行,false 表示用户取消 |
| applyDraft(draft) | (d: ComponentDraft) => void | 把外部草稿灌进编辑器(用于「恢复历史版本」),会把 dirty 设为 true |
| manualPreview() | () => void | autoPreview=false 时手动触发预览 |
| applyPreview() | () => void | 用 v-model 灌进新 source 后,如果想立刻刷预览(watch(modelValue) 已经会调,基本用不到) |
vue-router 离开守卫
import { onBeforeRouteLeave } from 'vue-router'
const editor = useTemplateRef<{ confirmLeave(): boolean }>('editor')
onBeforeRouteLeave(() => editor.value?.confirmLeave() ?? true)beforeunload(关浏览器、刷新)SDK 自动接,无需宿主写。
Monaco 类型注入
让组件作者写 __HYPERCARD__.libs.http. 时 Monaco 自动补全到具体方法。SDK 默认已经注册 __HYPERCARD__ / runtime / assets 三个 ambient,但 libs 的具体类型必须由宿主补(因为只有宿主知道你提供了 axios 还是 ky 还是 ElementPlus)。
import { HyperCardEditor, type ExtraLib } from '@hy-bricks/editor'
const extraLibs: ExtraLib[] = [
{
// 必须 file:/// 协议,Monaco 用这个判断模块身份
filePath: 'file:///host-libs.d.ts',
content: `
// 关键:用 declaration merging 跟 SDK 自带的 HyperCardLibs 接口合并,
// 不要写 export {},一旦文件里有 import/export,
// 它就变成 module 而不是 ambient,声明就泄漏不到全局了
interface HyperCardLibs {
http: {
get<T = any>(url: string, config?: any): Promise<{ data: T; status: number }>
post<T = any>(url: string, data?: any, config?: any): Promise<{ data: T; status: number }>
}
ui: { version: string; install: (...args: any[]) => void }
}
`,
},
]<HyperCardEditor v-model="source" :extra-libs="extraLibs" />⚠ 致命陷阱:
content字符串里不能出现export {}/import ...,否则 .d.ts 自动从 ambient 转成 module,顶层declare const __HYPERCARD__就不再是 global,组件作者那边补全直接灰掉。
主题定制
SDK 用 shadcn-vue 风的 HSL CSS variables。不需要重 build,在宿主自己的 :root 覆盖任意一条即可。
| 变量 | 默认 (light) | 用途 |
|---|---|---|
| --background / --foreground | 0 0% 100% / 222.2 84% 4.9% | 页面底色 / 主文字 |
| --primary / --primary-foreground | 222.2 47.4% 11.2% / 210 40% 98% | 「推版本」主按钮 |
| --secondary / --secondary-foreground | 210 40% 96.1% / 222.2 47.4% 11.2% | 次按钮 |
| --muted / --muted-foreground | 210 40% 96.1% / 215.4 16.3% 46.9% | 弱化区域、placeholder |
| --accent / --accent-foreground | 210 40% 96.1% / 222.2 47.4% 11.2% | hover 反馈 |
| --destructive / --destructive-foreground | 0 84.2% 60.2% / 210 40% 98% | 删除 / 警告 |
| --border / --input / --ring | 214.3 31.8% 91.4% / 同左 / 222.2 84% 4.9% | 边框 / input / focus ring |
| --radius | 0.5rem | 基础圆角(Button、面板) |
/* 改主色 + dark mode 一起改 */
:root {
--primary: 220 80% 50%;
--radius: 0.25rem;
}
.dark {
--primary: 220 70% 60%;
--background: 222.2 84% 4.9%;
}值要写纯 HSL 三元(无
hsl()包裹、无逗号),包内hsl(var(--primary) / <alpha>)会自己拼。
草稿持久化 + 离开守卫
| 场景 | SDK 做了什么 | 宿主做什么 |
|---|---|---|
| 设置 draft-key="abc" | 启用 localStorage,key = hc:draft:abc | 传 prop |
| 顶栏「保存草稿」/ Cmd+S | 写 localStorage,清除 dirty,触发 @save-draft | 接事件 toast 反馈 |
| 进入页面检测到旧草稿 | 顶栏渲染琥珀色 pill「本地草稿 · 5 分钟前 · [恢复] [丢弃]」 | — |
| 关浏览器 / 刷新 (beforeunload) | dirty 时弹浏览器原生 confirm | — |
| 切路由 (onBeforeRouteLeave) | 暴露 confirmLeave() | 在 router guard 中调:return editor.value?.confirmLeave() |
| 推版本成功 | — | 宿主自己 clearDraft(key)(从 SDK 导出) |
import { clearDraft, loadDraft, saveDraft } from '@hy-bricks/editor'
clearDraft('my-component') // 推版本成功后清
const d = loadDraft('my-component') // SSR / 列表预读
saveDraft('my-component', { html, javascript, css }) // 自己触发存(很少用)子组件单独导出(高级用户)
不想要默认的「IDE 顶栏 + 三栏 + 预览」布局?可以拿乐高块自己拼:
import {
// 布局
EditorLayout, EditorGroup, MonacoEditor, Splitter,
// multi-instance scope(每个独立编辑器一份,model 不串)
createEditorScope, useEditorScope, EDITOR_SCOPE_KEY,
// 布局算法(纯函数,immutable)
defaultLayout, activateTab, closeTab, splitWithTab, moveTab, setSplitRatio,
// 草稿
loadDraft, saveDraft, clearDraft,
// 预览跨窗口总线
createPreviewBus,
// shadcn-vue 风 Button(包内 vendor)
Button, buttonVariants,
// Monaco 类型钩子
setupHyperCardMonacoTypes, addMonacoExtraLib,
} from '@hy-bricks/editor'EditorLayout/EditorGroup/MonacoEditor/Splitter—— 自拼 IDE 界面createEditorScope/useEditorScope—— multi-instance 的 monaco model 隔离Button/buttonVariants—— vendor 自 shadcn-vue,跟 SDK 主题变量一致
Troubleshooting
❌ 装了 SDK,顶栏 Button 没样式 / 圆角错 / 颜色全黑
原因:Tailwind v3 出于性能默认 ignore node_modules,所以即便宿主 tailwind.config.js 的 content 写了 node_modules/@hy-bricks/**,也基本扫不到包内 .vue 里的 utility class。
解法:不要让宿主 Tailwind 扫这个包。本包发布时已经 self-compile 出 dist/style.css,把全部需要的 utility 都打进去。宿主只要:
import '@hy-bricks/editor/style.css'就拿到完整样式 + 主题变量。Tailwind preset 共享只是给「我也想跟本包用同一套色板」的宿主用,跟样式生效无关。
❌ Cannot read file ... ?worker 报错
原因:Vite 默认会把 node_modules 里的 ESM 包预 bundle(esbuild),但 esbuild 不认识 Vite 的 ?worker query —— monaco-editor 的 worker import 就这样炸。
解法:宿主 vite.config.ts 把 SDK 排除:
export default defineConfig({
optimizeDeps: {
exclude: ['@hy-bricks/core', '@hy-bricks/editor'],
},
})❌ PostCSS Cannot find module 'postcss' / CJS 加载失败
原因:Vite 把 SDK 当 ESM 加载时,如果宿主和 SDK 用了不同 PostCSS 版本,Node 模块解析会撞到 CJS/ESM 边界。
解法:SDK 已经把 postcss 处理打进自己的 dist 里了。如果宿主仍然另行 import postcss,在 vite.config.ts 里加 dedupe:
export default defineConfig({
resolve: {
dedupe: ['postcss', 'monaco-editor', 'vue'],
},
})❌ editor.value.isDirty 永远 truthy
原因:isDirty 暴露的是 Ref<boolean> 不是裸 boolean,而 Ref 对象本身永远 truthy。
解法:读 .value,或在模板里依赖自动 unwrap。TS 类型也要写对:
const editor = useTemplateRef<{ isDirty: Ref<boolean> }>('editor')
if (editor.value?.isDirty.value) { /* ... */ }或干脆听 @dirty 事件,最推荐:
<HyperCardEditor v-model="source" @dirty="d => isDirty = d" />❌ Monaco 补全 __HYPERCARD__.libs. 是空的
原因:你的 extraLibs[].content 里出现了 export {} / import x from ...,文件被 TS 识别成 module,声明不再 global。
解法:见 Monaco 类型注入。content 必须是纯 ambient,只有 declare / interface / type,没有任何 import / export。
⚠ 多个 <HyperCardEditor> 同页,样式 / 实例 id 区分
默认行为:不传 componentId 时,自动回退成实例级唯一 id(__hc_editor_<uid>__,基于 getCurrentInstance().uid),每个 <HyperCardEditor> 实例自动拿不同 id,不会撞。
仍建议:多实例并存时显式传 componentId(或不同 draftKey,会被自动回退用),让 runtime / cssScope / 调试堆栈里看得清是哪个实例:
<HyperCardEditor v-model="a" component-id="card-a" />
<HyperCardEditor v-model="b" component-id="card-b" />配合使用 / Ecosystem
- @hy-bricks/core — 运行时 SDK(editor 的 peerDep)
- @hy-bricks/canvas — 自由画布 + 多实例编排
- @hy-bricks/devtools — 运行时诊断浮窗
- shadcn-vue — UI 风格参考
- monaco-editor — 代码编辑器内核
License
MIT © hy_top
