@tnotesjs/ui 0.1.4 → 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 +28 -13
  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 +2 -2
  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
@@ -2,28 +2,31 @@
2
2
  import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
3
3
  import { isApplePlatform, primaryShortcut } from './platform'
4
4
 
5
- const props = withDefaults(defineProps<{
6
- position: { left: number; top: number }
7
- multiple?: boolean
8
- canInsertSibling?: boolean
9
- canInsertParent?: boolean
10
- canCut?: boolean
11
- canDuplicate?: boolean
12
- canDeleteOnly?: boolean
13
- canDeleteTree?: boolean
14
- canToggleSiblings?: boolean
15
- canFocus?: boolean
16
- }>(), {
17
- multiple: false,
18
- canInsertSibling: true,
19
- canInsertParent: true,
20
- canCut: true,
21
- canDuplicate: true,
22
- canDeleteOnly: true,
23
- canDeleteTree: true,
24
- canToggleSiblings: true,
25
- canFocus: true,
26
- })
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ position: { left: number; top: number }
8
+ multiple?: boolean
9
+ canInsertSibling?: boolean
10
+ canInsertParent?: boolean
11
+ canCut?: boolean
12
+ canDuplicate?: boolean
13
+ canDeleteOnly?: boolean
14
+ canDeleteTree?: boolean
15
+ canToggleSiblings?: boolean
16
+ canFocus?: boolean
17
+ }>(),
18
+ {
19
+ multiple: false,
20
+ canInsertSibling: true,
21
+ canInsertParent: true,
22
+ canCut: true,
23
+ canDuplicate: true,
24
+ canDeleteOnly: true,
25
+ canDeleteTree: true,
26
+ canToggleSiblings: true,
27
+ canFocus: true
28
+ }
29
+ )
27
30
 
28
31
  const emit = defineEmits<{
29
32
  close: []
@@ -48,10 +51,10 @@ const style = computed(() => {
48
51
  const viewportHeight = typeof window === 'undefined' ? 900 : window.innerHeight
49
52
  return {
50
53
  left: `${Math.max(8, Math.min(props.position.left, viewportWidth - width - 8))}px`,
51
- top: `${Math.max(8, Math.min(props.position.top, viewportHeight - estimatedHeight - 8))}px`,
54
+ top: `${Math.max(8, Math.min(props.position.top, viewportHeight - estimatedHeight - 8))}px`
52
55
  }
53
56
  })
54
- const shiftTab = computed(() => isApplePlatform() ? '⇧Tab' : 'Shift+Tab')
57
+ const shiftTab = computed(() => (isApplePlatform() ? '⇧Tab' : 'Shift+Tab'))
55
58
 
56
59
  type MenuAction =
57
60
  | 'close'
@@ -93,28 +96,85 @@ onBeforeUnmount(() => {
93
96
  </script>
94
97
 
95
98
  <template>
96
- <div ref="menu" class="canvas-context-menu" :style="style" role="menu" :aria-label="multiple ? '多主题右键菜单' : '主题右键菜单'" @contextmenu.prevent>
97
- <template v-if="!multiple">
98
- <button type="button" role="menuitem" :disabled="!canInsertSibling" @click="act('insertSibling')"><span>插入同级主题</span><kbd>Enter</kbd></button>
99
- <button type="button" role="menuitem" @click="act('insertChild')"><span>插入下级主题</span><kbd>Tab</kbd></button>
100
- <button type="button" role="menuitem" :disabled="!canInsertParent" @click="act('insertParent')"><span>插入上级主题</span><kbd>{{ shiftTab }}</kbd></button>
101
- <span class="context-divider" />
102
- </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">
108
+ <button
109
+ type="button"
110
+ role="menuitem"
111
+ :disabled="!canInsertSibling"
112
+ @click="act('insertSibling')"
113
+ >
114
+ <span>插入同级主题</span><kbd>Enter</kbd>
115
+ </button>
116
+ <button type="button" role="menuitem" @click="act('insertChild')">
117
+ <span>插入下级主题</span><kbd>Tab</kbd>
118
+ </button>
119
+ <button
120
+ type="button"
121
+ role="menuitem"
122
+ :disabled="!canInsertParent"
123
+ @click="act('insertParent')"
124
+ >
125
+ <span>插入上级主题</span><kbd>{{ shiftTab }}</kbd>
126
+ </button>
127
+ <span class="context-divider" />
128
+ </template>
103
129
 
104
- <button type="button" role="menuitem" @click="act('copy')"><span>复制</span><kbd>{{ primaryShortcut('C') }}</kbd></button>
105
- <button type="button" role="menuitem" :disabled="!canCut" @click="act('cut')"><span>剪切</span><kbd>{{ primaryShortcut('X') }}</kbd></button>
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>
106
136
 
107
- <template v-if="!multiple">
108
- <button type="button" role="menuitem" @click="act('paste')"><span>粘贴</span><kbd>{{ primaryShortcut('V') }}</kbd></button>
109
- <button type="button" role="menuitem" :disabled="!canDuplicate" @click="act('duplicate')"><span>创建副本</span><kbd>{{ primaryShortcut('D') }}</kbd></button>
110
- <button type="button" role="menuitem" :disabled="!canDeleteOnly" @click="act('deleteOnly')"><span>仅删除当前主题</span></button>
111
- </template>
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>
146
+ </button>
147
+ </template>
112
148
 
113
- <button type="button" role="menuitem" class="danger" :disabled="!canDeleteTree" @click="act('deleteTree')"><span>{{ multiple ? '删除所选主题及下级主题' : '删除当前主题及下级主题' }}</span><kbd>Delete</kbd></button>
114
- <span class="context-divider" />
115
- <button type="button" role="menuitem" :disabled="!canToggleSiblings" @click="act('toggleSiblings')"><span>展开/折叠同级主题</span><kbd>{{ primaryShortcut('.', { shift: true }) }}</kbd></button>
116
- <button v-if="!multiple" type="button" role="menuitem" :disabled="!canFocus" @click="act('focus')"><span>进入此主题</span><kbd>{{ primaryShortcut(']') }}</kbd></button>
117
- </div>
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>
118
178
  </template>
119
179
 
120
180
  <style scoped>
@@ -130,7 +190,7 @@ onBeforeUnmount(() => {
130
190
  border-radius: 10px;
131
191
  background: color-mix(in srgb, var(--mm-panel-bg) 96%, #3e424a 4%);
132
192
  color: var(--mm-text);
133
- box-shadow: 0 14px 42px rgb(0 0 0 / .28);
193
+ box-shadow: 0 14px 42px rgb(0 0 0 / 0.28);
134
194
  }
135
195
  .canvas-context-menu button {
136
196
  display: flex;
@@ -149,9 +209,26 @@ onBeforeUnmount(() => {
149
209
  cursor: pointer;
150
210
  }
151
211
  .canvas-context-menu button:hover:not(:disabled),
152
- .canvas-context-menu button:focus-visible { background: var(--mm-hover); outline: none; }
153
- .canvas-context-menu button:disabled { cursor: default; opacity: .38; }
154
- .canvas-context-menu button.danger { color: #e25a62; }
155
- .canvas-context-menu kbd { flex: none; color: var(--mm-text-dim); font: inherit; font-size: 12px; }
156
- .context-divider { height: 1px; margin: 6px 2px; background: var(--mm-border); }
212
+ .canvas-context-menu button:focus-visible {
213
+ background: var(--mm-hover);
214
+ outline: none;
215
+ }
216
+ .canvas-context-menu button:disabled {
217
+ cursor: default;
218
+ opacity: 0.38;
219
+ }
220
+ .canvas-context-menu button.danger {
221
+ color: #e25a62;
222
+ }
223
+ .canvas-context-menu kbd {
224
+ flex: none;
225
+ color: var(--mm-text-dim);
226
+ font: inherit;
227
+ font-size: 12px;
228
+ }
229
+ .context-divider {
230
+ height: 1px;
231
+ margin: 6px 2px;
232
+ background: var(--mm-border);
233
+ }
157
234
  </style>
@@ -2,11 +2,14 @@
2
2
  import { nextTick, ref, watch } from 'vue'
3
3
  import AppIcon from './AppIcon.vue'
4
4
 
5
- const props = withDefaults(defineProps<{
6
- url: string
7
- position: { left: number; top: number }
8
- startEditing?: boolean
9
- }>(), { startEditing: false })
5
+ const props = withDefaults(
6
+ defineProps<{
7
+ url: string
8
+ position: { left: number; top: number }
9
+ startEditing?: boolean
10
+ }>(),
11
+ { startEditing: false }
12
+ )
10
13
  const emit = defineEmits<{ save: [url: string]; remove: []; keep: []; leave: []; close: [] }>()
11
14
  const draft = ref(props.url)
12
15
  const editing = ref(props.startEditing)
@@ -40,22 +43,59 @@ function beginEdit() {
40
43
  })
41
44
  }
42
45
 
43
- function focusInput() { beginEdit() }
46
+ function focusInput() {
47
+ beginEdit()
48
+ }
44
49
  defineExpose({ focusInput })
45
50
  </script>
46
51
 
47
52
  <template>
48
53
  <div
49
- class="link-popover"
50
- :style="{ left: `${position.left}px`, top: `${position.top}px` }"
51
- @mouseenter="emit('keep')"
52
- @mouseleave="emit('leave')"
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"
76
+ >
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')"
53
95
  >
54
- <input ref="input" v-model="draft" class="link-input" :class="{ readonly: !editing }" :readonly="!editing" aria-label="链接地址" @dblclick="beginEdit" @keydown="onKeydown" />
55
- <button v-if="editing" type="button" class="link-action primary" title="更新链接 (Enter)" aria-label="更新链接" @click="submit">更新</button>
56
- <button v-else type="button" class="icon-action" title="编辑链接地址" aria-label="编辑链接地址" @click="beginEdit"><AppIcon name="edit" :size="18" /></button>
57
- <button type="button" class="icon-action" title="移除链接,保留文案" aria-label="移除链接" @click="emit('remove')"><AppIcon name="removeLink" :size="18" /></button>
58
- </div>
96
+ <AppIcon name="removeLink" :size="18" />
97
+ </button>
98
+ </div>
59
99
  </template>
60
100
 
61
101
  <style scoped>
@@ -70,14 +110,52 @@ defineExpose({ focusInput })
70
110
  border: 1px solid var(--mm-border);
71
111
  border-radius: 9px;
72
112
  background: var(--mm-panel-bg);
73
- box-shadow: 0 10px 28px rgb(0 0 0 / .2);
113
+ box-shadow: 0 10px 28px rgb(0 0 0 / 0.2);
74
114
  transform: translateX(-50%);
75
115
  }
76
- .link-input { flex: 1; min-width: 0; height: 32px; padding: 0 9px; border: 1px solid var(--mm-border); border-radius: 6px; outline: 0; background: var(--mm-canvas-bg); color: var(--mm-text); font-size: 13px; }
77
- .link-input:focus { border-color: var(--mm-accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--mm-accent) 20%, transparent); }
78
- .link-input.readonly { border-color: transparent; color: var(--mm-text-dim); cursor: default; }
79
- .link-action, .icon-action { height: 32px; border: 0; border-radius: 6px; cursor: pointer; }
80
- .link-action { padding: 0 11px; background: var(--mm-accent); color: white; }
81
- .icon-action { display: inline-flex; width: 32px; align-items: center; justify-content: center; background: transparent; color: var(--mm-text-dim); }
82
- .icon-action:hover { background: var(--mm-hover); color: var(--mm-text); }
116
+ .link-input {
117
+ flex: 1;
118
+ min-width: 0;
119
+ height: 32px;
120
+ padding: 0 9px;
121
+ border: 1px solid var(--mm-border);
122
+ border-radius: 6px;
123
+ outline: 0;
124
+ background: var(--mm-canvas-bg);
125
+ color: var(--mm-text);
126
+ font-size: 13px;
127
+ }
128
+ .link-input:focus {
129
+ border-color: var(--mm-accent);
130
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--mm-accent) 20%, transparent);
131
+ }
132
+ .link-input.readonly {
133
+ border-color: transparent;
134
+ color: var(--mm-text-dim);
135
+ cursor: default;
136
+ }
137
+ .link-action,
138
+ .icon-action {
139
+ height: 32px;
140
+ border: 0;
141
+ border-radius: 6px;
142
+ cursor: pointer;
143
+ }
144
+ .link-action {
145
+ padding: 0 11px;
146
+ background: var(--mm-accent);
147
+ color: white;
148
+ }
149
+ .icon-action {
150
+ display: inline-flex;
151
+ width: 32px;
152
+ align-items: center;
153
+ justify-content: center;
154
+ background: transparent;
155
+ color: var(--mm-text-dim);
156
+ }
157
+ .icon-action:hover {
158
+ background: var(--mm-hover);
159
+ color: var(--mm-text);
160
+ }
83
161
  </style>
@@ -2,9 +2,12 @@
2
2
  import { onBeforeUnmount, ref, watch } from 'vue'
3
3
  import type { MarkdownDiagnostic } from '@tnotesjs/mindmap-core'
4
4
 
5
- const props = withDefaults(defineProps<{ modelValue: string; diagnostics?: readonly MarkdownDiagnostic[] }>(), {
6
- diagnostics: () => [],
7
- })
5
+ const props = withDefaults(
6
+ defineProps<{ modelValue: string; diagnostics?: readonly MarkdownDiagnostic[] }>(),
7
+ {
8
+ diagnostics: () => []
9
+ }
10
+ )
8
11
  const emit = defineEmits<{
9
12
  'update:modelValue': [value: string]
10
13
  pasteImage: [blob: Blob, selectionStart: number, selectionEnd: number]
@@ -20,7 +23,7 @@ watch(
20
23
  (md) => {
21
24
  // 正在输入时不打断用户,输入停顿后由其它视图变更回流
22
25
  if (!typing) draft.value = md
23
- },
26
+ }
24
27
  )
25
28
 
26
29
  function onInput(e: Event) {
@@ -1,7 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  import { onBeforeUnmount, onMounted, ref } from 'vue'
3
3
  import { CanvasEditor, serializeSubtree } from '@tnotesjs/mindmap-core'
4
- import type { CanvasContextRequest, CanvasLinkHover, InlineFormat, MindmapNode, MindmapSession } from '@tnotesjs/mindmap-core'
4
+ import type {
5
+ CanvasContextRequest,
6
+ CanvasLinkHover,
7
+ InlineFormat,
8
+ MindmapNode,
9
+ MindmapSession
10
+ } from '@tnotesjs/mindmap-core'
5
11
  import CanvasContextMenu from './CanvasContextMenu.vue'
6
12
  import LinkPopover from './LinkPopover.vue'
7
13
  import SelectionToolbar from './SelectionToolbar.vue'
@@ -74,7 +80,9 @@ function applyNodeFormat(format: InlineFormat) {
74
80
  }
75
81
 
76
82
  function selectedMarkdown() {
77
- return selectedRoots().map((node) => serializeSubtree(node)).join('\n')
83
+ return selectedRoots()
84
+ .map((node) => serializeSubtree(node))
85
+ .join('\n')
78
86
  }
79
87
 
80
88
  async function copySelected(event?: ClipboardEvent | null) {
@@ -166,7 +174,7 @@ onMounted(() => {
166
174
  onPasteSelection: (event) => {
167
175
  void pasteAtSelection(undefined, event)
168
176
  },
169
- onPasteText: pasteTextAt,
177
+ onPasteText: pasteTextAt
170
178
  })
171
179
  emit('ready', editor)
172
180
  // 切回脑图视图:居中当前选中节点(无选中则保持 zoomToFit)