# UI 与主题

接入自己的设计体系，同时保持源码、命令和组件的边界。

## 外观由应用提供

MSFE 核心不依赖 QuireKit Vue 组件。应用可以选择自己的组件库；这里的文档和示例使用 QuireKit 公共组件与设计 token。

示例的 `shared/appearance.ts` 通过 `addCodeMirrorExtensions()` 安装主题和语法高亮。打开接入代码可以看到完整主题定义。

### basic/editor.vue

```vue
<script setup lang="ts">
import { computed, ref, shallowRef } from "vue";
import { useMsfeEditor, VueNodeViewHost } from "@quirekit/msfe-vue";
import { StarterKit } from "@quirekit/msfe-starter-kit";
import { QuireButton, QuireSegmented, QuireProseSource } from "@quirekit/ui/vue";
import { Appearance } from "../shared/appearance";
import { exampleMessages } from "../shared/messages";
import type { ExampleLocale } from "../shared/types";

const props = withDefaults(defineProps<{ /** Language supplied by the application. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const text = computed(() => exampleMessages[props.locale]);
const initial = text.value.initial;
const parent = shallowRef<HTMLElement | null>(null);
const mode = ref("live");
const markdown = ref<string>();
const items = computed(() => [{ value: "live", label: text.value.live }, { value: "source", label: text.value.source }, { value: "locked", label: text.value.locked }]);

// One editor instance owns source, selection and history for all three modes.
const { editor, host } = useMsfeEditor(parent, () => ({ markdown: initial, mode: "live",
  extensions: [StarterKit, Appearance], label: text.value.editor }));

/** @param value - Mode selected in the shared control. */
function changeMode(value: string | null | undefined): void {
  if (value === "source" || value === "live" || value === "locked") { mode.value = value; editor.value?.setMode(value); }
}
/** Serializes only for an explicit save/read action; typing never mirrors the entire source. */
function readMarkdown(): void { markdown.value = editor.value?.getMarkdown(); }
/** Starts a fresh document and history after the user explicitly resets this example. */
function reset(): void { editor.value?.replaceDocument(initial, "example-reset"); markdown.value = undefined; }
</script>
<template>
  <div class="grid gap-4 p-qk-surface">
    <div class="flex flex-wrap items-center justify-between gap-3">
      <QuireSegmented :model-value="mode" :items="items" :label="text.modes" size="sm" @update:model-value="changeMode" />
      <div class="flex gap-2">
        <QuireButton size="sm" variant="ghost" :disabled="mode === 'locked'" @click="editor?.undo()">{{ text.undo }}</QuireButton>
        <QuireButton size="sm" variant="ghost" @click="reset">{{ text.reset }}</QuireButton>
        <QuireButton size="sm" variant="outline" @click="readMarkdown">{{ text.readMarkdown }}</QuireButton>
      </div>
    </div>
    <div ref="parent" class="overflow-hidden rounded-qk-control border border-qk-border-subtle" />
    <VueNodeViewHost :host="host" />
    <QuireProseSource v-if="markdown !== undefined" :code="markdown" language="markdown" filename="document.md" :max-height="260" />
  </div>
</template>

```

### shared/appearance.ts

```ts
import { Extension } from "@quirekit/msfe-core";
import { EditorView } from "@quirekit/cm-view";
import { Prec } from "@quirekit/cm-state";
import { HighlightStyle, syntaxHighlighting } from "@quirekit/cm-language";
import { tags } from "@lezer/highlight";
import { themeVariableReferences as token } from "@quirekit/ui";

/** Consumer-owned theme; source semantics stay in the installed feature packages. */
export const Appearance = Extension.create({ name: "documentationAppearance",
  /** @returns Scoped typography, surfaces and syntax colors from the host design system. */
  addCodeMirrorExtensions() {
    return Prec.high([
      EditorView.theme({
        "&": { ...token.typography.body, color: token.color.text.primary, backgroundColor: token.color.surface.page, minHeight: "180px" },
        ".cm-scroller": { fontFamily: "inherit", lineHeight: "1.8" },
        ".cm-content": { padding: token.spacing.surface, caretColor: token.color.text.primary },
        ".cm-line": { padding: "0" },
        ".cm-gutters": { color: token.color.text.tertiary, backgroundColor: token.color.surface.subtle, border: "none" },
        ".cm-activeLine, .cm-activeLineGutter": { backgroundColor: "transparent" },
        ".cm-selectionBackground, &.cm-focused .cm-selectionBackground": { backgroundColor: token.color.selection.document },
        ".cm-cursor": { borderLeftColor: token.color.text.primary },
        ".msfe-h1": { ...token.typography.pageTitle },
        ".msfe-h2, .msfe-h3": { ...token.typography.sectionTitle },
        ".msfe-heading": { paddingBlock: token.spacing.inline },
        ".msfe-link": { color: token.color.text.accent },
        ".msfe-code-line, .msfe-inline-code": { ...token.typography.code },
        ".consumer-status": { verticalAlign: "middle" },
        "&.cm-focused": { outline: "none" },
      }),
      syntaxHighlighting(HighlightStyle.define([
        { tag: tags.heading, color: token.color.text.primary, fontWeight: "600" },
        { tag: tags.strong, fontWeight: "700" },
        { tag: tags.emphasis, fontStyle: "italic" },
        { tag: [tags.link, tags.url], color: token.color.text.accent },
        { tag: tags.processingInstruction, color: token.color.text.tertiary },
        { tag: tags.monospace, color: token.color.text.secondary },
      ])),
    ]);
  },
});

```

### shared/messages.ts

```ts
const chinese = {
  editor: "Markdown 示例编辑器", modes: "编辑器模式", source: "源码", live: "实时预览", locked: "只读",
  undo: "撤销", redo: "重做", reset: "重置", readMarkdown: "读取 Markdown", readingPreview: "阅读预览", insertStatus: "插入状态", done: "已完成", todo: "待处理",
  nativeEditor: "原生 DOM 示例编辑器", statusEditor: "自定义状态编辑器",
  initial: "## 把想法写下来\n\n在这里编辑 **Markdown**，也可以切换到源码。\n\n- [x] 保留 Markdown 原文\n- [ ] 试试修改这一行\n",
  nativeInitial: "## 原生 DOM 接入\n\n这个实例直接使用 `createMsfeEditor()`。\n",
  statusInitial: "## 自定义状态\n\n设计评审：[[todo]]\n\n点击状态按钮，然后读取 Markdown 或撤销。\n",
};
const english: { [Key in keyof typeof chinese]: typeof chinese[Key] } = {
  editor: "Markdown example editor", modes: "Editor mode", source: "Source", live: "Live", locked: "Locked",
  undo: "Undo", redo: "Redo", reset: "Reset", readMarkdown: "Read Markdown", readingPreview: "Reading preview", insertStatus: "Insert status", done: "Done", todo: "Pending",
  nativeEditor: "Native DOM example editor", statusEditor: "Custom status editor",
  initial: "## Write your ideas\n\nEdit **Markdown** here, or switch to source.\n\n- [x] Preserve the Markdown source\n- [ ] Try editing this line\n",
  nativeInitial: "## Native DOM integration\n\nThis instance uses `createMsfeEditor()` directly.\n",
  statusInitial: "## Custom status\n\nDesign review: [[todo]]\n\nClick the status, then read the Markdown or undo.\n",
};
export const exampleMessages = { "zh-CN": chinese, en: english };

```

### shared/types.ts

```ts
/** Example host language; independent of the documentation router. */
export type ExampleLocale = "zh-CN" | "en";

```

## 三类定制入口

| 要定制的内容 | 使用的接口 |
| --- | --- |
| 字体、颜色、间距、语法高亮 | CodeMirror `EditorView.theme()`、`HighlightStyle`，通过扩展安装 |
| 图片、任务、业务节点等源码绑定组件 | `addNodeView()` 与框架 renderer |
| 搜索面板、容器折叠及预览操作 | 相应功能的 `ComponentViewRenderer` 配置 |
| 外部工具栏、菜单与弹窗 | 宿主组件调用公开命令、状态查询与受控动作 |

`theme: 'light' | 'dark'` 和 `setTheme()` 是内置明暗设置；完整视觉主题通过扩展和 CSS 提供。

## 使用 QuireKit 组件

应用配置 QuireKit UnoCSS 预设、主题与图标 Provider 后，从 `@quirekit/ui/vue` 导入按钮等组件。主题变量通过 `@quirekit/ui` 的 `themeVariableReferences` 接入编辑器，保持页面和编辑器颜色一致。

```ts
import { defineQuireKitUnoConfig } from '@quirekit/unocss-preset'

export default defineQuireKitUnoConfig()
```

## 正文输入所有权

正文输入、选区和光标继续由 CodeMirror 管理。组件里的表单可以通过字段 codec 修改明确的源码字段；任意可编辑组件子树目前不在支持范围内。

## 一个可扩展的主题模块

~~~ts
import { Extension } from '@quirekit/msfe-core'
import { EditorView } from '@quirekit/cm-view'
import { themeVariableReferences as token } from '@quirekit/ui'

export const ProductTheme = Extension.create({
  name: 'productTheme',
  addCodeMirrorExtensions() {
    return EditorView.theme({
      '&': { color: token.color.text.primary, backgroundColor: token.color.surface.page },
      '.cm-content': { padding: token.spacing.surface },
      '.msfe-inline-code': { ...token.typography.code, backgroundColor: token.color.surface.subtle },
      '.msfe-link': { color: token.color.text.accent },
    })
  },
})
~~~

把它放入 `extensions`。`EditorView.theme()` 将规则作用于编辑器实例，不会污染整站的 pre、table 或 button。需要覆盖默认优先级时用 CodeMirror 的 `Prec`，而不是全局 `!important` 堆叠。

## 定制入口对照

| 视觉对象 | 入口 | 边界 |
| --- | --- | --- |
| 正文、行、选区、光标 | `.cm-content`、`.cm-line` 等 CodeMirror 主题 | 不改输入 DOM 结构 |
| 语义行和行内标记 | `.msfe-heading`、`.msfe-link`、代码/容器类 | 保留源码显露与原生选择 |
| 代码 token | `HighlightStyle` + `syntaxHighlighting` | 高亮语法需要安装语言 |
| 业务节点 | NodeView 的宿主组件 | 字段修改通过 codec |
| 预览和折叠操作 | ComponentView 的宿主组件 | 不接管可编辑正文 |
| 文档网站正文 | QuireKit `QuireProse` 组件 | 与编辑器内部主题是不同消费入口 |

类名依赖需要随版本回归，不能只依赖内部 DOM 层级。不要用 `.cm-content * { all: unset }` 这类规则重置控件和辅助技术语义。

## 深浅主题和字体

在 QuireKit Provider 管理应用主题时，编辑器可使用同一套 CSS 变量。`editor.setTheme()` 更新 MSFE 的配色设置，不会自动替换你第三方图表的皮肤；图表需观察自己的视觉参数并更新实例。

选区与高亮可以使用固定色，而不跟随主题主色：覆盖 `--qk-color-selection-document`（正文选区，建议带透明度）
和 `--qk-prose-mark-background`（`==高亮==`，未设置时由主色混合页面底色得到）。选择器需与主题变量同级，
例如 `html[data-qk-theme][data-qk-scheme] { --qk-prose-mark-background: #fff1a1; }`，深色方案再单独设置一组。

字体大小、行高和异步资源会改变几何。保持组件内行高可预测；必要时调用 `requestMeasure()`。不要通过固定所有源码行高度来隐藏预览溢出。

## 组件库替换顺序

先接好 Provider / UnoCSS / token，再替换节点组件和通用控件，最后补语义主题。框架 Host 应放在 Provider 内。本站所有新增按钮和选择器继续使用 QuireKit；业务扩展只负责选择组件、传状态和发动作。

完整例子：[容器样式](/extensions/containers)、[代码块工具栏](/extensions/code-blocks)、[图表主题与释放](/extensions/diagrams)、[搜索面板](/customization/search)。
