@tnotesjs/core 0.4.1 → 0.5.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.
@@ -28,7 +28,8 @@ var DEFAULT_OPTIMIZE_DEPS_INCLUDE = [
28
28
  // Mermaid 11 的 ESM chunk 默认导入 CommonJS fastdom。pnpm 的严格
29
29
  // 依赖布局下 Vite 无法从知识库根目录自动发现这条嵌套依赖,开发
30
30
  // 模式会直接把 fastdom.js 当作 ESM 加载并导致整页白屏。
31
- "@tnotesjs/core > mermaid > fastdom"
31
+ "@tnotesjs/core > mermaid > fastdom",
32
+ "@tnotesjs/core > mermaid > fastdom/extensions/fastdom-promised.js"
32
33
  ];
33
34
 
34
35
  // vitepress/configs/constants.ts
@@ -64,45 +65,24 @@ import mila from "markdown-it-link-attributes";
64
65
  import markdownItTaskLists from "markdown-it-task-lists";
65
66
  import path from "path";
66
67
 
67
- // vitepress/components/MindmapPreview/compat.ts
68
+ // vitepress/components/MindmapPreview/markdown.ts
68
69
  function cleanHeadingText(value) {
69
70
  return value.trim().replace(/\s+#+\s*$/, "").trim();
70
71
  }
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) {
72
+ function parseMindmapFence(openLine) {
84
73
  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();
74
+ const nameMatch = fenceBody.match(/^mindmap(?=\s|\[|$)/);
75
+ if (!nameMatch) return null;
76
+ let rest = fenceBody.slice(nameMatch[0].length).trim();
88
77
  const options = {};
89
78
  const titleMatch = rest.match(/\[([^\]]+)\]/);
90
79
  if (titleMatch) {
91
- options.title = titleMatch[1].trim();
80
+ options.title = cleanHeadingText(titleMatch[1]) || void 0;
92
81
  rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim();
93
82
  }
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);
83
+ if (rest && !/^\d+$/.test(rest)) return null;
84
+ if (rest) {
85
+ options.initialExpandLevel = Math.max(1, Number(rest));
106
86
  }
107
87
  return options;
108
88
  }
@@ -131,30 +111,12 @@ function normalizeMindmapMarkdown(source, options = {}) {
131
111
  break;
132
112
  }
133
113
  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}
114
+ const body = lines.filter((_, index) => index !== rootIndex);
115
+ while (body[0]?.trim() === "") body.shift();
116
+ while (body[body.length - 1]?.trim() === "") body.pop();
117
+ return body.length > 0 ? `# ${rootTitle}
156
118
 
157
- ${normalizedBody.join("\n")}
119
+ ${body.join("\n")}
158
120
  ` : `# ${rootTitle}
159
121
  `;
160
122
  }
@@ -191,145 +153,13 @@ var simpleMermaidMarkdown = (md) => {
191
153
  return fence(tokens, index, options, env, slf);
192
154
  };
193
155
  };
194
- function configureMindmapContainer(md) {
195
- md.use(markdownItContainer, "markmap", {
196
- marker: "`",
197
- validate(params) {
198
- return (params || "").trim().startsWith("markmap");
199
- },
200
- render() {
201
- return "";
202
- }
203
- });
204
- md.core.ruler.after("block", "tn_replace_markmap_container", (state) => {
205
- const src = state.env.source || "";
206
- const lines = src.split("\n");
207
- const tokens = state.tokens;
208
- for (let i = 0; i < tokens.length; i++) {
209
- const t = tokens[i];
210
- if (t.type === "container_markmap_open") {
211
- const containerName = "markmap";
212
- const closeType = "container_markmap_close";
213
- let j = i + 1;
214
- while (j < tokens.length && tokens[j].type !== closeType)
215
- j++;
216
- if (j >= tokens.length) continue;
217
- const open = t;
218
- const startLine = open.map ? open.map[0] + 1 : null;
219
- const endLine = open.map ? open.map[1] - 1 : null;
220
- const params = {};
221
- let explicitTitle;
222
- if (open.map && typeof open.map[0] === "number") {
223
- const openLine = (lines[open.map[0]] || "").trim();
224
- const fenceOptions = parseMarkmapFence(openLine);
225
- explicitTitle = fenceOptions.title;
226
- let paramPart = "";
227
- const braceMatch = openLine.match(/\{([^}]*)\}/);
228
- if (braceMatch) {
229
- paramPart = braceMatch[1].trim();
230
- } else {
231
- const after = openLine.replace(/^`+\s*/, "");
232
- if (after.startsWith(containerName)) {
233
- paramPart = after.slice(containerName.length).trim();
234
- }
235
- }
236
- if (fenceOptions.initialExpandLevel !== void 0) {
237
- params.initialExpandLevel = fenceOptions.initialExpandLevel;
238
- }
239
- if (paramPart) {
240
- const tokenArr = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) || [];
241
- let startIdx = 0;
242
- if (tokenArr.length > 0 && /^\d+$/.test(tokenArr[0])) {
243
- params.initialExpandLevel = Number(tokenArr[0]);
244
- startIdx = 1;
245
- }
246
- for (let k = startIdx; k < tokenArr.length; k++) {
247
- const pair = tokenArr[k];
248
- if (!pair) continue;
249
- const m = pair.match(/^([^=:\s]+)\s*(=|:)\s*(.+)$/);
250
- if (m) {
251
- const key = m[1];
252
- let val = m[3];
253
- if (/^".*"$/.test(val) && val.length >= 2 || /^'.*'$/.test(val) && val.length >= 2) {
254
- val = val.slice(1, -1);
255
- } else if (/^\d+$/.test(val)) {
256
- val = String(Number(val));
257
- }
258
- params[key] = val;
259
- }
260
- }
261
- }
262
- }
263
- let content = "";
264
- if (startLine !== null && endLine !== null) {
265
- for (let k = startLine; k <= endLine && k < lines.length; k++) {
266
- content += lines[k] + "\n";
267
- }
268
- } else {
269
- for (let k = i + 1; k < j; k++) {
270
- content += tokens[k].content || "";
271
- }
272
- }
273
- const firstNonEmptyLine = (content || "").split("\n").find((ln) => ln.trim() !== "") || "";
274
- const reference = parseMindmapReference(firstNonEmptyLine);
275
- let referencedTitle;
276
- if (reference) {
277
- const refRaw = reference.path;
278
- referencedTitle = reference.title;
279
- try {
280
- const env = state.env || {};
281
- const possibleRel = env.relativePath || env.path || env.filePath || env.file || "";
282
- let refFullPath = refRaw;
283
- if (!path.isAbsolute(refRaw)) {
284
- if (possibleRel) {
285
- const currentDir = path.dirname(possibleRel);
286
- refFullPath = path.resolve(process.cwd(), currentDir, refRaw);
287
- } else {
288
- refFullPath = path.resolve(process.cwd(), refRaw);
289
- }
290
- } else {
291
- refFullPath = refRaw;
292
- }
293
- const fileContent = fs.readFileSync(refFullPath, "utf-8");
294
- content = fileContent;
295
- } catch (err) {
296
- const errorMsg = err instanceof Error ? err.message : String(err);
297
- content = `- Failed to load referenced file: ${esc(String(refRaw))}
298
- - Error: ${esc(errorMsg)}`;
299
- }
300
- }
301
- content = normalizeMindmapMarkdown(content, {
302
- title: explicitTitle || referencedTitle
303
- });
304
- const encodedContent = encodeURIComponent(content.trim());
305
- let propsStr = `content="${encodedContent}"`;
306
- for (const [k, v] of Object.entries(params)) {
307
- if (typeof v === "number" || /^\d+$/.test(String(v))) {
308
- propsStr += ` :${k}="${v}"`;
309
- } else {
310
- const safe = String(v).replace(/"/g, "&quot;");
311
- propsStr += ` ${k}="${safe}"`;
312
- }
313
- }
314
- const html = `<MindmapPreview ${propsStr}></MindmapPreview>
315
- `;
316
- const htmlToken = new state.Token("html_block", "", 0);
317
- htmlToken.content = html;
318
- tokens.splice(i, j - i + 1, htmlToken);
319
- }
320
- }
321
- return true;
322
- });
323
- }
324
156
  function configureMindmapFence(md) {
325
157
  const fence = md.renderer.rules.fence ? md.renderer.rules.fence.bind(md.renderer.rules) : () => "";
326
158
  md.renderer.rules.fence = (tokens, index, options, env, slf) => {
327
159
  const token = tokens[index];
328
160
  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);
161
+ const fenceOptions = parseMindmapFence(info);
162
+ if (!fenceOptions) return fence(tokens, index, options, env, slf);
333
163
  let content = token.content;
334
164
  const firstNonEmptyLine = content.split("\n").find((line) => line.trim()) ?? "";
335
165
  const reference = parseMindmapReference(firstNonEmptyLine);
@@ -423,7 +253,6 @@ function getMarkdownConfig() {
423
253
  return true;
424
254
  });
425
255
  simpleMermaidMarkdown(md);
426
- configureMindmapContainer(md);
427
256
  configureMindmapFence(md);
428
257
  md.use(markdownItTaskLists);
429
258
  md.use(mila, {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@tnotesjs/core",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "TNotes 知识库核心框架 —— 基于 VitePress 的笔记管理系统",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.17.1",
7
7
  "bin": {
8
- "tnotes": "./dist/cli/index.js"
8
+ "tnotes": "dist/cli/index.js"
9
9
  },
10
10
  "exports": {
11
11
  ".": {
@@ -85,7 +85,7 @@
85
85
  "license": "MIT",
86
86
  "repository": {
87
87
  "type": "git",
88
- "url": "https://github.com/tnotesjs/core"
88
+ "url": "git+https://github.com/tnotesjs/core.git"
89
89
  },
90
90
  "keywords": [
91
91
  "tnotes",
@@ -3,9 +3,12 @@ import { CanvasViewer, MindmapSession } from '@tnotesjs/mindmap-core'
3
3
  import { onContentUpdated, useData } from 'vitepress'
4
4
  import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
5
5
 
6
- import { normalizeMindmapMarkdown } from './compat'
7
6
  import { applyInitialExpandLevel } from './expandLevel'
7
+ import { normalizeMindmapMarkdown } from './markdown'
8
8
  import MindmapOutlineNode from './MindmapOutlineNode.vue'
9
+ import MindmapViewIcon from './MindmapViewIcon.vue'
10
+ import { gateMindmapWheel } from './wheelInteraction'
11
+ import { icon__fullscreen, icon__fullscreen_exit, icon__zoom_fit } from '../../assets/icons'
9
12
 
10
13
  type PreviewView = 'mindmap' | 'outline' | 'source'
11
14
 
@@ -19,12 +22,21 @@ const props = withDefaults(defineProps<{
19
22
 
20
23
  const { isDark } = useData()
21
24
  const activeView = ref<PreviewView>('mindmap')
25
+ const previewRoot = ref<HTMLElement | null>(null)
22
26
  const canvasHost = ref<HTMLElement | null>(null)
23
27
  const session = shallowRef<MindmapSession | null>(null)
24
28
  const renderVersion = ref(0)
29
+ const isFullscreen = ref(false)
30
+ const isCanvasActive = ref(false)
25
31
  let viewer: CanvasViewer | null = null
26
32
  let mounted = false
27
33
 
34
+ const viewOptions = [
35
+ { value: 'mindmap', label: '脑图' },
36
+ { value: 'outline', label: '大纲' },
37
+ { value: 'source', label: '源码' },
38
+ ] as const
39
+
28
40
  function decodeContent(value: string): string {
29
41
  try {
30
42
  return decodeURIComponent(value)
@@ -61,6 +73,7 @@ function rebuildSession(): void {
61
73
 
62
74
  function setView(view: PreviewView): void {
63
75
  activeView.value = view
76
+ if (view !== 'mindmap') isCanvasActive.value = false
64
77
  if (view === 'mindmap') void nextTick(() => viewer?.zoomToFit())
65
78
  }
66
79
 
@@ -72,11 +85,51 @@ function exitFocusTo(index: number): void {
72
85
  session.value?.exitFocusTo(index)
73
86
  }
74
87
 
88
+ async function toggleFullscreen(): Promise<void> {
89
+ const root = previewRoot.value
90
+ if (!root) return
91
+
92
+ try {
93
+ if (document.fullscreenElement === root) await document.exitFullscreen()
94
+ else await root.requestFullscreen()
95
+ } catch {
96
+ // Fullscreen can be rejected by the browser or an embedded document policy.
97
+ }
98
+ }
99
+
100
+ function handleFullscreenChange(): void {
101
+ isFullscreen.value = document.fullscreenElement === previewRoot.value
102
+ if (activeView.value === 'mindmap') void nextTick(() => viewer?.zoomToFit())
103
+ }
104
+
105
+ function activateCanvas(): void {
106
+ isCanvasActive.value = true
107
+ }
108
+
109
+ function handleCanvasWheelCapture(event: WheelEvent): void {
110
+ gateMindmapWheel(event, isCanvasActive.value)
111
+ }
112
+
113
+ function handleDocumentPointerDown(event: PointerEvent): void {
114
+ if (event.target instanceof Node && !previewRoot.value?.contains(event.target)) {
115
+ isCanvasActive.value = false
116
+ }
117
+ }
118
+
119
+ function handleDocumentKeydown(event: KeyboardEvent): void {
120
+ if (event.key !== 'Escape' || !isCanvasActive.value) return
121
+ isCanvasActive.value = false
122
+ canvasHost.value?.blur()
123
+ }
124
+
75
125
  watch([normalizedContent, () => props.initialExpandLevel], rebuildSession, { immediate: true })
76
126
  watch(isDark, (dark) => viewer?.setTheme(dark ? 'dark' : 'light'))
77
127
 
78
128
  onMounted(() => {
79
129
  mounted = true
130
+ document.addEventListener('fullscreenchange', handleFullscreenChange)
131
+ document.addEventListener('pointerdown', handleDocumentPointerDown, true)
132
+ document.addEventListener('keydown', handleDocumentKeydown, true)
80
133
  createViewer()
81
134
  })
82
135
 
@@ -86,29 +139,58 @@ onContentUpdated(() => {
86
139
 
87
140
  onBeforeUnmount(() => {
88
141
  mounted = false
142
+ document.removeEventListener('fullscreenchange', handleFullscreenChange)
143
+ document.removeEventListener('pointerdown', handleDocumentPointerDown, true)
144
+ document.removeEventListener('keydown', handleDocumentKeydown, true)
89
145
  viewer?.destroy()
90
146
  viewer = null
91
147
  })
92
148
  </script>
93
149
 
94
150
  <template>
95
- <section class="mindmap-preview" :class="{ 'is-dark': isDark }">
96
- <header class="mindmap-preview-header">
151
+ <section
152
+ ref="previewRoot"
153
+ class="mindmap-preview"
154
+ :class="{ 'is-dark': isDark, 'is-fullscreen': isFullscreen }"
155
+ :data-version="renderVersion"
156
+ >
157
+ <div class="mindmap-preview-actions">
97
158
  <nav class="mindmap-preview-tabs" aria-label="脑图预览视图">
98
159
  <button
99
- v-for="item in ([['mindmap', '脑图'], ['outline', '大纲'], ['source', '源码']] as const)"
100
- :key="item[0]"
160
+ v-for="item in viewOptions"
161
+ :key="item.value"
101
162
  type="button"
102
- :class="{ 'is-active': activeView === item[0] }"
103
- @click="setView(item[0])"
163
+ class="mindmap-preview-action"
164
+ :class="{ 'is-active': activeView === item.value }"
165
+ :aria-label="item.label"
166
+ :aria-pressed="activeView === item.value"
167
+ :title="item.label"
168
+ @click="setView(item.value)"
104
169
  >
105
- {{ item[1] }}
170
+ <MindmapViewIcon :view="item.value" />
106
171
  </button>
107
172
  </nav>
108
- <button v-if="activeView === 'mindmap'" type="button" class="mindmap-fit" @click="viewer?.zoomToFit()">
109
- 适应视口
173
+ <span class="mindmap-preview-action-divider" aria-hidden="true" />
174
+ <button
175
+ v-if="activeView === 'mindmap'"
176
+ type="button"
177
+ class="mindmap-preview-action"
178
+ aria-label="适应视口"
179
+ title="适应视口"
180
+ @click="viewer?.zoomToFit()"
181
+ >
182
+ <img :src="icon__zoom_fit" alt="" />
110
183
  </button>
111
- </header>
184
+ <button
185
+ type="button"
186
+ class="mindmap-preview-action"
187
+ :aria-label="isFullscreen ? '退出全屏' : '全屏查看'"
188
+ :title="isFullscreen ? '退出全屏' : '全屏查看'"
189
+ @click="toggleFullscreen"
190
+ >
191
+ <img :src="isFullscreen ? icon__fullscreen_exit : icon__fullscreen" alt="" />
192
+ </button>
193
+ </div>
112
194
 
113
195
  <nav v-if="session && session.focusPath.length > 0" class="mindmap-focus-path" aria-label="当前主题路径">
114
196
  <button type="button" @click="exitFocusTo(0)">全部</button>
@@ -118,7 +200,14 @@ onBeforeUnmount(() => {
118
200
  </template>
119
201
  </nav>
120
202
 
121
- <div v-show="activeView === 'mindmap'" ref="canvasHost" class="mindmap-canvas-host" />
203
+ <div
204
+ v-show="activeView === 'mindmap'"
205
+ ref="canvasHost"
206
+ class="mindmap-canvas-host"
207
+ :class="{ 'is-interaction-active': isCanvasActive }"
208
+ @pointerdown.capture="activateCanvas"
209
+ @wheel.capture="handleCanvasWheelCapture"
210
+ />
122
211
 
123
212
  <div v-if="activeView === 'outline' && session" class="mindmap-outline" :data-version="renderVersion">
124
213
  <ul class="mindmap-outline-root">
@@ -139,6 +228,7 @@ onBeforeUnmount(() => {
139
228
  .mindmap-preview {
140
229
  --mindmap-panel: var(--vp-c-bg-soft);
141
230
  --mindmap-border: var(--vp-c-divider);
231
+ position: relative;
142
232
  margin: 1.5rem 0;
143
233
  overflow: hidden;
144
234
  border: 1px solid var(--mindmap-border);
@@ -146,36 +236,75 @@ onBeforeUnmount(() => {
146
236
  background: var(--vp-c-bg);
147
237
  }
148
238
 
149
- .mindmap-preview-header {
239
+ .mindmap-preview-actions {
240
+ position: absolute;
241
+ top: 8px;
242
+ right: 8px;
243
+ z-index: 20;
150
244
  display: flex;
151
245
  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);
246
+ gap: 4px;
247
+ padding: 2px;
248
+ border: .1px solid var(--vp-c-divider);
249
+ border-radius: 7px;
250
+ background-color: color-mix(in srgb, var(--vp-code-block-bg) 92%, transparent);
251
+ box-shadow: var(--vp-shadow-1);
252
+ opacity: 0;
253
+ pointer-events: none;
254
+ transition: opacity .2s;
157
255
  }
158
256
 
159
257
  .mindmap-preview-tabs {
160
258
  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
- }
259
+ gap: 4px;
260
+ }
261
+
262
+ .mindmap-preview:hover > .mindmap-preview-actions,
263
+ .mindmap-preview-actions:focus-within {
264
+ opacity: 1;
265
+ pointer-events: auto;
266
+ }
267
+
268
+ .mindmap-preview-action {
269
+ display: inline-flex;
270
+ align-items: center;
271
+ justify-content: center;
272
+ width: 32px;
273
+ height: 32px;
274
+ padding: 0;
275
+ border: 0;
276
+ border-radius: 6px;
277
+ background: transparent;
278
+ color: var(--vp-c-brand-1);
279
+ cursor: pointer;
280
+ transition: background-color .2s, transform .2s;
281
+
282
+ svg,
283
+ img {
284
+ width: 18px;
285
+ height: 18px;
286
+ }
287
+
288
+ &:hover,
289
+ &.is-active {
290
+ background-color: var(--vp-c-default-soft);
291
+ }
292
+
293
+ &.is-active {
294
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vp-c-brand-1) 35%, transparent);
175
295
  }
296
+
297
+ &:hover { transform: scale(1.05); }
298
+ &:active { transform: scale(.95); }
299
+ }
300
+
301
+ .mindmap-preview-action-divider {
302
+ width: 1px;
303
+ height: 20px;
304
+ margin: 0 1px;
305
+ background: var(--vp-c-divider);
176
306
  }
177
307
 
178
- .mindmap-fit,
179
308
  .mindmap-focus-path button {
180
309
  color: var(--vp-c-text-2);
181
310
  font-size: 12px;
@@ -202,6 +331,10 @@ onBeforeUnmount(() => {
202
331
  background: var(--vp-c-bg);
203
332
  touch-action: none;
204
333
  user-select: none;
334
+
335
+ &.is-interaction-active {
336
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--vp-c-brand-1) 38%, transparent);
337
+ }
205
338
  }
206
339
 
207
340
  .mindmap-canvas-host:deep(.mm-canvas),
@@ -279,9 +412,31 @@ onBeforeUnmount(() => {
279
412
  white-space: pre;
280
413
  }
281
414
 
415
+ .mindmap-preview:fullscreen {
416
+ display: flex;
417
+ width: 100%;
418
+ height: 100%;
419
+ margin: 0;
420
+ border: 0;
421
+ border-radius: 0;
422
+ background: var(--vp-c-bg);
423
+ flex-direction: column;
424
+
425
+ .mindmap-canvas-host,
426
+ .mindmap-outline,
427
+ .mindmap-source {
428
+ flex: 1 1 auto;
429
+ max-height: none;
430
+ min-height: 0;
431
+ }
432
+
433
+ .mindmap-canvas-host { height: auto; }
434
+ }
435
+
282
436
  @media (max-width: 768px) {
283
437
  .mindmap-canvas-host { height: 360px; }
284
- .mindmap-preview-tabs button { padding-inline: 9px; }
438
+ .mindmap-preview-actions { top: 6px; right: 6px; }
439
+ .mindmap-preview-action { width: 28px; height: 28px; }
285
440
  .mindmap-outline { padding-inline: 12px; }
286
441
  }
287
442
  </style>
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ view: 'mindmap' | 'outline' | 'source'
4
+ }>()
5
+ </script>
6
+
7
+ <template>
8
+ <svg v-if="view === 'mindmap'" aria-hidden="true" viewBox="0 0 48 48">
9
+ <path d="M0 0h48v48H0z" fill="none" />
10
+ <path
11
+ d="M26 24h16M26 38h16M26 10h16M18 24H6h4m8 14c-6-2-2-14-8-14m8-14c-6 2-2 14-8 14"
12
+ fill="none"
13
+ stroke="#636cff"
14
+ stroke-linecap="round"
15
+ stroke-linejoin="round"
16
+ stroke-width="4"
17
+ />
18
+ </svg>
19
+ <svg v-else-if="view === 'outline'" aria-hidden="true" viewBox="0 0 48 48">
20
+ <path d="M0 0h48v48H0z" fill="none" />
21
+ <path
22
+ d="M26 24h18m-30 0h4m0 14h26M6 38h4m8-28h26M6 10h4"
23
+ fill="none"
24
+ stroke="#636cff"
25
+ stroke-linecap="round"
26
+ stroke-linejoin="round"
27
+ stroke-width="4"
28
+ />
29
+ </svg>
30
+ <svg v-else aria-hidden="true" viewBox="0 0 24 24">
31
+ <path d="M0 0h24v24H0z" fill="none" />
32
+ <path
33
+ d="m17 8 1.84 1.85c.773.778 1.16 1.167 1.16 1.65s-.387.872-1.16 1.65L17 15M7 8 5.16 9.85C4.387 10.628 4 11.017 4 11.5s.387.872 1.16 1.65L7 15m7.5-11-5 16"
34
+ fill="none"
35
+ stroke="#636cff"
36
+ stroke-linecap="round"
37
+ stroke-linejoin="round"
38
+ stroke-width="1.5"
39
+ />
40
+ </svg>
41
+ </template>