@tnotesjs/core 0.2.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.
Files changed (47) hide show
  1. package/README.md +0 -1
  2. package/config/defaultConfig.ts +33 -4
  3. package/config/index.ts +6 -0
  4. package/core/NoteIndexCache.ts +7 -1
  5. package/core/NoteManager.ts +18 -30
  6. package/dist/{chunk-5Y5IYS6C.js → chunk-UJG3UU5X.js} +21 -5
  7. package/dist/{chunk-5QN44ES5.js → chunk-XCWFZLER.js} +48 -368
  8. package/dist/cli/index.js +26 -78
  9. package/dist/index.js +1 -1
  10. package/dist/vitepress/config/index.js +159 -18
  11. package/package.json +3 -6
  12. package/services/index.ts +0 -1
  13. package/services/init-sub-repo/initSubRepoLogic.ts +0 -1
  14. package/services/note/service.ts +0 -1
  15. package/types/config.ts +0 -3
  16. package/types/note.ts +0 -2
  17. package/vitepress/assets/icons/icon__cursor.svg +4 -0
  18. package/vitepress/assets/icons/index.ts +1 -0
  19. package/vitepress/components/Layout/AboutPanel.vue +0 -24
  20. package/vitepress/components/Layout/DocBeforeControls.vue +6 -14
  21. package/vitepress/components/Layout/DocFooter.vue +3 -3
  22. package/vitepress/components/Layout/Layout.vue +0 -30
  23. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +20 -11
  24. package/vitepress/components/Layout/homeReadme.data.ts +0 -48
  25. package/vitepress/components/MindmapPreview/InlineRuns.ts +25 -0
  26. package/vitepress/components/MindmapPreview/MindmapOutlineNode.vue +62 -0
  27. package/vitepress/components/MindmapPreview/MindmapPreview.vue +287 -0
  28. package/vitepress/components/MindmapPreview/compat.test.ts +93 -0
  29. package/vitepress/components/MindmapPreview/compat.ts +128 -0
  30. package/vitepress/components/MindmapPreview/expandLevel.test.ts +40 -0
  31. package/vitepress/components/MindmapPreview/expandLevel.ts +28 -0
  32. package/vitepress/components/MindmapPreview/legacyCorpus.test.ts +57 -0
  33. package/vitepress/components/Settings/Settings.vue +76 -73
  34. package/vitepress/components/SidebarCard/FolderTreeItems.vue +16 -6
  35. package/vitepress/components/SidebarCard/SidebarCard.vue +21 -27
  36. package/vitepress/components/composables/useLocalIde.ts +83 -0
  37. package/vitepress/components/constants.ts +5 -6
  38. package/vitepress/components/utils/vscodePaths.test.ts +22 -0
  39. package/vitepress/components/utils/vscodePaths.ts +24 -2
  40. package/vitepress/configs/markdown.config.ts +77 -20
  41. package/vitepress/theme/index.ts +4 -2
  42. package/vitepress/theme/styles/doc-layout.scss +4 -4
  43. package/vitepress/theme/styles/index.scss +0 -1
  44. package/services/timestamp/index.ts +0 -7
  45. package/services/timestamp/service.ts +0 -465
  46. package/vitepress/components/MarkMap/MarkMap.vue +0 -625
  47. package/vitepress/theme/styles/components/markmap.scss +0 -122
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import {
4
+ normalizeMindmapMarkdown,
5
+ parseMarkmapFence,
6
+ parseMindmapReference,
7
+ } from './compat'
8
+
9
+ describe('parseMarkmapFence', () => {
10
+ it.each([
11
+ ['```markmap', {}],
12
+ ['```markmap 2', { initialExpandLevel: 2 }],
13
+ ['```markmap {2}', { initialExpandLevel: 2 }],
14
+ ['```markmap {initialExpandLevel=3}', { initialExpandLevel: 3 }],
15
+ ['```markmap [ESM 模块] 2', { title: 'ESM 模块', initialExpandLevel: 2 }],
16
+ ['```markmap 2 [ESM 模块]', { title: 'ESM 模块', initialExpandLevel: 2 }],
17
+ ['```mindmap', {}],
18
+ ['```mindmap [项目架构] 1', { title: '项目架构', initialExpandLevel: 1 }],
19
+ ])('parses %s', (input, expected) => {
20
+ expect(parseMarkmapFence(input)).toEqual(expected)
21
+ })
22
+ })
23
+
24
+ describe('parseMindmapReference', () => {
25
+ it.each([
26
+ ['<<< ./assets/tree.md', { path: './assets/tree.md' }],
27
+ ['<<< ./assets/tree.md [项目架构]', { path: './assets/tree.md', title: '项目架构' }],
28
+ ['<<< "./assets/tree with spaces.md" [项目架构]', { path: './assets/tree with spaces.md', title: '项目架构' }],
29
+ ])('parses %s', (input, expected) => {
30
+ expect(parseMindmapReference(input)).toEqual(expected)
31
+ })
32
+ })
33
+
34
+ describe('normalizeMindmapMarkdown', () => {
35
+ it('injects the default root for historical unordered-list input', () => {
36
+ expect(normalizeMindmapMarkdown('- A\n - B')).toBe('# root\n\n- A\n - B\n')
37
+ })
38
+
39
+ it('promotes a single legacy root item instead of rendering duplicate roots', () => {
40
+ expect(normalizeMindmapMarkdown('- root\n - item1\n - item2')).toBe(
41
+ '# root\n\n- item1\n- item2\n',
42
+ )
43
+ expect(normalizeMindmapMarkdown('- Custom\n - item', { title: 'Custom' })).toBe(
44
+ '# Custom\n\n- item\n',
45
+ )
46
+ })
47
+
48
+ it('does not promote a matching root item when it has top-level siblings', () => {
49
+ expect(normalizeMindmapMarkdown('- root\n - nested\n- sibling')).toBe(
50
+ '# root\n\n- root\n - nested\n- sibling\n',
51
+ )
52
+ })
53
+
54
+ it('preserves an existing H1 and lets explicit fence metadata override it', () => {
55
+ const source = '# Existing\n\n- A\n'
56
+ expect(normalizeMindmapMarkdown(source)).toBe(source)
57
+ expect(normalizeMindmapMarkdown(source, { title: 'Explicit' })).toBe('# Explicit\n\n- A\n')
58
+ })
59
+
60
+ it('converts legacy H2/H3 sections into a nested unordered-list tree', () => {
61
+ const source = [
62
+ '# ESM',
63
+ '',
64
+ '## 基本语法',
65
+ '',
66
+ '### 导出方式',
67
+ '',
68
+ '- 命名导出',
69
+ ' - 统一导出',
70
+ '### 导入方式',
71
+ '- 默认导入',
72
+ ].join('\n')
73
+
74
+ expect(normalizeMindmapMarkdown(source)).toBe([
75
+ '# ESM',
76
+ '',
77
+ '- 基本语法',
78
+ '',
79
+ ' - 导出方式',
80
+ '',
81
+ ' - 命名导出',
82
+ ' - 统一导出',
83
+ ' - 导入方式',
84
+ ' - 默认导入',
85
+ '',
86
+ ].join('\n'))
87
+ })
88
+
89
+ it('is stable when normalized more than once', () => {
90
+ const once = normalizeMindmapMarkdown('- A\n - B')
91
+ expect(normalizeMindmapMarkdown(once)).toBe(once)
92
+ })
93
+ })
@@ -0,0 +1,128 @@
1
+ export interface MarkmapFenceOptions {
2
+ title?: string
3
+ initialExpandLevel?: number
4
+ }
5
+
6
+ export interface MindmapReference {
7
+ path: string
8
+ title?: string
9
+ }
10
+
11
+ function cleanHeadingText(value: string): string {
12
+ return value.trim().replace(/\s+#+\s*$/, '').trim()
13
+ }
14
+
15
+ function promoteLegacyRootList(body: string[], rootTitle: string): string[] {
16
+ const firstContentIndex = body.findIndex((line) => line.trim() !== '')
17
+ if (firstContentIndex < 0) return body
18
+
19
+ const firstItem = body[firstContentIndex].match(/^[-+*]\s+(.+?)\s*$/)
20
+ if (!firstItem || cleanHeadingText(firstItem[1]) !== rootTitle) return body
21
+
22
+ const descendants = body.slice(firstContentIndex + 1)
23
+ if (descendants.some((line) => line.trim() !== '' && !/^\s{2,}/.test(line))) return body
24
+
25
+ return [
26
+ ...body.slice(0, firstContentIndex),
27
+ ...descendants.map((line) => line.replace(/^ {2}/, '')),
28
+ ]
29
+ }
30
+
31
+ /** Parse the opening fence while retaining every legacy expand-level spelling. */
32
+ export function parseMarkmapFence(openLine: string): MarkmapFenceOptions {
33
+ const fenceBody = openLine.trim().replace(/^`+\s*/, '')
34
+ const nameMatch = fenceBody.match(/^(mindmap|markmap)(?=\s|\{|\[|$)/)
35
+ if (!nameMatch) return {}
36
+ let rest = fenceBody.slice(nameMatch[1].length).trim()
37
+ const options: MarkmapFenceOptions = {}
38
+
39
+ const titleMatch = rest.match(/\[([^\]]+)\]/)
40
+ if (titleMatch) {
41
+ options.title = titleMatch[1].trim()
42
+ rest = `${rest.slice(0, titleMatch.index)} ${rest.slice((titleMatch.index ?? 0) + titleMatch[0].length)}`.trim()
43
+ }
44
+
45
+ const braceMatch = rest.match(/\{([^}]*)\}/)
46
+ const paramPart = braceMatch ? braceMatch[1].trim() : rest
47
+ const tokens = paramPart.match(/"[^"]*"|'[^']*'|\S+/g) ?? []
48
+ for (const [index, token] of tokens.entries()) {
49
+ if (/^\d+$/.test(token) && index === 0) {
50
+ options.initialExpandLevel = Number(token)
51
+ continue
52
+ }
53
+ const pair = token.match(/^([^=:\s]+)\s*(?:=|:)\s*(.+)$/)
54
+ if (!pair || pair[1] !== 'initialExpandLevel') continue
55
+ const value = pair[2].replace(/^['"]|['"]$/g, '')
56
+ if (/^\d+$/.test(value)) options.initialExpandLevel = Number(value)
57
+ }
58
+ return options
59
+ }
60
+
61
+ /** Parse `<<< file.md [title]`; the title is optional and paths may be quoted. */
62
+ export function parseMindmapReference(line: string): MindmapReference | null {
63
+ const match = line.trim().match(/^<<<\s+(.+?)\s*$/)
64
+ if (!match) return null
65
+
66
+ let rest = match[1].trim()
67
+ let title: string | undefined
68
+ const titleMatch = rest.match(/\s+\[([^\]]+)\]\s*$/)
69
+ if (titleMatch) {
70
+ title = cleanHeadingText(titleMatch[1]) || undefined
71
+ rest = rest.slice(0, titleMatch.index).trim()
72
+ }
73
+
74
+ const path = rest.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, '$1$2').trim()
75
+ return path ? { path, title } : null
76
+ }
77
+
78
+ export interface NormalizeMindmapOptions {
79
+ title?: string
80
+ defaultTitle?: string
81
+ }
82
+
83
+ /** Convert historical VitePress markmap input into the strict mindmap-core format. */
84
+ export function normalizeMindmapMarkdown(
85
+ source: string,
86
+ options: NormalizeMindmapOptions = {},
87
+ ): string {
88
+ const lines = source.replace(/\r\n?/g, '\n').split('\n')
89
+ let existingTitle = ''
90
+ let rootIndex = -1
91
+
92
+ for (let index = 0; index < lines.length; index++) {
93
+ const match = lines[index].match(/^\s{0,3}#(?!#)\s+(.+?)\s*$/)
94
+ if (!match) continue
95
+ existingTitle = cleanHeadingText(match[1])
96
+ rootIndex = index
97
+ break
98
+ }
99
+
100
+ const rootTitle = cleanHeadingText(options.title || existingTitle || options.defaultTitle || 'root') || 'root'
101
+ const body: string[] = []
102
+ let headingDepth: number | null = null
103
+
104
+ for (let index = 0; index < lines.length; index++) {
105
+ if (index === rootIndex) continue
106
+ const line = lines[index]
107
+ const heading = line.match(/^\s{0,3}(#{2,6})\s+(.+?)\s*$/)
108
+ if (heading) {
109
+ headingDepth = heading[1].length - 2
110
+ body.push(`${' '.repeat(headingDepth)}- ${cleanHeadingText(heading[2])}`)
111
+ continue
112
+ }
113
+
114
+ const listItem = line.match(/^(\s*)([-+*])\s+(.+)$/)
115
+ if (listItem && headingDepth !== null) {
116
+ body.push(`${' '.repeat(headingDepth + 1)}${line}`)
117
+ continue
118
+ }
119
+ body.push(line)
120
+ }
121
+
122
+ const normalizedBody = promoteLegacyRootList(body, rootTitle)
123
+ while (normalizedBody[0]?.trim() === '') normalizedBody.shift()
124
+ while (normalizedBody[normalizedBody.length - 1]?.trim() === '') normalizedBody.pop()
125
+ return normalizedBody.length > 0
126
+ ? `# ${rootTitle}\n\n${normalizedBody.join('\n')}\n`
127
+ : `# ${rootTitle}\n`
128
+ }
@@ -0,0 +1,40 @@
1
+ import { MindmapSession } from '@tnotesjs/mindmap-core'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { applyInitialExpandLevel, normalizeExpandLevel } from './expandLevel'
5
+
6
+ function createSession(): MindmapSession {
7
+ return new MindmapSession({
8
+ markdown: '# root\n\n- 一级\n - 二级\n - 三级\n',
9
+ fileName: 'expand-level.tn-mindmap.md',
10
+ })
11
+ }
12
+
13
+ function visibleLabels(session: MindmapSession): string[] {
14
+ const result: string[] = []
15
+ const visit = (node: typeof session.document.root) => {
16
+ result.push(node.content.text)
17
+ if (!node.collapsed) node.children.forEach(visit)
18
+ }
19
+ visit(session.document.root)
20
+ return result
21
+ }
22
+
23
+ describe('applyInitialExpandLevel', () => {
24
+ it('clamps the minimum level to one', () => {
25
+ expect(normalizeExpandLevel(0)).toBe(1)
26
+ expect(normalizeExpandLevel(-2)).toBe(1)
27
+ })
28
+
29
+ it('shows only root and first-level children for level one', () => {
30
+ const session = createSession()
31
+ applyInitialExpandLevel(session, 1)
32
+ expect(visibleLabels(session)).toEqual(['root', '一级'])
33
+ })
34
+
35
+ it('shows root, first-level and second-level children for level two', () => {
36
+ const session = createSession()
37
+ applyInitialExpandLevel(session, 2)
38
+ expect(visibleLabels(session)).toEqual(['root', '一级', '二级'])
39
+ })
40
+ })
@@ -0,0 +1,28 @@
1
+ import type { MindmapNode, MindmapSession } from '@tnotesjs/mindmap-core'
2
+
3
+ export function normalizeExpandLevel(value: number): number {
4
+ return Math.max(1, Math.trunc(Number(value) || 1))
5
+ }
6
+
7
+ function childLevel(node: MindmapNode, root: MindmapNode): number {
8
+ let level = 0
9
+ let current: MindmapNode | null = node
10
+ while (current && current !== root) {
11
+ level += 1
12
+ current = current.parent
13
+ }
14
+ return level
15
+ }
16
+
17
+ /**
18
+ * Level 1 renders root + direct children; level 2 additionally renders
19
+ * grandchildren. Nodes at the last visible level are collapsed.
20
+ */
21
+ export function applyInitialExpandLevel(session: MindmapSession, value: number): void {
22
+ const visibleLevel = normalizeExpandLevel(value)
23
+ const root = session.document.root
24
+ session.document.traverse((node) => {
25
+ if (node === root || node.children.length === 0) return
26
+ session.setCollapsed(node.id, childLevel(node, root) >= visibleLevel)
27
+ })
28
+ }
@@ -0,0 +1,57 @@
1
+ import { parseMarkdown } from '@tnotesjs/mindmap-core'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { describe, expect, it } from 'vitest'
6
+
7
+ import {
8
+ normalizeMindmapMarkdown,
9
+ parseMarkmapFence,
10
+ parseMindmapReference,
11
+ } from './compat'
12
+
13
+ const tnotesRoot = path.resolve(fileURLToPath(new URL('../../../../', import.meta.url)))
14
+ const noteFiles = [
15
+ 'TNotes.canvas/notes/0035. 使用 ctx.drawImage 引入图像/README.md',
16
+ 'TNotes.docs/notes/0013. Mindmap/README.md',
17
+ 'TNotes.docs/notes/0014. 分仓库模式/README.md',
18
+ 'TNotes.javascript/notes/0070. CommonJS/README.md',
19
+ 'TNotes.javascript/notes/0071. ESM/README.md',
20
+ 'TNotes.sql/notes/0001. MySQL 8 从入门到精通/README.md',
21
+ 'TNotes.vite/notes/0014. vite 思维导图/README.md',
22
+ ].map((relative) => path.join(tnotesRoot, relative))
23
+
24
+ const corpusAvailable = noteFiles.every((file) => fs.existsSync(file))
25
+
26
+ describe.skipIf(!corpusAvailable)('legacy TNotes markmap corpus', () => {
27
+ it('normalizes every historical block into valid mindmap-core Markdown', () => {
28
+ const diagnostics: string[] = []
29
+ let blockCount = 0
30
+
31
+ for (const noteFile of noteFiles) {
32
+ const note = fs.readFileSync(noteFile, 'utf8')
33
+ const blocks = note.matchAll(/^(```(?:mindmap|markmap)[^\n]*)\n([\s\S]*?)^```\s*$/gm)
34
+ for (const match of blocks) {
35
+ blockCount += 1
36
+ const options = parseMarkmapFence(match[1])
37
+ let source = match[2]
38
+ const reference = parseMindmapReference(
39
+ source.split('\n').find((line) => line.trim()) ?? '',
40
+ )
41
+ if (reference) {
42
+ source = fs.readFileSync(path.resolve(path.dirname(noteFile), reference.path), 'utf8')
43
+ }
44
+ const normalized = normalizeMindmapMarkdown(source, {
45
+ title: options.title || reference?.title,
46
+ })
47
+ const result = parseMarkdown(normalized, path.basename(noteFile))
48
+ if (!result.valid) {
49
+ diagnostics.push(`${noteFile}\n${result.diagnostics.map((item) => item.message).join('\n')}`)
50
+ }
51
+ }
52
+ }
53
+
54
+ expect(blockCount).toBe(16)
55
+ expect(diagnostics).toEqual([])
56
+ })
57
+ })
@@ -10,33 +10,60 @@ vitepress/components/Settings/Settings.vue
10
10
  <div class="settingItem">
11
11
  <div class="settingMeta">
12
12
  <div class="labelLine">
13
- <span class="itemName">本地知识库路径</span>
13
+ <span class="itemName">本地 IDE 打开</span>
14
14
  <span class="infoWrap">
15
15
  <span class="infoIcon">?</span>
16
16
  <span class="tooltip">
17
- 适用于 PC 桌面环境,需要本地安装 VS Code。配置后可从页面快速打开本地笔记目录。
17
+ 适用于 PC 桌面环境。配置本地知识库 notes 目录路径,并选择 VS Code 或 Cursor,即可从页面快速打开知识库或笔记。
18
18
  </span>
19
19
  </span>
20
20
  </div>
21
21
  <span v-if="path" class="statusText">已配置</span>
22
22
  </div>
23
23
 
24
- <div class="inputGroup">
25
- <input
26
- v-model="path"
27
- type="text"
28
- placeholder="/Users/username/notes"
29
- class="input"
30
- />
31
- <button
32
- v-if="path"
33
- @click="clearPath"
34
- class="clearBtn"
35
- title="清空"
36
- type="button"
37
- >
38
- ×
39
- </button>
24
+ <div class="fieldStack">
25
+ <div class="controlRow">
26
+ <span class="controlLabel">IDE</span>
27
+ <div class="segmented segmented-two">
28
+ <label
29
+ v-for="option in localIdeOptions"
30
+ :key="option.value"
31
+ :class="[
32
+ 'segment',
33
+ { activeSegment: localIde === option.value },
34
+ ]"
35
+ >
36
+ <input
37
+ v-model="localIde"
38
+ type="radio"
39
+ name="local-ide"
40
+ :value="option.value"
41
+ />
42
+ <span>{{ option.label }}</span>
43
+ </label>
44
+ </div>
45
+ </div>
46
+
47
+ <label class="field">
48
+ <span class="controlLabel">知识库路径</span>
49
+ <div class="inputGroup">
50
+ <input
51
+ v-model="path"
52
+ type="text"
53
+ placeholder="/Users/username/notes"
54
+ class="input pathInput"
55
+ />
56
+ <button
57
+ v-if="path"
58
+ @click="clearPath"
59
+ class="clearBtn"
60
+ title="清空"
61
+ type="button"
62
+ >
63
+ ×
64
+ </button>
65
+ </div>
66
+ </label>
40
67
  </div>
41
68
  </div>
42
69
 
@@ -128,36 +155,6 @@ vitepress/components/Settings/Settings.vue
128
155
  </div>
129
156
  </div>
130
157
  </div>
131
-
132
- <div class="settingItem">
133
- <div class="settingMeta">
134
- <div class="labelLine">
135
- <span class="itemName">笔记内 MarkMap</span>
136
- <span class="infoWrap">
137
- <span class="infoIcon">?</span>
138
- <span class="tooltip">
139
- 配置笔记中 markmap 代码块的初始展开层级。
140
- </span>
141
- </span>
142
- </div>
143
- </div>
144
-
145
- <div class="inlineGrid">
146
- <label class="field">
147
- <span class="controlLabel">展开层级</span>
148
- <div class="inputWithUnit">
149
- <input
150
- v-model.number="markmapExpandLevel"
151
- type="number"
152
- min="1"
153
- max="100"
154
- class="input"
155
- />
156
- <span class="unit">层</span>
157
- </div>
158
- </label>
159
- </div>
160
- </div>
161
158
  </section>
162
159
 
163
160
  <div class="actionBar">
@@ -185,12 +182,18 @@ import { ref, computed, onMounted } from 'vue'
185
182
 
186
183
  import {
187
184
  NOTES_DIR_KEY,
188
- MARKMAP_EXPAND_LEVEL_KEY,
185
+ LOCAL_IDE_KEY,
189
186
  SIDEBAR_SHOW_NOTE_ID_KEY,
190
187
  SIDEBAR_DONE_PREFIX_KEY,
191
188
  SIDEBAR_UNDONE_PREFIX_KEY,
192
189
  CONTENT_WIDTH_MODE_KEY,
193
190
  } from '../constants'
191
+ import { useLocalIde } from '../composables/useLocalIde'
192
+ import {
193
+ DEFAULT_LOCAL_IDE,
194
+ normalizeLocalIde,
195
+ type LocalIdeId,
196
+ } from '../utils/vscodePaths'
194
197
  // @ts-expect-error - VitePress Data Loader
195
198
  import { data as tnotesConfig } from '../tnotes-config.data'
196
199
 
@@ -200,6 +203,8 @@ const DEFAULT_CONTENT_WIDTH_MODE: ContentWidthMode = 'wide'
200
203
  const DEFAULT_DONE_PREFIX = '✅'
201
204
  const DEFAULT_UNDONE_PREFIX = '⏰'
202
205
 
206
+ const { setLocalIde } = useLocalIde()
207
+
203
208
  const contentWidthModeOptions: Array<{
204
209
  label: string
205
210
  value: ContentWidthMode
@@ -208,10 +213,15 @@ const contentWidthModeOptions: Array<{
208
213
  { label: '标准页宽', value: 'standard' },
209
214
  ]
210
215
 
216
+ const localIdeOptions: Array<{ label: string; value: LocalIdeId }> = [
217
+ { label: 'VS Code', value: 'vscode' },
218
+ { label: 'Cursor', value: 'cursor' },
219
+ ]
220
+
211
221
  const path = ref('')
212
222
  const originalPath = ref('')
213
- const markmapExpandLevel = ref(5)
214
- const originalMarkmapExpandLevel = ref(5)
223
+ const localIde = ref<LocalIdeId>(DEFAULT_LOCAL_IDE)
224
+ const originalLocalIde = ref<LocalIdeId>(DEFAULT_LOCAL_IDE)
215
225
  const showNoteId = ref(false)
216
226
  const originalShowNoteId = ref(false)
217
227
  const donePrefix = ref(DEFAULT_DONE_PREFIX)
@@ -227,7 +237,7 @@ const showSuccessToast = ref(false)
227
237
  const hasChanges = computed(
228
238
  () =>
229
239
  path.value !== originalPath.value ||
230
- markmapExpandLevel.value !== originalMarkmapExpandLevel.value ||
240
+ localIde.value !== originalLocalIde.value ||
231
241
  showNoteId.value !== originalShowNoteId.value ||
232
242
  donePrefix.value !== originalDonePrefix.value ||
233
243
  undonePrefix.value !== originalUndonePrefix.value ||
@@ -246,9 +256,9 @@ onMounted(() => {
246
256
  path.value = savedPath
247
257
  originalPath.value = savedPath
248
258
 
249
- const savedLevel = localStorage.getItem(MARKMAP_EXPAND_LEVEL_KEY) || '5'
250
- markmapExpandLevel.value = parseInt(savedLevel, 10)
251
- originalMarkmapExpandLevel.value = markmapExpandLevel.value
259
+ const savedLocalIde = normalizeLocalIde(localStorage.getItem(LOCAL_IDE_KEY))
260
+ localIde.value = savedLocalIde
261
+ originalLocalIde.value = savedLocalIde
252
262
 
253
263
  const savedShowNoteId = localStorage.getItem(SIDEBAR_SHOW_NOTE_ID_KEY)
254
264
  showNoteId.value =
@@ -293,17 +303,14 @@ function save() {
293
303
  contentWidthMode.value !== originalContentWidthMode.value
294
304
 
295
305
  localStorage.setItem(NOTES_DIR_KEY, path.value)
296
- localStorage.setItem(
297
- MARKMAP_EXPAND_LEVEL_KEY,
298
- markmapExpandLevel.value.toString(),
299
- )
306
+ setLocalIde(localIde.value)
300
307
  localStorage.setItem(SIDEBAR_SHOW_NOTE_ID_KEY, showNoteId.value.toString())
301
308
  localStorage.setItem(SIDEBAR_DONE_PREFIX_KEY, donePrefix.value)
302
309
  localStorage.setItem(SIDEBAR_UNDONE_PREFIX_KEY, undonePrefix.value)
303
310
  localStorage.setItem(CONTENT_WIDTH_MODE_KEY, contentWidthMode.value)
304
311
 
305
312
  originalPath.value = path.value
306
- originalMarkmapExpandLevel.value = markmapExpandLevel.value
313
+ originalLocalIde.value = localIde.value
307
314
  originalShowNoteId.value = showNoteId.value
308
315
  originalDonePrefix.value = donePrefix.value
309
316
  originalUndonePrefix.value = undonePrefix.value
@@ -327,7 +334,7 @@ function save() {
327
334
 
328
335
  function reset() {
329
336
  path.value = originalPath.value
330
- markmapExpandLevel.value = originalMarkmapExpandLevel.value
337
+ localIde.value = originalLocalIde.value
331
338
  showNoteId.value = originalShowNoteId.value
332
339
  donePrefix.value = originalDonePrefix.value
333
340
  undonePrefix.value = originalUndonePrefix.value
@@ -400,10 +407,12 @@ function reset() {
400
407
 
401
408
  .tooltip {
402
409
  position: absolute;
403
- left: 50%;
404
- bottom: calc(100% + 8px);
405
- z-index: 10;
410
+ left: 0;
411
+ top: calc(100% + 8px);
412
+ bottom: auto;
413
+ z-index: 20;
406
414
  width: 240px;
415
+ max-width: min(240px, calc(100vw - 48px));
407
416
  padding: 8px 10px;
408
417
  color: var(--vp-c-text-1);
409
418
  background: var(--vp-c-bg-elv);
@@ -414,7 +423,7 @@ function reset() {
414
423
  line-height: 18px;
415
424
  opacity: 0;
416
425
  pointer-events: none;
417
- transform: translate(-50%, 4px);
426
+ transform: translateY(-4px);
418
427
  transition:
419
428
  opacity 0.15s ease,
420
429
  transform 0.15s ease;
@@ -423,7 +432,7 @@ function reset() {
423
432
  .infoWrap:hover .tooltip,
424
433
  .infoWrap:focus-within .tooltip {
425
434
  opacity: 1;
426
- transform: translate(-50%, 0);
435
+ transform: translateY(0);
427
436
  }
428
437
 
429
438
  .inputGroup {
@@ -699,14 +708,8 @@ function reset() {
699
708
  }
700
709
 
701
710
  .tooltip {
702
- left: 0;
703
711
  width: min(260px, calc(100vw - 48px));
704
- transform: translate(0, 4px);
705
- }
706
-
707
- .infoWrap:hover .tooltip,
708
- .infoWrap:focus-within .tooltip {
709
- transform: translate(0, 0);
712
+ max-width: min(260px, calc(100vw - 48px));
710
713
  }
711
714
 
712
715
  .actionBar {
@@ -6,8 +6,8 @@ vitepress/components/SidebarCard/FolderTreeItems.vue
6
6
  import {
7
7
  icon__sidebar_collapsed,
8
8
  icon__sidebar_opened,
9
- icon__vscode,
10
9
  } from '../../assets/icons'
10
+ import { useLocalIde } from '../composables/useLocalIde'
11
11
 
12
12
  const props = defineProps({
13
13
  nodes: {
@@ -30,6 +30,8 @@ const props = defineProps({
30
30
 
31
31
  const emit = defineEmits(['toggle-node', 'open-vscode'])
32
32
 
33
+ const { icon: localIdeIcon, openNoteTitle } = useLocalIde()
34
+
33
35
  function hasChildren(node) {
34
36
  return node.children?.length > 0
35
37
  }
@@ -109,21 +111,24 @@ function onOpenVscode(node, event) {
109
111
  <span class="tree-title">{{ getDisplayTitle(node) }}</span>
110
112
  </a>
111
113
 
112
- <span
114
+ <button
113
115
  v-else-if="hasChildren(node)"
116
+ type="button"
114
117
  class="tree-link tree-link-folder"
118
+ :title="isExpanded(node) ? '折叠分组' : '展开分组'"
119
+ @click="onToggle(node)"
115
120
  >
116
121
  <span class="tree-title">{{ getDisplayTitle(node) }}</span>
117
- </span>
122
+ </button>
118
123
 
119
124
  <button
120
125
  v-if="node.relativePath"
121
126
  class="vscode-article"
122
127
  type="button"
123
- title="在 VS Code 中打开笔记目录"
128
+ :title="openNoteTitle"
124
129
  @click="onOpenVscode(node, $event)"
125
130
  >
126
- <img :src="icon__vscode" alt="打开笔记目录" />
131
+ <img :src="localIdeIcon" :alt="openNoteTitle" />
127
132
  </button>
128
133
  </div>
129
134
 
@@ -225,9 +230,14 @@ export default {
225
230
  }
226
231
 
227
232
  .tree-link-folder {
228
- cursor: default;
233
+ cursor: pointer;
229
234
  font-weight: 600;
230
235
  color: var(--vp-c-text-1);
236
+ border: none;
237
+ background: none;
238
+ padding: 0;
239
+ text-align: left;
240
+ font: inherit;
231
241
  }
232
242
 
233
243
  .tree-status {