@tnotesjs/core 0.3.0 → 0.4.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.
@@ -17,6 +17,20 @@ import fs3 from "fs";
17
17
  import path5 from "path";
18
18
  import { defineConfig } from "vitepress";
19
19
 
20
+ // vitepress/config/dependencyOptimization.ts
21
+ var DEFAULT_OPTIMIZE_DEPS_INCLUDE = [
22
+ // VitePress 内部 CJS 依赖 —— 需要 Vite 预构建为 ESM
23
+ "vitepress > @vscode/markdown-it-katex",
24
+ "vitepress > @braintree/sanitize-url",
25
+ "vitepress > dayjs",
26
+ "vitepress > dayjs/plugin/utc",
27
+ "vitepress > dayjs/plugin/localizedFormat",
28
+ // Mermaid 11 的 ESM chunk 默认导入 CommonJS fastdom。pnpm 的严格
29
+ // 依赖布局下 Vite 无法从知识库根目录自动发现这条嵌套依赖,开发
30
+ // 模式会直接把 fastdom.js 当作 ESM 加载并导致整页白屏。
31
+ "@tnotesjs/core > mermaid > fastdom"
32
+ ];
33
+
20
34
  // vitepress/configs/constants.ts
21
35
  function getIgnoreList(config) {
22
36
  return [...config.ignore_dirs.map((dir) => `**/${dir}/**`)];
@@ -49,6 +63,103 @@ import markdownItContainer from "markdown-it-container";
49
63
  import mila from "markdown-it-link-attributes";
50
64
  import markdownItTaskLists from "markdown-it-task-lists";
51
65
  import path from "path";
66
+
67
+ // vitepress/components/MindmapPreview/compat.ts
68
+ function cleanHeadingText(value) {
69
+ return value.trim().replace(/\s+#+\s*$/, "").trim();
70
+ }
71
+ function promoteLegacyRootList(body, rootTitle) {
72
+ const firstContentIndex = body.findIndex((line) => line.trim() !== "");
73
+ if (firstContentIndex < 0) return body;
74
+ const firstItem = body[firstContentIndex].match(/^[-+*]\s+(.+?)\s*$/);
75
+ if (!firstItem || cleanHeadingText(firstItem[1]) !== rootTitle) return body;
76
+ const descendants = body.slice(firstContentIndex + 1);
77
+ if (descendants.some((line) => line.trim() !== "" && !/^\s{2,}/.test(line))) return body;
78
+ return [
79
+ ...body.slice(0, firstContentIndex),
80
+ ...descendants.map((line) => line.replace(/^ {2}/, ""))
81
+ ];
82
+ }
83
+ function parseMarkmapFence(openLine) {
84
+ const fenceBody = openLine.trim().replace(/^`+\s*/, "");
85
+ const nameMatch = fenceBody.match(/^(mindmap|markmap)(?=\s|\{|\[|$)/);
86
+ if (!nameMatch) return {};
87
+ let rest = fenceBody.slice(nameMatch[1].length).trim();
88
+ const options = {};
89
+ const titleMatch = rest.match(/\[([^\]]+)\]/);
90
+ if (titleMatch) {
91
+ options.title = titleMatch[1].trim();
92
+ rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim();
93
+ }
94
+ const braceMatch = rest.match(/\{([^}]*)\}/);
95
+ const paramPart = braceMatch ? braceMatch[1].trim() : rest;
96
+ const tokens = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
97
+ for (const [index, token] of tokens.entries()) {
98
+ if (/^\d+$/.test(token) && index === 0) {
99
+ options.initialExpandLevel = Number(token);
100
+ continue;
101
+ }
102
+ const pair = token.match(/^([^=:\s]+)\s*(?:=|:)\s*(.+)$/);
103
+ if (!pair || pair[1] !== "initialExpandLevel") continue;
104
+ const value = pair[2].replace(/^['"]|['"]$/g, "");
105
+ if (/^\d+$/.test(value)) options.initialExpandLevel = Number(value);
106
+ }
107
+ return options;
108
+ }
109
+ function parseMindmapReference(line) {
110
+ const match = line.trim().match(/^<<<\s+(.+?)\s*$/);
111
+ if (!match) return null;
112
+ let rest = match[1].trim();
113
+ let title;
114
+ const titleMatch = rest.match(/\s+\[([^\]]+)\]\s*$/);
115
+ if (titleMatch) {
116
+ title = cleanHeadingText(titleMatch[1]) || void 0;
117
+ rest = rest.slice(0, titleMatch.index).trim();
118
+ }
119
+ const path6 = rest.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, "$1$2").trim();
120
+ return path6 ? { path: path6, title } : null;
121
+ }
122
+ function normalizeMindmapMarkdown(source, options = {}) {
123
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
124
+ let existingTitle = "";
125
+ let rootIndex = -1;
126
+ for (let index = 0; index < lines.length; index++) {
127
+ const match = lines[index].match(/^\s{0,3}#(?!#)\s+(.+?)\s*$/);
128
+ if (!match) continue;
129
+ existingTitle = cleanHeadingText(match[1]);
130
+ rootIndex = index;
131
+ break;
132
+ }
133
+ const rootTitle = cleanHeadingText(options.title || existingTitle || options.defaultTitle || "root") || "root";
134
+ const body = [];
135
+ let headingDepth = null;
136
+ for (let index = 0; index < lines.length; index++) {
137
+ if (index === rootIndex) continue;
138
+ const line = lines[index];
139
+ const heading = line.match(/^\s{0,3}(#{2,6})\s+(.+?)\s*$/);
140
+ if (heading) {
141
+ headingDepth = heading[1].length - 2;
142
+ body.push(`${" ".repeat(headingDepth)}- ${cleanHeadingText(heading[2])}`);
143
+ continue;
144
+ }
145
+ const listItem = line.match(/^(\s*)([-+*])\s+(.+)$/);
146
+ if (listItem && headingDepth !== null) {
147
+ body.push(`${" ".repeat(headingDepth + 1)}${line}`);
148
+ continue;
149
+ }
150
+ body.push(line);
151
+ }
152
+ const normalizedBody = promoteLegacyRootList(body, rootTitle);
153
+ while (normalizedBody[0]?.trim() === "") normalizedBody.shift();
154
+ while (normalizedBody[normalizedBody.length - 1]?.trim() === "") normalizedBody.pop();
155
+ return normalizedBody.length > 0 ? `# ${rootTitle}
156
+
157
+ ${normalizedBody.join("\n")}
158
+ ` : `# ${rootTitle}
159
+ `;
160
+ }
161
+
162
+ // vitepress/configs/markdown.config.ts
52
163
  function esc(s = "") {
53
164
  return s.replace(
54
165
  /[&<>"']/g,
@@ -80,12 +191,11 @@ var simpleMermaidMarkdown = (md) => {
80
191
  return fence(tokens, index, options, env, slf);
81
192
  };
82
193
  };
83
- function configureMarkMapContainer(md) {
194
+ function configureMindmapContainer(md) {
84
195
  md.use(markdownItContainer, "markmap", {
85
196
  marker: "`",
86
197
  validate(params) {
87
- const p = (params || "").trim();
88
- return p.startsWith("markmap");
198
+ return (params || "").trim().startsWith("markmap");
89
199
  },
90
200
  render() {
91
201
  return "";
@@ -98,26 +208,34 @@ function configureMarkMapContainer(md) {
98
208
  for (let i = 0; i < tokens.length; i++) {
99
209
  const t = tokens[i];
100
210
  if (t.type === "container_markmap_open") {
211
+ const containerName = "markmap";
212
+ const closeType = "container_markmap_close";
101
213
  let j = i + 1;
102
- while (j < tokens.length && tokens[j].type !== "container_markmap_close")
214
+ while (j < tokens.length && tokens[j].type !== closeType)
103
215
  j++;
104
216
  if (j >= tokens.length) continue;
105
217
  const open = t;
106
218
  const startLine = open.map ? open.map[0] + 1 : null;
107
219
  const endLine = open.map ? open.map[1] - 1 : null;
108
220
  const params = {};
221
+ let explicitTitle;
109
222
  if (open.map && typeof open.map[0] === "number") {
110
223
  const openLine = (lines[open.map[0]] || "").trim();
224
+ const fenceOptions = parseMarkmapFence(openLine);
225
+ explicitTitle = fenceOptions.title;
111
226
  let paramPart = "";
112
227
  const braceMatch = openLine.match(/\{([^}]*)\}/);
113
228
  if (braceMatch) {
114
229
  paramPart = braceMatch[1].trim();
115
230
  } else {
116
231
  const after = openLine.replace(/^`+\s*/, "");
117
- if (after.startsWith("markmap")) {
118
- paramPart = after.slice("markmap".length).trim();
232
+ if (after.startsWith(containerName)) {
233
+ paramPart = after.slice(containerName.length).trim();
119
234
  }
120
235
  }
236
+ if (fenceOptions.initialExpandLevel !== void 0) {
237
+ params.initialExpandLevel = fenceOptions.initialExpandLevel;
238
+ }
121
239
  if (paramPart) {
122
240
  const tokenArr = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) || [];
123
241
  let startIdx = 0;
@@ -153,9 +271,11 @@ function configureMarkMapContainer(md) {
153
271
  }
154
272
  }
155
273
  const firstNonEmptyLine = (content || "").split("\n").find((ln) => ln.trim() !== "") || "";
156
- const refMatch = firstNonEmptyLine.trim().match(/^<<<\s*(.+)$/);
157
- if (refMatch) {
158
- const refRaw = refMatch[1].trim().replace(/^['"]|['"]$/g, "");
274
+ const reference = parseMindmapReference(firstNonEmptyLine);
275
+ let referencedTitle;
276
+ if (reference) {
277
+ const refRaw = reference.path;
278
+ referencedTitle = reference.title;
159
279
  try {
160
280
  const env = state.env || {};
161
281
  const possibleRel = env.relativePath || env.path || env.filePath || env.file || "";
@@ -174,13 +294,13 @@ function configureMarkMapContainer(md) {
174
294
  content = fileContent;
175
295
  } catch (err) {
176
296
  const errorMsg = err instanceof Error ? err.message : String(err);
177
- content = `Failed to load referenced file: ${esc(
178
- String(refRaw)
179
- )}
180
-
181
- Error: ${esc(errorMsg)}`;
297
+ content = `- Failed to load referenced file: ${esc(String(refRaw))}
298
+ - Error: ${esc(errorMsg)}`;
182
299
  }
183
300
  }
301
+ content = normalizeMindmapMarkdown(content, {
302
+ title: explicitTitle || referencedTitle
303
+ });
184
304
  const encodedContent = encodeURIComponent(content.trim());
185
305
  let propsStr = `content="${encodedContent}"`;
186
306
  for (const [k, v] of Object.entries(params)) {
@@ -191,7 +311,7 @@ Error: ${esc(errorMsg)}`;
191
311
  propsStr += ` ${k}="${safe}"`;
192
312
  }
193
313
  }
194
- const html = `<MarkMap ${propsStr}></MarkMap>
314
+ const html = `<MindmapPreview ${propsStr}></MindmapPreview>
195
315
  `;
196
316
  const htmlToken = new state.Token("html_block", "", 0);
197
317
  htmlToken.content = html;
@@ -201,6 +321,40 @@ Error: ${esc(errorMsg)}`;
201
321
  return true;
202
322
  });
203
323
  }
324
+ function configureMindmapFence(md) {
325
+ const fence = md.renderer.rules.fence ? md.renderer.rules.fence.bind(md.renderer.rules) : () => "";
326
+ md.renderer.rules.fence = (tokens, index, options, env, slf) => {
327
+ const token = tokens[index];
328
+ const info = token.info.trim();
329
+ if (!/^mindmap(?=\s|\{|\[|$)/.test(info)) {
330
+ return fence(tokens, index, options, env, slf);
331
+ }
332
+ const fenceOptions = parseMarkmapFence(info);
333
+ let content = token.content;
334
+ const firstNonEmptyLine = content.split("\n").find((line) => line.trim()) ?? "";
335
+ const reference = parseMindmapReference(firstNonEmptyLine);
336
+ if (reference) {
337
+ const possibleRel = env?.relativePath || env?.path || env?.filePath || env?.file || "";
338
+ const refFullPath = path.isAbsolute(reference.path) ? reference.path : path.resolve(process.cwd(), possibleRel ? path.dirname(possibleRel) : "", reference.path);
339
+ try {
340
+ content = fs.readFileSync(refFullPath, "utf8");
341
+ } catch (error) {
342
+ const message = error instanceof Error ? error.message : String(error);
343
+ content = `- Failed to load referenced file: ${reference.path}
344
+ - Error: ${message}`;
345
+ }
346
+ }
347
+ content = normalizeMindmapMarkdown(content, {
348
+ title: fenceOptions.title || reference?.title
349
+ });
350
+ const props = [
351
+ `content="${encodeURIComponent(content.trim())}"`,
352
+ fenceOptions.initialExpandLevel === void 0 ? "" : `:initialExpandLevel="${fenceOptions.initialExpandLevel}"`
353
+ ].filter(Boolean).join(" ");
354
+ return `<MindmapPreview ${props}></MindmapPreview>
355
+ `;
356
+ };
357
+ }
204
358
  function configureSwiperContainer(md) {
205
359
  let __tn_swiper_uid = 0;
206
360
  let __tn_rules_stack = [];
@@ -269,7 +423,8 @@ function getMarkdownConfig() {
269
423
  return true;
270
424
  });
271
425
  simpleMermaidMarkdown(md);
272
- configureMarkMapContainer(md);
426
+ configureMindmapContainer(md);
427
+ configureMindmapFence(md);
273
428
  md.use(markdownItTaskLists);
274
429
  md.use(mila, {
275
430
  attrs: {
@@ -1648,14 +1803,7 @@ function defineNotesConfig(overrides = {}) {
1648
1803
  ...overrideVite?.resolve
1649
1804
  },
1650
1805
  optimizeDeps: {
1651
- include: [
1652
- // VitePress 内部 CJS 依赖 —— 需要 Vite 预构建为 ESM
1653
- "vitepress > @vscode/markdown-it-katex",
1654
- "vitepress > @braintree/sanitize-url",
1655
- "vitepress > dayjs",
1656
- "vitepress > dayjs/plugin/utc",
1657
- "vitepress > dayjs/plugin/localizedFormat"
1658
- ],
1806
+ include: [...DEFAULT_OPTIMIZE_DEPS_INCLUDE],
1659
1807
  ...overrideVite?.optimizeDeps
1660
1808
  }
1661
1809
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tnotesjs/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "TNotes 知识库核心框架 —— 基于 VitePress 的笔记管理系统",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.17.1",
@@ -49,7 +49,7 @@
49
49
  "vue": "^3.5.0"
50
50
  },
51
51
  "dependencies": {
52
- "d3": "^7.9.0",
52
+ "@tnotesjs/mindmap-core": "^0.2.0",
53
53
  "echarts": "^6.0.0",
54
54
  "github-slugger": "^2.0.0",
55
55
  "markdown-it-container": "^4.0.0",
@@ -57,11 +57,8 @@
57
57
  "markdown-it-mathjax3": "^4.3.2",
58
58
  "markdown-it-task-lists": "^2.1.1",
59
59
  "marked": "^15.0.11",
60
- "minisearch": "^7.2.0",
61
- "markmap-lib": "^0.18.12",
62
- "markmap-toolbar": "^0.18.12",
63
- "markmap-view": "^0.18.12",
64
60
  "mermaid": "^11.5.0",
61
+ "minisearch": "^7.2.0",
65
62
  "sass-embedded": "^1.90.0",
66
63
  "swiper": "^11.2.1",
67
64
  "uuid": "^11.1.0",
@@ -0,0 +1,25 @@
1
+ import { parseInlineSegments } from '@tnotesjs/mindmap-core'
2
+ import { defineComponent, h } from 'vue'
3
+
4
+ export default defineComponent({
5
+ name: 'MindmapInlineRuns',
6
+ props: {
7
+ raw: { type: String, default: '' },
8
+ },
9
+ setup(props) {
10
+ return () => h('span', { class: 'mindmap-inline-runs' }, parseInlineSegments(props.raw).map((segment) => {
11
+ const classes = Object.entries(segment.marks)
12
+ .filter(([, enabled]) => enabled)
13
+ .map(([name]) => `is-${name}`)
14
+ if (segment.link) {
15
+ return h('a', {
16
+ class: ['mindmap-inline-run', 'is-link', ...classes],
17
+ href: segment.link.url,
18
+ target: '_blank',
19
+ rel: 'noopener noreferrer',
20
+ }, segment.text)
21
+ }
22
+ return h('span', { class: ['mindmap-inline-run', ...classes] }, segment.text)
23
+ }))
24
+ },
25
+ })
@@ -0,0 +1,62 @@
1
+ <script setup lang="ts">
2
+ import InlineRuns from './InlineRuns'
3
+
4
+ import type { MindmapNode } from '@tnotesjs/mindmap-core'
5
+
6
+ defineOptions({ name: 'MindmapOutlineNode' })
7
+
8
+ defineProps<{
9
+ node: MindmapNode
10
+ version: number
11
+ root?: boolean
12
+ }>()
13
+
14
+ defineEmits<{
15
+ toggle: [id: string]
16
+ }>()
17
+ </script>
18
+
19
+ <template>
20
+ <li class="mindmap-outline-node" :class="{ 'is-root': root, 'is-done': node.content.checked === true }">
21
+ <div class="mindmap-outline-row">
22
+ <button
23
+ v-if="node.children.length > 0"
24
+ type="button"
25
+ class="mindmap-outline-toggle"
26
+ :aria-label="node.collapsed ? '展开主题' : '折叠主题'"
27
+ :aria-expanded="!node.collapsed"
28
+ @click="$emit('toggle', node.id)"
29
+ >
30
+ {{ node.collapsed ? '›' : '⌄' }}
31
+ </button>
32
+ <span v-else class="mindmap-outline-leaf" aria-hidden="true">•</span>
33
+ <input
34
+ v-if="node.content.checked !== null && !root"
35
+ class="mindmap-outline-checkbox"
36
+ type="checkbox"
37
+ :checked="node.content.checked"
38
+ disabled
39
+ aria-label="只读待办状态"
40
+ />
41
+ <span class="mindmap-outline-label">
42
+ <InlineRuns :raw="node.content.image ? node.content.text : node.content.raw" />
43
+ </span>
44
+ </div>
45
+ <img
46
+ v-if="node.content.image"
47
+ class="mindmap-outline-image"
48
+ :src="node.content.image.src"
49
+ :alt="node.content.image.alt"
50
+ :style="node.content.image.width ? { width: `${node.content.image.width}px` } : undefined"
51
+ />
52
+ <ul v-if="node.children.length > 0 && !node.collapsed" class="mindmap-outline-children">
53
+ <MindmapOutlineNode
54
+ v-for="child in node.children"
55
+ :key="child.id"
56
+ :node="child"
57
+ :version="version"
58
+ @toggle="$emit('toggle', $event)"
59
+ />
60
+ </ul>
61
+ </li>
62
+ </template>
@@ -0,0 +1,287 @@
1
+ <script setup lang="ts">
2
+ import { CanvasViewer, MindmapSession } from '@tnotesjs/mindmap-core'
3
+ import { onContentUpdated, useData } from 'vitepress'
4
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
5
+
6
+ import { normalizeMindmapMarkdown } from './compat'
7
+ import { applyInitialExpandLevel } from './expandLevel'
8
+ import MindmapOutlineNode from './MindmapOutlineNode.vue'
9
+
10
+ type PreviewView = 'mindmap' | 'outline' | 'source'
11
+
12
+ const props = withDefaults(defineProps<{
13
+ content?: string
14
+ initialExpandLevel?: number
15
+ }>(), {
16
+ content: '',
17
+ initialExpandLevel: 3,
18
+ })
19
+
20
+ const { isDark } = useData()
21
+ const activeView = ref<PreviewView>('mindmap')
22
+ const canvasHost = ref<HTMLElement | null>(null)
23
+ const session = shallowRef<MindmapSession | null>(null)
24
+ const renderVersion = ref(0)
25
+ let viewer: CanvasViewer | null = null
26
+ let mounted = false
27
+
28
+ function decodeContent(value: string): string {
29
+ try {
30
+ return decodeURIComponent(value)
31
+ } catch {
32
+ return value
33
+ }
34
+ }
35
+
36
+ const normalizedContent = computed(() => normalizeMindmapMarkdown(decodeContent(props.content)))
37
+
38
+ function createViewer(): void {
39
+ if (!mounted || !canvasHost.value || !session.value || viewer) return
40
+ viewer = new CanvasViewer(canvasHost.value, session.value, {
41
+ theme: isDark.value ? 'dark' : 'light',
42
+ })
43
+ }
44
+
45
+ function rebuildSession(): void {
46
+ viewer?.destroy()
47
+ viewer = null
48
+ const next = new MindmapSession({
49
+ markdown: normalizedContent.value,
50
+ fileName: 'mindmap-preview.tn-mindmap.md',
51
+ })
52
+ applyInitialExpandLevel(next, props.initialExpandLevel)
53
+ const invalidate = () => { renderVersion.value += 1 }
54
+ next.on('collapseChange', invalidate)
55
+ next.on('focusChange', invalidate)
56
+ next.on('change', invalidate)
57
+ session.value = next
58
+ renderVersion.value += 1
59
+ void nextTick(createViewer)
60
+ }
61
+
62
+ function setView(view: PreviewView): void {
63
+ activeView.value = view
64
+ if (view === 'mindmap') void nextTick(() => viewer?.zoomToFit())
65
+ }
66
+
67
+ function toggleNode(id: string): void {
68
+ session.value?.toggleCollapse(id)
69
+ }
70
+
71
+ function exitFocusTo(index: number): void {
72
+ session.value?.exitFocusTo(index)
73
+ }
74
+
75
+ watch([normalizedContent, () => props.initialExpandLevel], rebuildSession, { immediate: true })
76
+ watch(isDark, (dark) => viewer?.setTheme(dark ? 'dark' : 'light'))
77
+
78
+ onMounted(() => {
79
+ mounted = true
80
+ createViewer()
81
+ })
82
+
83
+ onContentUpdated(() => {
84
+ if (activeView.value === 'mindmap') void nextTick(() => viewer?.zoomToFit())
85
+ })
86
+
87
+ onBeforeUnmount(() => {
88
+ mounted = false
89
+ viewer?.destroy()
90
+ viewer = null
91
+ })
92
+ </script>
93
+
94
+ <template>
95
+ <section class="mindmap-preview" :class="{ 'is-dark': isDark }">
96
+ <header class="mindmap-preview-header">
97
+ <nav class="mindmap-preview-tabs" aria-label="脑图预览视图">
98
+ <button
99
+ v-for="item in ([['mindmap', '脑图'], ['outline', '大纲'], ['source', '源码']] as const)"
100
+ :key="item[0]"
101
+ type="button"
102
+ :class="{ 'is-active': activeView === item[0] }"
103
+ @click="setView(item[0])"
104
+ >
105
+ {{ item[1] }}
106
+ </button>
107
+ </nav>
108
+ <button v-if="activeView === 'mindmap'" type="button" class="mindmap-fit" @click="viewer?.zoomToFit()">
109
+ 适应视口
110
+ </button>
111
+ </header>
112
+
113
+ <nav v-if="session && session.focusPath.length > 0" class="mindmap-focus-path" aria-label="当前主题路径">
114
+ <button type="button" @click="exitFocusTo(0)">全部</button>
115
+ <template v-for="(node, index) in session.focusPath" :key="node.id">
116
+ <span aria-hidden="true">/</span>
117
+ <button type="button" @click="exitFocusTo(index + 1)">{{ node.content.text }}</button>
118
+ </template>
119
+ </nav>
120
+
121
+ <div v-show="activeView === 'mindmap'" ref="canvasHost" class="mindmap-canvas-host" />
122
+
123
+ <div v-if="activeView === 'outline' && session" class="mindmap-outline" :data-version="renderVersion">
124
+ <ul class="mindmap-outline-root">
125
+ <MindmapOutlineNode
126
+ :node="session.focusRootNode"
127
+ :version="renderVersion"
128
+ root
129
+ @toggle="toggleNode"
130
+ />
131
+ </ul>
132
+ </div>
133
+
134
+ <pre v-if="activeView === 'source'" class="mindmap-source"><code>{{ normalizedContent }}</code></pre>
135
+ </section>
136
+ </template>
137
+
138
+ <style scoped lang="scss">
139
+ .mindmap-preview {
140
+ --mindmap-panel: var(--vp-c-bg-soft);
141
+ --mindmap-border: var(--vp-c-divider);
142
+ margin: 1.5rem 0;
143
+ overflow: hidden;
144
+ border: 1px solid var(--mindmap-border);
145
+ border-radius: 10px;
146
+ background: var(--vp-c-bg);
147
+ }
148
+
149
+ .mindmap-preview-header {
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: space-between;
153
+ min-height: 42px;
154
+ padding: 5px 8px;
155
+ border-bottom: 1px solid var(--mindmap-border);
156
+ background: var(--mindmap-panel);
157
+ }
158
+
159
+ .mindmap-preview-tabs {
160
+ display: flex;
161
+ gap: 3px;
162
+
163
+ button {
164
+ padding: 5px 12px;
165
+ border-radius: 6px;
166
+ color: var(--vp-c-text-2);
167
+ font-size: 13px;
168
+ font-weight: 600;
169
+
170
+ &:hover,
171
+ &.is-active {
172
+ color: var(--vp-c-text-1);
173
+ background: var(--vp-c-bg);
174
+ }
175
+ }
176
+ }
177
+
178
+ .mindmap-fit,
179
+ .mindmap-focus-path button {
180
+ color: var(--vp-c-text-2);
181
+ font-size: 12px;
182
+
183
+ &:hover { color: var(--vp-c-brand-1); }
184
+ }
185
+
186
+ .mindmap-focus-path {
187
+ display: flex;
188
+ gap: 6px;
189
+ align-items: center;
190
+ padding: 6px 12px;
191
+ overflow-x: auto;
192
+ border-bottom: 1px solid var(--mindmap-border);
193
+ white-space: nowrap;
194
+ color: var(--vp-c-text-3);
195
+ }
196
+
197
+ .mindmap-canvas-host {
198
+ position: relative;
199
+ width: 100%;
200
+ height: 440px;
201
+ overflow: hidden;
202
+ background: var(--vp-c-bg);
203
+ touch-action: none;
204
+ user-select: none;
205
+ }
206
+
207
+ .mindmap-canvas-host:deep(.mm-canvas),
208
+ .mindmap-canvas-host:deep(.mm-overlay) {
209
+ position: absolute;
210
+ inset: 0;
211
+ width: 100%;
212
+ height: 100%;
213
+ }
214
+
215
+ .mindmap-canvas-host:deep(.mm-overlay) { pointer-events: none; }
216
+ .mindmap-canvas-host:deep(.mm-editor) { outline: none; }
217
+
218
+ .mindmap-outline {
219
+ max-height: 560px;
220
+ padding: 18px 22px 22px;
221
+ overflow: auto;
222
+ }
223
+
224
+ .mindmap-outline-root,
225
+ .mindmap-outline :deep(ul) {
226
+ margin: 0;
227
+ padding: 0;
228
+ list-style: none;
229
+ }
230
+
231
+ .mindmap-outline :deep(.mindmap-outline-children) {
232
+ margin-left: 10px;
233
+ padding-left: 19px;
234
+ border-left: 1px solid var(--vp-c-divider);
235
+ }
236
+
237
+ .mindmap-outline :deep(.mindmap-outline-row) {
238
+ display: flex;
239
+ align-items: flex-start;
240
+ gap: 7px;
241
+ min-height: 30px;
242
+ padding: 3px 0;
243
+ color: var(--vp-c-text-1);
244
+ line-height: 24px;
245
+ }
246
+
247
+ .mindmap-outline :deep(.mindmap-outline-toggle),
248
+ .mindmap-outline :deep(.mindmap-outline-leaf) {
249
+ flex: 0 0 18px;
250
+ width: 18px;
251
+ color: var(--vp-c-text-3);
252
+ text-align: center;
253
+ }
254
+
255
+ .mindmap-outline :deep(.mindmap-outline-toggle:hover) { color: var(--vp-c-brand-1); }
256
+ .mindmap-outline :deep(.mindmap-outline-checkbox) { margin-top: 5px; }
257
+ .mindmap-outline :deep(.mindmap-outline-label) { min-width: 0; overflow-wrap: anywhere; }
258
+ .mindmap-outline :deep(.mindmap-outline-node.is-root > .mindmap-outline-row) { font-size: 18px; font-weight: 700; }
259
+ .mindmap-outline :deep(.mindmap-outline-node.is-done > .mindmap-outline-row .mindmap-outline-label) { opacity: .58; text-decoration: line-through; }
260
+ .mindmap-outline :deep(.mindmap-outline-image) { display: block; max-width: min(100%, 560px); max-height: 360px; margin: 5px 0 12px 25px; border-radius: 6px; }
261
+ .mindmap-outline :deep(.is-bold) { font-weight: 700; }
262
+ .mindmap-outline :deep(.is-italic) { font-style: italic; }
263
+ .mindmap-outline :deep(.is-underline) { text-decoration: underline; }
264
+ .mindmap-outline :deep(.is-strike) { text-decoration: line-through; }
265
+ .mindmap-outline :deep(.is-highlight) { padding: 0 2px; border-radius: 2px; background: #ffe56b; color: #252525; }
266
+ .mindmap-outline :deep(.is-code) { padding: 1px 5px; border-radius: 4px; background: var(--vp-c-bg-soft); color: var(--vp-c-danger-1); font-family: var(--vp-font-family-mono); }
267
+ .mindmap-outline :deep(.is-link) { color: var(--vp-c-brand-1); text-decoration: underline; text-underline-offset: 3px; }
268
+
269
+ .mindmap-source {
270
+ max-height: 560px;
271
+ margin: 0;
272
+ padding: 18px 22px;
273
+ overflow: auto;
274
+ border-radius: 0;
275
+ background: var(--vp-code-block-bg);
276
+ color: var(--vp-code-block-color);
277
+ font-size: 13px;
278
+ line-height: 1.65;
279
+ white-space: pre;
280
+ }
281
+
282
+ @media (max-width: 768px) {
283
+ .mindmap-canvas-host { height: 360px; }
284
+ .mindmap-preview-tabs button { padding-inline: 9px; }
285
+ .mindmap-outline { padding-inline: 12px; }
286
+ }
287
+ </style>