@tnotesjs/ui 0.1.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.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/package.json +68 -0
- package/src/components/BilibiliVideo/BilibiliVideo.vue +55 -0
- package/src/components/Footprints/Footprints.vue +339 -0
- package/src/components/Footprints/parse.ts +122 -0
- package/src/components/Mermaid/Mermaid.vue +553 -0
- package/src/components/Mermaid/icons/icon__center_off.svg +1 -0
- package/src/components/Mermaid/icons/icon__center_on.svg +1 -0
- package/src/components/Mermaid/icons/icon__check.svg +3 -0
- package/src/components/Mermaid/icons/icon__clipboard.svg +8 -0
- package/src/components/Mermaid/icons/icon__fullscreen.svg +1 -0
- package/src/components/Mermaid/icons/icon__fullscreen_exit.svg +1 -0
- package/src/components/Mindmap/FocusBreadcrumbs.test.ts +265 -0
- package/src/components/Mindmap/FocusBreadcrumbs.vue +436 -0
- package/src/components/Mindmap/InlineRuns.ts +25 -0
- package/src/components/Mindmap/Mindmap.vue +1210 -0
- package/src/components/Mindmap/MindmapOutlineNode.vue +62 -0
- package/src/components/Mindmap/MindmapViewIcon.vue +41 -0
- package/src/components/Mindmap/editor/AppIcon.vue +107 -0
- package/src/components/Mindmap/editor/CanvasContextMenu.vue +157 -0
- package/src/components/Mindmap/editor/LinkPopover.vue +83 -0
- package/src/components/Mindmap/editor/MarkdownView.vue +163 -0
- package/src/components/Mindmap/editor/MindmapView.vue +253 -0
- package/src/components/Mindmap/editor/OutlineView.vue +2494 -0
- package/src/components/Mindmap/editor/RichInlineEditor.vue +393 -0
- package/src/components/Mindmap/editor/SelectionToolbar.vue +191 -0
- package/src/components/Mindmap/editor/canvasClipboard.ts +6 -0
- package/src/components/Mindmap/editor/imagePaste.ts +32 -0
- package/src/components/Mindmap/editor/mindmapClipboard.ts +93 -0
- package/src/components/Mindmap/editor/outlineDrag.ts +29 -0
- package/src/components/Mindmap/editor/platform.ts +18 -0
- package/src/components/Mindmap/expandLevel.ts +28 -0
- package/src/components/Mindmap/icons/icon__fullscreen.svg +1 -0
- package/src/components/Mindmap/icons/icon__fullscreen_exit.svg +1 -0
- package/src/components/Mindmap/icons/icon__zoom_fit.svg +1 -0
- package/src/components/Mindmap/markdown.ts +83 -0
- package/src/components/Mindmap/wheelInteraction.ts +7 -0
- package/src/components/NotesTable/NotesTable.vue +119 -0
- package/src/components/NotesTable/types.ts +6 -0
- package/src/components/WordList/RightClickMenu.vue +106 -0
- package/src/components/WordList/WordList.vue +692 -0
- package/src/components/WordList/wordListFeatures.ts +38 -0
- package/src/index.ts +30 -0
- package/src/styles/tokens.css +35 -0
|
@@ -0,0 +1,1210 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { CanvasEditor, CanvasViewer, MindmapSession } from '@tnotesjs/mindmap-core'
|
|
3
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
|
|
4
|
+
|
|
5
|
+
import { applyInitialExpandLevel, normalizeExpandLevel } from './expandLevel'
|
|
6
|
+
import { normalizeMindmapMarkdown } from './markdown'
|
|
7
|
+
import MindmapOutlineNode from './MindmapOutlineNode.vue'
|
|
8
|
+
import MindmapViewIcon from './MindmapViewIcon.vue'
|
|
9
|
+
import FocusBreadcrumbs from './FocusBreadcrumbs.vue'
|
|
10
|
+
import MindmapCanvasEditor from './editor/MindmapView.vue'
|
|
11
|
+
import MindmapOutlineEditor from './editor/OutlineView.vue'
|
|
12
|
+
import MindmapMarkdownEditor from './editor/MarkdownView.vue'
|
|
13
|
+
import { insertImageIntoSource } from './editor/imagePaste'
|
|
14
|
+
import { gateMindmapWheel } from './wheelInteraction'
|
|
15
|
+
import iconFullscreen from './icons/icon__fullscreen.svg?url'
|
|
16
|
+
import iconFullscreenExit from './icons/icon__fullscreen_exit.svg?url'
|
|
17
|
+
import iconZoomFit from './icons/icon__zoom_fit.svg?url'
|
|
18
|
+
|
|
19
|
+
type PreviewView = 'mindmap' | 'outline' | 'source'
|
|
20
|
+
|
|
21
|
+
/** One fullscreen mindmap at a time across preview instances on the page. */
|
|
22
|
+
const FS_GATE_KEY = '__tnotesjs_mindmap_fullscreen_gate__'
|
|
23
|
+
const FS_BODY_ATTR = 'tnMindmapFs'
|
|
24
|
+
const FORCE_EXIT_FULLSCREEN_EVENT = 'tnotes-mindmap-force-exit-fullscreen'
|
|
25
|
+
|
|
26
|
+
type FsGateState = { owners: Map<symbol, () => void> }
|
|
27
|
+
|
|
28
|
+
function fsGateState(): FsGateState {
|
|
29
|
+
const g = globalThis as typeof globalThis & { [FS_GATE_KEY]?: FsGateState }
|
|
30
|
+
if (!g[FS_GATE_KEY]) g[FS_GATE_KEY] = { owners: new Map() }
|
|
31
|
+
return g[FS_GATE_KEY]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function registerMindmapFullscreenOwner(id: symbol, exit: () => void): () => void {
|
|
35
|
+
const { owners } = fsGateState()
|
|
36
|
+
owners.set(id, exit)
|
|
37
|
+
return () => {
|
|
38
|
+
owners.delete(id)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function claimMindmapFullscreen(id: symbol): void {
|
|
43
|
+
for (const [otherId, exit] of [...fsGateState().owners]) {
|
|
44
|
+
if (otherId === id) continue
|
|
45
|
+
try {
|
|
46
|
+
exit()
|
|
47
|
+
} catch {
|
|
48
|
+
/* ignore */
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function syncBodyFullscreenAttr(activeRoot: HTMLElement | null): void {
|
|
54
|
+
if (typeof document === 'undefined') return
|
|
55
|
+
if (activeRoot?.classList.contains('is-fullscreen')) {
|
|
56
|
+
document.body.dataset[FS_BODY_ATTR] = '1'
|
|
57
|
+
document.documentElement.dataset[FS_BODY_ATTR] = '1'
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
if (!document.querySelector('.mindmap-preview.is-fullscreen')) {
|
|
61
|
+
delete document.body.dataset[FS_BODY_ATTR]
|
|
62
|
+
delete document.documentElement.dataset[FS_BODY_ATTR]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function forceExitPeerFullscreen(activeRoot: HTMLElement | null): void {
|
|
67
|
+
if (typeof document === 'undefined') return
|
|
68
|
+
for (const el of document.querySelectorAll('.mindmap-preview')) {
|
|
69
|
+
if (!(el instanceof HTMLElement) || el === activeRoot) continue
|
|
70
|
+
el.dispatchEvent(new CustomEvent(FORCE_EXIT_FULLSCREEN_EVENT))
|
|
71
|
+
el.classList.remove('is-fullscreen', 'is-interaction-active')
|
|
72
|
+
}
|
|
73
|
+
syncBodyFullscreenAttr(activeRoot)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const props = withDefaults(defineProps<{
|
|
77
|
+
content?: string
|
|
78
|
+
/** Plain mindmap markdown (preferred over URI-encoded content). */
|
|
79
|
+
source?: string
|
|
80
|
+
initialExpandLevel?: number
|
|
81
|
+
/** When true, canvas uses CanvasEditor and edits emit `change`. */
|
|
82
|
+
editable?: boolean
|
|
83
|
+
/** Show expand-level control on the chrome (Desk). */
|
|
84
|
+
expandLevelControl?: boolean
|
|
85
|
+
/** Explicit dark mode; omit to auto-detect html.dark / data-theme=dark. */
|
|
86
|
+
isDark?: boolean
|
|
87
|
+
/**
|
|
88
|
+
* Resolve markdown image paths for canvas display (Desk: tnotes-asset protocol).
|
|
89
|
+
* Defaults to identity.
|
|
90
|
+
*/
|
|
91
|
+
resolveImageSrc?: (src: string) => string
|
|
92
|
+
/**
|
|
93
|
+
* Persist a pasted/dropped image blob and return the markdown-relative path
|
|
94
|
+
* (e.g. `./assets/foo.png`). Required for editable paste-to-assets.
|
|
95
|
+
*/
|
|
96
|
+
writeAsset?: (blob: Blob) => Promise<{ relativePath: string; alt?: string }>
|
|
97
|
+
}>(), {
|
|
98
|
+
content: '',
|
|
99
|
+
source: '',
|
|
100
|
+
initialExpandLevel: 3,
|
|
101
|
+
editable: false,
|
|
102
|
+
expandLevelControl: false,
|
|
103
|
+
isDark: undefined,
|
|
104
|
+
resolveImageSrc: undefined,
|
|
105
|
+
writeAsset: undefined,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const emit = defineEmits<{
|
|
109
|
+
change: [markdown: string]
|
|
110
|
+
expandLevelChange: [level: number]
|
|
111
|
+
}>()
|
|
112
|
+
|
|
113
|
+
function detectDark(): boolean {
|
|
114
|
+
if (typeof props.isDark === 'boolean') return props.isDark
|
|
115
|
+
const root = document.documentElement
|
|
116
|
+
return root.classList.contains('dark') || root.dataset.theme === 'dark'
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const dark = ref(detectDark())
|
|
120
|
+
const activeView = ref<PreviewView>('mindmap')
|
|
121
|
+
const previewRoot = ref<HTMLElement | null>(null)
|
|
122
|
+
const canvasHost = ref<HTMLElement | null>(null)
|
|
123
|
+
const outlineEditorRef = ref<{
|
|
124
|
+
selectAllFromHost?: () => void
|
|
125
|
+
undoFromHost?: () => void
|
|
126
|
+
redoFromHost?: () => void
|
|
127
|
+
} | null>(null)
|
|
128
|
+
const sourceEditorRef = ref<{ selectAllFromHost?: () => void } | null>(null)
|
|
129
|
+
const session = shallowRef<MindmapSession | null>(null)
|
|
130
|
+
const renderVersion = ref(0)
|
|
131
|
+
const isFullscreen = ref(false)
|
|
132
|
+
/** How fullscreen was entered — Electron usually needs the CSS overlay path. */
|
|
133
|
+
let fullscreenMode: 'native' | 'css' | null = null
|
|
134
|
+
/** Stable id for exclusive fullscreen coordination across preview instances. */
|
|
135
|
+
const fullscreenOwnerId = Symbol('mindmap-fullscreen')
|
|
136
|
+
let unregisterFullscreenOwner: (() => void) | null = null
|
|
137
|
+
const isCanvasActive = ref(false)
|
|
138
|
+
const expandLevel = ref(normalizeExpandLevel(props.initialExpandLevel))
|
|
139
|
+
/** Readonly CanvasViewer host; editable mode uses MindmapCanvasEditor instead. */
|
|
140
|
+
let viewer: CanvasViewer | null = null
|
|
141
|
+
let editorRef: CanvasEditor | null = null
|
|
142
|
+
let mounted = false
|
|
143
|
+
let suppressChangeEmit = false
|
|
144
|
+
const sessionEpoch = ref(0)
|
|
145
|
+
|
|
146
|
+
const viewOptions = [
|
|
147
|
+
{ value: 'mindmap', label: '脑图' },
|
|
148
|
+
{ value: 'outline', label: '大纲' },
|
|
149
|
+
{ value: 'source', label: '源码' },
|
|
150
|
+
] as const
|
|
151
|
+
|
|
152
|
+
function decodeContent(value: string): string {
|
|
153
|
+
try {
|
|
154
|
+
return decodeURIComponent(value)
|
|
155
|
+
} catch {
|
|
156
|
+
return value
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const rawContent = computed(() => props.source || decodeContent(props.content || ''))
|
|
161
|
+
const normalizedContent = computed(() => normalizeMindmapMarkdown(rawContent.value))
|
|
162
|
+
|
|
163
|
+
function destroyViewer(): void {
|
|
164
|
+
viewer?.destroy()
|
|
165
|
+
viewer = null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function handlePasteImage(anchorId: string, blob: Blob): Promise<void> {
|
|
169
|
+
if (!props.editable || !session.value || !props.writeAsset) return
|
|
170
|
+
try {
|
|
171
|
+
const asset = await props.writeAsset(blob)
|
|
172
|
+
const prepared = session.value.prepareImageInsertion(
|
|
173
|
+
anchorId,
|
|
174
|
+
asset.relativePath,
|
|
175
|
+
asset.alt ?? '截图'
|
|
176
|
+
)
|
|
177
|
+
if (!prepared) return
|
|
178
|
+
prepared.commit()
|
|
179
|
+
} catch {
|
|
180
|
+
/* Host surfaces upload errors; keep canvas usable. */
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createViewer(): void {
|
|
185
|
+
// Editable canvas is owned by MindmapCanvasEditor (toolbar / context menu).
|
|
186
|
+
if (props.editable) return
|
|
187
|
+
if (!mounted || !canvasHost.value || !session.value || viewer) return
|
|
188
|
+
const theme = dark.value ? 'dark' : 'light'
|
|
189
|
+
viewer = new CanvasViewer(canvasHost.value, session.value, {
|
|
190
|
+
theme,
|
|
191
|
+
resolveImageSrc: (src) => props.resolveImageSrc?.(src) ?? src,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function onEditorReady(editor: CanvasEditor): void {
|
|
196
|
+
editorRef = editor
|
|
197
|
+
editor.setTheme(dark.value ? 'dark' : 'light')
|
|
198
|
+
if (activeView.value === 'mindmap') {
|
|
199
|
+
requestAnimationFrame(() => editor.zoomToFit())
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function resolveImageSrcProp(src: string): string {
|
|
204
|
+
return props.resolveImageSrc?.(src) ?? src
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function rebuildSession(): void {
|
|
208
|
+
destroyViewer()
|
|
209
|
+
editorRef = null
|
|
210
|
+
suppressChangeEmit = true
|
|
211
|
+
const next = new MindmapSession({
|
|
212
|
+
markdown: normalizedContent.value,
|
|
213
|
+
fileName: 'mindmap-preview.tn-mindmap.md',
|
|
214
|
+
})
|
|
215
|
+
applyInitialExpandLevel(next, expandLevel.value)
|
|
216
|
+
const invalidate = () => { renderVersion.value += 1 }
|
|
217
|
+
next.on('collapseChange', invalidate)
|
|
218
|
+
next.on('focusChange', invalidate)
|
|
219
|
+
next.on('selectionChange', invalidate)
|
|
220
|
+
next.on('change', (markdown) => {
|
|
221
|
+
invalidate()
|
|
222
|
+
if (suppressChangeEmit || !props.editable) return
|
|
223
|
+
emit('change', markdown)
|
|
224
|
+
})
|
|
225
|
+
session.value = next
|
|
226
|
+
sessionEpoch.value += 1
|
|
227
|
+
renderVersion.value += 1
|
|
228
|
+
void nextTick(() => {
|
|
229
|
+
createViewer()
|
|
230
|
+
suppressChangeEmit = false
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function onSourceMarkdown(value: string): void {
|
|
235
|
+
if (!session.value || !props.editable) return
|
|
236
|
+
if (value === session.value.getMarkdown()) return
|
|
237
|
+
session.value.setMarkdown(value)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function onSourcePasteImage(
|
|
241
|
+
blob: Blob,
|
|
242
|
+
selectionStart: number,
|
|
243
|
+
selectionEnd: number,
|
|
244
|
+
): Promise<void> {
|
|
245
|
+
if (!props.editable || !session.value || !props.writeAsset) return
|
|
246
|
+
try {
|
|
247
|
+
const asset = await props.writeAsset(blob)
|
|
248
|
+
const next = insertImageIntoSource(
|
|
249
|
+
session.value.getMarkdown(),
|
|
250
|
+
selectionStart,
|
|
251
|
+
selectionEnd,
|
|
252
|
+
asset.relativePath,
|
|
253
|
+
asset.alt ?? '截图',
|
|
254
|
+
)
|
|
255
|
+
onSourceMarkdown(next)
|
|
256
|
+
} catch {
|
|
257
|
+
/* Host surfaces upload errors. */
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function setExpandLevel(raw: number): void {
|
|
262
|
+
const level = normalizeExpandLevel(raw)
|
|
263
|
+
if (level === expandLevel.value) return
|
|
264
|
+
expandLevel.value = level
|
|
265
|
+
if (session.value) applyInitialExpandLevel(session.value, level)
|
|
266
|
+
renderVersion.value += 1
|
|
267
|
+
emit('expandLevelChange', level)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function onExpandLevelInput(event: Event): void {
|
|
271
|
+
const value = Number((event.target as HTMLInputElement).value)
|
|
272
|
+
setExpandLevel(value)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function setView(view: PreviewView): void {
|
|
276
|
+
if (activeView.value === view) return
|
|
277
|
+
if (activeView.value === 'mindmap' && view !== 'mindmap') {
|
|
278
|
+
editorRef = null
|
|
279
|
+
destroyViewer()
|
|
280
|
+
}
|
|
281
|
+
activeView.value = view
|
|
282
|
+
if (view !== 'mindmap') isCanvasActive.value = false
|
|
283
|
+
if (view === 'mindmap') {
|
|
284
|
+
// Host remounts via v-if; recreate readonly viewer then fit.
|
|
285
|
+
void nextTick(() => {
|
|
286
|
+
createViewer()
|
|
287
|
+
zoomToFit()
|
|
288
|
+
requestAnimationFrame(() => zoomToFit())
|
|
289
|
+
window.setTimeout(() => zoomToFit(), 80)
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Switch on pointerdown so Desk/PM cannot steal the gesture before click. */
|
|
295
|
+
function onViewTabPointerDown(view: PreviewView, event: PointerEvent): void {
|
|
296
|
+
event.stopPropagation()
|
|
297
|
+
setView(view)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function toggleNode(id: string): void {
|
|
301
|
+
session.value?.toggleCollapse(id)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function exitFullscreenOverlay(): void {
|
|
305
|
+
// no-op when idle — claimMindmapFullscreen() invokes exit on peers only.
|
|
306
|
+
if (!isFullscreen.value && fullscreenMode == null) return
|
|
307
|
+
|
|
308
|
+
const root = previewRoot.value
|
|
309
|
+
if (fullscreenMode === 'native' && root && document.fullscreenElement === root) {
|
|
310
|
+
void document.exitFullscreen().catch(() => {
|
|
311
|
+
/* ignore */
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
fullscreenMode = null
|
|
315
|
+
isFullscreen.value = false
|
|
316
|
+
root?.classList.remove('is-fullscreen', 'is-interaction-active')
|
|
317
|
+
syncBodyFullscreenAttr(null)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Peer claim / DOM scrub — clear Vue state so is-fullscreen does not come back. */
|
|
321
|
+
function onForceExitFullscreen(): void {
|
|
322
|
+
fullscreenMode = null
|
|
323
|
+
isFullscreen.value = false
|
|
324
|
+
previewRoot.value?.classList.remove('is-fullscreen', 'is-interaction-active')
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function toggleFullscreen(): Promise<void> {
|
|
328
|
+
const root = previewRoot.value
|
|
329
|
+
if (!root) return
|
|
330
|
+
|
|
331
|
+
if (isFullscreen.value) {
|
|
332
|
+
exitFullscreenOverlay()
|
|
333
|
+
void nextTick(() => zoomToFit())
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
claimMindmapFullscreen(fullscreenOwnerId)
|
|
338
|
+
forceExitPeerFullscreen(null)
|
|
339
|
+
|
|
340
|
+
// CSS overlay first — Electron often rejects/hangs on requestFullscreen.
|
|
341
|
+
fullscreenMode = 'css'
|
|
342
|
+
isFullscreen.value = true
|
|
343
|
+
root.classList.add('is-fullscreen')
|
|
344
|
+
forceExitPeerFullscreen(root)
|
|
345
|
+
void nextTick(() => zoomToFit())
|
|
346
|
+
|
|
347
|
+
// Best-effort native fullscreen for browser / VitePress (non-blocking).
|
|
348
|
+
if (!document.fullscreenEnabled || typeof root.requestFullscreen !== 'function') return
|
|
349
|
+
if (/Electron/i.test(navigator.userAgent)) return
|
|
350
|
+
try {
|
|
351
|
+
await Promise.race([
|
|
352
|
+
root.requestFullscreen(),
|
|
353
|
+
new Promise<never>((_, reject) => {
|
|
354
|
+
window.setTimeout(() => reject(new Error('fullscreen-timeout')), 500)
|
|
355
|
+
}),
|
|
356
|
+
])
|
|
357
|
+
if (document.fullscreenElement === root) fullscreenMode = 'native'
|
|
358
|
+
} catch {
|
|
359
|
+
/* keep CSS overlay */
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function handleFullscreenChange(): void {
|
|
364
|
+
const root = previewRoot.value
|
|
365
|
+
if (root && document.fullscreenElement === root) {
|
|
366
|
+
claimMindmapFullscreen(fullscreenOwnerId)
|
|
367
|
+
fullscreenMode = 'native'
|
|
368
|
+
isFullscreen.value = true
|
|
369
|
+
root.classList.add('is-fullscreen')
|
|
370
|
+
forceExitPeerFullscreen(root)
|
|
371
|
+
} else if (fullscreenMode === 'native') {
|
|
372
|
+
fullscreenMode = null
|
|
373
|
+
isFullscreen.value = false
|
|
374
|
+
syncBodyFullscreenAttr(null)
|
|
375
|
+
}
|
|
376
|
+
if (activeView.value === 'mindmap') void nextTick(() => zoomToFit())
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function zoomToFit(): void {
|
|
380
|
+
editorRef?.zoomToFit()
|
|
381
|
+
viewer?.zoomToFit()
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function activateCanvas(): void {
|
|
385
|
+
isCanvasActive.value = true
|
|
386
|
+
if (!props.editable) return
|
|
387
|
+
const editorEl = canvasHost.value?.querySelector<HTMLElement>('.mm-editor')
|
|
388
|
+
editorEl?.focus({ preventScroll: true })
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function handleCanvasWheelCapture(event: WheelEvent): void {
|
|
392
|
+
gateMindmapWheel(event, isCanvasActive.value)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** After the canvas handles paste, stop bubbling into Milkdown's uploader. */
|
|
396
|
+
function handlePreviewPasteBubble(event: ClipboardEvent): void {
|
|
397
|
+
if (!props.editable) return
|
|
398
|
+
const hasImage = [...(event.clipboardData?.items ?? [])].some(
|
|
399
|
+
(item) => item.kind === 'file' && item.type.startsWith('image/'),
|
|
400
|
+
)
|
|
401
|
+
if (!hasImage) return
|
|
402
|
+
event.stopPropagation()
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function handleDocumentPointerDown(event: PointerEvent): void {
|
|
406
|
+
if (event.target instanceof Node && !previewRoot.value?.contains(event.target)) {
|
|
407
|
+
isCanvasActive.value = false
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function mindmapIslandOwnsKeyboard(): boolean {
|
|
412
|
+
const root = previewRoot.value
|
|
413
|
+
if (!root || !props.editable) return false
|
|
414
|
+
const active = document.activeElement
|
|
415
|
+
const activeInside = active instanceof Node && root.contains(active)
|
|
416
|
+
const hostBlock = root.closest('.desk-raw-block--mindmap')
|
|
417
|
+
return (
|
|
418
|
+
isCanvasActive.value ||
|
|
419
|
+
activeInside ||
|
|
420
|
+
root.matches(':focus-within') ||
|
|
421
|
+
Boolean(hostBlock?.classList.contains('is-mindmap-island-active')) ||
|
|
422
|
+
Boolean(hostBlock?.classList.contains('ProseMirror-selectednode'))
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function handleDocumentKeydown(event: KeyboardEvent): void {
|
|
427
|
+
if (event.key === 'Escape' && isFullscreen.value && fullscreenMode === 'css') {
|
|
428
|
+
event.preventDefault()
|
|
429
|
+
exitFullscreenOverlay()
|
|
430
|
+
void nextTick(() => zoomToFit())
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Keep Mod+A / Mod+Z / Mod+Y / Mod+C / Mod+X / Mod+V inside the mindmap island.
|
|
435
|
+
if (props.editable && (event.metaKey || event.ctrlKey) && !event.altKey) {
|
|
436
|
+
const key = event.key.toLowerCase()
|
|
437
|
+
const isSelectAll = key === 'a' && !event.shiftKey
|
|
438
|
+
const isUndo = key === 'z' && !event.shiftKey
|
|
439
|
+
const isRedo = key === 'y' || (key === 'z' && event.shiftKey)
|
|
440
|
+
const isCopy = key === 'c' && !event.shiftKey
|
|
441
|
+
const isCut = key === 'x' && !event.shiftKey
|
|
442
|
+
const isPaste = key === 'v' && !event.shiftKey
|
|
443
|
+
if (
|
|
444
|
+
(isSelectAll || isUndo || isRedo || isCopy || isCut || isPaste) &&
|
|
445
|
+
mindmapIslandOwnsKeyboard()
|
|
446
|
+
) {
|
|
447
|
+
const root = previewRoot.value
|
|
448
|
+
const target = event.target
|
|
449
|
+
|
|
450
|
+
// Source view textarea: keep native clipboard / history; only block ProseMirror.
|
|
451
|
+
if (activeView.value === 'source' && (isUndo || isRedo || isCopy || isCut || isPaste)) {
|
|
452
|
+
const inSource =
|
|
453
|
+
target instanceof Element
|
|
454
|
+
? target.closest('.md-textarea, textarea, .markdown-view')
|
|
455
|
+
: target instanceof Node
|
|
456
|
+
? (target.parentElement?.closest('.md-textarea, textarea, .markdown-view') ?? null)
|
|
457
|
+
: null
|
|
458
|
+
if (inSource && root?.contains(inSource)) {
|
|
459
|
+
event.stopPropagation()
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Outline / canvas text editing: keep native clipboard into the caret.
|
|
465
|
+
if (isCopy || isCut || isPaste) {
|
|
466
|
+
const inTextEdit =
|
|
467
|
+
target instanceof Element
|
|
468
|
+
? target.closest(
|
|
469
|
+
'.mm-edit-input, .rich-inline-editor, [contenteditable="true"], textarea, input',
|
|
470
|
+
)
|
|
471
|
+
: null
|
|
472
|
+
if (inTextEdit && root?.contains(inTextEdit)) return
|
|
473
|
+
if (activeView.value !== 'mindmap') return
|
|
474
|
+
event.preventDefault()
|
|
475
|
+
event.stopPropagation()
|
|
476
|
+
if (isCopy) editorRef?.copyFromHost?.()
|
|
477
|
+
else if (isCut) editorRef?.cutFromHost?.()
|
|
478
|
+
else editorRef?.pasteFromHost?.()
|
|
479
|
+
isCanvasActive.value = true
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
event.preventDefault()
|
|
484
|
+
event.stopPropagation()
|
|
485
|
+
|
|
486
|
+
if (isSelectAll) {
|
|
487
|
+
if (activeView.value === 'outline') {
|
|
488
|
+
outlineEditorRef.value?.selectAllFromHost?.()
|
|
489
|
+
} else if (activeView.value === 'source') {
|
|
490
|
+
sourceEditorRef.value?.selectAllFromHost?.()
|
|
491
|
+
} else {
|
|
492
|
+
editorRef?.selectAllFromHost?.()
|
|
493
|
+
isCanvasActive.value = true
|
|
494
|
+
}
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (activeView.value === 'outline') {
|
|
499
|
+
if (isUndo) outlineEditorRef.value?.undoFromHost?.()
|
|
500
|
+
else outlineEditorRef.value?.redoFromHost?.()
|
|
501
|
+
} else if (activeView.value === 'source') {
|
|
502
|
+
if (isUndo) session.value?.undo()
|
|
503
|
+
else session.value?.redo()
|
|
504
|
+
} else if (isUndo) {
|
|
505
|
+
if (typeof editorRef?.undoFromHost === 'function') editorRef.undoFromHost()
|
|
506
|
+
else session.value?.undo()
|
|
507
|
+
isCanvasActive.value = true
|
|
508
|
+
} else {
|
|
509
|
+
if (typeof editorRef?.redoFromHost === 'function') editorRef.redoFromHost()
|
|
510
|
+
else session.value?.redo()
|
|
511
|
+
isCanvasActive.value = true
|
|
512
|
+
}
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (event.key !== 'Escape' || !isCanvasActive.value) return
|
|
518
|
+
isCanvasActive.value = false
|
|
519
|
+
canvasHost.value?.blur()
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
watch(
|
|
523
|
+
normalizedContent,
|
|
524
|
+
(value) => {
|
|
525
|
+
if (
|
|
526
|
+
session.value &&
|
|
527
|
+
normalizeMindmapMarkdown(session.value.getMarkdown()) === value
|
|
528
|
+
) {
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
rebuildSession()
|
|
532
|
+
},
|
|
533
|
+
{ immediate: true },
|
|
534
|
+
)
|
|
535
|
+
watch(
|
|
536
|
+
() => props.initialExpandLevel,
|
|
537
|
+
(value) => {
|
|
538
|
+
const level = normalizeExpandLevel(value)
|
|
539
|
+
if (level === expandLevel.value) return
|
|
540
|
+
expandLevel.value = level
|
|
541
|
+
if (session.value) {
|
|
542
|
+
applyInitialExpandLevel(session.value, level)
|
|
543
|
+
renderVersion.value += 1
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
)
|
|
547
|
+
watch(
|
|
548
|
+
() => props.editable,
|
|
549
|
+
() => {
|
|
550
|
+
destroyViewer()
|
|
551
|
+
editorRef = null
|
|
552
|
+
void nextTick(createViewer)
|
|
553
|
+
},
|
|
554
|
+
)
|
|
555
|
+
watch(dark, (value) => {
|
|
556
|
+
const theme = value ? 'dark' : 'light'
|
|
557
|
+
viewer?.setTheme(theme)
|
|
558
|
+
editorRef?.setTheme(theme)
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
function observeTheme() {
|
|
562
|
+
const observer = new MutationObserver(() => {
|
|
563
|
+
const next = detectDark()
|
|
564
|
+
if (next !== dark.value) dark.value = next
|
|
565
|
+
})
|
|
566
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'data-theme'] })
|
|
567
|
+
return observer
|
|
568
|
+
}
|
|
569
|
+
let themeObserver: MutationObserver | null = null
|
|
570
|
+
|
|
571
|
+
onMounted(() => {
|
|
572
|
+
mounted = true
|
|
573
|
+
themeObserver = observeTheme()
|
|
574
|
+
unregisterFullscreenOwner = registerMindmapFullscreenOwner(
|
|
575
|
+
fullscreenOwnerId,
|
|
576
|
+
exitFullscreenOverlay,
|
|
577
|
+
)
|
|
578
|
+
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
|
579
|
+
document.addEventListener('pointerdown', handleDocumentPointerDown, true)
|
|
580
|
+
document.addEventListener('keydown', handleDocumentKeydown, true)
|
|
581
|
+
void nextTick(() => {
|
|
582
|
+
previewRoot.value?.addEventListener(FORCE_EXIT_FULLSCREEN_EVENT, onForceExitFullscreen)
|
|
583
|
+
})
|
|
584
|
+
createViewer()
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
onBeforeUnmount(() => {
|
|
588
|
+
mounted = false
|
|
589
|
+
exitFullscreenOverlay()
|
|
590
|
+
unregisterFullscreenOwner?.()
|
|
591
|
+
unregisterFullscreenOwner = null
|
|
592
|
+
themeObserver?.disconnect()
|
|
593
|
+
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
|
594
|
+
document.removeEventListener('pointerdown', handleDocumentPointerDown, true)
|
|
595
|
+
document.removeEventListener('keydown', handleDocumentKeydown, true)
|
|
596
|
+
previewRoot.value?.removeEventListener(FORCE_EXIT_FULLSCREEN_EVENT, onForceExitFullscreen)
|
|
597
|
+
destroyViewer()
|
|
598
|
+
})
|
|
599
|
+
</script>
|
|
600
|
+
|
|
601
|
+
<template>
|
|
602
|
+
<section
|
|
603
|
+
ref="previewRoot"
|
|
604
|
+
class="mindmap-preview"
|
|
605
|
+
:class="{
|
|
606
|
+
'is-dark': dark,
|
|
607
|
+
'is-fullscreen': isFullscreen,
|
|
608
|
+
'is-editable': editable,
|
|
609
|
+
'is-interaction-active': isCanvasActive,
|
|
610
|
+
}"
|
|
611
|
+
:data-view="activeView"
|
|
612
|
+
:data-version="renderVersion"
|
|
613
|
+
@paste="handlePreviewPasteBubble"
|
|
614
|
+
>
|
|
615
|
+
<div class="mindmap-preview-actions">
|
|
616
|
+
<nav class="mindmap-preview-tabs" aria-label="脑图预览视图">
|
|
617
|
+
<button
|
|
618
|
+
v-for="item in viewOptions"
|
|
619
|
+
:key="item.value"
|
|
620
|
+
type="button"
|
|
621
|
+
class="mindmap-preview-action"
|
|
622
|
+
:class="{ 'is-active': activeView === item.value }"
|
|
623
|
+
:data-view-tab="item.value"
|
|
624
|
+
:aria-label="item.label"
|
|
625
|
+
:aria-pressed="activeView === item.value"
|
|
626
|
+
:title="item.label"
|
|
627
|
+
@pointerdown="onViewTabPointerDown(item.value, $event)"
|
|
628
|
+
>
|
|
629
|
+
<MindmapViewIcon :view="item.value" />
|
|
630
|
+
</button>
|
|
631
|
+
</nav>
|
|
632
|
+
<span class="mindmap-preview-action-divider" aria-hidden="true" />
|
|
633
|
+
<label
|
|
634
|
+
v-if="expandLevelControl"
|
|
635
|
+
class="mindmap-preview-expand"
|
|
636
|
+
title="默认展开层级"
|
|
637
|
+
>
|
|
638
|
+
<span class="mindmap-preview-expand-label">层</span>
|
|
639
|
+
<input
|
|
640
|
+
class="mindmap-preview-expand-input"
|
|
641
|
+
type="number"
|
|
642
|
+
min="1"
|
|
643
|
+
max="20"
|
|
644
|
+
:value="expandLevel"
|
|
645
|
+
aria-label="默认展开层级"
|
|
646
|
+
@change="onExpandLevelInput"
|
|
647
|
+
/>
|
|
648
|
+
</label>
|
|
649
|
+
<button
|
|
650
|
+
v-if="activeView === 'mindmap'"
|
|
651
|
+
type="button"
|
|
652
|
+
class="mindmap-preview-action"
|
|
653
|
+
aria-label="适应视口"
|
|
654
|
+
title="适应视口"
|
|
655
|
+
@click="zoomToFit"
|
|
656
|
+
>
|
|
657
|
+
<img :src="iconZoomFit" alt="" />
|
|
658
|
+
</button>
|
|
659
|
+
<button
|
|
660
|
+
type="button"
|
|
661
|
+
class="mindmap-preview-action"
|
|
662
|
+
:aria-label="isFullscreen ? '退出全屏' : '全屏查看'"
|
|
663
|
+
:title="isFullscreen ? '退出全屏' : '全屏查看'"
|
|
664
|
+
@click="toggleFullscreen"
|
|
665
|
+
>
|
|
666
|
+
<img :src="isFullscreen ? iconFullscreenExit : iconFullscreen" alt="" />
|
|
667
|
+
</button>
|
|
668
|
+
</div>
|
|
669
|
+
|
|
670
|
+
<FocusBreadcrumbs
|
|
671
|
+
v-if="session && session.focusPath.length > 0"
|
|
672
|
+
:session="session"
|
|
673
|
+
:version="renderVersion"
|
|
674
|
+
/>
|
|
675
|
+
|
|
676
|
+
<!-- Exclusive panes: destroy canvas so it cannot linger over outline/source. -->
|
|
677
|
+
<div
|
|
678
|
+
v-if="activeView === 'mindmap'"
|
|
679
|
+
ref="canvasHost"
|
|
680
|
+
class="mindmap-canvas-host mindmap-pane"
|
|
681
|
+
:class="{ 'is-interaction-active': isCanvasActive }"
|
|
682
|
+
@pointerdown.capture="activateCanvas"
|
|
683
|
+
@wheel.capture="handleCanvasWheelCapture"
|
|
684
|
+
>
|
|
685
|
+
<MindmapCanvasEditor
|
|
686
|
+
v-if="editable && session"
|
|
687
|
+
:key="sessionEpoch"
|
|
688
|
+
:session="session"
|
|
689
|
+
:resolve-image-src="resolveImageSrcProp"
|
|
690
|
+
@ready="onEditorReady"
|
|
691
|
+
@paste-image="handlePasteImage"
|
|
692
|
+
/>
|
|
693
|
+
</div>
|
|
694
|
+
|
|
695
|
+
<div
|
|
696
|
+
v-else-if="activeView === 'outline' && session"
|
|
697
|
+
class="mindmap-outline mindmap-pane"
|
|
698
|
+
:class="{ 'is-editable': editable }"
|
|
699
|
+
:data-version="renderVersion"
|
|
700
|
+
>
|
|
701
|
+
<MindmapOutlineEditor
|
|
702
|
+
v-if="editable"
|
|
703
|
+
ref="outlineEditorRef"
|
|
704
|
+
:session="session"
|
|
705
|
+
:version="renderVersion"
|
|
706
|
+
:resolve-image-src="resolveImageSrcProp"
|
|
707
|
+
@paste-image="handlePasteImage"
|
|
708
|
+
/>
|
|
709
|
+
<ul v-else class="mindmap-outline-root">
|
|
710
|
+
<MindmapOutlineNode
|
|
711
|
+
:node="session.focusRootNode"
|
|
712
|
+
:version="renderVersion"
|
|
713
|
+
root
|
|
714
|
+
@toggle="toggleNode"
|
|
715
|
+
/>
|
|
716
|
+
</ul>
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
<div
|
|
720
|
+
v-else-if="activeView === 'source'"
|
|
721
|
+
class="mindmap-source-wrap mindmap-pane"
|
|
722
|
+
:class="{ 'is-editable': editable }"
|
|
723
|
+
>
|
|
724
|
+
<MindmapMarkdownEditor
|
|
725
|
+
v-if="editable && session"
|
|
726
|
+
ref="sourceEditorRef"
|
|
727
|
+
:model-value="session.getMarkdown()"
|
|
728
|
+
:diagnostics="session.diagnostics"
|
|
729
|
+
@update:model-value="onSourceMarkdown"
|
|
730
|
+
@paste-image="onSourcePasteImage"
|
|
731
|
+
/>
|
|
732
|
+
<pre v-else class="mindmap-source"><code>{{ normalizedContent }}</code></pre>
|
|
733
|
+
</div>
|
|
734
|
+
</section>
|
|
735
|
+
</template>
|
|
736
|
+
|
|
737
|
+
<style scoped lang="scss">
|
|
738
|
+
.mindmap-preview {
|
|
739
|
+
--mindmap-panel: var(--tn-c-bg-soft);
|
|
740
|
+
--mindmap-border: var(--tn-c-divider);
|
|
741
|
+
position: relative;
|
|
742
|
+
margin: 1.5rem 0;
|
|
743
|
+
overflow: hidden;
|
|
744
|
+
border: 1px solid var(--mindmap-border);
|
|
745
|
+
border-radius: 10px;
|
|
746
|
+
background: var(--tn-c-bg);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.mindmap-preview.is-editable {
|
|
750
|
+
/* Shared with fixed chrome (toolbar / context menu) via inheritance. */
|
|
751
|
+
--mm-canvas-bg: var(--tn-c-bg);
|
|
752
|
+
--mm-panel-bg: var(--tn-c-bg-soft, var(--tn-c-bg));
|
|
753
|
+
--mm-text: var(--tn-c-text);
|
|
754
|
+
--mm-text-dim: var(--tn-c-text-2, var(--tn-c-text));
|
|
755
|
+
--mm-accent: var(--tn-c-brand);
|
|
756
|
+
--mm-border: var(--tn-c-divider, var(--mindmap-border));
|
|
757
|
+
--mm-hover: var(--tn-c-default-soft, color-mix(in srgb, var(--tn-c-text) 8%, transparent));
|
|
758
|
+
--mm-selected-bg: color-mix(in srgb, var(--tn-c-brand) 22%, transparent);
|
|
759
|
+
--mm-edit-bg: var(--tn-c-bg-elv, var(--tn-c-bg));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.mindmap-preview.is-editable.is-interaction-active {
|
|
763
|
+
outline: 1px solid color-mix(in srgb, var(--tn-c-brand, #3b82f6) 55%, transparent);
|
|
764
|
+
outline-offset: 0;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.mindmap-preview-actions {
|
|
768
|
+
position: absolute;
|
|
769
|
+
top: 8px;
|
|
770
|
+
right: 8px;
|
|
771
|
+
/* Above SelectionToolbar / LinkPopover (fixed ~120) so view tabs stay clickable. */
|
|
772
|
+
z-index: 200;
|
|
773
|
+
display: flex;
|
|
774
|
+
align-items: center;
|
|
775
|
+
gap: 4px;
|
|
776
|
+
padding: 2px;
|
|
777
|
+
border: .1px solid var(--tn-c-divider);
|
|
778
|
+
border-radius: 7px;
|
|
779
|
+
background-color: color-mix(in srgb, var(--tn-c-bg-elv) 92%, transparent);
|
|
780
|
+
box-shadow: var(--tn-shadow-2);
|
|
781
|
+
opacity: 0;
|
|
782
|
+
pointer-events: none;
|
|
783
|
+
transition: opacity .2s;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/* Editable Desk island: chrome must stay hittable. */
|
|
787
|
+
.mindmap-preview.is-editable > .mindmap-preview-actions {
|
|
788
|
+
opacity: 1;
|
|
789
|
+
pointer-events: auto;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.mindmap-preview-tabs {
|
|
793
|
+
display: flex;
|
|
794
|
+
gap: 4px;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.mindmap-preview:hover > .mindmap-preview-actions,
|
|
798
|
+
.mindmap-preview-actions:focus-within,
|
|
799
|
+
.mindmap-preview.is-fullscreen > .mindmap-preview-actions,
|
|
800
|
+
.mindmap-preview:fullscreen > .mindmap-preview-actions {
|
|
801
|
+
opacity: 1;
|
|
802
|
+
pointer-events: auto;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.mindmap-preview-action {
|
|
806
|
+
display: inline-flex;
|
|
807
|
+
align-items: center;
|
|
808
|
+
justify-content: center;
|
|
809
|
+
width: 32px;
|
|
810
|
+
height: 32px;
|
|
811
|
+
padding: 0;
|
|
812
|
+
border: 0;
|
|
813
|
+
border-radius: 6px;
|
|
814
|
+
background: transparent;
|
|
815
|
+
color: var(--tn-c-brand);
|
|
816
|
+
cursor: pointer;
|
|
817
|
+
transition: background-color .2s, transform .2s;
|
|
818
|
+
|
|
819
|
+
svg,
|
|
820
|
+
img {
|
|
821
|
+
width: 18px;
|
|
822
|
+
height: 18px;
|
|
823
|
+
pointer-events: none;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
&:hover {
|
|
827
|
+
background-color: var(--tn-c-default-soft);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
&.is-active {
|
|
831
|
+
background-color: color-mix(in srgb, var(--tn-c-brand) 28%, transparent);
|
|
832
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tn-c-brand) 55%, transparent);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
&:hover { transform: scale(1.05); }
|
|
836
|
+
&:active { transform: scale(.95); }
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.mindmap-preview-action-divider {
|
|
840
|
+
width: 1px;
|
|
841
|
+
height: 20px;
|
|
842
|
+
margin: 0 1px;
|
|
843
|
+
background: var(--tn-c-divider);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.mindmap-preview-expand {
|
|
847
|
+
display: inline-flex;
|
|
848
|
+
align-items: center;
|
|
849
|
+
gap: 4px;
|
|
850
|
+
height: 32px;
|
|
851
|
+
padding: 0 6px;
|
|
852
|
+
border-radius: 6px;
|
|
853
|
+
color: var(--tn-c-brand);
|
|
854
|
+
font-size: 12px;
|
|
855
|
+
cursor: default;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.mindmap-preview-expand-label {
|
|
859
|
+
opacity: 0.85;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
.mindmap-preview-expand-input {
|
|
863
|
+
width: 2.4rem;
|
|
864
|
+
height: 22px;
|
|
865
|
+
padding: 0 4px;
|
|
866
|
+
border: 1px solid var(--tn-c-divider);
|
|
867
|
+
border-radius: 4px;
|
|
868
|
+
background: var(--tn-c-bg);
|
|
869
|
+
color: var(--tn-c-text);
|
|
870
|
+
font-size: 12px;
|
|
871
|
+
line-height: 22px;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
.mindmap-canvas-host {
|
|
875
|
+
position: relative;
|
|
876
|
+
width: 100%;
|
|
877
|
+
height: 440px;
|
|
878
|
+
overflow: hidden;
|
|
879
|
+
background: var(--tn-c-bg);
|
|
880
|
+
touch-action: none;
|
|
881
|
+
user-select: none;
|
|
882
|
+
|
|
883
|
+
/* CanvasEditor overlay + chrome tokens (aligned with mindmap-web). */
|
|
884
|
+
--mm-canvas-bg: var(--tn-c-bg);
|
|
885
|
+
--mm-panel-bg: var(--tn-c-bg-soft, var(--tn-c-bg));
|
|
886
|
+
--mm-text: var(--tn-c-text);
|
|
887
|
+
--mm-text-dim: var(--tn-c-text-2, var(--tn-c-text));
|
|
888
|
+
--mm-accent: var(--tn-c-brand);
|
|
889
|
+
--mm-border: var(--tn-c-divider, var(--mindmap-border));
|
|
890
|
+
--mm-hover: var(--tn-c-default-soft, color-mix(in srgb, var(--tn-c-text) 8%, transparent));
|
|
891
|
+
--mm-edit-bg: var(--tn-c-bg-elv, var(--tn-c-bg));
|
|
892
|
+
|
|
893
|
+
&.is-interaction-active {
|
|
894
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--tn-c-brand) 38%, transparent);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.mindmap-canvas-host :deep(.mindmap-view-host) {
|
|
899
|
+
width: 100%;
|
|
900
|
+
height: 100%;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.mindmap-canvas-host:deep(.mm-editor) {
|
|
904
|
+
position: relative;
|
|
905
|
+
width: 100%;
|
|
906
|
+
height: 100%;
|
|
907
|
+
overflow: hidden;
|
|
908
|
+
outline: none;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.mindmap-canvas-host:deep(.mm-canvas),
|
|
912
|
+
.mindmap-canvas-host:deep(.mm-overlay) {
|
|
913
|
+
position: absolute;
|
|
914
|
+
inset: 0;
|
|
915
|
+
width: 100%;
|
|
916
|
+
height: 100%;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.mindmap-canvas-host:deep(.mm-overlay) {
|
|
920
|
+
pointer-events: none;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/* Marquee box — required for box-select feedback (aligned with mindmap-web). */
|
|
924
|
+
.mindmap-canvas-host:deep(.mm-selection-box) {
|
|
925
|
+
position: absolute;
|
|
926
|
+
z-index: 8;
|
|
927
|
+
pointer-events: none;
|
|
928
|
+
border: 1px solid color-mix(in srgb, var(--mm-accent) 88%, white);
|
|
929
|
+
border-radius: 3px;
|
|
930
|
+
background: color-mix(in srgb, var(--mm-accent) 16%, transparent);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
.mindmap-canvas-host:deep(.mm-edit-input) {
|
|
934
|
+
position: absolute;
|
|
935
|
+
z-index: 10;
|
|
936
|
+
pointer-events: auto;
|
|
937
|
+
box-sizing: border-box;
|
|
938
|
+
padding: 4px 8px;
|
|
939
|
+
overflow: hidden;
|
|
940
|
+
border: 2px solid var(--mm-accent);
|
|
941
|
+
border-radius: 6px;
|
|
942
|
+
outline: none;
|
|
943
|
+
background: var(--mm-edit-bg);
|
|
944
|
+
box-shadow: 0 4px 16px rgb(0 0 0 / 0.15);
|
|
945
|
+
color: var(--mm-text);
|
|
946
|
+
/* Desk ProseMirror sets caret-color: transparent for virtual cursor — restore. */
|
|
947
|
+
caret-color: var(--mm-accent, #3b82f6);
|
|
948
|
+
font-family: inherit;
|
|
949
|
+
line-height: 1.4;
|
|
950
|
+
white-space: pre-wrap;
|
|
951
|
+
word-break: normal;
|
|
952
|
+
overflow-wrap: anywhere;
|
|
953
|
+
resize: none;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
.mindmap-canvas-host:deep(.mm-edit-input.is-root) {
|
|
957
|
+
border-radius: 9px;
|
|
958
|
+
background: #2b3139;
|
|
959
|
+
color: #fff;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
.mindmap-canvas-host:deep(.mm-edit-input.is-primary) {
|
|
963
|
+
background: #e7eaf0;
|
|
964
|
+
color: #2b3139;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
.mindmap-canvas-host:deep(.mm-edit-input.is-secondary),
|
|
968
|
+
.mindmap-canvas-host:deep(.mm-edit-input.is-tertiary) {
|
|
969
|
+
background: color-mix(in srgb, var(--mm-canvas-bg) 90%, var(--mm-accent) 10%);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
.mindmap-preview.is-dark .mindmap-canvas-host:deep(.mm-edit-input.is-root) {
|
|
973
|
+
background: #dedede;
|
|
974
|
+
color: #1d1d1f;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
.mindmap-preview.is-dark .mindmap-canvas-host:deep(.mm-edit-input.is-primary) {
|
|
978
|
+
background: #3b3b3d;
|
|
979
|
+
color: #f0f0f2;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.bold) { font-weight: 700; }
|
|
983
|
+
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.italic) { font-style: italic; }
|
|
984
|
+
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.underline) {
|
|
985
|
+
text-decoration-line: underline;
|
|
986
|
+
text-underline-offset: 3px;
|
|
987
|
+
}
|
|
988
|
+
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.strike) { text-decoration-line: line-through; }
|
|
989
|
+
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.underline.strike) {
|
|
990
|
+
text-decoration-line: underline line-through;
|
|
991
|
+
}
|
|
992
|
+
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.highlight:not(.code)) {
|
|
993
|
+
padding: 0 1px;
|
|
994
|
+
border-radius: 2px;
|
|
995
|
+
background: #fff36a;
|
|
996
|
+
color: #242424;
|
|
997
|
+
}
|
|
998
|
+
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.code) {
|
|
999
|
+
padding: 0 4px;
|
|
1000
|
+
border-radius: 4px;
|
|
1001
|
+
background: color-mix(in srgb, var(--mm-canvas-bg) 82%, var(--mm-text) 18%);
|
|
1002
|
+
color: var(--tn-c-danger, #e85d5d);
|
|
1003
|
+
font-family: var(--tn-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
1004
|
+
font-size: 0.92em;
|
|
1005
|
+
}
|
|
1006
|
+
.mindmap-canvas-host:deep(.mm-edit-input .inline-run.link) {
|
|
1007
|
+
color: var(--mm-accent);
|
|
1008
|
+
text-decoration: underline;
|
|
1009
|
+
text-underline-offset: 3px;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
.mindmap-canvas-host:deep(.mm-edit-add-button) {
|
|
1013
|
+
position: absolute;
|
|
1014
|
+
z-index: 12;
|
|
1015
|
+
display: inline-flex;
|
|
1016
|
+
align-items: center;
|
|
1017
|
+
justify-content: center;
|
|
1018
|
+
width: 20px;
|
|
1019
|
+
height: 20px;
|
|
1020
|
+
padding: 0;
|
|
1021
|
+
border: 0;
|
|
1022
|
+
border-radius: 50%;
|
|
1023
|
+
outline: none;
|
|
1024
|
+
background: var(--mm-text);
|
|
1025
|
+
box-shadow: 0 0 0 2px var(--mm-canvas-bg);
|
|
1026
|
+
color: var(--mm-canvas-bg);
|
|
1027
|
+
font: 600 18px/1 sans-serif;
|
|
1028
|
+
pointer-events: auto;
|
|
1029
|
+
cursor: pointer;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
.mindmap-canvas-host:deep(.mm-edit-add-button:hover),
|
|
1033
|
+
.mindmap-canvas-host:deep(.mm-edit-add-button:focus-visible) {
|
|
1034
|
+
background: var(--mm-accent);
|
|
1035
|
+
color: #fff;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
.mindmap-outline {
|
|
1039
|
+
max-height: 560px;
|
|
1040
|
+
padding: 18px 22px 22px;
|
|
1041
|
+
overflow: auto;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
.mindmap-outline.is-editable {
|
|
1045
|
+
height: 440px;
|
|
1046
|
+
max-height: none;
|
|
1047
|
+
padding: 0;
|
|
1048
|
+
overflow: hidden;
|
|
1049
|
+
display: flex;
|
|
1050
|
+
flex-direction: column;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.mindmap-source-wrap.is-editable {
|
|
1054
|
+
height: 440px;
|
|
1055
|
+
overflow: hidden;
|
|
1056
|
+
display: flex;
|
|
1057
|
+
flex-direction: column;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
.mindmap-outline.is-editable :deep(.outline-view),
|
|
1061
|
+
.mindmap-source-wrap.is-editable :deep(.markdown-view) {
|
|
1062
|
+
flex: 1 1 0;
|
|
1063
|
+
min-height: 0;
|
|
1064
|
+
height: 100%;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.mindmap-outline-root,
|
|
1068
|
+
.mindmap-outline :deep(ul) {
|
|
1069
|
+
margin: 0;
|
|
1070
|
+
padding: 0;
|
|
1071
|
+
list-style: none;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
.mindmap-outline :deep(.mindmap-outline-children) {
|
|
1075
|
+
margin-left: 10px;
|
|
1076
|
+
padding-left: 19px;
|
|
1077
|
+
border-left: 1px solid var(--tn-c-divider);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
.mindmap-outline :deep(.mindmap-outline-row) {
|
|
1081
|
+
display: flex;
|
|
1082
|
+
align-items: flex-start;
|
|
1083
|
+
gap: 7px;
|
|
1084
|
+
min-height: 30px;
|
|
1085
|
+
padding: 3px 0;
|
|
1086
|
+
color: var(--tn-c-text);
|
|
1087
|
+
line-height: 24px;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.mindmap-outline :deep(.mindmap-outline-toggle),
|
|
1091
|
+
.mindmap-outline :deep(.mindmap-outline-leaf) {
|
|
1092
|
+
flex: 0 0 18px;
|
|
1093
|
+
width: 18px;
|
|
1094
|
+
color: var(--tn-c-text-2);
|
|
1095
|
+
text-align: center;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
.mindmap-outline :deep(.mindmap-outline-toggle:hover) { color: var(--tn-c-brand); }
|
|
1099
|
+
.mindmap-outline :deep(.mindmap-outline-checkbox) { margin-top: 5px; }
|
|
1100
|
+
.mindmap-outline :deep(.mindmap-outline-label) { min-width: 0; overflow-wrap: anywhere; }
|
|
1101
|
+
.mindmap-outline :deep(.mindmap-outline-node.is-root > .mindmap-outline-row) { font-size: 18px; font-weight: 700; }
|
|
1102
|
+
.mindmap-outline :deep(.mindmap-outline-node.is-done > .mindmap-outline-row .mindmap-outline-label) { opacity: .58; text-decoration: line-through; }
|
|
1103
|
+
.mindmap-outline :deep(.mindmap-outline-image) { display: block; max-width: min(100%, 560px); max-height: 360px; margin: 5px 0 12px 25px; border-radius: 6px; }
|
|
1104
|
+
.mindmap-outline :deep(.is-bold) { font-weight: 700; }
|
|
1105
|
+
.mindmap-outline :deep(.is-italic) { font-style: italic; }
|
|
1106
|
+
.mindmap-outline :deep(.is-underline) { text-decoration: underline; }
|
|
1107
|
+
.mindmap-outline :deep(.is-strike) { text-decoration: line-through; }
|
|
1108
|
+
.mindmap-outline :deep(.is-highlight) { padding: 0 2px; border-radius: 2px; background: #ffe56b; color: #252525; }
|
|
1109
|
+
.mindmap-outline :deep(.is-code) { padding: 1px 5px; border-radius: 4px; background: var(--tn-c-bg-soft); color: var(--tn-c-danger); font-family: var(--tn-font-mono); }
|
|
1110
|
+
.mindmap-outline :deep(.is-link) { color: var(--tn-c-brand); text-decoration: underline; text-underline-offset: 3px; }
|
|
1111
|
+
|
|
1112
|
+
.mindmap-source {
|
|
1113
|
+
max-height: 560px;
|
|
1114
|
+
margin: 0;
|
|
1115
|
+
padding: 18px 22px;
|
|
1116
|
+
overflow: auto;
|
|
1117
|
+
border-radius: 0;
|
|
1118
|
+
background: var(--tn-c-bg-elv);
|
|
1119
|
+
color: var(--tn-c-text);
|
|
1120
|
+
font-size: 13px;
|
|
1121
|
+
line-height: 1.65;
|
|
1122
|
+
white-space: pre;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
.mindmap-preview:fullscreen,
|
|
1126
|
+
.mindmap-preview.is-fullscreen {
|
|
1127
|
+
/* Desk frameless titlebar is ~42px with -webkit-app-region:drag. */
|
|
1128
|
+
--mm-fs-chrome-top: 48px;
|
|
1129
|
+
position: fixed;
|
|
1130
|
+
inset: 0;
|
|
1131
|
+
z-index: 200000;
|
|
1132
|
+
display: flex;
|
|
1133
|
+
box-sizing: border-box;
|
|
1134
|
+
width: 100%;
|
|
1135
|
+
height: 100%;
|
|
1136
|
+
margin: 0;
|
|
1137
|
+
padding-top: var(--mm-fs-chrome-top);
|
|
1138
|
+
border: 0;
|
|
1139
|
+
border-radius: 0;
|
|
1140
|
+
background: var(--tn-c-bg);
|
|
1141
|
+
flex-direction: column;
|
|
1142
|
+
overflow: hidden;
|
|
1143
|
+
|
|
1144
|
+
> .mindmap-preview-actions {
|
|
1145
|
+
top: var(--mm-fs-chrome-top);
|
|
1146
|
+
right: 12px;
|
|
1147
|
+
z-index: 10;
|
|
1148
|
+
opacity: 1;
|
|
1149
|
+
pointer-events: auto;
|
|
1150
|
+
-webkit-app-region: no-drag;
|
|
1151
|
+
app-region: no-drag;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
> .focus-breadcrumbs {
|
|
1155
|
+
-webkit-app-region: no-drag;
|
|
1156
|
+
app-region: no-drag;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/*
|
|
1160
|
+
* Only one .mindmap-pane exists (v-if). Use flex-basis:0 + grow — do NOT set
|
|
1161
|
+
* height:0 as a separate property (locks the pane at 0px in WebKit/Electron).
|
|
1162
|
+
*/
|
|
1163
|
+
.mindmap-pane {
|
|
1164
|
+
flex: 1 1 0%;
|
|
1165
|
+
width: 100%;
|
|
1166
|
+
min-height: 0;
|
|
1167
|
+
max-height: none;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
.mindmap-canvas-host,
|
|
1171
|
+
.mindmap-outline,
|
|
1172
|
+
.mindmap-outline.is-editable,
|
|
1173
|
+
.mindmap-source-wrap,
|
|
1174
|
+
.mindmap-source-wrap.is-editable {
|
|
1175
|
+
height: auto;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.mindmap-outline.is-editable,
|
|
1179
|
+
.mindmap-source-wrap.is-editable {
|
|
1180
|
+
display: flex;
|
|
1181
|
+
flex-direction: column;
|
|
1182
|
+
overflow: hidden;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
.mindmap-outline.is-editable :deep(.outline-view),
|
|
1186
|
+
.mindmap-source-wrap.is-editable :deep(.markdown-view) {
|
|
1187
|
+
flex: 1 1 0%;
|
|
1188
|
+
min-height: 0;
|
|
1189
|
+
height: auto;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
@media (max-width: 768px) {
|
|
1194
|
+
.mindmap-canvas-host { height: 360px; }
|
|
1195
|
+
.mindmap-preview-actions { top: 6px; right: 6px; }
|
|
1196
|
+
.mindmap-preview-action { width: 28px; height: 28px; }
|
|
1197
|
+
.mindmap-outline { padding-inline: 12px; }
|
|
1198
|
+
}
|
|
1199
|
+
</style>
|
|
1200
|
+
|
|
1201
|
+
<!-- Unscoped: while one mindmap owns fullscreen, hide chrome on all others. -->
|
|
1202
|
+
<style lang="scss">
|
|
1203
|
+
html[data-tn-mindmap-fs] .mindmap-preview:not(.is-fullscreen) .mindmap-preview-actions,
|
|
1204
|
+
body[data-tn-mindmap-fs] .mindmap-preview:not(.is-fullscreen) .mindmap-preview-actions {
|
|
1205
|
+
display: none !important;
|
|
1206
|
+
opacity: 0 !important;
|
|
1207
|
+
pointer-events: none !important;
|
|
1208
|
+
visibility: hidden !important;
|
|
1209
|
+
}
|
|
1210
|
+
</style>
|