@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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/package.json +68 -0
  4. package/src/components/BilibiliVideo/BilibiliVideo.vue +55 -0
  5. package/src/components/Footprints/Footprints.vue +339 -0
  6. package/src/components/Footprints/parse.ts +122 -0
  7. package/src/components/Mermaid/Mermaid.vue +553 -0
  8. package/src/components/Mermaid/icons/icon__center_off.svg +1 -0
  9. package/src/components/Mermaid/icons/icon__center_on.svg +1 -0
  10. package/src/components/Mermaid/icons/icon__check.svg +3 -0
  11. package/src/components/Mermaid/icons/icon__clipboard.svg +8 -0
  12. package/src/components/Mermaid/icons/icon__fullscreen.svg +1 -0
  13. package/src/components/Mermaid/icons/icon__fullscreen_exit.svg +1 -0
  14. package/src/components/Mindmap/FocusBreadcrumbs.test.ts +265 -0
  15. package/src/components/Mindmap/FocusBreadcrumbs.vue +436 -0
  16. package/src/components/Mindmap/InlineRuns.ts +25 -0
  17. package/src/components/Mindmap/Mindmap.vue +1210 -0
  18. package/src/components/Mindmap/MindmapOutlineNode.vue +62 -0
  19. package/src/components/Mindmap/MindmapViewIcon.vue +41 -0
  20. package/src/components/Mindmap/editor/AppIcon.vue +107 -0
  21. package/src/components/Mindmap/editor/CanvasContextMenu.vue +157 -0
  22. package/src/components/Mindmap/editor/LinkPopover.vue +83 -0
  23. package/src/components/Mindmap/editor/MarkdownView.vue +163 -0
  24. package/src/components/Mindmap/editor/MindmapView.vue +253 -0
  25. package/src/components/Mindmap/editor/OutlineView.vue +2494 -0
  26. package/src/components/Mindmap/editor/RichInlineEditor.vue +393 -0
  27. package/src/components/Mindmap/editor/SelectionToolbar.vue +191 -0
  28. package/src/components/Mindmap/editor/canvasClipboard.ts +6 -0
  29. package/src/components/Mindmap/editor/imagePaste.ts +32 -0
  30. package/src/components/Mindmap/editor/mindmapClipboard.ts +93 -0
  31. package/src/components/Mindmap/editor/outlineDrag.ts +29 -0
  32. package/src/components/Mindmap/editor/platform.ts +18 -0
  33. package/src/components/Mindmap/expandLevel.ts +28 -0
  34. package/src/components/Mindmap/icons/icon__fullscreen.svg +1 -0
  35. package/src/components/Mindmap/icons/icon__fullscreen_exit.svg +1 -0
  36. package/src/components/Mindmap/icons/icon__zoom_fit.svg +1 -0
  37. package/src/components/Mindmap/markdown.ts +83 -0
  38. package/src/components/Mindmap/wheelInteraction.ts +7 -0
  39. package/src/components/NotesTable/NotesTable.vue +119 -0
  40. package/src/components/NotesTable/types.ts +6 -0
  41. package/src/components/WordList/RightClickMenu.vue +106 -0
  42. package/src/components/WordList/WordList.vue +692 -0
  43. package/src/components/WordList/wordListFeatures.ts +38 -0
  44. package/src/index.ts +30 -0
  45. package/src/styles/tokens.css +35 -0
@@ -0,0 +1,253 @@
1
+ <script setup lang="ts">
2
+ import { onBeforeUnmount, onMounted, ref } from 'vue'
3
+ import { CanvasEditor, serializeSubtree } from '@tnotesjs/mindmap-core'
4
+ import type { CanvasContextRequest, CanvasLinkHover, InlineFormat, MindmapNode, MindmapSession } from '@tnotesjs/mindmap-core'
5
+ import CanvasContextMenu from './CanvasContextMenu.vue'
6
+ import LinkPopover from './LinkPopover.vue'
7
+ import SelectionToolbar from './SelectionToolbar.vue'
8
+ import { pasteCanvasOutline, readMindmapClipboard, writeMindmapClipboard } from './mindmapClipboard'
9
+
10
+ const props = defineProps<{ session: MindmapSession; resolveImageSrc?: (src: string) => string }>()
11
+
12
+ const emit = defineEmits<{
13
+ ready: [editor: CanvasEditor]
14
+ requestSearch: []
15
+ imagePreview: [src: string]
16
+ pasteImage: [anchorId: string, blob: Blob]
17
+ }>()
18
+
19
+ const host = ref<HTMLElement>()
20
+ let editor: CanvasEditor | null = null
21
+ const selectedCount = ref(0)
22
+ const linkEditor = ref<CanvasLinkHover | null>(null)
23
+ const contextMenu = ref<CanvasContextRequest | null>(null)
24
+ let linkCloseTimer: ReturnType<typeof setTimeout> | null = null
25
+
26
+ function keepLinkPopover() {
27
+ if (linkCloseTimer) clearTimeout(linkCloseTimer)
28
+ linkCloseTimer = null
29
+ }
30
+
31
+ function closeLinkPopoverSoon() {
32
+ keepLinkPopover()
33
+ linkCloseTimer = setTimeout(() => (linkEditor.value = null), 180)
34
+ }
35
+
36
+ function onCanvasLinkHover(link: CanvasLinkHover | null) {
37
+ if (!link) {
38
+ closeLinkPopoverSoon()
39
+ return
40
+ }
41
+ keepLinkPopover()
42
+ linkEditor.value = link
43
+ }
44
+
45
+ function saveLink(url: string) {
46
+ const link = linkEditor.value
47
+ if (!link) return
48
+ props.session.updateNodeInlineLink(link.nodeId, link.rawStart, link.rawEnd, url)
49
+ // URL 长度变化会让原始 Markdown 范围失效;关闭后按下一次 hover 的新范围重开。
50
+ linkEditor.value = null
51
+ }
52
+
53
+ function removeLink() {
54
+ const link = linkEditor.value
55
+ if (!link) return
56
+ props.session.updateNodeInlineLink(link.nodeId, link.rawStart, link.rawEnd, null)
57
+ linkEditor.value = null
58
+ }
59
+
60
+ function selectedRoots(): MindmapNode[] {
61
+ const selected = props.session.selectionIds
62
+ return props.session.selectedNodes.filter((node) => {
63
+ let parent = node.parent
64
+ while (parent) {
65
+ if (selected.has(parent.id)) return false
66
+ parent = parent.parent
67
+ }
68
+ return true
69
+ })
70
+ }
71
+
72
+ function applyNodeFormat(format: InlineFormat) {
73
+ props.session.formatSelectedNodes(format)
74
+ }
75
+
76
+ function selectedMarkdown() {
77
+ return selectedRoots().map((node) => serializeSubtree(node)).join('\n')
78
+ }
79
+
80
+ async function copySelected(event?: ClipboardEvent | null) {
81
+ const text = selectedMarkdown()
82
+ if (!text) return
83
+ writeMindmapClipboard(text, event)
84
+ }
85
+
86
+ async function cutSelected(event?: ClipboardEvent | null) {
87
+ const text = selectedMarkdown()
88
+ if (!text) return
89
+ const ids = [...props.session.selectionIds]
90
+ writeMindmapClipboard(text, event)
91
+ props.session.removeNodesByIds(ids)
92
+ }
93
+
94
+ async function pasteAtSelection(anchorId?: string, event?: ClipboardEvent | null) {
95
+ const targetId = anchorId ?? props.session.selectedNode?.id ?? props.session.focusRootNode.id
96
+ const text = await readMindmapClipboard(event)
97
+ if (text) pasteCanvasOutline(props.session, targetId, text)
98
+ }
99
+
100
+ async function pasteAtContextNode() {
101
+ const target = contextMenu.value
102
+ if (!target) return
103
+ await pasteAtSelection(target.nodeId)
104
+ }
105
+
106
+ function pasteTextAt(text: string, anchorId: string) {
107
+ writeMindmapClipboard(text)
108
+ pasteCanvasOutline(props.session, anchorId, text)
109
+ }
110
+
111
+ function editCreated(node: MindmapNode | null) {
112
+ if (node) editor?.startEdit(node.id)
113
+ }
114
+
115
+ function insertSibling() {
116
+ const target = contextMenu.value
117
+ if (target) editCreated(props.session.insertSiblingOf(target.nodeId))
118
+ }
119
+
120
+ function insertChild() {
121
+ const target = contextMenu.value
122
+ if (target) editCreated(props.session.insertChildOf(target.nodeId))
123
+ }
124
+
125
+ function insertParent() {
126
+ const target = contextMenu.value
127
+ if (target) editCreated(props.session.insertParentOf(target.nodeId))
128
+ }
129
+
130
+ function deleteOnly() {
131
+ const target = contextMenu.value
132
+ if (target) props.session.removeNodeOnly(target.nodeId)
133
+ }
134
+
135
+ function toggleContextSiblings() {
136
+ const target = contextMenu.value
137
+ if (target) props.session.toggleCollapseSiblings(target.nodeId)
138
+ }
139
+
140
+ function focusContextNode() {
141
+ const target = contextMenu.value
142
+ if (target) props.session.focusNode(target.nodeId)
143
+ }
144
+
145
+ onMounted(() => {
146
+ editor = new CanvasEditor(host.value!, props.session, {
147
+ onRequestSearch: () => emit('requestSearch'),
148
+ onImagePreview: (src) => emit('imagePreview', src),
149
+ onPasteImage: (anchorId, blob) => emit('pasteImage', anchorId, blob),
150
+ resolveImageSrc: (src) => props.resolveImageSrc?.(src) ?? src,
151
+ onSelectionPositionChange: (_position, count) => {
152
+ // Canvas toolbar is absolutely anchored; only the selection count gates visibility.
153
+ selectedCount.value = count
154
+ },
155
+ onLinkHover: onCanvasLinkHover,
156
+ onContextMenu: (request) => {
157
+ contextMenu.value = request
158
+ if (request) linkEditor.value = null
159
+ },
160
+ onCopySelection: (event) => {
161
+ void copySelected(event)
162
+ },
163
+ onCutSelection: (event) => {
164
+ void cutSelected(event)
165
+ },
166
+ onPasteSelection: (event) => {
167
+ void pasteAtSelection(undefined, event)
168
+ },
169
+ onPasteText: pasteTextAt,
170
+ })
171
+ emit('ready', editor)
172
+ // 切回脑图视图:居中当前选中节点(无选中则保持 zoomToFit)
173
+ const sel = props.session.selectedNode
174
+ if (sel) {
175
+ requestAnimationFrame(() => editor?.centerOnNode(sel.id, false))
176
+ setTimeout(() => editor?.centerOnNode(sel.id, false), 80)
177
+ }
178
+ })
179
+
180
+ onBeforeUnmount(() => {
181
+ if (linkCloseTimer) clearTimeout(linkCloseTimer)
182
+ editor?.destroy()
183
+ editor = null
184
+ })
185
+ </script>
186
+
187
+ <template>
188
+ <div class="mindmap-view-root">
189
+ <div ref="host" class="mindmap-view-host" />
190
+ <SelectionToolbar
191
+ v-if="selectedCount > 0"
192
+ mode="nodes"
193
+ placement="canvas-bottom"
194
+ @format="applyNodeFormat"
195
+ @task="props.session.toggleTaskSelectedNodes()"
196
+ @copy="copySelected"
197
+ @clear="props.session.clearSelectedNodeFormats()"
198
+ @delete="props.session.removeSelectedNodes()"
199
+ />
200
+ <LinkPopover
201
+ v-if="linkEditor"
202
+ :url="linkEditor.url"
203
+ :position="linkEditor.position"
204
+ @save="saveLink"
205
+ @remove="removeLink"
206
+ @keep="keepLinkPopover"
207
+ @leave="closeLinkPopoverSoon"
208
+ @close="linkEditor = null"
209
+ />
210
+ <CanvasContextMenu
211
+ v-if="contextMenu"
212
+ :position="contextMenu.position"
213
+ :multiple="contextMenu.multiple"
214
+ :can-insert-sibling="contextMenu.canInsertSibling"
215
+ :can-insert-parent="contextMenu.canInsertParent"
216
+ :can-cut="contextMenu.canCut"
217
+ :can-duplicate="contextMenu.canDuplicate"
218
+ :can-delete-only="contextMenu.canDeleteOnly"
219
+ :can-delete-tree="contextMenu.canDeleteTree"
220
+ :can-toggle-siblings="contextMenu.canToggleSiblings"
221
+ :can-focus="contextMenu.canFocus"
222
+ @insert-sibling="insertSibling"
223
+ @insert-child="insertChild"
224
+ @insert-parent="insertParent"
225
+ @copy="copySelected"
226
+ @cut="cutSelected"
227
+ @paste="pasteAtContextNode"
228
+ @duplicate="props.session.duplicateSelectedNodes()"
229
+ @delete-only="deleteOnly"
230
+ @delete-tree="props.session.removeSelectedNodes()"
231
+ @toggle-siblings="toggleContextSiblings"
232
+ @focus="focusContextNode"
233
+ @close="contextMenu = null"
234
+ />
235
+ </div>
236
+ </template>
237
+
238
+ <style scoped>
239
+ .mindmap-view-root {
240
+ position: relative;
241
+ width: 100%;
242
+ height: 100%;
243
+ min-height: 0;
244
+ }
245
+ .mindmap-view-host {
246
+ position: relative;
247
+ width: 100%;
248
+ height: 100%;
249
+ overflow: hidden;
250
+ outline: none;
251
+ background: var(--mm-canvas-bg);
252
+ }
253
+ </style>