@tnotesjs/ui 0.3.3 → 0.4.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/README.md CHANGED
@@ -100,35 +100,3 @@ console.log('a')
100
100
  | `enableCopy` | `true` |
101
101
  | `enableFullscreen` | `true` |
102
102
  | `enableCenterToggle` | `true` |
103
-
104
- ### `MindmapEditor`(`@tnotesjs/ui/mindmap-editor`)
105
-
106
- 思维导图编辑器的**唯一实现**。`@tnotesjs/ui/mindmap` 那个块(`Mindmap.vue`)是给 SSG / Desk 笔记
107
- 内嵌用的「预览 + 可选编辑」;本入口导出它内部复用的编辑器组件,供自带页面外壳的宿主直接组装。
108
- 当前宿主:`apps/mindmap-web`、`apps/mindmap-vscode`。
109
-
110
- | 导出 | 说明 |
111
- | ----------------------------------------------------------------------------- | ---------------------------------------------- |
112
- | `MindmapView` | 脑图画布(含选区工具条 / 右键菜单 / 链接浮层) |
113
- | `OutlineView` | 大纲视图(拖拽、行内富文本、折叠) |
114
- | `MarkdownView` | 源码视图(防抖回流 + 诊断定位) |
115
- | `SearchBar` | 三视图共用的搜索条 |
116
- | `FocusBreadcrumbs` | 进入主题后的面包屑(与主 barrel 同源) |
117
- | `AppIcon` / `IconButton` / `CollapseMenu` | 外壳按钮与图标 |
118
- | `CanvasContextMenu` / `LinkPopover` / `RichInlineEditor` / `SelectionToolbar` | 上面几个组件的组成件,供未来宿主自行组装 |
119
- | `insertImageIntoSource` | 源码视图的整节点图片插入 |
120
- | `pasteCanvasOutline` / `readMindmapClipboard` / `writeMindmapClipboard` | 脑图剪贴板(含无权限时的进程内回退) |
121
- | `resolveAfterDropLevel` / `DropLevel` | 大纲拖拽落点解析 |
122
- | `primaryShortcut` / `altShortcut` / `isApplePlatform` | 平台快捷键文案 |
123
-
124
- **浮层挂载点(`teleportTo`)**:`MindmapView`(并透传给 `CanvasContextMenu` / `LinkPopover`)接受
125
- `teleportTo?: string`。默认 `undefined` = 就地渲染——浮层是 `position: fixed`,嵌在笔记里的宿主
126
- (Desk / SSG)teleport 到 `body` 会脱离笔记的层叠上下文。整页宿主传 `'body'`。
127
-
128
- **宿主职责(不属于本包)**:`MindmapSession` 的归属与生命周期、`--mm-*` 主题变量
129
- (`--mm-canvas-bg` / `--mm-panel-bg` / `--mm-border` / `--mm-text` / `--mm-text-dim` / `--mm-hover` /
130
- `--mm-selected-bg` / `--mm-accent` / `--mm-edit-bg`)、落盘、图片写入、宿主快捷键到组件
131
- `defineExpose` 钩子的桥接(`OutlineView` 的 `locateNode` / `selectAllFromHost` / `undoFromHost` /
132
- `redoFromHost`,`MarkdownView` 的 `selectAllFromHost` / `flushDraft`)。
133
-
134
- 改公共交互只改这里一处,三个宿主同时生效。
@@ -1,8 +1,13 @@
1
1
  interface MindmapFenceOptions {
2
2
  title?: string;
3
3
  initialExpandLevel?: number;
4
+ /** Pane height in px (`h=480` in the fence info). */
5
+ height?: number;
4
6
  }
5
- /** Parse the canonical `mindmap [title] 2` fence metadata. */
7
+ declare const MINDMAP_MIN_HEIGHT = 200;
8
+ declare const MINDMAP_MAX_HEIGHT = 1600;
9
+ declare function clampMindmapHeight(value: number): number;
10
+ /** Parse the canonical `mindmap [title] 2 h=480` fence metadata. */
6
11
  declare function parseMindmapFence(openLine: string): MindmapFenceOptions | null;
7
12
  interface NormalizeMindmapOptions {
8
13
  title?: string;
@@ -11,4 +16,4 @@ interface NormalizeMindmapOptions {
11
16
  /** Ensure canonical mindmap Markdown has exactly one H1 root title. */
12
17
  declare function normalizeMindmapMarkdown(source: string, options?: NormalizeMindmapOptions): string;
13
18
 
14
- export { type MindmapFenceOptions, type NormalizeMindmapOptions, normalizeMindmapMarkdown, parseMindmapFence };
19
+ export { MINDMAP_MAX_HEIGHT, MINDMAP_MIN_HEIGHT, type MindmapFenceOptions, type NormalizeMindmapOptions, clampMindmapHeight, normalizeMindmapMarkdown, parseMindmapFence };
@@ -1,4 +1,9 @@
1
1
  // src/components/Mindmap/markdown.ts
2
+ var MINDMAP_MIN_HEIGHT = 200;
3
+ var MINDMAP_MAX_HEIGHT = 1600;
4
+ function clampMindmapHeight(value) {
5
+ return Math.round(Math.min(MINDMAP_MAX_HEIGHT, Math.max(MINDMAP_MIN_HEIGHT, value)));
6
+ }
2
7
  function cleanHeadingText(value) {
3
8
  return value.trim().replace(/\s+#+\s*$/, "").trim();
4
9
  }
@@ -13,9 +18,15 @@ function parseMindmapFence(openLine) {
13
18
  options.title = cleanHeadingText(titleMatch[1]) || void 0;
14
19
  rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim();
15
20
  }
16
- if (rest && !/^\d+$/.test(rest)) return null;
17
- if (rest) {
18
- options.initialExpandLevel = Math.max(1, Number(rest));
21
+ for (const token of rest.split(/\s+/).filter(Boolean)) {
22
+ const height = token.match(/^h=(\d+)$/);
23
+ if (height) {
24
+ options.height = clampMindmapHeight(Number(height[1]));
25
+ } else if (/^\d+$/.test(token) && options.initialExpandLevel === void 0) {
26
+ options.initialExpandLevel = Math.max(1, Number(token));
27
+ } else {
28
+ return null;
29
+ }
19
30
  }
20
31
  return options;
21
32
  }
@@ -41,6 +52,9 @@ ${body.join("\n")}
41
52
  `;
42
53
  }
43
54
  export {
55
+ MINDMAP_MAX_HEIGHT,
56
+ MINDMAP_MIN_HEIGHT,
57
+ clampMindmapHeight,
44
58
  normalizeMindmapMarkdown,
45
59
  parseMindmapFence
46
60
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tnotesjs/ui",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Shared Vue UI for TNotes built-in blocks — consumed by the TNotes SSG and Desk.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -60,11 +60,6 @@
60
60
  "types": "./src/entries/excalidraw-editor.ts",
61
61
  "import": "./src/entries/excalidraw-editor.ts",
62
62
  "default": "./src/entries/excalidraw-editor.ts"
63
- },
64
- "./mindmap-editor": {
65
- "types": "./src/entries/mindmap-editor.ts",
66
- "import": "./src/entries/mindmap-editor.ts",
67
- "default": "./src/entries/mindmap-editor.ts"
68
63
  }
69
64
  },
70
65
  "files": [
@@ -88,7 +83,7 @@
88
83
  "react-dom": "18.3.1",
89
84
  "sass": "^1.90.0",
90
85
  "shiki": "4.4.2",
91
- "@tnotesjs/mindmap-core": "^0.2.2"
86
+ "@tnotesjs/mindmap-core": "^0.2.3"
92
87
  },
93
88
  "devDependencies": {
94
89
  "@eslint/js": "^10.0.1",
@@ -111,7 +106,8 @@
111
106
  },
112
107
  "publishConfig": {
113
108
  "access": "public",
114
- "registry": "https://registry.npmjs.org/"
109
+ "registry": "https://registry.npmjs.org/",
110
+ "provenance": true
115
111
  },
116
112
  "repository": {
117
113
  "type": "git",
@@ -136,4 +132,4 @@
136
132
  "lint": "eslint . --max-warnings=0",
137
133
  "lint:fix": "eslint . --fix"
138
134
  }
139
- }
135
+ }
@@ -82,6 +82,8 @@ const props = withDefaults(
82
82
  editable?: boolean
83
83
  /** Show expand-level control on the chrome (Desk). */
84
84
  expandLevelControl?: boolean
85
+ /** Pane height in px; omit for the default 440px canvas. */
86
+ height?: number
85
87
  /** Explicit dark mode; omit to auto-detect html.dark / data-theme=dark. */
86
88
  isDark?: boolean
87
89
  /**
@@ -101,6 +103,7 @@ const props = withDefaults(
101
103
  initialExpandLevel: 3,
102
104
  editable: false,
103
105
  expandLevelControl: false,
106
+ height: undefined,
104
107
  isDark: undefined,
105
108
  resolveImageSrc: undefined,
106
109
  writeAsset: undefined
@@ -161,6 +164,9 @@ function decodeContent(value: string): string {
161
164
  }
162
165
  }
163
166
 
167
+ const paneStyle = computed(() =>
168
+ props.height ? { '--mindmap-pane-height': `${props.height}px` } : undefined
169
+ )
164
170
  const rawContent = computed(() => props.source || decodeContent(props.content || ''))
165
171
  const normalizedContent = computed(() => normalizeMindmapMarkdown(rawContent.value))
166
172
 
@@ -508,9 +514,14 @@ function zoomToFit(): void {
508
514
  viewer?.zoomToFit()
509
515
  }
510
516
 
511
- function activateCanvas(): void {
517
+ /** 画布上的浮层自己管焦点;被抢走会让节点编辑提前提交,工具栏点了也不生效 */
518
+ const CANVAS_OVERLAY_SELECTOR =
519
+ '.selection-toolbar, .mm-edit-input, .link-popover, .canvas-context-menu'
520
+
521
+ function activateCanvas(event: PointerEvent): void {
512
522
  isCanvasActive.value = true
513
523
  if (!props.editable) return
524
+ if (event.target instanceof Element && event.target.closest(CANVAS_OVERLAY_SELECTOR)) return
514
525
  const editorEl = canvasHost.value?.querySelector<HTMLElement>('.mm-editor')
515
526
  editorEl?.focus({ preventScroll: true })
516
527
  }
@@ -729,6 +740,7 @@ onBeforeUnmount(() => {
729
740
  <section
730
741
  ref="previewRoot"
731
742
  class="mindmap-preview"
743
+ :style="paneStyle"
732
744
  :class="{
733
745
  'is-dark': dark,
734
746
  'is-fullscreen': isFullscreen,
@@ -1039,7 +1051,7 @@ onBeforeUnmount(() => {
1039
1051
  .mindmap-canvas-host {
1040
1052
  position: relative;
1041
1053
  width: 100%;
1042
- height: 440px;
1054
+ height: var(--mindmap-pane-height, 440px);
1043
1055
  overflow: hidden;
1044
1056
  /*
1045
1057
  * No background of its own: the canvas is cleared to transparent, and the
@@ -1051,7 +1063,7 @@ onBeforeUnmount(() => {
1051
1063
  touch-action: none;
1052
1064
  user-select: none;
1053
1065
 
1054
- /* CanvasEditor overlay + chrome tokens (aligned with mindmap-web). */
1066
+ /* CanvasEditor overlay + chrome tokens. */
1055
1067
  --mm-canvas-bg: var(--tn-c-bg);
1056
1068
  --mm-panel-bg: var(--tn-c-bg-soft, var(--tn-c-bg));
1057
1069
  --mm-text: var(--tn-c-text);
@@ -1091,7 +1103,7 @@ onBeforeUnmount(() => {
1091
1103
  pointer-events: none;
1092
1104
  }
1093
1105
 
1094
- /* Marquee box — required for box-select feedback (aligned with mindmap-web). */
1106
+ /* Marquee box — required for box-select feedback. */
1095
1107
  .mindmap-canvas-host:deep(.mm-selection-box) {
1096
1108
  position: absolute;
1097
1109
  z-index: 8;
@@ -1169,8 +1181,7 @@ onBeforeUnmount(() => {
1169
1181
  .mindmap-canvas-host:deep(.mm-edit-input .inline-run.highlight:not(.code)) {
1170
1182
  padding: 0 1px;
1171
1183
  border-radius: 2px;
1172
- background: #fff36a;
1173
- color: #242424;
1184
+ background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
1174
1185
  }
1175
1186
  .mindmap-canvas-host:deep(.mm-edit-input .inline-run.code) {
1176
1187
  padding: 0 4px;
@@ -1213,13 +1224,13 @@ onBeforeUnmount(() => {
1213
1224
  }
1214
1225
 
1215
1226
  .mindmap-outline {
1216
- max-height: 560px;
1227
+ max-height: var(--mindmap-pane-height, 560px);
1217
1228
  padding: 18px 22px 22px;
1218
1229
  overflow: auto;
1219
1230
  }
1220
1231
 
1221
1232
  .mindmap-outline.is-editable {
1222
- height: 440px;
1233
+ height: var(--mindmap-pane-height, 440px);
1223
1234
  max-height: none;
1224
1235
  padding: 0;
1225
1236
  overflow: hidden;
@@ -1228,7 +1239,7 @@ onBeforeUnmount(() => {
1228
1239
  }
1229
1240
 
1230
1241
  .mindmap-source-wrap.is-editable {
1231
- height: 440px;
1242
+ height: var(--mindmap-pane-height, 440px);
1232
1243
  overflow: hidden;
1233
1244
  display: flex;
1234
1245
  flex-direction: column;
@@ -1332,8 +1343,7 @@ onBeforeUnmount(() => {
1332
1343
  .mindmap-outline :deep(.is-highlight) {
1333
1344
  padding: 0 2px;
1334
1345
  border-radius: 2px;
1335
- background: #ffe56b;
1336
- color: #252525;
1346
+ background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
1337
1347
  }
1338
1348
  .mindmap-outline :deep(.is-code) {
1339
1349
  padding: 1px 5px;
@@ -1349,7 +1359,7 @@ onBeforeUnmount(() => {
1349
1359
  }
1350
1360
 
1351
1361
  .mindmap-source {
1352
- max-height: 560px;
1362
+ max-height: var(--mindmap-pane-height, 560px);
1353
1363
  margin: 0;
1354
1364
  padding: 18px 22px;
1355
1365
  overflow: auto;
@@ -1431,7 +1441,7 @@ onBeforeUnmount(() => {
1431
1441
 
1432
1442
  @media (max-width: 768px) {
1433
1443
  .mindmap-canvas-host {
1434
- height: 360px;
1444
+ height: var(--mindmap-pane-height, 360px);
1435
1445
  }
1436
1446
  .mindmap-preview-actions {
1437
1447
  top: 6px;
@@ -1,9 +1,11 @@
1
1
  <script setup lang="ts">
2
- import { onBeforeUnmount, onMounted, ref } from 'vue'
2
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
3
3
  import { CanvasEditor, serializeSubtree } from '@tnotesjs/mindmap-core'
4
4
  import type {
5
5
  CanvasContextRequest,
6
+ CanvasEditState,
6
7
  CanvasLinkHover,
8
+ CanvasLinkRequest,
7
9
  InlineFormat,
8
10
  MindmapNode,
9
11
  MindmapSession
@@ -33,6 +35,27 @@ const emit = defineEmits<{
33
35
  const host = ref<HTMLElement>()
34
36
  let editor: CanvasEditor | null = null
35
37
  const selectedCount = ref(0)
38
+ /** 选中的不全是图片节点;图片说明是纯文本,不出格式按钮 */
39
+ const selectionFormattable = ref(true)
40
+ /** 内联编辑态:编辑框里有文字选区时,弹出与大纲视图同一套文字格式工具栏 */
41
+ const editState = ref<CanvasEditState | null>(null)
42
+ const selectionLink = ref<CanvasLinkRequest | null>(null)
43
+ const textSelection = computed(() => {
44
+ const state = editState.value
45
+ return state && !state.image ? state.selection : null
46
+ })
47
+ const textFormats = computed<Partial<Record<InlineFormat, boolean>>>(() => {
48
+ const state = editState.value
49
+ const selection = textSelection.value
50
+ if (!state || !selection) return {}
51
+ const formats: InlineFormat[] = ['bold', 'italic', 'underline', 'strike', 'highlight', 'code']
52
+ return Object.fromEntries(
53
+ formats.map((format) => [
54
+ format,
55
+ props.session.inlineFormatActive(state.nodeId, selection.start, selection.end, format)
56
+ ])
57
+ )
58
+ })
36
59
  const linkEditor = ref<CanvasLinkHover | null>(null)
37
60
  const contextMenu = ref<CanvasContextRequest | null>(null)
38
61
  let linkCloseTimer: ReturnType<typeof setTimeout> | null = null
@@ -87,6 +110,41 @@ function applyNodeFormat(format: InlineFormat) {
87
110
  props.session.formatSelectedNodes(format)
88
111
  }
89
112
 
113
+ function formatEditing(format: InlineFormat) {
114
+ editor?.formatEditing(format)
115
+ }
116
+
117
+ function clearEditingFormats() {
118
+ editor?.clearEditingFormats()
119
+ }
120
+
121
+ function linkEditing() {
122
+ editor?.linkEditing()
123
+ }
124
+
125
+ function toggleEditingTask() {
126
+ editor?.commitEditing()
127
+ props.session.toggleTaskSelectedNodes()
128
+ }
129
+
130
+ function removeEditingNode() {
131
+ editor?.commitEditing()
132
+ props.session.removeSelectedNodes()
133
+ }
134
+
135
+ function saveSelectionLink(url: string) {
136
+ const request = selectionLink.value
137
+ selectionLink.value = null
138
+ if (!request) return
139
+ props.session.setNodeInlineLink(request.nodeId, request.start, request.end, url.trim() || null)
140
+ }
141
+
142
+ function removeSelectionLink() {
143
+ const request = selectionLink.value
144
+ selectionLink.value = null
145
+ if (request) props.session.setNodeInlineLink(request.nodeId, request.start, request.end, null)
146
+ }
147
+
90
148
  function selectedMarkdown() {
91
149
  return selectedRoots()
92
150
  .map((node) => serializeSubtree(node))
@@ -167,6 +225,14 @@ onMounted(() => {
167
225
  onSelectionPositionChange: (_position, count) => {
168
226
  // Canvas toolbar is absolutely anchored; only the selection count gates visibility.
169
227
  selectedCount.value = count
228
+ selectionFormattable.value = !props.session.selectedNodes.every((node) => node.content.image)
229
+ },
230
+ onEditStateChange: (state) => {
231
+ editState.value = state
232
+ },
233
+ onRequestLink: (request) => {
234
+ linkEditor.value = null
235
+ selectionLink.value = request
170
236
  },
171
237
  onLinkHover: onCanvasLinkHover,
172
238
  onContextMenu: (request) => {
@@ -204,9 +270,22 @@ onBeforeUnmount(() => {
204
270
  <div class="mindmap-view-root">
205
271
  <div ref="host" class="mindmap-view-host" />
206
272
  <SelectionToolbar
207
- v-if="selectedCount > 0"
273
+ v-if="textSelection && !selectionLink"
274
+ mode="text"
275
+ :position="textSelection.position"
276
+ :active-formats="textFormats"
277
+ :can-insert-image="false"
278
+ @format="formatEditing"
279
+ @task="toggleEditingTask"
280
+ @link="linkEditing"
281
+ @clear="clearEditingFormats"
282
+ @delete="removeEditingNode"
283
+ />
284
+ <SelectionToolbar
285
+ v-else-if="selectedCount > 0 && !editState"
208
286
  mode="nodes"
209
287
  placement="canvas-bottom"
288
+ :formattable="selectionFormattable"
210
289
  @format="applyNodeFormat"
211
290
  @task="props.session.toggleTaskSelectedNodes()"
212
291
  @copy="copySelected"
@@ -224,6 +303,16 @@ onBeforeUnmount(() => {
224
303
  @leave="closeLinkPopoverSoon"
225
304
  @close="linkEditor = null"
226
305
  />
306
+ <LinkPopover
307
+ v-if="selectionLink"
308
+ :url="selectionLink.url"
309
+ :position="selectionLink.position"
310
+ :teleport-to="teleportTo"
311
+ start-editing
312
+ @save="saveSelectionLink"
313
+ @remove="removeSelectionLink"
314
+ @close="selectionLink = null"
315
+ />
227
316
  <CanvasContextMenu
228
317
  v-if="contextMenu"
229
318
  :position="contextMenu.position"
@@ -361,7 +361,7 @@ function selectionPosition(input: RichInlineEditorElement): { left: number; top:
361
361
  function syncTextSelection(nodeId: string, input: RichInlineEditorElement) {
362
362
  const start = input.selectionStart ?? 0
363
363
  const end = input.selectionEnd ?? 0
364
- if (start === end) {
364
+ if (start === end || props.session.document.find(nodeId)?.content.image) {
365
365
  if (textSelection.value?.nodeId === nodeId) textSelection.value = null
366
366
  return
367
367
  }
@@ -713,6 +713,8 @@ function applyInputFormatShortcut(
713
713
  input: RichInlineEditorElement,
714
714
  format: InlineFormat
715
715
  ) {
716
+ // 图片节点的文字是图片说明:纯文本,不接受行内格式
717
+ if (node.content.image) return
716
718
  const caretStart = input.selectionStart ?? 0
717
719
  const caretEnd = input.selectionEnd ?? 0
718
720
  if (input.value.length === 0) return
@@ -741,6 +743,8 @@ function applyInputFormatShortcut(
741
743
  }
742
744
 
743
745
  function applyInputClearFormats(node: MindmapNode, input: RichInlineEditorElement) {
746
+ // 图片节点的文字是图片说明:纯文本,不接受行内格式
747
+ if (node.content.image) return
744
748
  const caretStart = input.selectionStart ?? 0
745
749
  const caretEnd = input.selectionEnd ?? 0
746
750
  if (input.value.length === 0) return
@@ -416,8 +416,7 @@ onBeforeUnmount(emitCommit)
416
416
  .rich-inline-editor :deep(.inline-run.highlight:not(.code)) {
417
417
  padding: 0 1px;
418
418
  border-radius: 2px;
419
- background: #fff36a;
420
- color: #242424;
419
+ background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
421
420
  }
422
421
  .rich-inline-editor :deep(.inline-run.code) {
423
422
  padding: 2px 6px;
@@ -16,8 +16,18 @@ const props = withDefaults(
16
16
  */
17
17
  placement?: 'fixed' | 'canvas-bottom'
18
18
  activeFormats?: Partial<Record<InlineFormat, boolean>>
19
+ /** 宿主没有插入图片入口时隐藏(脑图编辑框) */
20
+ canInsertImage?: boolean
21
+ /** 选中的全是图片节点时为 false:图片说明是纯文本,不出格式按钮 */
22
+ formattable?: boolean
19
23
  }>(),
20
- { placement: 'fixed', position: undefined, activeFormats: undefined }
24
+ {
25
+ placement: 'fixed',
26
+ position: undefined,
27
+ activeFormats: undefined,
28
+ canInsertImage: true,
29
+ formattable: true
30
+ }
21
31
  )
22
32
 
23
33
  const emit = defineEmits<{
@@ -90,21 +100,24 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
90
100
  role="toolbar"
91
101
  :aria-label="mode === 'text' ? '文字格式工具栏' : '多主题工具栏'"
92
102
  @pointerdown.prevent
103
+ @mousedown.prevent
93
104
  >
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" />
105
+ <template v-if="formattable">
106
+ <button
107
+ v-for="item in formats"
108
+ :key="item.id"
109
+ type="button"
110
+ class="format-button"
111
+ :class="[{ active: activeFormats?.[item.id] }, `is-${item.id}`]"
112
+ :data-tooltip="item.label"
113
+ :aria-label="item.label"
114
+ @click="emit('format', item.id)"
115
+ >
116
+ <AppIcon v-if="item.id === 'highlight'" name="highlight" :size="21" />
117
+ <template v-else>{{ item.text }}</template>
118
+ </button>
119
+ <span class="toolbar-divider" />
120
+ </template>
108
121
  <button
109
122
  type="button"
110
123
  class="tool-button"
@@ -116,6 +129,7 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
116
129
  </button>
117
130
  <template v-if="mode === 'text'">
118
131
  <button
132
+ v-if="canInsertImage"
119
133
  type="button"
120
134
  class="tool-button"
121
135
  :data-tooltip="`添加图片 (${altShortcut('Enter')})`"
@@ -135,12 +149,12 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
135
149
  </button>
136
150
  <button
137
151
  type="button"
138
- class="tool-button code-button"
152
+ class="tool-button"
139
153
  :data-tooltip="`行内代码 (${primaryShortcut('E')})`"
140
154
  aria-label="行内代码"
141
155
  @click="emit('format', 'code')"
142
156
  >
143
- &lt;/&gt;
157
+ <AppIcon name="code" :size="20" />
144
158
  </button>
145
159
  </template>
146
160
  <button
@@ -155,6 +169,7 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
155
169
  </button>
156
170
  <span class="toolbar-divider" />
157
171
  <button
172
+ v-if="formattable"
158
173
  type="button"
159
174
  class="tool-button"
160
175
  :data-tooltip="`清除样式 (${primaryShortcut('\\')})`"
@@ -217,7 +232,13 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
217
232
  .tool-button::after {
218
233
  position: absolute;
219
234
  z-index: 2;
235
+ /* 宿主可能有全局 [data-tooltip]::after(例如 Desk 往下弹),这里把方向和显隐都定死 */
236
+ top: auto;
220
237
  bottom: calc(100% + 9px);
238
+ min-width: 0;
239
+ border: 0;
240
+ visibility: visible;
241
+ transition: none;
221
242
  left: 50%;
222
243
  width: max-content;
223
244
  max-width: min(260px, calc(100vw - 16px));
@@ -266,11 +287,6 @@ onBeforeUnmount(() => window.removeEventListener('resize', measureToolbar))
266
287
  .format-button.is-highlight {
267
288
  color: #d3c900;
268
289
  }
269
- .tool-button.code-button {
270
- font-family: ui-monospace, monospace;
271
- font-size: 15px;
272
- font-weight: 700;
273
- }
274
290
  .tool-button.danger {
275
291
  color: #e14f5b;
276
292
  }
@@ -1,6 +1,15 @@
1
1
  export interface MindmapFenceOptions {
2
2
  title?: string
3
3
  initialExpandLevel?: number
4
+ /** Pane height in px (`h=480` in the fence info). */
5
+ height?: number
6
+ }
7
+
8
+ export const MINDMAP_MIN_HEIGHT = 200
9
+ export const MINDMAP_MAX_HEIGHT = 1600
10
+
11
+ export function clampMindmapHeight(value: number): number {
12
+ return Math.round(Math.min(MINDMAP_MAX_HEIGHT, Math.max(MINDMAP_MIN_HEIGHT, value)))
4
13
  }
5
14
 
6
15
  function cleanHeadingText(value: string): string {
@@ -10,7 +19,7 @@ function cleanHeadingText(value: string): string {
10
19
  .trim()
11
20
  }
12
21
 
13
- /** Parse the canonical `mindmap [title] 2` fence metadata. */
22
+ /** Parse the canonical `mindmap [title] 2 h=480` fence metadata. */
14
23
  export function parseMindmapFence(openLine: string): MindmapFenceOptions | null {
15
24
  const fenceBody = openLine.trim().replace(/^`+\s*/, '')
16
25
  const nameMatch = fenceBody.match(/^mindmap(?=\s|\[|$)/)
@@ -25,9 +34,15 @@ export function parseMindmapFence(openLine: string): MindmapFenceOptions | null
25
34
  `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim()
26
35
  }
27
36
 
28
- if (rest && !/^\d+$/.test(rest)) return null
29
- if (rest) {
30
- options.initialExpandLevel = Math.max(1, Number(rest))
37
+ for (const token of rest.split(/\s+/).filter(Boolean)) {
38
+ const height = token.match(/^h=(\d+)$/)
39
+ if (height) {
40
+ options.height = clampMindmapHeight(Number(height[1]))
41
+ } else if (/^\d+$/.test(token) && options.initialExpandLevel === undefined) {
42
+ options.initialExpandLevel = Math.max(1, Number(token))
43
+ } else {
44
+ return null
45
+ }
31
46
  }
32
47
  return options
33
48
  }
package/src/index.ts CHANGED
@@ -11,7 +11,13 @@ export {
11
11
  resolveWordListFeatures
12
12
  } from './components/WordList/wordListFeatures'
13
13
  export type { WordListFeatures } from './components/WordList/wordListFeatures'
14
- export { normalizeMindmapMarkdown, parseMindmapFence } from './components/Mindmap/markdown'
14
+ export {
15
+ clampMindmapHeight,
16
+ MINDMAP_MAX_HEIGHT,
17
+ MINDMAP_MIN_HEIGHT,
18
+ normalizeMindmapMarkdown,
19
+ parseMindmapFence
20
+ } from './components/Mindmap/markdown'
15
21
  export type { MindmapFenceOptions, NormalizeMindmapOptions } from './components/Mindmap/markdown'
16
22
  export type { NotesTableRow } from './components/NotesTable/types'
17
23
  export {
@@ -159,7 +159,7 @@ html[data-theme='dark'] .tn-code-highlight span:not(.line) {
159
159
  }
160
160
  .tn-code-group {
161
161
  position: relative;
162
- margin: 1rem 0;
162
+ margin: 0;
163
163
  }
164
164
  .tn-code-group__tabs {
165
165
  position: absolute;
@@ -112,6 +112,12 @@
112
112
  font: 0.875em var(--tn-font-mono);
113
113
  padding: 3px 6px;
114
114
  }
115
+ :is(.tn-prose, .vp-doc) :where(mark) {
116
+ color: inherit;
117
+ background: var(--tn-c-mark-bg);
118
+ border-radius: 2px;
119
+ padding: 0 2px;
120
+ }
115
121
  :is(.tn-prose, .vp-doc) :where(blockquote) {
116
122
  margin: 16px 0;
117
123
  border-left: 2px solid var(--tn-c-divider);
@@ -185,7 +191,7 @@
185
191
  }
186
192
 
187
193
  /* Callouts — same soft-tint cards as Desk visual (no left accent bar). */
188
- :is(.tn-prose, .vp-doc) :where(.tn-custom-block) {
194
+ :is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block) {
189
195
  box-sizing: border-box;
190
196
  margin: 16px 0;
191
197
  padding: 12px 16px;
@@ -195,37 +201,37 @@
195
201
  font-size: inherit;
196
202
  line-height: 1.5;
197
203
  }
198
- :is(.tn-prose, .vp-doc) :where(.tn-custom-block.tip) {
204
+ :is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.tip) {
199
205
  background-color: var(--tn-c-highlight);
200
206
  }
201
- :is(.tn-prose, .vp-doc) :where(.tn-custom-block.info),
202
- :is(.tn-prose, .vp-doc) :where(.tn-custom-block.note),
203
- :is(.tn-prose, .vp-doc) :where(.tn-custom-block.details) {
207
+ :is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.info),
208
+ :is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.note),
209
+ :is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.details) {
204
210
  background-color: var(--tn-c-hover);
205
211
  }
206
- :is(.tn-prose, .vp-doc) :where(.tn-custom-block.warning) {
212
+ :is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.warning) {
207
213
  background-color: var(--tn-c-warning-soft);
208
214
  }
209
- :is(.tn-prose, .vp-doc) :where(.tn-custom-block.danger) {
215
+ :is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block.danger) {
210
216
  background-color: var(--tn-c-danger-soft);
211
217
  }
212
- :is(.tn-prose, .vp-doc) :where(.tn-custom-block-title) {
218
+ :is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block-title) {
213
219
  margin: 0 0 2px;
214
220
  font-size: inherit;
215
221
  font-weight: 600;
216
222
  line-height: 1.5;
217
223
  color: var(--tn-c-text);
218
224
  }
219
- :is(.tn-prose, .vp-doc) :where(.tn-custom-block > p:not(.tn-custom-block-title)) {
225
+ :is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block > p:not(.tn-custom-block-title)) {
220
226
  margin: 0;
221
227
  padding: 4px 0;
222
228
  font-size: 1em;
223
229
  line-height: 1.5;
224
230
  }
225
- :is(.tn-prose, .vp-doc) :where(.tn-custom-block > :last-child) {
231
+ :is(.tn-prose, .vp-doc, .cm-lp-card) :where(.tn-custom-block > :last-child) {
226
232
  margin-bottom: 0;
227
233
  }
228
- :is(.tn-prose, .vp-doc) :where(details.tn-custom-block > summary) {
234
+ :is(.tn-prose, .vp-doc, .cm-lp-card) :where(details.tn-custom-block > summary) {
229
235
  display: flex;
230
236
  align-items: center;
231
237
  gap: 8px;
@@ -236,13 +242,14 @@
236
242
  font-weight: 600;
237
243
  line-height: 1.5;
238
244
  }
239
- :is(.tn-prose, .vp-doc) :where(details.tn-custom-block:not([open]) > summary) {
245
+ :is(.tn-prose, .vp-doc, .cm-lp-card) :where(details.tn-custom-block:not([open]) > summary) {
240
246
  margin-bottom: 0;
241
247
  }
242
- :is(.tn-prose, .vp-doc) :where(details.tn-custom-block > summary::-webkit-details-marker) {
248
+ :is(.tn-prose, .vp-doc, .cm-lp-card)
249
+ :where(details.tn-custom-block > summary::-webkit-details-marker) {
243
250
  display: none;
244
251
  }
245
- :is(.tn-prose, .vp-doc) :where(details.tn-custom-block > summary::before) {
252
+ :is(.tn-prose, .vp-doc, .cm-lp-card) :where(details.tn-custom-block > summary::before) {
246
253
  content: '';
247
254
  flex: none;
248
255
  box-sizing: border-box;
@@ -256,7 +263,7 @@
256
263
  transform-origin: 50% 50%;
257
264
  transition: transform 120ms ease;
258
265
  }
259
- :is(.tn-prose, .vp-doc) :where(details.tn-custom-block[open] > summary::before) {
266
+ :is(.tn-prose, .vp-doc, .cm-lp-card) :where(details.tn-custom-block[open] > summary::before) {
260
267
  margin-top: -0.05em;
261
268
  transform: rotate(45deg);
262
269
  }
@@ -22,6 +22,8 @@
22
22
  --tn-c-warning-soft: rgba(234, 179, 8, 0.14);
23
23
  --tn-c-danger-soft: rgba(244, 63, 94, 0.14);
24
24
  --tn-c-default-soft: #e8e8e9;
25
+ /* ==高亮== 的底色:正文、Desk 编辑器、SSG 共用 */
26
+ --tn-c-mark-bg: rgba(255, 221, 0, 0.45);
25
27
  /* 任务列表复选框(对齐语雀):16px 方框、圆角 4px、选中填充品牌绿 + 白色对勾。 */
26
28
  --tn-c-task-check: #00b96b;
27
29
  --tn-c-task-check-border: #d9d9d9;
@@ -69,6 +71,7 @@
69
71
  --tn-c-warning-soft: rgba(234, 179, 8, 0.16);
70
72
  --tn-c-danger-soft: rgba(244, 63, 94, 0.14);
71
73
  --tn-c-default-soft: #2e2e32;
74
+ --tn-c-mark-bg: rgba(250, 204, 21, 0.32);
72
75
  --tn-c-task-check-border: #4a4d54;
73
76
  --tn-c-task-check-bg: transparent;
74
77
  --tn-shadow-2: 0 4px 12px rgb(0 0 0 / 0.35);
@@ -1,37 +0,0 @@
1
- /**
2
- * 思维导图编辑器外壳入口:Web 与 VS Code 扩展共用同一套三视图 UI。
3
- *
4
- * 与 `./mindmap` 的分工:`./mindmap` 是 SSG / Desk 笔记里内嵌的「预览 + 可选编辑」块
5
- * (`components/Mindmap/Mindmap.vue`,自带 `--mm-*` 主题变量与只读渲染);本入口导出它
6
- * 内部复用的编辑器组件,供自带页面外壳的宿主(Web 整页、VS Code webview)直接组装。
7
- *
8
- * 宿主职责:`MindmapSession` 归属、`--mm-*` 变量定义、落盘 / 剪贴板 / 图片写入
9
- * (`apps/mindmap-web`、`apps/mindmap-vscode`)。浮层默认不 teleport,嵌入笔记的宿主
10
- * 保持默认即可;整页宿主传 `teleport-to="body"`。
11
- */
12
- export { default as AppIcon } from '../components/Mindmap/editor/AppIcon.vue'
13
- export { default as CanvasContextMenu } from '../components/Mindmap/editor/CanvasContextMenu.vue'
14
- export { default as CollapseMenu } from '../components/Mindmap/editor/CollapseMenu.vue'
15
- export { default as IconButton } from '../components/Mindmap/editor/IconButton.vue'
16
- export { default as LinkPopover } from '../components/Mindmap/editor/LinkPopover.vue'
17
- export { default as MarkdownView } from '../components/Mindmap/editor/MarkdownView.vue'
18
- export { default as MindmapView } from '../components/Mindmap/editor/MindmapView.vue'
19
- export { default as OutlineView } from '../components/Mindmap/editor/OutlineView.vue'
20
- export { default as RichInlineEditor } from '../components/Mindmap/editor/RichInlineEditor.vue'
21
- export { default as SearchBar } from '../components/Mindmap/editor/SearchBar.vue'
22
- export { default as SelectionToolbar } from '../components/Mindmap/editor/SelectionToolbar.vue'
23
- export { default as FocusBreadcrumbs } from '../components/Mindmap/FocusBreadcrumbs.vue'
24
-
25
- export { insertImageIntoSource } from '../components/Mindmap/editor/imagePaste'
26
- export {
27
- pasteCanvasOutline,
28
- readMindmapClipboard,
29
- writeMindmapClipboard
30
- } from '../components/Mindmap/editor/mindmapClipboard'
31
- export { resolveAfterDropLevel } from '../components/Mindmap/editor/outlineDrag'
32
- export type { DropLevel } from '../components/Mindmap/editor/outlineDrag'
33
- export {
34
- altShortcut,
35
- isApplePlatform,
36
- primaryShortcut
37
- } from '../components/Mindmap/editor/platform'