@tnotesjs/core 0.3.0 → 0.4.0

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.
@@ -49,6 +49,103 @@ import markdownItContainer from "markdown-it-container";
49
49
  import mila from "markdown-it-link-attributes";
50
50
  import markdownItTaskLists from "markdown-it-task-lists";
51
51
  import path from "path";
52
+
53
+ // vitepress/components/MindmapPreview/compat.ts
54
+ function cleanHeadingText(value) {
55
+ return value.trim().replace(/\s+#+\s*$/, "").trim();
56
+ }
57
+ function promoteLegacyRootList(body, rootTitle) {
58
+ const firstContentIndex = body.findIndex((line) => line.trim() !== "");
59
+ if (firstContentIndex < 0) return body;
60
+ const firstItem = body[firstContentIndex].match(/^[-+*]\s+(.+?)\s*$/);
61
+ if (!firstItem || cleanHeadingText(firstItem[1]) !== rootTitle) return body;
62
+ const descendants = body.slice(firstContentIndex + 1);
63
+ if (descendants.some((line) => line.trim() !== "" && !/^\s{2,}/.test(line))) return body;
64
+ return [
65
+ ...body.slice(0, firstContentIndex),
66
+ ...descendants.map((line) => line.replace(/^ {2}/, ""))
67
+ ];
68
+ }
69
+ function parseMarkmapFence(openLine) {
70
+ const fenceBody = openLine.trim().replace(/^`+\s*/, "");
71
+ const nameMatch = fenceBody.match(/^(mindmap|markmap)(?=\s|\{|\[|$)/);
72
+ if (!nameMatch) return {};
73
+ let rest = fenceBody.slice(nameMatch[1].length).trim();
74
+ const options = {};
75
+ const titleMatch = rest.match(/\[([^\]]+)\]/);
76
+ if (titleMatch) {
77
+ options.title = titleMatch[1].trim();
78
+ rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim();
79
+ }
80
+ const braceMatch = rest.match(/\{([^}]*)\}/);
81
+ const paramPart = braceMatch ? braceMatch[1].trim() : rest;
82
+ const tokens = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
83
+ for (const [index, token] of tokens.entries()) {
84
+ if (/^\d+$/.test(token) && index === 0) {
85
+ options.initialExpandLevel = Number(token);
86
+ continue;
87
+ }
88
+ const pair = token.match(/^([^=:\s]+)\s*(?:=|:)\s*(.+)$/);
89
+ if (!pair || pair[1] !== "initialExpandLevel") continue;
90
+ const value = pair[2].replace(/^['"]|['"]$/g, "");
91
+ if (/^\d+$/.test(value)) options.initialExpandLevel = Number(value);
92
+ }
93
+ return options;
94
+ }
95
+ function parseMindmapReference(line) {
96
+ const match = line.trim().match(/^<<<\s+(.+?)\s*$/);
97
+ if (!match) return null;
98
+ let rest = match[1].trim();
99
+ let title;
100
+ const titleMatch = rest.match(/\s+\[([^\]]+)\]\s*$/);
101
+ if (titleMatch) {
102
+ title = cleanHeadingText(titleMatch[1]) || void 0;
103
+ rest = rest.slice(0, titleMatch.index).trim();
104
+ }
105
+ const path6 = rest.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, "$1$2").trim();
106
+ return path6 ? { path: path6, title } : null;
107
+ }
108
+ function normalizeMindmapMarkdown(source, options = {}) {
109
+ const lines = source.replace(/\r\n?/g, "\n").split("\n");
110
+ let existingTitle = "";
111
+ let rootIndex = -1;
112
+ for (let index = 0; index < lines.length; index++) {
113
+ const match = lines[index].match(/^\s{0,3}#(?!#)\s+(.+?)\s*$/);
114
+ if (!match) continue;
115
+ existingTitle = cleanHeadingText(match[1]);
116
+ rootIndex = index;
117
+ break;
118
+ }
119
+ const rootTitle = cleanHeadingText(options.title || existingTitle || options.defaultTitle || "root") || "root";
120
+ const body = [];
121
+ let headingDepth = null;
122
+ for (let index = 0; index < lines.length; index++) {
123
+ if (index === rootIndex) continue;
124
+ const line = lines[index];
125
+ const heading = line.match(/^\s{0,3}(#{2,6})\s+(.+?)\s*$/);
126
+ if (heading) {
127
+ headingDepth = heading[1].length - 2;
128
+ body.push(`${" ".repeat(headingDepth)}- ${cleanHeadingText(heading[2])}`);
129
+ continue;
130
+ }
131
+ const listItem = line.match(/^(\s*)([-+*])\s+(.+)$/);
132
+ if (listItem && headingDepth !== null) {
133
+ body.push(`${" ".repeat(headingDepth + 1)}${line}`);
134
+ continue;
135
+ }
136
+ body.push(line);
137
+ }
138
+ const normalizedBody = promoteLegacyRootList(body, rootTitle);
139
+ while (normalizedBody[0]?.trim() === "") normalizedBody.shift();
140
+ while (normalizedBody[normalizedBody.length - 1]?.trim() === "") normalizedBody.pop();
141
+ return normalizedBody.length > 0 ? `# ${rootTitle}
142
+
143
+ ${normalizedBody.join("\n")}
144
+ ` : `# ${rootTitle}
145
+ `;
146
+ }
147
+
148
+ // vitepress/configs/markdown.config.ts
52
149
  function esc(s = "") {
53
150
  return s.replace(
54
151
  /[&<>"']/g,
@@ -80,12 +177,11 @@ var simpleMermaidMarkdown = (md) => {
80
177
  return fence(tokens, index, options, env, slf);
81
178
  };
82
179
  };
83
- function configureMarkMapContainer(md) {
180
+ function configureMindmapContainer(md) {
84
181
  md.use(markdownItContainer, "markmap", {
85
182
  marker: "`",
86
183
  validate(params) {
87
- const p = (params || "").trim();
88
- return p.startsWith("markmap");
184
+ return (params || "").trim().startsWith("markmap");
89
185
  },
90
186
  render() {
91
187
  return "";
@@ -98,26 +194,34 @@ function configureMarkMapContainer(md) {
98
194
  for (let i = 0; i < tokens.length; i++) {
99
195
  const t = tokens[i];
100
196
  if (t.type === "container_markmap_open") {
197
+ const containerName = "markmap";
198
+ const closeType = "container_markmap_close";
101
199
  let j = i + 1;
102
- while (j < tokens.length && tokens[j].type !== "container_markmap_close")
200
+ while (j < tokens.length && tokens[j].type !== closeType)
103
201
  j++;
104
202
  if (j >= tokens.length) continue;
105
203
  const open = t;
106
204
  const startLine = open.map ? open.map[0] + 1 : null;
107
205
  const endLine = open.map ? open.map[1] - 1 : null;
108
206
  const params = {};
207
+ let explicitTitle;
109
208
  if (open.map && typeof open.map[0] === "number") {
110
209
  const openLine = (lines[open.map[0]] || "").trim();
210
+ const fenceOptions = parseMarkmapFence(openLine);
211
+ explicitTitle = fenceOptions.title;
111
212
  let paramPart = "";
112
213
  const braceMatch = openLine.match(/\{([^}]*)\}/);
113
214
  if (braceMatch) {
114
215
  paramPart = braceMatch[1].trim();
115
216
  } else {
116
217
  const after = openLine.replace(/^`+\s*/, "");
117
- if (after.startsWith("markmap")) {
118
- paramPart = after.slice("markmap".length).trim();
218
+ if (after.startsWith(containerName)) {
219
+ paramPart = after.slice(containerName.length).trim();
119
220
  }
120
221
  }
222
+ if (fenceOptions.initialExpandLevel !== void 0) {
223
+ params.initialExpandLevel = fenceOptions.initialExpandLevel;
224
+ }
121
225
  if (paramPart) {
122
226
  const tokenArr = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) || [];
123
227
  let startIdx = 0;
@@ -153,9 +257,11 @@ function configureMarkMapContainer(md) {
153
257
  }
154
258
  }
155
259
  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, "");
260
+ const reference = parseMindmapReference(firstNonEmptyLine);
261
+ let referencedTitle;
262
+ if (reference) {
263
+ const refRaw = reference.path;
264
+ referencedTitle = reference.title;
159
265
  try {
160
266
  const env = state.env || {};
161
267
  const possibleRel = env.relativePath || env.path || env.filePath || env.file || "";
@@ -174,13 +280,13 @@ function configureMarkMapContainer(md) {
174
280
  content = fileContent;
175
281
  } catch (err) {
176
282
  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)}`;
283
+ content = `- Failed to load referenced file: ${esc(String(refRaw))}
284
+ - Error: ${esc(errorMsg)}`;
182
285
  }
183
286
  }
287
+ content = normalizeMindmapMarkdown(content, {
288
+ title: explicitTitle || referencedTitle
289
+ });
184
290
  const encodedContent = encodeURIComponent(content.trim());
185
291
  let propsStr = `content="${encodedContent}"`;
186
292
  for (const [k, v] of Object.entries(params)) {
@@ -191,7 +297,7 @@ Error: ${esc(errorMsg)}`;
191
297
  propsStr += ` ${k}="${safe}"`;
192
298
  }
193
299
  }
194
- const html = `<MarkMap ${propsStr}></MarkMap>
300
+ const html = `<MindmapPreview ${propsStr}></MindmapPreview>
195
301
  `;
196
302
  const htmlToken = new state.Token("html_block", "", 0);
197
303
  htmlToken.content = html;
@@ -201,6 +307,40 @@ Error: ${esc(errorMsg)}`;
201
307
  return true;
202
308
  });
203
309
  }
310
+ function configureMindmapFence(md) {
311
+ const fence = md.renderer.rules.fence ? md.renderer.rules.fence.bind(md.renderer.rules) : () => "";
312
+ md.renderer.rules.fence = (tokens, index, options, env, slf) => {
313
+ const token = tokens[index];
314
+ const info = token.info.trim();
315
+ if (!/^mindmap(?=\s|\{|\[|$)/.test(info)) {
316
+ return fence(tokens, index, options, env, slf);
317
+ }
318
+ const fenceOptions = parseMarkmapFence(info);
319
+ let content = token.content;
320
+ const firstNonEmptyLine = content.split("\n").find((line) => line.trim()) ?? "";
321
+ const reference = parseMindmapReference(firstNonEmptyLine);
322
+ if (reference) {
323
+ const possibleRel = env?.relativePath || env?.path || env?.filePath || env?.file || "";
324
+ const refFullPath = path.isAbsolute(reference.path) ? reference.path : path.resolve(process.cwd(), possibleRel ? path.dirname(possibleRel) : "", reference.path);
325
+ try {
326
+ content = fs.readFileSync(refFullPath, "utf8");
327
+ } catch (error) {
328
+ const message = error instanceof Error ? error.message : String(error);
329
+ content = `- Failed to load referenced file: ${reference.path}
330
+ - Error: ${message}`;
331
+ }
332
+ }
333
+ content = normalizeMindmapMarkdown(content, {
334
+ title: fenceOptions.title || reference?.title
335
+ });
336
+ const props = [
337
+ `content="${encodeURIComponent(content.trim())}"`,
338
+ fenceOptions.initialExpandLevel === void 0 ? "" : `:initialExpandLevel="${fenceOptions.initialExpandLevel}"`
339
+ ].filter(Boolean).join(" ");
340
+ return `<MindmapPreview ${props}></MindmapPreview>
341
+ `;
342
+ };
343
+ }
204
344
  function configureSwiperContainer(md) {
205
345
  let __tn_swiper_uid = 0;
206
346
  let __tn_rules_stack = [];
@@ -269,7 +409,8 @@ function getMarkdownConfig() {
269
409
  return true;
270
410
  });
271
411
  simpleMermaidMarkdown(md);
272
- configureMarkMapContainer(md);
412
+ configureMindmapContainer(md);
413
+ configureMindmapFence(md);
273
414
  md.use(markdownItTaskLists);
274
415
  md.use(mila, {
275
416
  attrs: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tnotesjs/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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>