@tnotesjs/core 0.4.2 → 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.
@@ -65,45 +65,24 @@ import mila from "markdown-it-link-attributes";
65
65
  import markdownItTaskLists from "markdown-it-task-lists";
66
66
  import path from "path";
67
67
 
68
- // vitepress/components/MindmapPreview/compat.ts
68
+ // vitepress/components/MindmapPreview/markdown.ts
69
69
  function cleanHeadingText(value) {
70
70
  return value.trim().replace(/\s+#+\s*$/, "").trim();
71
71
  }
72
- function promoteLegacyRootList(body, rootTitle) {
73
- const firstContentIndex = body.findIndex((line) => line.trim() !== "");
74
- if (firstContentIndex < 0) return body;
75
- const firstItem = body[firstContentIndex].match(/^[-+*]\s+(.+?)\s*$/);
76
- if (!firstItem || cleanHeadingText(firstItem[1]) !== rootTitle) return body;
77
- const descendants = body.slice(firstContentIndex + 1);
78
- if (descendants.some((line) => line.trim() !== "" && !/^\s{2,}/.test(line))) return body;
79
- return [
80
- ...body.slice(0, firstContentIndex),
81
- ...descendants.map((line) => line.replace(/^ {2}/, ""))
82
- ];
83
- }
84
- function parseMarkmapFence(openLine) {
72
+ function parseMindmapFence(openLine) {
85
73
  const fenceBody = openLine.trim().replace(/^`+\s*/, "");
86
- const nameMatch = fenceBody.match(/^(mindmap|markmap)(?=\s|\{|\[|$)/);
87
- if (!nameMatch) return {};
88
- 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();
89
77
  const options = {};
90
78
  const titleMatch = rest.match(/\[([^\]]+)\]/);
91
79
  if (titleMatch) {
92
- options.title = titleMatch[1].trim();
80
+ options.title = cleanHeadingText(titleMatch[1]) || void 0;
93
81
  rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim();
94
82
  }
95
- const braceMatch = rest.match(/\{([^}]*)\}/);
96
- const paramPart = braceMatch ? braceMatch[1].trim() : rest;
97
- const tokens = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
98
- for (const [index, token] of tokens.entries()) {
99
- if (/^\d+$/.test(token) && index === 0) {
100
- options.initialExpandLevel = Number(token);
101
- continue;
102
- }
103
- const pair = token.match(/^([^=:\s]+)\s*(?:=|:)\s*(.+)$/);
104
- if (!pair || pair[1] !== "initialExpandLevel") continue;
105
- const value = pair[2].replace(/^['"]|['"]$/g, "");
106
- 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));
107
86
  }
108
87
  return options;
109
88
  }
@@ -132,30 +111,12 @@ function normalizeMindmapMarkdown(source, options = {}) {
132
111
  break;
133
112
  }
134
113
  const rootTitle = cleanHeadingText(options.title || existingTitle || options.defaultTitle || "root") || "root";
135
- const body = [];
136
- let headingDepth = null;
137
- for (let index = 0; index < lines.length; index++) {
138
- if (index === rootIndex) continue;
139
- const line = lines[index];
140
- const heading = line.match(/^\s{0,3}(#{2,6})\s+(.+?)\s*$/);
141
- if (heading) {
142
- headingDepth = heading[1].length - 2;
143
- body.push(`${" ".repeat(headingDepth)}- ${cleanHeadingText(heading[2])}`);
144
- continue;
145
- }
146
- const listItem = line.match(/^(\s*)([-+*])\s+(.+)$/);
147
- if (listItem && headingDepth !== null) {
148
- body.push(`${" ".repeat(headingDepth + 1)}${line}`);
149
- continue;
150
- }
151
- body.push(line);
152
- }
153
- const normalizedBody = promoteLegacyRootList(body, rootTitle);
154
- while (normalizedBody[0]?.trim() === "") normalizedBody.shift();
155
- while (normalizedBody[normalizedBody.length - 1]?.trim() === "") normalizedBody.pop();
156
- 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}
157
118
 
158
- ${normalizedBody.join("\n")}
119
+ ${body.join("\n")}
159
120
  ` : `# ${rootTitle}
160
121
  `;
161
122
  }
@@ -192,145 +153,13 @@ var simpleMermaidMarkdown = (md) => {
192
153
  return fence(tokens, index, options, env, slf);
193
154
  };
194
155
  };
195
- function configureMindmapContainer(md) {
196
- md.use(markdownItContainer, "markmap", {
197
- marker: "`",
198
- validate(params) {
199
- return (params || "").trim().startsWith("markmap");
200
- },
201
- render() {
202
- return "";
203
- }
204
- });
205
- md.core.ruler.after("block", "tn_replace_markmap_container", (state) => {
206
- const src = state.env.source || "";
207
- const lines = src.split("\n");
208
- const tokens = state.tokens;
209
- for (let i = 0; i < tokens.length; i++) {
210
- const t = tokens[i];
211
- if (t.type === "container_markmap_open") {
212
- const containerName = "markmap";
213
- const closeType = "container_markmap_close";
214
- let j = i + 1;
215
- while (j < tokens.length && tokens[j].type !== closeType)
216
- j++;
217
- if (j >= tokens.length) continue;
218
- const open = t;
219
- const startLine = open.map ? open.map[0] + 1 : null;
220
- const endLine = open.map ? open.map[1] - 1 : null;
221
- const params = {};
222
- let explicitTitle;
223
- if (open.map && typeof open.map[0] === "number") {
224
- const openLine = (lines[open.map[0]] || "").trim();
225
- const fenceOptions = parseMarkmapFence(openLine);
226
- explicitTitle = fenceOptions.title;
227
- let paramPart = "";
228
- const braceMatch = openLine.match(/\{([^}]*)\}/);
229
- if (braceMatch) {
230
- paramPart = braceMatch[1].trim();
231
- } else {
232
- const after = openLine.replace(/^`+\s*/, "");
233
- if (after.startsWith(containerName)) {
234
- paramPart = after.slice(containerName.length).trim();
235
- }
236
- }
237
- if (fenceOptions.initialExpandLevel !== void 0) {
238
- params.initialExpandLevel = fenceOptions.initialExpandLevel;
239
- }
240
- if (paramPart) {
241
- const tokenArr = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) || [];
242
- let startIdx = 0;
243
- if (tokenArr.length > 0 && /^\d+$/.test(tokenArr[0])) {
244
- params.initialExpandLevel = Number(tokenArr[0]);
245
- startIdx = 1;
246
- }
247
- for (let k = startIdx; k < tokenArr.length; k++) {
248
- const pair = tokenArr[k];
249
- if (!pair) continue;
250
- const m = pair.match(/^([^=:\s]+)\s*(=|:)\s*(.+)$/);
251
- if (m) {
252
- const key = m[1];
253
- let val = m[3];
254
- if (/^".*"$/.test(val) && val.length >= 2 || /^'.*'$/.test(val) && val.length >= 2) {
255
- val = val.slice(1, -1);
256
- } else if (/^\d+$/.test(val)) {
257
- val = String(Number(val));
258
- }
259
- params[key] = val;
260
- }
261
- }
262
- }
263
- }
264
- let content = "";
265
- if (startLine !== null && endLine !== null) {
266
- for (let k = startLine; k <= endLine && k < lines.length; k++) {
267
- content += lines[k] + "\n";
268
- }
269
- } else {
270
- for (let k = i + 1; k < j; k++) {
271
- content += tokens[k].content || "";
272
- }
273
- }
274
- const firstNonEmptyLine = (content || "").split("\n").find((ln) => ln.trim() !== "") || "";
275
- const reference = parseMindmapReference(firstNonEmptyLine);
276
- let referencedTitle;
277
- if (reference) {
278
- const refRaw = reference.path;
279
- referencedTitle = reference.title;
280
- try {
281
- const env = state.env || {};
282
- const possibleRel = env.relativePath || env.path || env.filePath || env.file || "";
283
- let refFullPath = refRaw;
284
- if (!path.isAbsolute(refRaw)) {
285
- if (possibleRel) {
286
- const currentDir = path.dirname(possibleRel);
287
- refFullPath = path.resolve(process.cwd(), currentDir, refRaw);
288
- } else {
289
- refFullPath = path.resolve(process.cwd(), refRaw);
290
- }
291
- } else {
292
- refFullPath = refRaw;
293
- }
294
- const fileContent = fs.readFileSync(refFullPath, "utf-8");
295
- content = fileContent;
296
- } catch (err) {
297
- const errorMsg = err instanceof Error ? err.message : String(err);
298
- content = `- Failed to load referenced file: ${esc(String(refRaw))}
299
- - Error: ${esc(errorMsg)}`;
300
- }
301
- }
302
- content = normalizeMindmapMarkdown(content, {
303
- title: explicitTitle || referencedTitle
304
- });
305
- const encodedContent = encodeURIComponent(content.trim());
306
- let propsStr = `content="${encodedContent}"`;
307
- for (const [k, v] of Object.entries(params)) {
308
- if (typeof v === "number" || /^\d+$/.test(String(v))) {
309
- propsStr += ` :${k}="${v}"`;
310
- } else {
311
- const safe = String(v).replace(/"/g, "&quot;");
312
- propsStr += ` ${k}="${safe}"`;
313
- }
314
- }
315
- const html = `<MindmapPreview ${propsStr}></MindmapPreview>
316
- `;
317
- const htmlToken = new state.Token("html_block", "", 0);
318
- htmlToken.content = html;
319
- tokens.splice(i, j - i + 1, htmlToken);
320
- }
321
- }
322
- return true;
323
- });
324
- }
325
156
  function configureMindmapFence(md) {
326
157
  const fence = md.renderer.rules.fence ? md.renderer.rules.fence.bind(md.renderer.rules) : () => "";
327
158
  md.renderer.rules.fence = (tokens, index, options, env, slf) => {
328
159
  const token = tokens[index];
329
160
  const info = token.info.trim();
330
- if (!/^mindmap(?=\s|\{|\[|$)/.test(info)) {
331
- return fence(tokens, index, options, env, slf);
332
- }
333
- const fenceOptions = parseMarkmapFence(info);
161
+ const fenceOptions = parseMindmapFence(info);
162
+ if (!fenceOptions) return fence(tokens, index, options, env, slf);
334
163
  let content = token.content;
335
164
  const firstNonEmptyLine = content.split("\n").find((line) => line.trim()) ?? "";
336
165
  const reference = parseMindmapReference(firstNonEmptyLine);
@@ -424,7 +253,6 @@ function getMarkdownConfig() {
424
253
  return true;
425
254
  });
426
255
  simpleMermaidMarkdown(md);
427
- configureMindmapContainer(md);
428
256
  configureMindmapFence(md);
429
257
  md.use(markdownItTaskLists);
430
258
  md.use(mila, {
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@tnotesjs/core",
3
- "version": "0.4.2",
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>