@tnotesjs/ui 0.3.2 → 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.2",
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
+ }
@@ -2,7 +2,16 @@
2
2
  import { CanvasEditor, CanvasViewer, MindmapSession } from '@tnotesjs/mindmap-core'
3
3
  import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
4
4
 
5
- import { applyInitialExpandLevel, normalizeExpandLevel } from './expandLevel'
5
+ import {
6
+ MAX_EXPAND_LEVEL,
7
+ MIN_EXPAND_LEVEL,
8
+ acceptsExpandLevelEdit,
9
+ applyInitialExpandLevel,
10
+ normalizeExpandLevel,
11
+ resolvedExpandLevel,
12
+ stepExpandLevel
13
+ } from './expandLevel'
14
+ import { FORCE_EXIT_FULLSCREEN_EVENT, reconcileMindmapFullscreen } from './fullscreenFlag'
6
15
  import { normalizeMindmapMarkdown } from './markdown'
7
16
  import MindmapOutlineNode from './MindmapOutlineNode.vue'
8
17
  import MindmapViewIcon from './MindmapViewIcon.vue'
@@ -20,8 +29,6 @@ type PreviewView = 'mindmap' | 'outline' | 'source'
20
29
 
21
30
  /** One fullscreen mindmap at a time across preview instances on the page. */
22
31
  const FS_GATE_KEY = '__tnotesjs_mindmap_fullscreen_gate__'
23
- const FS_BODY_ATTR = 'tnMindmapFs'
24
- const FORCE_EXIT_FULLSCREEN_EVENT = 'tnotes-mindmap-force-exit-fullscreen'
25
32
 
26
33
  type FsGateState = { owners: Map<symbol, () => void> }
27
34
 
@@ -52,15 +59,7 @@ function claimMindmapFullscreen(id: symbol): void {
52
59
 
53
60
  function syncBodyFullscreenAttr(activeRoot: HTMLElement | null): void {
54
61
  if (typeof document === 'undefined') return
55
- if (activeRoot?.classList.contains('is-fullscreen')) {
56
- document.body.dataset[FS_BODY_ATTR] = '1'
57
- document.documentElement.dataset[FS_BODY_ATTR] = '1'
58
- return
59
- }
60
- if (!document.querySelector('.mindmap-preview.is-fullscreen')) {
61
- delete document.body.dataset[FS_BODY_ATTR]
62
- delete document.documentElement.dataset[FS_BODY_ATTR]
63
- }
62
+ reconcileMindmapFullscreen(activeRoot, document.querySelectorAll<HTMLElement>('.mindmap-preview'))
64
63
  }
65
64
 
66
65
  function forceExitPeerFullscreen(activeRoot: HTMLElement | null): void {
@@ -83,6 +82,8 @@ const props = withDefaults(
83
82
  editable?: boolean
84
83
  /** Show expand-level control on the chrome (Desk). */
85
84
  expandLevelControl?: boolean
85
+ /** Pane height in px; omit for the default 440px canvas. */
86
+ height?: number
86
87
  /** Explicit dark mode; omit to auto-detect html.dark / data-theme=dark. */
87
88
  isDark?: boolean
88
89
  /**
@@ -102,6 +103,7 @@ const props = withDefaults(
102
103
  initialExpandLevel: 3,
103
104
  editable: false,
104
105
  expandLevelControl: false,
106
+ height: undefined,
105
107
  isDark: undefined,
106
108
  resolveImageSrc: undefined,
107
109
  writeAsset: undefined
@@ -162,6 +164,9 @@ function decodeContent(value: string): string {
162
164
  }
163
165
  }
164
166
 
167
+ const paneStyle = computed(() =>
168
+ props.height ? { '--mindmap-pane-height': `${props.height}px` } : undefined
169
+ )
165
170
  const rawContent = computed(() => props.source || decodeContent(props.content || ''))
166
171
  const normalizedContent = computed(() => normalizeMindmapMarkdown(rawContent.value))
167
172
 
@@ -274,9 +279,127 @@ function setExpandLevel(raw: number): void {
274
279
  emit('expandLevelChange', level)
275
280
  }
276
281
 
282
+ /** The value the field held when it was focused — what ESC / an empty blur restores. */
283
+ let expandLevelBeforeFocus: number | null = null
284
+
285
+ /**
286
+ * Refuse a non-digit before it ever reaches the field. Cancelling `beforeinput`
287
+ * leaves the value and the selection untouched, which is what keeps the field
288
+ * usable after a rejected key — restoring `input.value` after the fact leaves
289
+ * the browser's editing state wedged, and nothing types into the field again.
290
+ *
291
+ * The first character cannot be `0` (the range starts at 1); a second character
292
+ * completes a 1–99 number, so `0` is allowed there (`10`, `20`, …).
293
+ */
294
+ function onExpandLevelBeforeInput(event: InputEvent): void {
295
+ // Only text being inserted is judged; deletions and history carry no `data`.
296
+ if (event.data === null) return
297
+ if (event.inputType !== 'insertText') return
298
+ if (!/^\d+$/.test(event.data)) {
299
+ event.preventDefault()
300
+ return
301
+ }
302
+ const input = event.target as HTMLInputElement
303
+ const replacingAll = input.selectionStart === 0 && input.selectionEnd === input.value.length
304
+ const next = replacingAll
305
+ ? event.data
306
+ : input.value.slice(0, input.selectionStart ?? input.value.length) +
307
+ event.data +
308
+ input.value.slice(input.selectionEnd ?? input.value.length)
309
+ if (!acceptsExpandLevelEdit(next)) event.preventDefault()
310
+ }
311
+
277
312
  function onExpandLevelInput(event: Event): void {
278
- const value = Number((event.target as HTMLInputElement).value)
279
- setExpandLevel(value)
313
+ const input = event.target as HTMLInputElement
314
+ // Safety net for input that skips `beforeinput` (paste, IME, drag-and-drop, a
315
+ // programmatic value): anything that cannot be a 1–99 number is dropped.
316
+ if (!acceptsExpandLevelEdit(input.value)) {
317
+ input.value = String(expandLevel.value)
318
+ input.select()
319
+ return
320
+ }
321
+ if (input.value === '') return
322
+ const typed = Number(input.value)
323
+ // `0` (and anything above the cap) stays in the field uncommitted; blur clamps it.
324
+ if (typed >= MIN_EXPAND_LEVEL && typed <= MAX_EXPAND_LEVEL) setExpandLevel(typed)
325
+ }
326
+
327
+ /**
328
+ * Editing ends: an empty field means "no input", so the pre-focus value comes
329
+ * back; otherwise whatever was typed is clamped into 1–99 and committed.
330
+ */
331
+ function onExpandLevelBlur(event: Event): void {
332
+ const input = event.target as HTMLInputElement
333
+ const restore = expandLevelBeforeFocus
334
+ expandLevelBeforeFocus = null
335
+ const level = resolvedExpandLevel(input.value, restore ?? expandLevel.value)
336
+ input.value = String(level)
337
+ setExpandLevel(level)
338
+ }
339
+
340
+ /** ESC abandons the edit outright, wherever the field happens to be. */
341
+ function onExpandLevelEscape(event: KeyboardEvent): boolean {
342
+ if (event.key !== 'Escape') return false
343
+ const restore = expandLevelBeforeFocus ?? expandLevel.value
344
+ event.preventDefault()
345
+ event.stopPropagation()
346
+ const input = event.target as HTMLInputElement
347
+ input.value = String(restore)
348
+ setExpandLevel(restore)
349
+ input.blur()
350
+ return true
351
+ }
352
+
353
+ /**
354
+ * Backspace clears the field in one press. Editing a single number digit by digit
355
+ * has no use: an empty field here means "no input" — it shows the hint, and blur
356
+ * puts the pre-focus value back.
357
+ */
358
+ function onExpandLevelClearKey(event: KeyboardEvent): boolean {
359
+ if (event.key !== 'Backspace' && event.key !== 'Delete') return false
360
+ const input = event.target as HTMLInputElement
361
+ if (input.value === '') return false
362
+ event.preventDefault()
363
+ input.value = ''
364
+ return true
365
+ }
366
+
367
+ /**
368
+ * All four arrows step the level. The field is a text input (see
369
+ * `onExpandLevelFocus`), so ↑/↓ no longer come free from the number input and
370
+ * have to be handled here alongside ←/→ — either hand stays on the arrows.
371
+ * A caret inside one number has nothing to scroll, which is what leaves the
372
+ * horizontal arrows free to take.
373
+ */
374
+ function onExpandLevelKeydown(event: KeyboardEvent): void {
375
+ if (onExpandLevelEscape(event)) return
376
+ if (onExpandLevelClearKey(event)) return
377
+ const step = { ArrowUp: 1, ArrowRight: 1, ArrowDown: -1, ArrowLeft: -1 }[event.key]
378
+ if (step === undefined) return
379
+ // Leave modifiers alone: Cmd/Alt+← are word-jump / back shortcuts.
380
+ if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return
381
+ const input = event.target as HTMLInputElement
382
+ const current = Number(input.value)
383
+ const base = /^\d{1,2}$/.test(input.value) ? current : expandLevel.value
384
+ // Clamp rather than lean on `setExpandLevel`: a clamped step is still a step,
385
+ // and the value stays inside the documented 1–99.
386
+ const bounded = stepExpandLevel(base, step as -1 | 1)
387
+ event.preventDefault()
388
+ input.value = String(bounded)
389
+ setExpandLevel(bounded)
390
+ }
391
+
392
+ /**
393
+ * Two digits mean "type the level, not append to it": select the current value on
394
+ * focus so a keystroke replaces it. This is also why the field is a `text` input
395
+ * with `inputmode="numeric"` rather than `type=number` — selection APIs are not
396
+ * available on the latter (the digit would be appended, giving `39`).
397
+ * The pre-focus value is remembered for ESC and for an empty blur.
398
+ */
399
+ function onExpandLevelFocus(event: FocusEvent): void {
400
+ const input = event.target as HTMLInputElement
401
+ expandLevelBeforeFocus = expandLevel.value
402
+ input.select()
280
403
  }
281
404
 
282
405
  function setView(view: PreviewView): void {
@@ -378,6 +501,9 @@ function handleFullscreenChange(): void {
378
501
  } else if (fullscreenMode === 'native') {
379
502
  fullscreenMode = null
380
503
  isFullscreen.value = false
504
+ // `syncBodyFullscreenAttr` also drops the stale `is-fullscreen` class, so
505
+ // ESC (which only fires `fullscreenchange`) lands in the same state as the
506
+ // button path and the toolbar comes back.
381
507
  syncBodyFullscreenAttr(null)
382
508
  }
383
509
  if (activeView.value === 'mindmap') void nextTick(() => zoomToFit())
@@ -388,9 +514,14 @@ function zoomToFit(): void {
388
514
  viewer?.zoomToFit()
389
515
  }
390
516
 
391
- 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 {
392
522
  isCanvasActive.value = true
393
523
  if (!props.editable) return
524
+ if (event.target instanceof Element && event.target.closest(CANVAS_OVERLAY_SELECTOR)) return
394
525
  const editorEl = canvasHost.value?.querySelector<HTMLElement>('.mm-editor')
395
526
  editorEl?.focus({ preventScroll: true })
396
527
  }
@@ -609,6 +740,7 @@ onBeforeUnmount(() => {
609
740
  <section
610
741
  ref="previewRoot"
611
742
  class="mindmap-preview"
743
+ :style="paneStyle"
612
744
  :class="{
613
745
  'is-dark': dark,
614
746
  'is-fullscreen': isFullscreen,
@@ -637,16 +769,24 @@ onBeforeUnmount(() => {
637
769
  </button>
638
770
  </nav>
639
771
  <span class="mindmap-preview-action-divider" aria-hidden="true" />
640
- <label v-if="expandLevelControl" class="mindmap-preview-expand" title="默认展开层级">
641
- <span class="mindmap-preview-expand-label">层</span>
772
+ <label
773
+ v-if="expandLevelControl"
774
+ class="mindmap-preview-expand"
775
+ :title="`默认展开层级(${MIN_EXPAND_LEVEL}~${MAX_EXPAND_LEVEL})`"
776
+ >
642
777
  <input
643
778
  class="mindmap-preview-expand-input"
644
- type="number"
645
- min="1"
646
- max="20"
779
+ type="text"
780
+ inputmode="numeric"
781
+ maxlength="2"
782
+ :placeholder="`${MIN_EXPAND_LEVEL}~${MAX_EXPAND_LEVEL}`"
647
783
  :value="expandLevel"
648
- aria-label="默认展开层级"
649
- @change="onExpandLevelInput"
784
+ :aria-label="`默认展开层级,${MIN_EXPAND_LEVEL} 到 ${MAX_EXPAND_LEVEL}`"
785
+ @focus="onExpandLevelFocus"
786
+ @beforeinput="onExpandLevelBeforeInput"
787
+ @input="onExpandLevelInput"
788
+ @blur="onExpandLevelBlur"
789
+ @keydown="onExpandLevelKeydown"
650
790
  />
651
791
  </label>
652
792
  <button
@@ -865,32 +1005,65 @@ onBeforeUnmount(() => {
865
1005
  cursor: default;
866
1006
  }
867
1007
 
868
- .mindmap-preview-expand-label {
869
- opacity: 0.85;
870
- }
871
-
1008
+ /*
1009
+ * Just the number: no label, no box, no fill — an underline is enough to say
1010
+ * "you can type here". The field is as wide as its placeholder (`1~99`) needs,
1011
+ * so the hint is never clipped, and the value is centred so the caret and the
1012
+ * number do not jump as it changes.
1013
+ */
872
1014
  .mindmap-preview-expand-input {
873
- width: 2.4rem;
874
- height: 22px;
875
- padding: 0 4px;
876
- border: 1px solid var(--tn-c-divider);
877
- border-radius: 4px;
878
- background: var(--tn-c-bg);
879
- color: var(--tn-c-text);
880
- font-size: 12px;
881
- line-height: 22px;
1015
+ width: 2.9rem;
1016
+ height: 20px;
1017
+ padding: 0;
1018
+ border: 0;
1019
+ border-bottom: 1px solid var(--tn-c-divider);
1020
+ border-radius: 0;
1021
+ background: transparent;
1022
+ color: var(--tn-c-brand);
1023
+ font-size: 13px;
1024
+ font-weight: 600;
1025
+ line-height: 20px;
1026
+ text-align: center;
1027
+ cursor: text;
1028
+ appearance: textfield;
1029
+ -moz-appearance: textfield;
1030
+
1031
+ &::placeholder {
1032
+ color: var(--tn-c-text-3, var(--tn-c-text-2));
1033
+ font-weight: 400;
1034
+ opacity: 0.75;
1035
+ }
1036
+
1037
+ &:focus {
1038
+ border-bottom-color: var(--tn-c-brand);
1039
+ outline: none;
1040
+ }
1041
+
1042
+ /* Chromium keeps a spinner on text-ish numeric fields; we step via keys. */
1043
+ &::-webkit-outer-spin-button,
1044
+ &::-webkit-inner-spin-button {
1045
+ margin: 0;
1046
+ appearance: none;
1047
+ -webkit-appearance: none;
1048
+ }
882
1049
  }
883
1050
 
884
1051
  .mindmap-canvas-host {
885
1052
  position: relative;
886
1053
  width: 100%;
887
- height: 440px;
1054
+ height: var(--mindmap-pane-height, 440px);
888
1055
  overflow: hidden;
889
- background: var(--tn-c-bg);
1056
+ /*
1057
+ * No background of its own: the canvas is cleared to transparent, and the
1058
+ * container above (`.mindmap-preview`) already paints `--tn-c-bg` with the
1059
+ * panel border. Painting it again here only made the canvas read as a second,
1060
+ * inset panel inside the editor.
1061
+ */
1062
+ background: transparent;
890
1063
  touch-action: none;
891
1064
  user-select: none;
892
1065
 
893
- /* CanvasEditor overlay + chrome tokens (aligned with mindmap-web). */
1066
+ /* CanvasEditor overlay + chrome tokens. */
894
1067
  --mm-canvas-bg: var(--tn-c-bg);
895
1068
  --mm-panel-bg: var(--tn-c-bg-soft, var(--tn-c-bg));
896
1069
  --mm-text: var(--tn-c-text);
@@ -930,7 +1103,7 @@ onBeforeUnmount(() => {
930
1103
  pointer-events: none;
931
1104
  }
932
1105
 
933
- /* Marquee box — required for box-select feedback (aligned with mindmap-web). */
1106
+ /* Marquee box — required for box-select feedback. */
934
1107
  .mindmap-canvas-host:deep(.mm-selection-box) {
935
1108
  position: absolute;
936
1109
  z-index: 8;
@@ -1008,8 +1181,7 @@ onBeforeUnmount(() => {
1008
1181
  .mindmap-canvas-host:deep(.mm-edit-input .inline-run.highlight:not(.code)) {
1009
1182
  padding: 0 1px;
1010
1183
  border-radius: 2px;
1011
- background: #fff36a;
1012
- color: #242424;
1184
+ background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
1013
1185
  }
1014
1186
  .mindmap-canvas-host:deep(.mm-edit-input .inline-run.code) {
1015
1187
  padding: 0 4px;
@@ -1052,13 +1224,13 @@ onBeforeUnmount(() => {
1052
1224
  }
1053
1225
 
1054
1226
  .mindmap-outline {
1055
- max-height: 560px;
1227
+ max-height: var(--mindmap-pane-height, 560px);
1056
1228
  padding: 18px 22px 22px;
1057
1229
  overflow: auto;
1058
1230
  }
1059
1231
 
1060
1232
  .mindmap-outline.is-editable {
1061
- height: 440px;
1233
+ height: var(--mindmap-pane-height, 440px);
1062
1234
  max-height: none;
1063
1235
  padding: 0;
1064
1236
  overflow: hidden;
@@ -1067,7 +1239,7 @@ onBeforeUnmount(() => {
1067
1239
  }
1068
1240
 
1069
1241
  .mindmap-source-wrap.is-editable {
1070
- height: 440px;
1242
+ height: var(--mindmap-pane-height, 440px);
1071
1243
  overflow: hidden;
1072
1244
  display: flex;
1073
1245
  flex-direction: column;
@@ -1105,9 +1277,28 @@ onBeforeUnmount(() => {
1105
1277
 
1106
1278
  .mindmap-outline :deep(.mindmap-outline-toggle),
1107
1279
  .mindmap-outline :deep(.mindmap-outline-leaf) {
1280
+ display: flex;
1108
1281
  flex: 0 0 18px;
1282
+ align-items: center;
1283
+ justify-content: center;
1109
1284
  width: 18px;
1285
+ height: 24px;
1110
1286
  color: var(--tn-c-text-2);
1287
+ /*
1288
+ * The toggle is a real `<button>`, so it arrives with the browser's default
1289
+ * chrome — 2px outset border, button-face background, 1px 6px padding and a
1290
+ * 13.33px font. Desk never saw it because the preview renders inside
1291
+ * `.milkdown`, whose theme reset (`.milkdown button { border/background: none }`)
1292
+ * swallowed it; the SSG site has no such ancestor, so the box showed up and
1293
+ * the chevron read as a filled square next to the plain `•` leaf. Reset it
1294
+ * here so the outline looks the same wherever the component is mounted.
1295
+ */
1296
+ padding: 0;
1297
+ border: 0;
1298
+ background: none;
1299
+ box-shadow: none;
1300
+ font: inherit;
1301
+ line-height: inherit;
1111
1302
  text-align: center;
1112
1303
  }
1113
1304
 
@@ -1152,8 +1343,7 @@ onBeforeUnmount(() => {
1152
1343
  .mindmap-outline :deep(.is-highlight) {
1153
1344
  padding: 0 2px;
1154
1345
  border-radius: 2px;
1155
- background: #ffe56b;
1156
- color: #252525;
1346
+ background: var(--tn-c-mark-bg, rgba(255, 221, 0, 0.45));
1157
1347
  }
1158
1348
  .mindmap-outline :deep(.is-code) {
1159
1349
  padding: 1px 5px;
@@ -1169,7 +1359,7 @@ onBeforeUnmount(() => {
1169
1359
  }
1170
1360
 
1171
1361
  .mindmap-source {
1172
- max-height: 560px;
1362
+ max-height: var(--mindmap-pane-height, 560px);
1173
1363
  margin: 0;
1174
1364
  padding: 18px 22px;
1175
1365
  overflow: auto;
@@ -1251,7 +1441,7 @@ onBeforeUnmount(() => {
1251
1441
 
1252
1442
  @media (max-width: 768px) {
1253
1443
  .mindmap-canvas-host {
1254
- height: 360px;
1444
+ height: var(--mindmap-pane-height, 360px);
1255
1445
  }
1256
1446
  .mindmap-preview-actions {
1257
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,7 +1,46 @@
1
1
  import type { MindmapNode, MindmapSession } from '@tnotesjs/mindmap-core'
2
2
 
3
+ /**
4
+ * The level field holds at most two digits: 1–99. Kept here as the one source of
5
+ * truth for the clamp, the key handler and the field's own width/placeholder.
6
+ */
7
+ export const MIN_EXPAND_LEVEL = 1
8
+ export const MAX_EXPAND_LEVEL = 99
9
+
3
10
  export function normalizeExpandLevel(value: number): number {
4
- return Math.max(1, Math.trunc(Number(value) || 1))
11
+ const level = Math.trunc(Number(value) || MIN_EXPAND_LEVEL)
12
+ return Math.min(MAX_EXPAND_LEVEL, Math.max(MIN_EXPAND_LEVEL, level))
13
+ }
14
+
15
+ /**
16
+ * What the level field accepts as its complete text: one or two digits, no
17
+ * leading zero (`0` is below the range, `07` is not how a level is written).
18
+ * Empty is allowed while editing — it means "no input yet".
19
+ */
20
+ export function isValidExpandLevelText(text: string): boolean {
21
+ return text === '' || /^[1-9]\d?$/.test(text)
22
+ }
23
+
24
+ /**
25
+ * Whether an edit would leave the field in an acceptable state. `beforeinput`
26
+ * asks this before the browser applies the change, so a rejected key leaves the
27
+ * value *and* the selection untouched — restoring the value afterwards wedges the
28
+ * browser's editing state and the field stops accepting input.
29
+ *
30
+ * `next` is what the field would read after the edit.
31
+ */
32
+ export function acceptsExpandLevelEdit(next: string): boolean {
33
+ return isValidExpandLevelText(next)
34
+ }
35
+
36
+ /** One arrow-key step, clamped to the range. */
37
+ export function stepExpandLevel(value: number, direction: -1 | 1): number {
38
+ return Math.min(MAX_EXPAND_LEVEL, Math.max(MIN_EXPAND_LEVEL, value + direction))
39
+ }
40
+
41
+ /** Editing ended: an empty field drops the current value back. */
42
+ export function resolvedExpandLevel(text: string, previous: number): number {
43
+ return text === '' ? previous : normalizeExpandLevel(Number(text))
5
44
  }
6
45
 
7
46
  function childLevel(node: MindmapNode, root: MindmapNode): number {
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The document-wide "a mindmap owns fullscreen" flag.
3
+ *
4
+ * The flag drives an **unscoped**, `!important` rule (in `Mindmap.vue`) that
5
+ * hides the child buttons (`.mindmap-preview-actions`) of every *non*-fullscreen
6
+ * mindmap. So the flag and the `is-fullscreen` classes have to be reconciled
7
+ * together — a stale flag, or a stale class left behind by a path that only
8
+ * observed `fullscreenchange` (ESC) or unmounted a preview, hides every toolbar
9
+ * with no way back short of a page reload.
10
+ *
11
+ * Kept as plain DOM code in its own module so the invariant is unit-testable
12
+ * without mounting the canvas-backed component.
13
+ */
14
+
15
+ export const FS_BODY_ATTR = 'tnMindmapFs'
16
+
17
+ /** Custom event peers dispatch to make each other leave fullscreen. */
18
+ export const FORCE_EXIT_FULLSCREEN_EVENT = 'tnotes-mindmap-force-exit-fullscreen'
19
+
20
+ /**
21
+ * Reconcile the document flag with the fullscreen classes.
22
+ *
23
+ * - `activeRoot` still marked fullscreen → keep the flag (a peer's stray
24
+ * `.is-fullscreen` must not be mistaken for the owner);
25
+ * - otherwise → drop the class from every tracked preview and delete the flag.
26
+ */
27
+ export function reconcileMindmapFullscreen(
28
+ activeRoot: HTMLElement | null,
29
+ previews: ArrayLike<HTMLElement>
30
+ ): void {
31
+ if (typeof document === 'undefined') return
32
+ if (activeRoot?.classList.contains('is-fullscreen')) {
33
+ document.body.dataset[FS_BODY_ATTR] = '1'
34
+ document.documentElement.dataset[FS_BODY_ATTR] = '1'
35
+ return
36
+ }
37
+ for (const el of Array.from(previews)) el.classList.remove('is-fullscreen')
38
+ delete document.body.dataset[FS_BODY_ATTR]
39
+ delete document.documentElement.dataset[FS_BODY_ATTR]
40
+ }
@@ -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'