@tnotesjs/ui 0.2.1 → 0.3.1

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.
@@ -2,6 +2,13 @@
2
2
  import { nextTick, onMounted, ref, watch } from 'vue'
3
3
  import CodeBlock from '../CodeBlock/CodeBlock.vue'
4
4
  import { parseCodeMeta } from '../../code/highlight'
5
+ import {
6
+ applyCollapseChrome,
7
+ codeBlockIn,
8
+ expandCollapsedCodeBlocks,
9
+ isCodeBlockCollapsed,
10
+ toggleCodeBlockCollapsed
11
+ } from '../../code/collapse'
5
12
 
6
13
  export interface CodeGroupItem {
7
14
  code?: string
@@ -11,16 +18,47 @@ export interface CodeGroupItem {
11
18
  }
12
19
  const props = defineProps<{ items: CodeGroupItem[] }>()
13
20
  const root = ref<HTMLElement>()
21
+ const collapseButton = ref<HTMLButtonElement>()
14
22
  const active = ref(0)
15
- function syncPanels(): void {
16
- const panels =
17
- root.value?.querySelectorAll<HTMLElement>(
23
+
24
+ function panels(): HTMLElement[] {
25
+ return [
26
+ ...(root.value?.querySelectorAll<HTMLElement>(
18
27
  ':scope > .tn-code-group__panels > .tn-code-group__panel'
19
- ) ?? []
20
- panels.forEach((panel, index) => {
28
+ ) ?? [])
29
+ ]
30
+ }
31
+
32
+ function activePanel(): HTMLElement | null {
33
+ return panels()[active.value] ?? null
34
+ }
35
+
36
+ function activeBlock(): HTMLElement | null {
37
+ return codeBlockIn(activePanel())
38
+ }
39
+
40
+ function toggleCollapse(): void {
41
+ const block = activeBlock()
42
+ if (!block) return
43
+ toggleCodeBlockCollapsed(block)
44
+ applyCollapseChrome(collapseButton.value ?? null, isCodeBlockCollapsed(block))
45
+ }
46
+
47
+ /** 面板自带的那颗折叠按钮在分组里会被 tab 行盖住,所以 chrome 挂在 tab 行这颗上。 */
48
+ function syncCollapseButton(): void {
49
+ applyCollapseChrome(collapseButton.value ?? null, isCodeBlockCollapsed(activeBlock()))
50
+ }
51
+
52
+ function syncPanels(): void {
53
+ const list = panels()
54
+ list.forEach((panel, index) => {
21
55
  panel.hidden = index !== active.value
22
56
  })
57
+ // 切到的面板如果处于收起状态就自动展开:读者不该看到一片被裁掉的代码。
58
+ expandCollapsedCodeBlocks(activePanel())
59
+ syncCollapseButton()
23
60
  }
61
+
24
62
  watch(active, syncPanels, { flush: 'post' })
25
63
  watch(
26
64
  () => props.items.length,
@@ -30,6 +68,7 @@ watch(
30
68
  }
31
69
  )
32
70
  onMounted(syncPanels)
71
+
33
72
  function navigate(event: KeyboardEvent, index: number): void {
34
73
  let next = index
35
74
  if (event.key === 'ArrowRight') next = (index + 1) % props.items.length
@@ -40,19 +79,43 @@ function navigate(event: KeyboardEvent, index: number): void {
40
79
  event.preventDefault()
41
80
  active.value = next
42
81
  ;(event.currentTarget as HTMLElement).parentElement
43
- ?.querySelectorAll<HTMLButtonElement>('button')
82
+ ?.querySelectorAll<HTMLButtonElement>('button[role="tab"]')
44
83
  [next]?.focus()
45
84
  }
46
85
  </script>
47
86
 
48
87
  <template>
49
- <section ref="root" class="tn-code-group">
88
+ <section ref="root" class="tn-code-group" :class="{ 'has-tabs': items.length > 1 }">
50
89
  <div
51
90
  v-if="items.length > 1"
52
91
  class="tn-code-group__tabs code-group-tabs"
53
92
  role="tablist"
54
93
  aria-label="代码分组"
55
94
  >
95
+ <button
96
+ ref="collapseButton"
97
+ type="button"
98
+ class="tn-code-block__icon-btn tn-code-group__collapse-btn"
99
+ aria-expanded="true"
100
+ aria-label="收起代码"
101
+ title="收起代码"
102
+ @click="toggleCollapse"
103
+ >
104
+ <svg
105
+ xmlns="http://www.w3.org/2000/svg"
106
+ width="16"
107
+ height="16"
108
+ viewBox="0 0 24 24"
109
+ fill="none"
110
+ stroke="currentColor"
111
+ stroke-width="2"
112
+ stroke-linecap="round"
113
+ stroke-linejoin="round"
114
+ aria-hidden="true"
115
+ >
116
+ <path d="m6 9 6 6 6-6" />
117
+ </svg>
118
+ </button>
56
119
  <button
57
120
  v-for="(item, index) in items"
58
121
  :key="item.key || index"
@@ -786,19 +786,20 @@ onBeforeUnmount(() => {
786
786
  transition: opacity 0.2s;
787
787
  }
788
788
 
789
- /* Editable Desk island: chrome must stay hittable. */
790
- .mindmap-preview.is-editable > .mindmap-preview-actions {
791
- opacity: 1;
792
- pointer-events: auto;
793
- }
794
-
789
+ /* 工具条按需出现:hover、聚焦(键盘 Tab 进来也算)、画布交互中、全屏。
790
+ Desk 的可编辑岛原本额外钉了 `opacity: 1`(旧注释:“chrome must stay hittable”),
791
+ 结果每张脑图右上角常驻一排按钮,既像噪声又占掉画布右上角的拖拽区;
792
+ 可点击性并没丢 —— 指针进入即 hover 显现,`:focus-within` 保住键盘路径。
793
+ 只读岛(站点)本来就是这套行为,这里让两边一致。 */
795
794
  .mindmap-preview-tabs {
796
795
  display: flex;
797
796
  gap: 4px;
798
797
  }
799
798
 
800
799
  .mindmap-preview:hover > .mindmap-preview-actions,
800
+ .mindmap-preview:focus-within > .mindmap-preview-actions,
801
801
  .mindmap-preview-actions:focus-within,
802
+ .mindmap-preview.is-interaction-active > .mindmap-preview-actions,
802
803
  .mindmap-preview.is-fullscreen > .mindmap-preview-actions,
803
804
  .mindmap-preview:fullscreen > .mindmap-preview-actions {
804
805
  opacity: 1;
@@ -5,6 +5,12 @@ import { isApplePlatform, primaryShortcut } from './platform'
5
5
  const props = withDefaults(
6
6
  defineProps<{
7
7
  position: { left: number; top: number }
8
+ /**
9
+ * 浮层挂载点。默认 `undefined` = 就地渲染,供嵌在笔记里的宿主(Desk / SSG)
10
+ * 使用——菜单是 `position: fixed`,teleport 到 body 会脱离笔记的层叠上下文。
11
+ * 自带整页外壳的宿主(Web / VS Code webview)传 `'body'`。
12
+ */
13
+ teleportTo?: string
8
14
  multiple?: boolean
9
15
  canInsertSibling?: boolean
10
16
  canInsertParent?: boolean
@@ -16,6 +22,7 @@ const props = withDefaults(
16
22
  canFocus?: boolean
17
23
  }>(),
18
24
  {
25
+ teleportTo: undefined,
19
26
  multiple: false,
20
27
  canInsertSibling: true,
21
28
  canInsertParent: true,
@@ -96,85 +103,87 @@ onBeforeUnmount(() => {
96
103
  </script>
97
104
 
98
105
  <template>
99
- <div
100
- ref="menu"
101
- class="canvas-context-menu"
102
- :style="style"
103
- role="menu"
104
- :aria-label="multiple ? '多主题右键菜单' : '主题右键菜单'"
105
- @contextmenu.prevent
106
- >
107
- <template v-if="!multiple">
106
+ <Teleport :to="teleportTo" :disabled="!teleportTo">
107
+ <div
108
+ ref="menu"
109
+ class="canvas-context-menu"
110
+ :style="style"
111
+ role="menu"
112
+ :aria-label="multiple ? '多主题右键菜单' : '主题右键菜单'"
113
+ @contextmenu.prevent
114
+ >
115
+ <template v-if="!multiple">
116
+ <button
117
+ type="button"
118
+ role="menuitem"
119
+ :disabled="!canInsertSibling"
120
+ @click="act('insertSibling')"
121
+ >
122
+ <span>插入同级主题</span><kbd>Enter</kbd>
123
+ </button>
124
+ <button type="button" role="menuitem" @click="act('insertChild')">
125
+ <span>插入下级主题</span><kbd>Tab</kbd>
126
+ </button>
127
+ <button
128
+ type="button"
129
+ role="menuitem"
130
+ :disabled="!canInsertParent"
131
+ @click="act('insertParent')"
132
+ >
133
+ <span>插入上级主题</span><kbd>{{ shiftTab }}</kbd>
134
+ </button>
135
+ <span class="context-divider" />
136
+ </template>
137
+
138
+ <button type="button" role="menuitem" @click="act('copy')">
139
+ <span>复制</span><kbd>{{ primaryShortcut('C') }}</kbd>
140
+ </button>
141
+ <button type="button" role="menuitem" :disabled="!canCut" @click="act('cut')">
142
+ <span>剪切</span><kbd>{{ primaryShortcut('X') }}</kbd>
143
+ </button>
144
+
145
+ <template v-if="!multiple">
146
+ <button type="button" role="menuitem" @click="act('paste')">
147
+ <span>粘贴</span><kbd>{{ primaryShortcut('V') }}</kbd>
148
+ </button>
149
+ <button type="button" role="menuitem" :disabled="!canDuplicate" @click="act('duplicate')">
150
+ <span>创建副本</span><kbd>{{ primaryShortcut('D') }}</kbd>
151
+ </button>
152
+ <button type="button" role="menuitem" :disabled="!canDeleteOnly" @click="act('deleteOnly')">
153
+ <span>仅删除当前主题</span>
154
+ </button>
155
+ </template>
156
+
108
157
  <button
109
158
  type="button"
110
159
  role="menuitem"
111
- :disabled="!canInsertSibling"
112
- @click="act('insertSibling')"
160
+ class="danger"
161
+ :disabled="!canDeleteTree"
162
+ @click="act('deleteTree')"
113
163
  >
114
- <span>插入同级主题</span><kbd>Enter</kbd>
115
- </button>
116
- <button type="button" role="menuitem" @click="act('insertChild')">
117
- <span>插入下级主题</span><kbd>Tab</kbd>
164
+ <span>{{ multiple ? '删除所选主题及下级主题' : '删除当前主题及下级主题' }}</span
165
+ ><kbd>Delete</kbd>
118
166
  </button>
167
+ <span class="context-divider" />
119
168
  <button
120
169
  type="button"
121
170
  role="menuitem"
122
- :disabled="!canInsertParent"
123
- @click="act('insertParent')"
171
+ :disabled="!canToggleSiblings"
172
+ @click="act('toggleSiblings')"
124
173
  >
125
- <span>插入上级主题</span><kbd>{{ shiftTab }}</kbd>
174
+ <span>展开/折叠同级主题</span><kbd>{{ primaryShortcut('.', { shift: true }) }}</kbd>
126
175
  </button>
127
- <span class="context-divider" />
128
- </template>
129
-
130
- <button type="button" role="menuitem" @click="act('copy')">
131
- <span>复制</span><kbd>{{ primaryShortcut('C') }}</kbd>
132
- </button>
133
- <button type="button" role="menuitem" :disabled="!canCut" @click="act('cut')">
134
- <span>剪切</span><kbd>{{ primaryShortcut('X') }}</kbd>
135
- </button>
136
-
137
- <template v-if="!multiple">
138
- <button type="button" role="menuitem" @click="act('paste')">
139
- <span>粘贴</span><kbd>{{ primaryShortcut('V') }}</kbd>
140
- </button>
141
- <button type="button" role="menuitem" :disabled="!canDuplicate" @click="act('duplicate')">
142
- <span>创建副本</span><kbd>{{ primaryShortcut('D') }}</kbd>
143
- </button>
144
- <button type="button" role="menuitem" :disabled="!canDeleteOnly" @click="act('deleteOnly')">
145
- <span>仅删除当前主题</span>
176
+ <button
177
+ v-if="!multiple"
178
+ type="button"
179
+ role="menuitem"
180
+ :disabled="!canFocus"
181
+ @click="act('focus')"
182
+ >
183
+ <span>进入此主题</span><kbd>{{ primaryShortcut(']') }}</kbd>
146
184
  </button>
147
- </template>
148
-
149
- <button
150
- type="button"
151
- role="menuitem"
152
- class="danger"
153
- :disabled="!canDeleteTree"
154
- @click="act('deleteTree')"
155
- >
156
- <span>{{ multiple ? '删除所选主题及下级主题' : '删除当前主题及下级主题' }}</span
157
- ><kbd>Delete</kbd>
158
- </button>
159
- <span class="context-divider" />
160
- <button
161
- type="button"
162
- role="menuitem"
163
- :disabled="!canToggleSiblings"
164
- @click="act('toggleSiblings')"
165
- >
166
- <span>展开/折叠同级主题</span><kbd>{{ primaryShortcut('.', { shift: true }) }}</kbd>
167
- </button>
168
- <button
169
- v-if="!multiple"
170
- type="button"
171
- role="menuitem"
172
- :disabled="!canFocus"
173
- @click="act('focus')"
174
- >
175
- <span>进入此主题</span><kbd>{{ primaryShortcut(']') }}</kbd>
176
- </button>
177
- </div>
185
+ </div>
186
+ </Teleport>
178
187
  </template>
179
188
 
180
189
  <style scoped>
@@ -0,0 +1,113 @@
1
+ <script setup lang="ts">
2
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
3
+ import IconButton from './IconButton.vue'
4
+ import { isApplePlatform } from './platform'
5
+
6
+ const emit = defineEmits<{
7
+ all: []
8
+ level: [level: 1 | 2 | 3]
9
+ }>()
10
+
11
+ const root = ref<HTMLElement>()
12
+ const open = ref(false)
13
+ const modifier = computed(() => (isApplePlatform() ? '⌘⌥' : 'Ctrl+Alt+'))
14
+ const allShortcut = computed(() => (isApplePlatform() ? '⌘⌥⇧.' : 'Ctrl+Alt+Shift+.'))
15
+
16
+ function chooseAll() {
17
+ emit('all')
18
+ open.value = false
19
+ }
20
+
21
+ function chooseLevel(level: 1 | 2 | 3) {
22
+ emit('level', level)
23
+ open.value = false
24
+ }
25
+
26
+ function onDocumentPointerDown(event: PointerEvent) {
27
+ if (open.value && !root.value?.contains(event.target as Node)) open.value = false
28
+ }
29
+
30
+ function onDocumentKeydown(event: KeyboardEvent) {
31
+ if (event.key === 'Escape') open.value = false
32
+ }
33
+
34
+ onMounted(() => {
35
+ document.addEventListener('pointerdown', onDocumentPointerDown)
36
+ document.addEventListener('keydown', onDocumentKeydown)
37
+ })
38
+
39
+ onBeforeUnmount(() => {
40
+ document.removeEventListener('pointerdown', onDocumentPointerDown)
41
+ document.removeEventListener('keydown', onDocumentKeydown)
42
+ })
43
+ </script>
44
+
45
+ <template>
46
+ <div ref="root" class="collapse-menu">
47
+ <IconButton icon="collapse" label="展开/折叠主题" :active="open" @click="open = !open" />
48
+ <div v-if="open" class="collapse-menu-popover" role="menu" aria-label="展开/折叠主题">
49
+ <div class="collapse-menu-title">展开/折叠主题</div>
50
+ <button type="button" role="menuitem" @click="chooseAll">
51
+ <span>全部主题</span><kbd>{{ allShortcut }}</kbd>
52
+ </button>
53
+ <button
54
+ v-for="level in [1, 2, 3] as const"
55
+ :key="level"
56
+ type="button"
57
+ role="menuitem"
58
+ @click="chooseLevel(level)"
59
+ >
60
+ <span>{{ level }} 级主题</span><kbd>{{ modifier }}{{ level }}</kbd>
61
+ </button>
62
+ </div>
63
+ </div>
64
+ </template>
65
+
66
+ <style scoped>
67
+ .collapse-menu {
68
+ position: relative;
69
+ }
70
+ .collapse-menu-popover {
71
+ position: absolute;
72
+ z-index: 85;
73
+ top: calc(100% + 8px);
74
+ right: 0;
75
+ width: 260px;
76
+ padding: 8px;
77
+ border: 1px solid var(--mm-border);
78
+ border-radius: 12px;
79
+ background: var(--mm-panel-bg);
80
+ color: var(--mm-text);
81
+ box-shadow: 0 14px 38px rgb(0 0 0 / 0.24);
82
+ }
83
+ .collapse-menu-title {
84
+ padding: 7px 11px 9px;
85
+ color: var(--mm-text-dim);
86
+ font-size: 12px;
87
+ }
88
+ .collapse-menu-popover button {
89
+ display: flex;
90
+ width: 100%;
91
+ min-height: 42px;
92
+ align-items: center;
93
+ justify-content: space-between;
94
+ padding: 0 11px;
95
+ border: 0;
96
+ border-radius: 7px;
97
+ background: transparent;
98
+ color: inherit;
99
+ font: inherit;
100
+ text-align: left;
101
+ cursor: pointer;
102
+ }
103
+ .collapse-menu-popover button:hover,
104
+ .collapse-menu-popover button:focus-visible {
105
+ background: var(--mm-hover);
106
+ outline: none;
107
+ }
108
+ .collapse-menu-popover kbd {
109
+ color: var(--mm-text-dim);
110
+ font: inherit;
111
+ font-size: 12px;
112
+ }
113
+ </style>
@@ -0,0 +1,63 @@
1
+ <script setup lang="ts">
2
+ import AppIcon from './AppIcon.vue'
3
+
4
+ withDefaults(
5
+ defineProps<{
6
+ icon: string
7
+ label: string
8
+ size?: number
9
+ active?: boolean
10
+ danger?: boolean
11
+ disabled?: boolean
12
+ }>(),
13
+ { size: 18, active: false, danger: false, disabled: false }
14
+ )
15
+
16
+ defineEmits<{ click: [event: MouseEvent] }>()
17
+ </script>
18
+
19
+ <template>
20
+ <button
21
+ type="button"
22
+ class="icon-button"
23
+ :class="{ active, danger }"
24
+ :title="label"
25
+ :aria-label="label"
26
+ :disabled="disabled"
27
+ @click="$emit('click', $event)"
28
+ >
29
+ <AppIcon :name="icon" :size="size" />
30
+ </button>
31
+ </template>
32
+
33
+ <style scoped>
34
+ .icon-button {
35
+ display: inline-flex;
36
+ width: 32px;
37
+ height: 32px;
38
+ align-items: center;
39
+ justify-content: center;
40
+ padding: 0;
41
+ border: 0;
42
+ border-radius: 7px;
43
+ background: transparent;
44
+ color: var(--mm-text-dim);
45
+ cursor: pointer;
46
+ }
47
+ .icon-button:hover:not(:disabled),
48
+ .icon-button.active {
49
+ background: var(--mm-hover);
50
+ color: var(--mm-text);
51
+ }
52
+ .icon-button.danger {
53
+ color: #d84d57;
54
+ }
55
+ .icon-button:focus-visible {
56
+ outline: 2px solid var(--mm-accent);
57
+ outline-offset: 1px;
58
+ }
59
+ .icon-button:disabled {
60
+ cursor: default;
61
+ opacity: 0.38;
62
+ }
63
+ </style>
@@ -7,8 +7,10 @@ const props = withDefaults(
7
7
  url: string
8
8
  position: { left: number; top: number }
9
9
  startEditing?: boolean
10
+ /** 见 `CanvasContextMenu`:默认就地渲染,整页宿主传 `'body'`。 */
11
+ teleportTo?: string
10
12
  }>(),
11
- { startEditing: false }
13
+ { startEditing: false, teleportTo: undefined }
12
14
  )
13
15
  const emit = defineEmits<{ save: [url: string]; remove: []; keep: []; leave: []; close: [] }>()
14
16
  const draft = ref(props.url)
@@ -50,52 +52,54 @@ defineExpose({ focusInput })
50
52
  </script>
51
53
 
52
54
  <template>
53
- <div
54
- class="link-popover"
55
- :style="{ left: `${position.left}px`, top: `${position.top}px` }"
56
- @mouseenter="emit('keep')"
57
- @mouseleave="emit('leave')"
58
- >
59
- <input
60
- ref="input"
61
- v-model="draft"
62
- class="link-input"
63
- :class="{ readonly: !editing }"
64
- :readonly="!editing"
65
- aria-label="链接地址"
66
- @dblclick="beginEdit"
67
- @keydown="onKeydown"
68
- />
69
- <button
70
- v-if="editing"
71
- type="button"
72
- class="link-action primary"
73
- title="更新链接 (Enter)"
74
- aria-label="更新链接"
75
- @click="submit"
55
+ <Teleport :to="teleportTo" :disabled="!teleportTo">
56
+ <div
57
+ class="link-popover"
58
+ :style="{ left: `${position.left}px`, top: `${position.top}px` }"
59
+ @mouseenter="emit('keep')"
60
+ @mouseleave="emit('leave')"
76
61
  >
77
- 更新
78
- </button>
79
- <button
80
- v-else
81
- type="button"
82
- class="icon-action"
83
- title="编辑链接地址"
84
- aria-label="编辑链接地址"
85
- @click="beginEdit"
86
- >
87
- <AppIcon name="edit" :size="18" />
88
- </button>
89
- <button
90
- type="button"
91
- class="icon-action"
92
- title="移除链接,保留文案"
93
- aria-label="移除链接"
94
- @click="emit('remove')"
95
- >
96
- <AppIcon name="removeLink" :size="18" />
97
- </button>
98
- </div>
62
+ <input
63
+ ref="input"
64
+ v-model="draft"
65
+ class="link-input"
66
+ :class="{ readonly: !editing }"
67
+ :readonly="!editing"
68
+ aria-label="链接地址"
69
+ @dblclick="beginEdit"
70
+ @keydown="onKeydown"
71
+ />
72
+ <button
73
+ v-if="editing"
74
+ type="button"
75
+ class="link-action primary"
76
+ title="更新链接 (Enter)"
77
+ aria-label="更新链接"
78
+ @click="submit"
79
+ >
80
+ 更新
81
+ </button>
82
+ <button
83
+ v-else
84
+ type="button"
85
+ class="icon-action"
86
+ title="编辑链接地址"
87
+ aria-label="编辑链接地址"
88
+ @click="beginEdit"
89
+ >
90
+ <AppIcon name="edit" :size="18" />
91
+ </button>
92
+ <button
93
+ type="button"
94
+ class="icon-action"
95
+ title="移除链接,保留文案"
96
+ aria-label="移除链接"
97
+ @click="emit('remove')"
98
+ >
99
+ <AppIcon name="removeLink" :size="18" />
100
+ </button>
101
+ </div>
102
+ </Teleport>
99
103
  </template>
100
104
 
101
105
  <style scoped>
@@ -37,12 +37,14 @@ function onInput(e: Event) {
37
37
  }, 300)
38
38
  }
39
39
 
40
- function flushDraft() {
41
- if (!typing) return
40
+ /** 有未提交的防抖输入时立即回流,返回被 flush 的草稿;没有则返回 `null`。 */
41
+ function flushDraft(): string | null {
42
+ if (!typing) return null
42
43
  if (timer) clearTimeout(timer)
43
44
  timer = null
44
45
  typing = false
45
46
  emit('update:modelValue', draft.value)
47
+ return draft.value
46
48
  }
47
49
 
48
50
  function goToDiagnostic(item: MarkdownDiagnostic) {
@@ -63,7 +65,7 @@ function selectAllFromHost(): void {
63
65
  input.setSelectionRange(0, input.value.length)
64
66
  }
65
67
 
66
- defineExpose({ selectAllFromHost })
68
+ defineExpose({ selectAllFromHost, flushDraft })
67
69
 
68
70
  function onPaste(e: ClipboardEvent) {
69
71
  const image = [...(e.clipboardData?.items ?? [])]
@@ -13,7 +13,15 @@ import LinkPopover from './LinkPopover.vue'
13
13
  import SelectionToolbar from './SelectionToolbar.vue'
14
14
  import { pasteCanvasOutline, readMindmapClipboard, writeMindmapClipboard } from './mindmapClipboard'
15
15
 
16
- const props = defineProps<{ session: MindmapSession; resolveImageSrc?: (src: string) => string }>()
16
+ const props = defineProps<{
17
+ session: MindmapSession
18
+ resolveImageSrc?: (src: string) => string
19
+ /**
20
+ * 浮层挂载点,透传给 `CanvasContextMenu` / `LinkPopover`。
21
+ * 默认 `undefined` = 就地渲染(嵌在笔记里的 Desk / SSG);整页宿主传 `'body'`。
22
+ */
23
+ teleportTo?: string
24
+ }>()
17
25
 
18
26
  const emit = defineEmits<{
19
27
  ready: [editor: CanvasEditor]
@@ -209,6 +217,7 @@ onBeforeUnmount(() => {
209
217
  v-if="linkEditor"
210
218
  :url="linkEditor.url"
211
219
  :position="linkEditor.position"
220
+ :teleport-to="teleportTo"
212
221
  @save="saveLink"
213
222
  @remove="removeLink"
214
223
  @keep="keepLinkPopover"
@@ -227,6 +236,7 @@ onBeforeUnmount(() => {
227
236
  :can-delete-tree="contextMenu.canDeleteTree"
228
237
  :can-toggle-siblings="contextMenu.canToggleSiblings"
229
238
  :can-focus="contextMenu.canFocus"
239
+ :teleport-to="teleportTo"
230
240
  @insert-sibling="insertSibling"
231
241
  @insert-child="insertChild"
232
242
  @insert-parent="insertParent"