@tnotesjs/ui 0.3.3 → 0.4.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.
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.1",
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.4"
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
+ }
@@ -186,6 +186,9 @@ watch(
186
186
  <style scoped lang="scss">
187
187
  .tn-footprints {
188
188
  margin: 1rem 0;
189
+ padding: 12px 16px;
190
+ border: 1px solid var(--tn-c-divider);
191
+ border-radius: 8px;
189
192
  color: var(--tn-c-text);
190
193
  }
191
194
 
@@ -312,6 +315,7 @@ watch(
312
315
  .tn-footprints__modal-img {
313
316
  max-width: min(92vw, 960px);
314
317
  max-height: 88vh;
318
+ border-radius: 4px;
315
319
  object-fit: contain;
316
320
  }
317
321
 
@@ -225,6 +225,7 @@ onBeforeUnmount(() => {
225
225
  .tn-image-preview > img {
226
226
  max-width: 90vw;
227
227
  max-height: 90vh;
228
+ border-radius: 4px;
228
229
  user-select: none;
229
230
  cursor: grab;
230
231
  transition: transform 100ms ease-out;
@@ -502,8 +502,7 @@ watch(
502
502
  .tn-mermaid {
503
503
  position: relative;
504
504
  margin: 1rem 0;
505
- /* Rest: no frame. Border appears with the action menu (hover / menu focus / copy feedback). */
506
- border: 1px solid transparent;
505
+ border: 1px solid var(--tn-c-divider);
507
506
  border-radius: 8px;
508
507
  background: var(--tn-c-bg);
509
508
  overflow: hidden;
@@ -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,16 +1181,17 @@ 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;
1188
+ border: 1px solid var(--tn-c-inline-code-border);
1177
1189
  border-radius: 4px;
1178
- background: color-mix(in srgb, var(--mm-canvas-bg) 82%, var(--mm-text) 18%);
1179
- color: var(--tn-c-danger, #e85d5d);
1190
+ background: var(--tn-c-inline-code-bg);
1191
+ color: inherit;
1180
1192
  font-family: var(--tn-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
1181
- font-size: 0.92em;
1193
+ font-size: 1em;
1194
+ font-weight: 400;
1182
1195
  }
1183
1196
  .mindmap-canvas-host:deep(.mm-edit-input .inline-run.link) {
1184
1197
  color: var(--mm-accent);
@@ -1213,13 +1226,13 @@ onBeforeUnmount(() => {
1213
1226
  }
1214
1227
 
1215
1228
  .mindmap-outline {
1216
- max-height: 560px;
1229
+ max-height: var(--mindmap-pane-height, 560px);
1217
1230
  padding: 18px 22px 22px;
1218
1231
  overflow: auto;
1219
1232
  }
1220
1233
 
1221
1234
  .mindmap-outline.is-editable {
1222
- height: 440px;
1235
+ height: var(--mindmap-pane-height, 440px);
1223
1236
  max-height: none;
1224
1237
  padding: 0;
1225
1238
  overflow: hidden;
@@ -1228,7 +1241,7 @@ onBeforeUnmount(() => {
1228
1241
  }
1229
1242
 
1230
1243
  .mindmap-source-wrap.is-editable {
1231
- height: 440px;
1244
+ height: var(--mindmap-pane-height, 440px);
1232
1245
  overflow: hidden;
1233
1246
  display: flex;
1234
1247
  flex-direction: column;
@@ -1315,7 +1328,7 @@ onBeforeUnmount(() => {
1315
1328
  max-width: min(100%, 560px);
1316
1329
  max-height: 360px;
1317
1330
  margin: 5px 0 12px 25px;
1318
- border-radius: 6px;
1331
+ border-radius: 4px;
1319
1332
  }
1320
1333
  .mindmap-outline :deep(.is-bold) {
1321
1334
  font-weight: 700;
@@ -1332,15 +1345,17 @@ onBeforeUnmount(() => {
1332
1345
  .mindmap-outline :deep(.is-highlight) {
1333
1346
  padding: 0 2px;
1334
1347
  border-radius: 2px;
1335
- background: #ffe56b;
1336
- color: #252525;
1348
+ background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
1337
1349
  }
1338
1350
  .mindmap-outline :deep(.is-code) {
1339
- padding: 1px 5px;
1351
+ padding: 0 4px;
1352
+ border: 1px solid var(--tn-c-inline-code-border);
1340
1353
  border-radius: 4px;
1341
- background: var(--tn-c-bg-soft);
1342
- color: var(--tn-c-danger);
1354
+ background: var(--tn-c-inline-code-bg);
1355
+ color: inherit;
1343
1356
  font-family: var(--tn-font-mono);
1357
+ font-size: 1em;
1358
+ font-weight: 400;
1344
1359
  }
1345
1360
  .mindmap-outline :deep(.is-link) {
1346
1361
  color: var(--tn-c-brand);
@@ -1349,7 +1364,7 @@ onBeforeUnmount(() => {
1349
1364
  }
1350
1365
 
1351
1366
  .mindmap-source {
1352
- max-height: 560px;
1367
+ max-height: var(--mindmap-pane-height, 560px);
1353
1368
  margin: 0;
1354
1369
  padding: 18px 22px;
1355
1370
  overflow: auto;
@@ -1431,7 +1446,7 @@ onBeforeUnmount(() => {
1431
1446
 
1432
1447
  @media (max-width: 768px) {
1433
1448
  .mindmap-canvas-host {
1434
- height: 360px;
1449
+ height: var(--mindmap-pane-height, 360px);
1435
1450
  }
1436
1451
  .mindmap-preview-actions {
1437
1452
  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
@@ -2341,7 +2345,7 @@ function onDragUp() {
2341
2345
  position: relative;
2342
2346
  max-width: 100%;
2343
2347
  line-height: 0;
2344
- border-radius: 6px;
2348
+ border-radius: 4px;
2345
2349
  overflow: hidden;
2346
2350
  background: color-mix(in srgb, var(--mm-text-dim) 12%, transparent);
2347
2351
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--mm-text-dim) 25%, transparent);
@@ -416,16 +416,17 @@ 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
- padding: 2px 6px;
424
- border-radius: 5px;
425
- background: color-mix(in srgb, var(--mm-text) 18%, transparent);
426
- color: #f08a6e;
427
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
428
- font-size: 0.92em;
422
+ padding: 0 4px;
423
+ border: 1px solid var(--tn-c-inline-code-border, #e7e9e8);
424
+ border-radius: 4px;
425
+ background: var(--tn-c-inline-code-bg, rgba(0, 0, 0, 0.06));
426
+ color: inherit;
427
+ font-family: var(--tn-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
428
+ font-size: 1em;
429
+ font-weight: 400;
429
430
  box-decoration-break: clone;
430
431
  -webkit-box-decoration-break: clone;
431
432
  }
@@ -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
  }