@tnotesjs/ui 0.1.5 → 0.2.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.
Files changed (67) hide show
  1. package/README.md +30 -97
  2. package/dist/code/highlight.js +6 -15
  3. package/dist/components/Footprints/parse.js +1 -3
  4. package/dist/components/Mindmap/markdown.d.ts +1 -7
  5. package/dist/components/Mindmap/markdown.js +1 -15
  6. package/package.json +24 -8
  7. package/src/browser/clipboard.ts +16 -19
  8. package/src/browser/theme.ts +10 -15
  9. package/src/code/highlight.test.ts +23 -6
  10. package/src/code/highlight.ts +89 -105
  11. package/src/components/Badge/Badge.vue +4 -4
  12. package/src/components/CodeBlock/CodeBlock.vue +43 -53
  13. package/src/components/CodeGroup/CodeGroup.vue +32 -35
  14. package/src/components/Footprints/Footprints.vue +2 -14
  15. package/src/components/Footprints/parse.ts +1 -3
  16. package/src/components/ImagePreview/ImagePreview.vue +101 -126
  17. package/src/components/Mermaid/Mermaid.vue +9 -12
  18. package/src/components/Mindmap/FocusBreadcrumbs.test.ts +40 -30
  19. package/src/components/Mindmap/FocusBreadcrumbs.vue +31 -28
  20. package/src/components/Mindmap/InlineRuns.ts +25 -16
  21. package/src/components/Mindmap/Mindmap.vue +147 -81
  22. package/src/components/Mindmap/MindmapOutlineNode.vue +4 -1
  23. package/src/components/Mindmap/editor/AppIcon.vue +32 -11
  24. package/src/components/Mindmap/editor/CanvasContextMenu.vue +126 -49
  25. package/src/components/Mindmap/editor/LinkPopover.vue +101 -23
  26. package/src/components/Mindmap/editor/MarkdownView.vue +7 -4
  27. package/src/components/Mindmap/editor/MindmapView.vue +11 -3
  28. package/src/components/Mindmap/editor/OutlineView.vue +144 -58
  29. package/src/components/Mindmap/editor/RichInlineEditor.vue +93 -44
  30. package/src/components/Mindmap/editor/SelectionToolbar.vue +133 -41
  31. package/src/components/Mindmap/editor/canvasClipboard.ts +1 -5
  32. package/src/components/Mindmap/editor/imagePaste.ts +1 -1
  33. package/src/components/Mindmap/editor/mindmapClipboard.ts +17 -5
  34. package/src/components/Mindmap/editor/outlineDrag.ts +1 -1
  35. package/src/components/Mindmap/markdown.ts +10 -29
  36. package/src/components/WordList/RightClickMenu.vue +10 -35
  37. package/src/components/WordList/WordList.vue +19 -42
  38. package/src/components/sharedMedia.test.ts +94 -112
  39. package/src/entries/bilibili-video.ts +1 -1
  40. package/src/entries/excalidraw-editor.ts +20 -0
  41. package/src/entries/excalidraw-view.ts +15 -0
  42. package/src/entries/footprints.ts +1 -1
  43. package/src/entries/mermaid.ts +1 -1
  44. package/src/entries/mindmap.ts +1 -1
  45. package/src/entries/notes-table.ts +2 -2
  46. package/src/entries/word-list.ts +1 -1
  47. package/src/excalidraw/ExcalidrawSvg.vue +125 -0
  48. package/src/excalidraw/editorHost.ts +110 -0
  49. package/src/excalidraw/editorSession.test.ts +205 -0
  50. package/src/excalidraw/editorSession.ts +224 -0
  51. package/src/excalidraw/exporter.ts +50 -0
  52. package/src/excalidraw/focus.test.ts +60 -0
  53. package/src/excalidraw/focus.ts +33 -0
  54. package/src/excalidraw/fonts.test.ts +66 -0
  55. package/src/excalidraw/fonts.ts +92 -0
  56. package/src/excalidraw/renderer.test.ts +148 -0
  57. package/src/excalidraw/renderer.ts +105 -0
  58. package/src/excalidraw/scene.ts +93 -0
  59. package/src/index.ts +25 -33
  60. package/src/markdown/image.test.ts +1 -3
  61. package/src/markdown/image.ts +4 -13
  62. package/src/styles/code.css +5 -5
  63. package/src/styles/prose.css +1 -1
  64. package/src/styles/swiper.css +1 -1
  65. package/src/styles/tokens.css +5 -6
  66. package/src/swiper/hydrate.test.ts +37 -41
  67. package/src/swiper/hydrate.ts +91 -102
@@ -8,22 +8,25 @@ import {
8
8
  replaceInlineRange,
9
9
  richSelectionOffsets,
10
10
  setRichSelection,
11
- stripInline,
11
+ stripInline
12
12
  } from '@tnotesjs/mindmap-core'
13
13
  import type { InlineLink, RichInlineEditorElement } from '@tnotesjs/mindmap-core'
14
14
 
15
- const props = withDefaults(defineProps<{
16
- editorId: string
17
- raw: string
18
- active: boolean
19
- done?: boolean
20
- placeholder?: string
21
- pasteMode?: 'single-line' | 'outline'
22
- }>(), {
23
- done: false,
24
- placeholder: '',
25
- pasteMode: 'single-line',
26
- })
15
+ const props = withDefaults(
16
+ defineProps<{
17
+ editorId: string
18
+ raw: string
19
+ active: boolean
20
+ done?: boolean
21
+ placeholder?: string
22
+ pasteMode?: 'single-line' | 'outline'
23
+ }>(),
24
+ {
25
+ done: false,
26
+ placeholder: '',
27
+ pasteMode: 'single-line'
28
+ }
29
+ )
27
30
 
28
31
  interface DraftPayload {
29
32
  raw: string
@@ -89,7 +92,10 @@ function payload(): DraftPayload {
89
92
  return { raw: draftRaw.value, text: stripInline(draftRaw.value) }
90
93
  }
91
94
 
92
- function renderDraft(nextRaw: string, selection?: { start: number; end?: number; direction?: 'forward' | 'backward' }) {
95
+ function renderDraft(
96
+ nextRaw: string,
97
+ selection?: { start: number; end?: number; direction?: 'forward' | 'backward' }
98
+ ) {
93
99
  draftRaw.value = nextRaw
94
100
  dirty = nextRaw !== sourceRaw
95
101
  renderInlineDom()
@@ -112,7 +118,7 @@ function replaceSelection(textToInsert: string) {
112
118
  end: text.value.length,
113
119
  anchor: text.value.length,
114
120
  focus: text.value.length,
115
- direction: 'forward' as const,
121
+ direction: 'forward' as const
116
122
  }
117
123
  const nextRaw = replaceInlineRange(draftRaw.value, selection.start, selection.end, textToInsert)
118
124
  const caret = selection.start + textToInsert.length
@@ -136,7 +142,7 @@ function onBeforeInput(event: InputEvent) {
136
142
  end: text.value.length,
137
143
  anchor: text.value.length,
138
144
  focus: text.value.length,
139
- direction: 'forward' as const,
145
+ direction: 'forward' as const
140
146
  }
141
147
 
142
148
  if (event.inputType === 'insertText' || event.inputType === 'insertReplacementText') {
@@ -149,25 +155,31 @@ function onBeforeInput(event: InputEvent) {
149
155
 
150
156
  if (event.inputType === 'deleteContentBackward') {
151
157
  event.preventDefault()
152
- const start = selection.start === selection.end
153
- ? previousGraphemeOffset(text.value, selection.start)
154
- : selection.start
158
+ const start =
159
+ selection.start === selection.end
160
+ ? previousGraphemeOffset(text.value, selection.start)
161
+ : selection.start
155
162
  renderDraft(replaceInlineRange(draftRaw.value, start, selection.end, ''), { start })
156
163
  return
157
164
  }
158
165
 
159
166
  if (event.inputType === 'deleteContentForward') {
160
167
  event.preventDefault()
161
- const end = selection.start === selection.end
162
- ? nextGraphemeOffset(text.value, selection.end)
163
- : selection.end
164
- renderDraft(replaceInlineRange(draftRaw.value, selection.start, end, ''), { start: selection.start })
168
+ const end =
169
+ selection.start === selection.end
170
+ ? nextGraphemeOffset(text.value, selection.end)
171
+ : selection.end
172
+ renderDraft(replaceInlineRange(draftRaw.value, selection.start, end, ''), {
173
+ start: selection.start
174
+ })
165
175
  return
166
176
  }
167
177
 
168
178
  if (event.inputType === 'deleteByCut') {
169
179
  event.preventDefault()
170
- renderDraft(replaceInlineRange(draftRaw.value, selection.start, selection.end, ''), { start: selection.start })
180
+ renderDraft(replaceInlineRange(draftRaw.value, selection.start, selection.end, ''), {
181
+ start: selection.start
182
+ })
171
183
  return
172
184
  }
173
185
 
@@ -240,8 +252,11 @@ function onLinkClick(link: InlineLink, event: Event) {
240
252
  emit('linkClick', link, event)
241
253
  }
242
254
 
243
- function linkFromTarget(target: EventTarget | null): { element: HTMLElement; link: InlineLink } | null {
244
- const element = target instanceof HTMLElement ? target.closest<HTMLElement>('.inline-run.link') : null
255
+ function linkFromTarget(
256
+ target: EventTarget | null
257
+ ): { element: HTMLElement; link: InlineLink } | null {
258
+ const element =
259
+ target instanceof HTMLElement ? target.closest<HTMLElement>('.inline-run.link') : null
245
260
  const root = rootRef.value
246
261
  if (!element || !root?.contains(element) || !element.dataset.linkUrl) return null
247
262
  return {
@@ -249,22 +264,30 @@ function linkFromTarget(target: EventTarget | null): { element: HTMLElement; lin
249
264
  link: {
250
265
  url: element.dataset.linkUrl,
251
266
  rawStart: Number(element.dataset.linkRawStart ?? 0),
252
- rawEnd: Number(element.dataset.linkRawEnd ?? 0),
253
- },
267
+ rawEnd: Number(element.dataset.linkRawEnd ?? 0)
268
+ }
254
269
  }
255
270
  }
256
271
 
257
272
  function onMouseOver(event: MouseEvent) {
258
273
  if (props.active) return
259
274
  const found = linkFromTarget(event.target)
260
- if (!found || (event.relatedTarget instanceof Node && found.element.contains(event.relatedTarget))) return
275
+ if (
276
+ !found ||
277
+ (event.relatedTarget instanceof Node && found.element.contains(event.relatedTarget))
278
+ )
279
+ return
261
280
  emit('linkEnter', found.link, event)
262
281
  }
263
282
 
264
283
  function onMouseOut(event: MouseEvent) {
265
284
  if (props.active) return
266
285
  const found = linkFromTarget(event.target)
267
- if (!found || (event.relatedTarget instanceof Node && found.element.contains(event.relatedTarget))) return
286
+ if (
287
+ !found ||
288
+ (event.relatedTarget instanceof Node && found.element.contains(event.relatedTarget))
289
+ )
290
+ return
268
291
  emit('linkLeave')
269
292
  }
270
293
 
@@ -288,13 +311,13 @@ function installCompatibilityApi(root: RichInlineEditorElement) {
288
311
  get: () => stripInline(draftRaw.value),
289
312
  set: (next: string) => {
290
313
  renderDraft(replaceInlineDisplayText(draftRaw.value, String(next)))
291
- },
314
+ }
292
315
  },
293
316
  rawValue: { configurable: true, get: () => draftRaw.value },
294
317
  selectionStart: { configurable: true, get: () => richSelectionOffsets(root)?.start ?? 0 },
295
318
  selectionEnd: { configurable: true, get: () => richSelectionOffsets(root)?.end ?? 0 },
296
319
  isDirty: { configurable: true, get: () => dirty },
297
- isComposing: { configurable: true, get: () => composing },
320
+ isComposing: { configurable: true, get: () => composing }
298
321
  })
299
322
  root.setSelectionRange = (start: number, end: number) => setRichSelection(root, start, end)
300
323
  root.markCommitted = () => {
@@ -320,7 +343,7 @@ watch(
320
343
  draftRaw.value = raw
321
344
  renderInlineDom()
322
345
  }
323
- },
346
+ }
324
347
  )
325
348
 
326
349
  watch(
@@ -328,7 +351,7 @@ watch(
328
351
  (active) => {
329
352
  if (!active) emitCommit()
330
353
  if (!composing && !dirty) renderInlineDom()
331
- },
354
+ }
332
355
  )
333
356
 
334
357
  onBeforeUnmount(emitCommit)
@@ -371,23 +394,49 @@ onBeforeUnmount(emitCommit)
371
394
  color: var(--mm-text-dim);
372
395
  pointer-events: none;
373
396
  }
374
- .rich-inline-editor.active { cursor: text; }
375
- .rich-inline-editor :deep(.inline-run.bold) { font-weight: 700; }
376
- .rich-inline-editor :deep(.inline-run.italic) { font-style: italic; }
377
- .rich-inline-editor :deep(.inline-run.underline) { text-decoration-line: underline; text-underline-offset: 3px; }
378
- .rich-inline-editor :deep(.inline-run.strike) { text-decoration-line: line-through; }
379
- .rich-inline-editor :deep(.inline-run.underline.strike) { text-decoration-line: underline line-through; }
380
- .rich-inline-editor :deep(.inline-run.highlight:not(.code)) { padding: 0 1px; border-radius: 2px; background: #fff36a; color: #242424; }
397
+ .rich-inline-editor.active {
398
+ cursor: text;
399
+ }
400
+ .rich-inline-editor :deep(.inline-run.bold) {
401
+ font-weight: 700;
402
+ }
403
+ .rich-inline-editor :deep(.inline-run.italic) {
404
+ font-style: italic;
405
+ }
406
+ .rich-inline-editor :deep(.inline-run.underline) {
407
+ text-decoration-line: underline;
408
+ text-underline-offset: 3px;
409
+ }
410
+ .rich-inline-editor :deep(.inline-run.strike) {
411
+ text-decoration-line: line-through;
412
+ }
413
+ .rich-inline-editor :deep(.inline-run.underline.strike) {
414
+ text-decoration-line: underline line-through;
415
+ }
416
+ .rich-inline-editor :deep(.inline-run.highlight:not(.code)) {
417
+ padding: 0 1px;
418
+ border-radius: 2px;
419
+ background: #fff36a;
420
+ color: #242424;
421
+ }
381
422
  .rich-inline-editor :deep(.inline-run.code) {
382
423
  padding: 2px 6px;
383
424
  border-radius: 5px;
384
425
  background: color-mix(in srgb, var(--mm-text) 18%, transparent);
385
426
  color: #f08a6e;
386
427
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
387
- font-size: .92em;
428
+ font-size: 0.92em;
388
429
  box-decoration-break: clone;
389
430
  -webkit-box-decoration-break: clone;
390
431
  }
391
- .rich-inline-editor :deep(.inline-run.link) { color: var(--mm-accent); cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
392
- .rich-inline-editor.done { color: var(--mm-text-dim); text-decoration: line-through; }
432
+ .rich-inline-editor :deep(.inline-run.link) {
433
+ color: var(--mm-accent);
434
+ cursor: pointer;
435
+ text-decoration: underline;
436
+ text-underline-offset: 3px;
437
+ }
438
+ .rich-inline-editor.done {
439
+ color: var(--mm-text-dim);
440
+ text-decoration: line-through;
441
+ }
393
442
  </style>
@@ -17,7 +17,7 @@ const props = withDefaults(
17
17
  placement?: 'fixed' | 'canvas-bottom'
18
18
  activeFormats?: Partial<Record<InlineFormat, boolean>>
19
19
  }>(),
20
- { placement: 'fixed' },
20
+ { placement: 'fixed' }
21
21
  )
22
22
 
23
23
  const emit = defineEmits<{
@@ -35,7 +35,7 @@ const formats: Array<{ id: InlineFormat; text: string; label: string }> = [
35
35
  { id: 'italic', text: 'I', label: `斜体 (${primaryShortcut('I')})` },
36
36
  { id: 'underline', text: 'U', label: `下划线 (${primaryShortcut('U')})` },
37
37
  { id: 'strike', text: 'S', label: `删除线 (${primaryShortcut('Enter')})` },
38
- { id: 'highlight', text: '▰', label: '高亮' },
38
+ { id: 'highlight', text: '▰', label: '高亮' }
39
39
  ]
40
40
 
41
41
  const toolbarRef = ref<HTMLElement | null>(null)
@@ -55,7 +55,10 @@ const toolbarStyle = computed(() => {
55
55
  const left = Math.min(maxLeft, Math.max(VIEWPORT_MARGIN, point.left - width / 2))
56
56
  const preferredTop = point.top - height - ANCHOR_GAP
57
57
  const belowTop = point.top + ANCHOR_GAP
58
- const top = Math.min(maxTop, Math.max(VIEWPORT_MARGIN, preferredTop < VIEWPORT_MARGIN ? belowTop : preferredTop))
58
+ const top = Math.min(
59
+ maxTop,
60
+ Math.max(VIEWPORT_MARGIN, preferredTop < VIEWPORT_MARGIN ? belowTop : preferredTop)
61
+ )
59
62
  return { left: `${left}px`, top: `${top}px` }
60
63
  })
61
64
 
@@ -69,7 +72,7 @@ function measureToolbar() {
69
72
 
70
73
  watch(
71
74
  () => [props.mode, props.placement, props.position?.left, props.position?.top],
72
- measureToolbar,
75
+ measureToolbar
73
76
  )
74
77
  onMounted(() => {
75
78
  measureToolbar()
@@ -88,33 +91,88 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
88
91
  :aria-label="mode === 'text' ? '文字格式工具栏' : '多主题工具栏'"
89
92
  @pointerdown.prevent
90
93
  >
94
+ <button
95
+ v-for="item in formats"
96
+ :key="item.id"
97
+ type="button"
98
+ class="format-button"
99
+ :class="[{ active: activeFormats?.[item.id] }, `is-${item.id}`]"
100
+ :data-tooltip="item.label"
101
+ :aria-label="item.label"
102
+ @click="emit('format', item.id)"
103
+ >
104
+ <AppIcon v-if="item.id === 'highlight'" name="highlight" :size="21" />
105
+ <template v-else>{{ item.text }}</template>
106
+ </button>
107
+ <span class="toolbar-divider" />
108
+ <button
109
+ type="button"
110
+ class="tool-button"
111
+ :data-tooltip="`添加/取消待办 (${primaryShortcut('L', { shift: true })})`"
112
+ aria-label="添加或取消待办"
113
+ @click="emit('task')"
114
+ >
115
+ <AppIcon name="check" :size="20" />
116
+ </button>
117
+ <template v-if="mode === 'text'">
91
118
  <button
92
- v-for="item in formats"
93
- :key="item.id"
94
119
  type="button"
95
- class="format-button"
96
- :class="[{ active: activeFormats?.[item.id] }, `is-${item.id}`]"
97
- :data-tooltip="item.label"
98
- :aria-label="item.label"
99
- @click="emit('format', item.id)"
120
+ class="tool-button"
121
+ :data-tooltip="`添加图片 (${altShortcut('Enter')})`"
122
+ aria-label="添加图片"
123
+ @click="emit('image')"
100
124
  >
101
- <AppIcon v-if="item.id === 'highlight'" name="highlight" :size="21" />
102
- <template v-else>{{ item.text }}</template>
125
+ <AppIcon name="image" :size="20" />
103
126
  </button>
104
- <span class="toolbar-divider" />
105
- <button type="button" class="tool-button" :data-tooltip="`添加/取消待办 (${primaryShortcut('L', { shift: true })})`" aria-label="添加或取消待办" @click="emit('task')">
106
- <AppIcon name="check" :size="20" />
127
+ <button
128
+ type="button"
129
+ class="tool-button"
130
+ :data-tooltip="`添加链接 (${primaryShortcut('K')})`"
131
+ aria-label="添加链接"
132
+ @click="emit('link')"
133
+ >
134
+ <AppIcon name="link" :size="20" />
107
135
  </button>
108
- <template v-if="mode === 'text'">
109
- <button type="button" class="tool-button" :data-tooltip="`添加图片 (${altShortcut('Enter')})`" aria-label="添加图片" @click="emit('image')"><AppIcon name="image" :size="20" /></button>
110
- <button type="button" class="tool-button" :data-tooltip="`添加链接 (${primaryShortcut('K')})`" aria-label="添加链接" @click="emit('link')"><AppIcon name="link" :size="20" /></button>
111
- <button type="button" class="tool-button code-button" :data-tooltip="`行内代码 (${primaryShortcut('E')})`" aria-label="行内代码" @click="emit('format', 'code')">&lt;/&gt;</button>
112
- </template>
113
- <button v-else type="button" class="tool-button" :data-tooltip="`复制 (${primaryShortcut('C')})`" aria-label="复制所选主题" @click="emit('copy')"><AppIcon name="copy" :size="20" /></button>
114
- <span class="toolbar-divider" />
115
- <button type="button" class="tool-button" :data-tooltip="`清除样式 (${primaryShortcut('\\')})`" aria-label="清除样式" @click="emit('clear')"><AppIcon name="clearFormat" :size="20" /></button>
116
- <button type="button" class="tool-button danger" :data-tooltip="`删除 (${primaryShortcut('D', { shift: true })})`" aria-label="删除" @click="emit('delete')"><AppIcon name="trash" :size="20" /></button>
117
- </div>
136
+ <button
137
+ type="button"
138
+ class="tool-button code-button"
139
+ :data-tooltip="`行内代码 (${primaryShortcut('E')})`"
140
+ aria-label="行内代码"
141
+ @click="emit('format', 'code')"
142
+ >
143
+ &lt;/&gt;
144
+ </button>
145
+ </template>
146
+ <button
147
+ v-else
148
+ type="button"
149
+ class="tool-button"
150
+ :data-tooltip="`复制 (${primaryShortcut('C')})`"
151
+ aria-label="复制所选主题"
152
+ @click="emit('copy')"
153
+ >
154
+ <AppIcon name="copy" :size="20" />
155
+ </button>
156
+ <span class="toolbar-divider" />
157
+ <button
158
+ type="button"
159
+ class="tool-button"
160
+ :data-tooltip="`清除样式 (${primaryShortcut('\\')})`"
161
+ aria-label="清除样式"
162
+ @click="emit('clear')"
163
+ >
164
+ <AppIcon name="clearFormat" :size="20" />
165
+ </button>
166
+ <button
167
+ type="button"
168
+ class="tool-button danger"
169
+ :data-tooltip="`删除 (${primaryShortcut('D', { shift: true })})`"
170
+ aria-label="删除"
171
+ @click="emit('delete')"
172
+ >
173
+ <AppIcon name="trash" :size="20" />
174
+ </button>
175
+ </div>
118
176
  </template>
119
177
 
120
178
  <style scoped>
@@ -130,7 +188,7 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
130
188
  border-radius: 11px;
131
189
  background: color-mix(in srgb, var(--mm-panel-bg) 94%, #545760 6%);
132
190
  color: var(--mm-text);
133
- box-shadow: 0 10px 30px rgb(0 0 0 / .24);
191
+ box-shadow: 0 10px 30px rgb(0 0 0 / 0.24);
134
192
  }
135
193
  .selection-toolbar.is-canvas-anchored {
136
194
  position: absolute;
@@ -140,7 +198,8 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
140
198
  bottom: 18px;
141
199
  transform: translateX(-50%);
142
200
  }
143
- .format-button, .tool-button {
201
+ .format-button,
202
+ .tool-button {
144
203
  position: relative;
145
204
  display: inline-flex;
146
205
  width: 34px;
@@ -154,7 +213,8 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
154
213
  cursor: pointer;
155
214
  font-size: 21px;
156
215
  }
157
- .format-button::after, .tool-button::after {
216
+ .format-button::after,
217
+ .tool-button::after {
158
218
  position: absolute;
159
219
  z-index: 2;
160
220
  bottom: calc(100% + 9px);
@@ -165,7 +225,7 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
165
225
  border-radius: 7px;
166
226
  background: #f5f5f7;
167
227
  color: #25262b;
168
- box-shadow: 0 5px 18px rgb(0 0 0 / .22);
228
+ box-shadow: 0 5px 18px rgb(0 0 0 / 0.22);
169
229
  content: attr(data-tooltip);
170
230
  font-family: system-ui, sans-serif;
171
231
  font-size: 13px;
@@ -177,15 +237,47 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
177
237
  transform: translateX(-50%);
178
238
  white-space: nowrap;
179
239
  }
180
- .format-button:hover::after, .tool-button:hover::after,
181
- .format-button:focus-visible::after, .tool-button:focus-visible::after { opacity: 1; }
182
- .format-button:hover, .tool-button:hover, .format-button.active { background: var(--mm-hover); color: var(--mm-accent); }
183
- .format-button.is-bold { font-weight: 800; }
184
- .format-button.is-italic { font-family: Georgia, serif; font-style: italic; }
185
- .format-button.is-underline { text-decoration: underline; text-underline-offset: 4px; }
186
- .format-button.is-strike { text-decoration: line-through; }
187
- .format-button.is-highlight { color: #d3c900; }
188
- .tool-button.code-button { font-family: ui-monospace, monospace; font-size: 15px; font-weight: 700; }
189
- .tool-button.danger { color: #e14f5b; }
190
- .toolbar-divider { width: 1px; height: 25px; margin: 0 3px; background: var(--mm-border); }
240
+ .format-button:hover::after,
241
+ .tool-button:hover::after,
242
+ .format-button:focus-visible::after,
243
+ .tool-button:focus-visible::after {
244
+ opacity: 1;
245
+ }
246
+ .format-button:hover,
247
+ .tool-button:hover,
248
+ .format-button.active {
249
+ background: var(--mm-hover);
250
+ color: var(--mm-accent);
251
+ }
252
+ .format-button.is-bold {
253
+ font-weight: 800;
254
+ }
255
+ .format-button.is-italic {
256
+ font-family: Georgia, serif;
257
+ font-style: italic;
258
+ }
259
+ .format-button.is-underline {
260
+ text-decoration: underline;
261
+ text-underline-offset: 4px;
262
+ }
263
+ .format-button.is-strike {
264
+ text-decoration: line-through;
265
+ }
266
+ .format-button.is-highlight {
267
+ color: #d3c900;
268
+ }
269
+ .tool-button.code-button {
270
+ font-family: ui-monospace, monospace;
271
+ font-size: 15px;
272
+ font-weight: 700;
273
+ }
274
+ .tool-button.danger {
275
+ color: #e14f5b;
276
+ }
277
+ .toolbar-divider {
278
+ width: 1px;
279
+ height: 25px;
280
+ margin: 0 3px;
281
+ background: var(--mm-border);
282
+ }
191
283
  </style>
@@ -1,6 +1,2 @@
1
1
  /** @deprecated Import from `./mindmapClipboard` — kept so Vite stale graphs do not 404. */
2
- export {
3
- pasteCanvasOutline,
4
- readMindmapClipboard,
5
- writeMindmapClipboard,
6
- } from './mindmapClipboard'
2
+ export { pasteCanvasOutline, readMindmapClipboard, writeMindmapClipboard } from './mindmapClipboard'
@@ -7,7 +7,7 @@ export function insertImageIntoSource(
7
7
  selectionStart: number,
8
8
  selectionEnd: number,
9
9
  relativePath: string,
10
- alt = '截图',
10
+ alt = '截图'
11
11
  ): string {
12
12
  const image = `![${alt.replace(/\]/g, '').trim() || '截图'}](${relativePath})`
13
13
  const start = Math.max(0, Math.min(markdown.length, selectionStart))
@@ -69,25 +69,37 @@ export async function readMindmapClipboard(event?: ClipboardEvent | null): Promi
69
69
  }
70
70
 
71
71
  /** 把剪贴板中的普通文字 / Markdown 列表作为当前主题后的同级子树插入。 */
72
- export function pasteCanvasOutline(session: MindmapSession, anchorId: string, text: string): string[] {
72
+ export function pasteCanvasOutline(
73
+ session: MindmapSession,
74
+ anchorId: string,
75
+ text: string
76
+ ): string[] {
73
77
  const nodes = clipboardNodes(text)
74
78
  const anchor = session.document.find(anchorId)
75
79
  if (!anchor || nodes.length === 0) return []
76
80
  const insertedIds: string[] = []
77
81
  session.transact((doc) => {
78
- const parent = anchor === session.focusRootNode ? anchor : (anchor.parent ?? session.focusRootNode)
79
- let index = anchor === session.focusRootNode ? parent.children.length : parent.children.indexOf(anchor) + 1
82
+ const parent =
83
+ anchor === session.focusRootNode ? anchor : (anchor.parent ?? session.focusRootNode)
84
+ let index =
85
+ anchor === session.focusRootNode
86
+ ? parent.children.length
87
+ : parent.children.indexOf(anchor) + 1
80
88
  for (const source of nodes) {
81
89
  const inserted = doc.addNode(
82
90
  parent,
83
91
  { ...source.content, image: source.content.image ? { ...source.content.image } : null },
84
- index++,
92
+ index++
85
93
  )
86
94
  inserted.collapsed = source.collapsed
87
95
  for (const child of [...source.children]) doc.move(child, inserted, inserted.children.length)
88
96
  insertedIds.push(inserted.id)
89
97
  }
90
98
  })
91
- session.selectMany(insertedIds, insertedIds[insertedIds.length - 1] ?? null, insertedIds[0] ?? null)
99
+ session.selectMany(
100
+ insertedIds,
101
+ insertedIds[insertedIds.length - 1] ?? null,
102
+ insertedIds[0] ?? null
103
+ )
92
104
  return insertedIds
93
105
  }
@@ -15,7 +15,7 @@ export function resolveAfterDropLevel(
15
15
  clientX: number,
16
16
  indentEdge: number,
17
17
  depth: number,
18
- indent: number,
18
+ indent: number
19
19
  ): DropLevel {
20
20
  let node = anchor
21
21
  let nextDepth = depth
@@ -3,13 +3,11 @@ export interface MindmapFenceOptions {
3
3
  initialExpandLevel?: number
4
4
  }
5
5
 
6
- export interface MindmapReference {
7
- path: string
8
- title?: string
9
- }
10
-
11
6
  function cleanHeadingText(value: string): string {
12
- return value.trim().replace(/\s+#+\s*$/, '').trim()
7
+ return value
8
+ .trim()
9
+ .replace(/\s+#+\s*$/, '')
10
+ .trim()
13
11
  }
14
12
 
15
13
  /** Parse the canonical `mindmap [title] 2` fence metadata. */
@@ -23,7 +21,8 @@ export function parseMindmapFence(openLine: string): MindmapFenceOptions | null
23
21
  const titleMatch = rest.match(/\[([^\]]+)\]/)
24
22
  if (titleMatch) {
25
23
  options.title = cleanHeadingText(titleMatch[1]) || undefined
26
- rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim()
24
+ rest =
25
+ `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim()
27
26
  }
28
27
 
29
28
  if (rest && !/^\d+$/.test(rest)) return null
@@ -33,23 +32,6 @@ export function parseMindmapFence(openLine: string): MindmapFenceOptions | null
33
32
  return options
34
33
  }
35
34
 
36
- /** @deprecated Mindmap fences no longer resolve `<<<` includes; kept for body-include parsers / tests. */
37
- export function parseMindmapReference(line: string): MindmapReference | null {
38
- const match = line.trim().match(/^<<<\s+(.+?)\s*$/)
39
- if (!match) return null
40
-
41
- let rest = match[1].trim()
42
- let title: string | undefined
43
- const titleMatch = rest.match(/\s+\[([^\]]+)\]\s*$/)
44
- if (titleMatch) {
45
- title = cleanHeadingText(titleMatch[1]) || undefined
46
- rest = rest.slice(0, titleMatch.index).trim()
47
- }
48
-
49
- const path = rest.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, '$1$2').trim()
50
- return path ? { path, title } : null
51
- }
52
-
53
35
  export interface NormalizeMindmapOptions {
54
36
  title?: string
55
37
  defaultTitle?: string
@@ -58,7 +40,7 @@ export interface NormalizeMindmapOptions {
58
40
  /** Ensure canonical mindmap Markdown has exactly one H1 root title. */
59
41
  export function normalizeMindmapMarkdown(
60
42
  source: string,
61
- options: NormalizeMindmapOptions = {},
43
+ options: NormalizeMindmapOptions = {}
62
44
  ): string {
63
45
  const lines = source.replace(/\r\n?/g, '\n').split('\n')
64
46
  let existingTitle = ''
@@ -72,12 +54,11 @@ export function normalizeMindmapMarkdown(
72
54
  break
73
55
  }
74
56
 
75
- const rootTitle = cleanHeadingText(options.title || existingTitle || options.defaultTitle || 'root') || 'root'
57
+ const rootTitle =
58
+ cleanHeadingText(options.title || existingTitle || options.defaultTitle || 'root') || 'root'
76
59
  const body = lines.filter((_, index) => index !== rootIndex)
77
60
  while (body[0]?.trim() === '') body.shift()
78
61
  while (body[body.length - 1]?.trim() === '') body.pop()
79
62
 
80
- return body.length > 0
81
- ? `# ${rootTitle}\n\n${body.join('\n')}\n`
82
- : `# ${rootTitle}\n`
63
+ return body.length > 0 ? `# ${rootTitle}\n\n${body.join('\n')}\n` : `# ${rootTitle}\n`
83
64
  }