@tnotesjs/core 0.1.15 → 0.1.16

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 (115) hide show
  1. package/README.md +87 -87
  2. package/dist/{chunk-FVKQPBPP.js → chunk-4MO7F7Q4.js} +2005 -1769
  3. package/dist/cli/index.js +1 -1
  4. package/dist/vitepress/config/index.js +212 -4
  5. package/package.json +1 -1
  6. package/types/config.ts +61 -61
  7. package/types/index.ts +11 -11
  8. package/types/note.ts +33 -33
  9. package/vitepress/assets/icons/icon__check.svg +3 -3
  10. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  11. package/vitepress/assets/icons/icon__close.svg +1 -1
  12. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  13. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  14. package/vitepress/assets/icons/icon__copy.svg +4 -4
  15. package/vitepress/assets/icons/icon__focus.svg +1 -1
  16. package/vitepress/assets/icons/icon__fold.svg +3 -3
  17. package/vitepress/assets/icons/icon__folder.svg +1 -1
  18. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  19. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  20. package/vitepress/assets/icons/icon__github.svg +3 -3
  21. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  22. package/vitepress/assets/icons/icon__next.svg +1 -1
  23. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  24. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  25. package/vitepress/assets/icons/icon__prev.svg +1 -1
  26. package/vitepress/assets/icons/icon__restore.svg +1 -1
  27. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  28. package/vitepress/assets/icons/icon__search.svg +1 -1
  29. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  30. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  31. package/vitepress/assets/icons/icon__totop.svg +5 -5
  32. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  33. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  34. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  35. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  36. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  37. package/vitepress/assets/icons/index.ts +39 -39
  38. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  39. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +373 -373
  40. package/vitepress/components/CodeBlockFullscreen/index.ts +115 -115
  41. package/vitepress/components/CodeBlockFullscreen/styles.css +64 -64
  42. package/vitepress/components/Discussions/Discussions.module.scss +32 -32
  43. package/vitepress/components/Discussions/Discussions.vue +211 -211
  44. package/vitepress/components/EnWordList/EnWordList.module.scss +124 -124
  45. package/vitepress/components/EnWordList/EnWordList.vue +543 -543
  46. package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -22
  47. package/vitepress/components/EnWordList/RightClickMenu.vue +66 -66
  48. package/vitepress/components/Footprints/Footprints.module.scss +93 -93
  49. package/vitepress/components/Footprints/Footprints.vue +377 -377
  50. package/vitepress/components/Layout/AboutModal.module.scss +233 -233
  51. package/vitepress/components/Layout/AboutModal.vue +105 -105
  52. package/vitepress/components/Layout/AboutPanel.vue +270 -270
  53. package/vitepress/components/Layout/ContentCollapse.vue +603 -603
  54. package/vitepress/components/Layout/CustomSidebar.vue +878 -608
  55. package/vitepress/components/Layout/DocBeforeControls.vue +149 -149
  56. package/vitepress/components/Layout/DocFooter.vue +233 -233
  57. package/vitepress/components/Layout/ImagePreview.module.scss +201 -201
  58. package/vitepress/components/Layout/ImagePreview.vue +281 -281
  59. package/vitepress/components/Layout/Layout.module.scss +661 -661
  60. package/vitepress/components/Layout/Layout.vue +618 -559
  61. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  62. package/vitepress/components/Layout/SidebarItems.vue +1083 -268
  63. package/vitepress/components/Layout/SidebarNavBefore.vue +171 -108
  64. package/vitepress/components/Layout/Swiper.vue +167 -167
  65. package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -11
  66. package/vitepress/components/Layout/ToggleFullContent.vue +35 -35
  67. package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -11
  68. package/vitepress/components/Layout/ToggleSidebar.vue +36 -36
  69. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  70. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
  71. package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
  72. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  73. package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
  74. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  75. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  76. package/vitepress/components/MarkMap/MarkMap.module.scss +159 -159
  77. package/vitepress/components/MarkMap/MarkMap.vue +405 -405
  78. package/vitepress/components/Mermaid/Mermaid.module.scss +275 -275
  79. package/vitepress/components/Mermaid/Mermaid.vue +365 -365
  80. package/vitepress/components/NotesTable/NotesTable.module.scss +77 -77
  81. package/vitepress/components/NotesTable/NotesTable.vue +98 -98
  82. package/vitepress/components/NotesTable/README.md +67 -67
  83. package/vitepress/components/Settings/Settings.module.scss +375 -433
  84. package/vitepress/components/Settings/Settings.vue +376 -306
  85. package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
  86. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  87. package/vitepress/components/SidebarCard/SidebarCard.vue +946 -946
  88. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  89. package/vitepress/components/constants.ts +109 -91
  90. package/vitepress/components/notesConfig.data.ts +73 -73
  91. package/vitepress/components/sidebar.data.ts +59 -59
  92. package/vitepress/components/tnotes-config.data.ts +21 -21
  93. package/vitepress/components/utils.ts +26 -26
  94. package/vitepress/config/index.ts +169 -167
  95. package/vitepress/configs/constants.ts +26 -26
  96. package/vitepress/configs/head.config.ts +25 -25
  97. package/vitepress/configs/index.ts +9 -9
  98. package/vitepress/configs/markdown-it.d.ts +23 -23
  99. package/vitepress/configs/markdown.config.ts +366 -366
  100. package/vitepress/configs/theme.config.ts +108 -108
  101. package/vitepress/plugins/buildProgressPlugin.ts +434 -434
  102. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  103. package/vitepress/plugins/renameNotePlugin.ts +68 -68
  104. package/vitepress/plugins/sidebarStructurePlugin.ts +260 -0
  105. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  106. package/vitepress/theme/index.ts +137 -137
  107. package/vitepress/theme/styles/base.scss +50 -50
  108. package/vitepress/theme/styles/components/404.scss +31 -31
  109. package/vitepress/theme/styles/components/collapse.scss +175 -175
  110. package/vitepress/theme/styles/components/markmap.scss +101 -101
  111. package/vitepress/theme/styles/components/swiper.scss +255 -255
  112. package/vitepress/theme/styles/index.scss +25 -25
  113. package/vitepress/theme/styles/layout.scss +70 -62
  114. package/vitepress/theme/styles/utilities.scss +39 -39
  115. package/vitepress/theme/styles/vitepress-override.scss +25 -25
@@ -1,268 +1,1083 @@
1
- <template>
2
- <template v-for="item in items" :key="getItemKey(item)">
3
- <!-- 只有在不超过最大层级时才渲染组 -->
4
- <div
5
- v-if="hasChildren(item) && depth < maxDepth - 1"
6
- class="group"
7
- :style="getGroupStyle(depth)"
8
- >
9
- <button
10
- class="group-title"
11
- :class="`group-title-level-${depth}`"
12
- @click="toggleItem(item)"
13
- >
14
- <span>{{ item.text }}</span>
15
-
16
- <span class="arrow" :class="{ collapsed: item.collapsed }">
17
- <img
18
- :src="
19
- item.collapsed ? icon__sidebar_collapsed : icon__sidebar_opened
20
- "
21
- alt=""
22
- />
23
- </span>
24
- </button>
25
-
26
- <div v-show="!item.collapsed" class="group-items">
27
- <SidebarItems
28
- :items="getChildItems(item)"
29
- :depth="depth + 1"
30
- :max-depth="maxDepth"
31
- :show-note-id="showNoteId"
32
- :base="base"
33
- :current-path="currentPath"
34
- :item-depth="depth"
35
- />
36
- </div>
37
- </div>
38
-
39
- <!-- 如果是链接项,或者是超过最大层级的组,都不渲染 -->
40
- <a
41
- v-else-if="!hasChildren(item)"
42
- :href="getFullLink(item.link)"
43
- :class="[
44
- 'nav-item',
45
- { active: isActive(item.link) },
46
- `nav-item-${extractNoteIdFromLink(item.link)}`,
47
- `nav-item-level-${actualItemDepth + 1}`,
48
- ]"
49
- :data-note-id="extractNoteIdFromLink(item.link)"
50
- :style="getItemStyle(actualItemDepth)"
51
- >
52
- {{ getNoteDisplayText(item.text, item.link) }}
53
- </a>
54
- </template>
55
- </template>
56
-
57
- <script setup lang="ts">
58
- import { computed } from 'vue'
59
-
60
- import { icon__sidebar_opened, icon__sidebar_collapsed } from '../../assets/icons'
61
-
62
- interface SidebarItem {
63
- text: string
64
- link?: string
65
- items?: SidebarItem[]
66
- collapsed?: boolean
67
- }
68
-
69
- interface Props {
70
- items: SidebarItem[]
71
- depth: number
72
- maxDepth: number
73
- showNoteId: boolean
74
- base: string
75
- currentPath: string
76
- itemDepth?: number // 用于计算链接项的缩进,默认等于 depth
77
- }
78
-
79
- const props = withDefaults(defineProps<Props>(), {
80
- depth: 0,
81
- maxDepth: 3,
82
- showNoteId: false,
83
- base: '/',
84
- currentPath: '',
85
- itemDepth: undefined,
86
- })
87
-
88
- // 获取实际的 item depth(用于链接项的缩进)
89
- const actualItemDepth = computed(() => props.itemDepth ?? props.depth)
90
-
91
- // 判断项是否有子项
92
- function hasChildren(item: SidebarItem): boolean {
93
- return !!(item.items && item.items.length > 0)
94
- }
95
-
96
- function getChildItems(item: SidebarItem): SidebarItem[] {
97
- return item.items ?? []
98
- }
99
-
100
- // 获取项的唯一 key
101
- function getItemKey(item: SidebarItem): string {
102
- return item.link || item.text
103
- }
104
-
105
- // 切换项的展开/折叠状态
106
- function toggleItem(item: SidebarItem) {
107
- item.collapsed = !item.collapsed
108
- }
109
-
110
- // 根据深度获取组的缩进样式
111
- function getGroupStyle(depth: number) {
112
- // 从第二层组开始才需要缩进(depth >= 1)
113
- if (depth === 0) {
114
- return {}
115
- }
116
- return {
117
- paddingLeft: `${depth * 16}px`,
118
- }
119
- }
120
-
121
- // 根据深度获取项的缩进样式
122
- function getItemStyle(depth: number) {
123
- // 项的缩进基于其父组的depth
124
- // 第一层组(depth=0)的子项不需要额外缩进
125
- // 第二层组(depth=1)的子项需要缩进 16px
126
- if (depth === 0) {
127
- return {}
128
- }
129
- return {
130
- paddingLeft: `${depth * 16}px`,
131
- }
132
- }
133
-
134
- // 获取完整链接(包含 base)
135
- function getFullLink(link?: string) {
136
- if (!link) return '#'
137
- const cleanLink = link.startsWith('/') ? link.slice(1) : link
138
- return props.base + cleanLink
139
- }
140
-
141
- // 判断链接是否激活
142
- function isActive(link?: string) {
143
- if (!link) return false
144
- const fullLink = getFullLink(link)
145
- const decodedRoutePath = decodeURIComponent(props.currentPath)
146
- const decodedFullLink = decodeURIComponent(fullLink)
147
-
148
- return (
149
- decodedRoutePath === decodedFullLink ||
150
- decodedRoutePath === decodedFullLink + '.html'
151
- )
152
- }
153
-
154
- // 从链接中提取笔记 ID
155
- function extractNoteIdFromLink(link?: string): string | null {
156
- if (!link) return null
157
- const match = link.match(/\/notes\/(\d{4})\./)
158
- return match ? match[1] : null
159
- }
160
-
161
- // 提取文本开头的 emoji
162
- function extractEmoji(text: string): { emoji: string; rest: string } {
163
- const emojiMatch = text.match(
164
- /^([\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}✅❌⏰]+)\s*/u
165
- )
166
-
167
- if (emojiMatch) {
168
- return {
169
- emoji: emojiMatch[1],
170
- rest: text.slice(emojiMatch[0].length),
171
- }
172
- }
173
-
174
- return { emoji: '', rest: text }
175
- }
176
-
177
- // 获取笔记显示文本
178
- function getNoteDisplayText(text: string, link?: string): string {
179
- const { emoji, rest } = extractEmoji(text)
180
-
181
- if (props.showNoteId) {
182
- if (/^\d{4}\./.test(rest)) {
183
- return emoji ? `${emoji} ${rest}` : rest
184
- }
185
-
186
- const noteId = extractNoteIdFromLink(link)
187
- if (noteId) {
188
- return emoji ? `${emoji} ${noteId}. ${rest}` : `${noteId}. ${rest}`
189
- }
190
-
191
- return text
192
- } else {
193
- const cleanRest = rest.replace(/^\d{4}\.\s*/, '')
194
- return emoji ? `${emoji} ${cleanRest}` : cleanRest
195
- }
196
- }
197
- </script>
198
-
199
- <style scoped>
200
- .group {
201
- margin-bottom: 16px;
202
- }
203
-
204
- .group-title {
205
- display: flex;
206
- justify-content: space-between;
207
- align-items: center;
208
- width: 100%;
209
- padding: 6px 0;
210
- font-weight: 600;
211
- color: var(--vp-c-text-1);
212
- background: none;
213
- border: none;
214
- cursor: pointer;
215
- text-align: left;
216
- transition: color 0.25s;
217
- }
218
-
219
- .group-title:hover {
220
- color: var(--vp-c-brand-1);
221
- }
222
-
223
- .arrow {
224
- font-size: 10px;
225
- transform: rotate(90deg);
226
- transition: transform 0.25s;
227
- }
228
-
229
- .arrow.collapsed {
230
- transform: rotate(0deg);
231
- }
232
-
233
- .nav-item {
234
- display: block;
235
- padding: 4px;
236
- color: var(--vp-c-text-2);
237
- text-decoration: none;
238
- border-radius: 4px;
239
- font-size: 14px;
240
- line-height: 24px;
241
- transition: all 0.25s;
242
- }
243
-
244
- .nav-item:hover {
245
- color: var(--vp-c-brand-1);
246
- background-color: var(--vp-c-default-soft);
247
- }
248
-
249
- .nav-item.active {
250
- color: var(--vp-c-brand-1);
251
- font-weight: 600;
252
- }
253
-
254
- /* 聚焦高亮动画 */
255
- .nav-item.focus-highlight {
256
- animation: focusPulse 1s ease-in-out;
257
- }
258
-
259
- @keyframes focusPulse {
260
- 0%,
261
- 100% {
262
- background-color: transparent;
263
- }
264
- 50% {
265
- background-color: var(--vp-c-brand-soft);
266
- }
267
- }
268
- </style>
1
+ <template>
2
+ <template v-for="item in items" :key="getItemKey(item)">
3
+ <!-- 只有在不超过最大层级时才渲染组 -->
4
+ <div
5
+ v-if="hasChildren(item) && depth < maxDepth - 1"
6
+ class="group"
7
+ :class="getDensityClass()"
8
+ >
9
+ <div
10
+ class="sidebar-row group-row"
11
+ :class="[getGroupDropClass(item), getDensityClass()]"
12
+ :style="getNodeStyle(depth)"
13
+ :draggable="isDev"
14
+ @dragstart="onGroupDragStart($event, item)"
15
+ @dragend="clearDropTarget"
16
+ @dragover.prevent.stop="onGroupDragOver($event, item)"
17
+ @drop.stop="onGroupDrop($event, item)"
18
+ >
19
+ <button
20
+ class="group-title row-main"
21
+ :class="`group-title-level-${depth}`"
22
+ @click="toggleItem(item)"
23
+ >
24
+ <span class="arrow" :class="{ collapsed: item.collapsed }">
25
+ <img
26
+ :src="
27
+ item.collapsed ? icon__sidebar_collapsed : icon__sidebar_opened
28
+ "
29
+ alt=""
30
+ />
31
+ </span>
32
+
33
+ <span class="group-title-text">{{ item.text }}</span>
34
+ </button>
35
+
36
+ <div
37
+ v-if="isDev"
38
+ class="row-actions"
39
+ :class="{ 'menu-open': isGroupMenuOpen(item) }"
40
+ @click.stop
41
+ >
42
+ <button
43
+ class="action-btn menu-trigger"
44
+ title="更多操作"
45
+ type="button"
46
+ @click="toggleMenu(getGroupMenuKey(item, 'more'))"
47
+ >
48
+ <span class="ellipsis">...</span>
49
+ </button>
50
+
51
+ <div
52
+ v-if="isMenuOpen(getGroupMenuKey(item, 'more'))"
53
+ class="action-menu"
54
+ >
55
+ <button class="menu-item" type="button" @click="renameGroup(item)">
56
+ 重命名目录
57
+ </button>
58
+ <button
59
+ class="menu-item danger"
60
+ type="button"
61
+ @click="deleteGroup(item)"
62
+ >
63
+ 删除目录
64
+ </button>
65
+ </div>
66
+
67
+ <button
68
+ class="action-btn menu-trigger add-trigger"
69
+ title="新增"
70
+ type="button"
71
+ @click="toggleMenu(getGroupMenuKey(item, 'add'))"
72
+ >
73
+ +
74
+ </button>
75
+
76
+ <div
77
+ v-if="isMenuOpen(getGroupMenuKey(item, 'add'))"
78
+ class="action-menu"
79
+ >
80
+ <button
81
+ class="menu-item"
82
+ type="button"
83
+ @click="createNoteInGroup(item)"
84
+ >
85
+ 新增笔记
86
+ </button>
87
+ <button
88
+ class="menu-item"
89
+ type="button"
90
+ @click="createNotesInGroup(item)"
91
+ >
92
+ 新增多篇笔记
93
+ </button>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <div v-show="!item.collapsed" class="group-items">
99
+ <SidebarItems
100
+ :items="getChildItems(item)"
101
+ :depth="depth + 1"
102
+ :max-depth="maxDepth"
103
+ :show-note-id="showNoteId"
104
+ :base="base"
105
+ :current-path="currentPath"
106
+ :item-depth="depth + 1"
107
+ :group-path="getGroupPath(item)"
108
+ :is-dev="isDev"
109
+ :sidebar-density="sidebarDensity"
110
+ :done-prefix="donePrefix"
111
+ :undone-prefix="undonePrefix"
112
+ @rename-group="$emit('rename-group', $event)"
113
+ @delete-group="$emit('delete-group', $event)"
114
+ @create-note-in-group="$emit('create-note-in-group', $event)"
115
+ @create-notes-in-group="$emit('create-notes-in-group', $event)"
116
+ @rename-note="$emit('rename-note', $event)"
117
+ @delete-note="$emit('delete-note', $event)"
118
+ @create-note-around="$emit('create-note-around', $event)"
119
+ @open-note-about="$emit('open-note-about', $event)"
120
+ @reorder-sidebar="$emit('reorder-sidebar', $event)"
121
+ />
122
+ </div>
123
+ </div>
124
+
125
+ <!-- 如果是链接项,或者是超过最大层级的组,都不渲染 -->
126
+ <div
127
+ v-else-if="!hasChildren(item)"
128
+ class="sidebar-row note-row"
129
+ :class="[getNoteDropClass(item), getDensityClass()]"
130
+ :style="getNodeStyle(actualItemDepth)"
131
+ :draggable="isDev"
132
+ @dragstart="onNoteDragStart($event, item)"
133
+ @dragend="clearDropTarget"
134
+ @dragover.prevent.stop="onNoteDragOver($event, item)"
135
+ @drop.stop="onNoteDrop($event, item)"
136
+ >
137
+ <a
138
+ :href="getFullLink(item.link)"
139
+ :class="[
140
+ 'nav-item',
141
+ 'row-main',
142
+ { active: isActive(item.link) },
143
+ `nav-item-${extractNoteIdFromLink(item.link)}`,
144
+ `nav-item-level-${actualItemDepth + 1}`,
145
+ ]"
146
+ :data-note-id="extractNoteIdFromLink(item.link)"
147
+ >
148
+ {{ getNoteDisplayText(item.text, item.link) }}
149
+ </a>
150
+
151
+ <div
152
+ v-if="isDev"
153
+ class="row-actions"
154
+ :class="{ 'menu-open': isNoteMenuOpen(item) }"
155
+ @click.stop
156
+ >
157
+ <button
158
+ class="action-btn menu-trigger"
159
+ title="更多操作"
160
+ type="button"
161
+ @click="toggleMenu(getNoteMenuKey(item, 'more'))"
162
+ >
163
+ <span class="ellipsis">...</span>
164
+ </button>
165
+
166
+ <div
167
+ v-if="isMenuOpen(getNoteMenuKey(item, 'more'))"
168
+ class="action-menu"
169
+ >
170
+ <button class="menu-item" type="button" @click="renameNote(item)">
171
+ 重命名
172
+ </button>
173
+ <button
174
+ class="menu-item danger"
175
+ type="button"
176
+ @click="deleteNote(item)"
177
+ >
178
+ 删除笔记
179
+ </button>
180
+ <button class="menu-item" type="button" @click="openNoteAbout(item)">
181
+ 关于
182
+ </button>
183
+ </div>
184
+
185
+ <button
186
+ class="action-btn menu-trigger add-trigger"
187
+ title="新增"
188
+ type="button"
189
+ @click="toggleMenu(getNoteMenuKey(item, 'add'))"
190
+ >
191
+ +
192
+ </button>
193
+
194
+ <div
195
+ v-if="isMenuOpen(getNoteMenuKey(item, 'add'))"
196
+ class="action-menu"
197
+ >
198
+ <button
199
+ class="menu-item"
200
+ type="button"
201
+ @click="createNoteAround(item, 'before')"
202
+ >
203
+ 在上方新增笔记
204
+ </button>
205
+ <button
206
+ class="menu-item"
207
+ type="button"
208
+ @click="createNoteAround(item, 'after')"
209
+ >
210
+ 在下方新增笔记
211
+ </button>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </template>
216
+ </template>
217
+
218
+ <script setup lang="ts">
219
+ import { computed, onBeforeUnmount, ref } from 'vue'
220
+
221
+ import {
222
+ icon__sidebar_opened,
223
+ icon__sidebar_collapsed,
224
+ } from '../../assets/icons'
225
+
226
+ interface SidebarItem {
227
+ text: string
228
+ link?: string
229
+ items?: SidebarItem[]
230
+ collapsed?: boolean
231
+ }
232
+
233
+ interface Props {
234
+ items: SidebarItem[]
235
+ depth: number
236
+ maxDepth: number
237
+ showNoteId: boolean
238
+ base: string
239
+ currentPath: string
240
+ itemDepth?: number // 用于计算链接项的缩进,默认等于 depth
241
+ groupPath?: string[]
242
+ isDev?: boolean
243
+ sidebarDensity?: SidebarDensity
244
+ donePrefix?: string
245
+ undonePrefix?: string
246
+ }
247
+
248
+ const props = withDefaults(defineProps<Props>(), {
249
+ depth: 0,
250
+ maxDepth: 3,
251
+ showNoteId: false,
252
+ base: '/',
253
+ currentPath: '',
254
+ itemDepth: undefined,
255
+ groupPath: () => [],
256
+ isDev: false,
257
+ sidebarDensity: 'default',
258
+ donePrefix: '✅',
259
+ undonePrefix: '⏰',
260
+ })
261
+
262
+ const emit = defineEmits<{
263
+ 'rename-group': [payload: GroupPayload]
264
+ 'delete-group': [payload: GroupPayload]
265
+ 'create-note-in-group': [payload: GroupPayload]
266
+ 'create-notes-in-group': [payload: GroupPayload]
267
+ 'rename-note': [payload: NotePayload]
268
+ 'delete-note': [payload: NotePayload]
269
+ 'create-note-around': [
270
+ payload: NotePayload & { placement: 'before' | 'after' },
271
+ ]
272
+ 'open-note-about': [payload: NotePayload]
273
+ 'reorder-sidebar': [payload: ReorderPayload]
274
+ }>()
275
+
276
+ interface GroupPayload {
277
+ groupPath: string[]
278
+ text: string
279
+ }
280
+
281
+ interface NotePayload {
282
+ noteIndex: string
283
+ text: string
284
+ link?: string
285
+ }
286
+
287
+ interface DragPayload {
288
+ type: 'group' | 'note'
289
+ groupPath?: string[]
290
+ noteIndex?: string
291
+ }
292
+
293
+ interface DropTarget {
294
+ type: 'group' | 'note'
295
+ key: string
296
+ placement: 'before' | 'after' | 'inside'
297
+ }
298
+
299
+ interface ReorderPayload {
300
+ dragType: 'group' | 'note'
301
+ groupPath?: string[]
302
+ noteIndex?: string
303
+ targetType: 'group' | 'note'
304
+ targetGroupPath?: string[]
305
+ targetNoteIndex?: string
306
+ placement?: 'before' | 'after'
307
+ }
308
+
309
+ type MenuType = 'more' | 'add'
310
+ type SidebarDensity = 'compact' | 'default' | 'loose'
311
+ type NoteStatus = 'done' | 'undone' | null
312
+
313
+ // 获取实际的 item depth(用于链接项的缩进)
314
+ const actualItemDepth = computed(() => props.itemDepth ?? props.depth)
315
+ const openMenuKey = ref<string | null>(null)
316
+ const dropTarget = ref<DropTarget | null>(null)
317
+ let ignoredToggleKey: string | null = null
318
+
319
+ const dragDataType = 'application/x-tnotes-sidebar-node'
320
+ const dragNoteType = 'application/x-tnotes-sidebar-note'
321
+ const dragGroupType = 'application/x-tnotes-sidebar-group'
322
+
323
+ if (typeof document !== 'undefined') {
324
+ document.addEventListener('click', closeMenuOnDocumentClick, true)
325
+ document.addEventListener('dragend', clearDropTarget, true)
326
+ document.addEventListener('drop', clearDropTarget, true)
327
+ }
328
+
329
+ onBeforeUnmount(() => {
330
+ if (typeof document !== 'undefined') {
331
+ document.removeEventListener('click', closeMenuOnDocumentClick, true)
332
+ document.removeEventListener('dragend', clearDropTarget, true)
333
+ document.removeEventListener('drop', clearDropTarget, true)
334
+ }
335
+ })
336
+
337
+ // 判断项是否有子项
338
+ function hasChildren(item: SidebarItem): boolean {
339
+ return !!(item.items && item.items.length > 0)
340
+ }
341
+
342
+ function getChildItems(item: SidebarItem): SidebarItem[] {
343
+ return item.items ?? []
344
+ }
345
+
346
+ // 获取项的唯一 key
347
+ function getItemKey(item: SidebarItem): string {
348
+ return item.link || item.text
349
+ }
350
+
351
+ // 切换项的展开/折叠状态
352
+ function toggleItem(item: SidebarItem) {
353
+ item.collapsed = !item.collapsed
354
+ }
355
+
356
+ function getNodeStyle(depth: number) {
357
+ return {
358
+ paddingLeft: `${depth * getIndentSize()}px`,
359
+ }
360
+ }
361
+
362
+ function getDensityClass(): string {
363
+ return `density-${props.sidebarDensity}`
364
+ }
365
+
366
+ function getIndentSize(): number {
367
+ if (props.sidebarDensity === 'compact') return 20
368
+ if (props.sidebarDensity === 'loose') return 28
369
+ return 24
370
+ }
371
+
372
+ function getGroupPath(item: SidebarItem): string[] {
373
+ return [...props.groupPath, item.text]
374
+ }
375
+
376
+ function getGroupPayload(item: SidebarItem): GroupPayload {
377
+ return {
378
+ groupPath: getGroupPath(item),
379
+ text: item.text,
380
+ }
381
+ }
382
+
383
+ function getNotePayload(item: SidebarItem): NotePayload {
384
+ return {
385
+ noteIndex: extractNoteIdFromLink(item.link) || '',
386
+ text: item.text,
387
+ link: item.link,
388
+ }
389
+ }
390
+
391
+ function getGroupMenuKey(item: SidebarItem, menuType: MenuType): string {
392
+ return `group:${menuType}:${getGroupPath(item).join('/')}`
393
+ }
394
+
395
+ function getGroupDropKey(item: SidebarItem): string {
396
+ return `group:${getGroupPath(item).join('/')}`
397
+ }
398
+
399
+ function getNoteMenuKey(item: SidebarItem, menuType: MenuType): string {
400
+ return `note:${menuType}:${extractNoteIdFromLink(item.link) || item.link || item.text}`
401
+ }
402
+
403
+ function getNoteDropKey(item: SidebarItem): string {
404
+ return `note:${extractNoteIdFromLink(item.link) || item.link || item.text}`
405
+ }
406
+
407
+ function toggleMenu(menuKey: string) {
408
+ if (ignoredToggleKey === menuKey) {
409
+ ignoredToggleKey = null
410
+ return
411
+ }
412
+
413
+ openMenuKey.value = openMenuKey.value === menuKey ? null : menuKey
414
+ }
415
+
416
+ function closeMenu() {
417
+ openMenuKey.value = null
418
+ }
419
+
420
+ function isMenuOpen(menuKey: string): boolean {
421
+ return openMenuKey.value === menuKey
422
+ }
423
+
424
+ function closeMenuOnDocumentClick() {
425
+ if (!openMenuKey.value) return
426
+
427
+ const closedMenuKey = openMenuKey.value
428
+ openMenuKey.value = null
429
+ ignoredToggleKey = closedMenuKey
430
+
431
+ window.setTimeout(() => {
432
+ if (ignoredToggleKey === closedMenuKey) {
433
+ ignoredToggleKey = null
434
+ }
435
+ })
436
+ }
437
+
438
+ function isGroupMenuOpen(item: SidebarItem): boolean {
439
+ return (
440
+ isMenuOpen(getGroupMenuKey(item, 'more')) ||
441
+ isMenuOpen(getGroupMenuKey(item, 'add'))
442
+ )
443
+ }
444
+
445
+ function isNoteMenuOpen(item: SidebarItem): boolean {
446
+ return (
447
+ isMenuOpen(getNoteMenuKey(item, 'more')) ||
448
+ isMenuOpen(getNoteMenuKey(item, 'add'))
449
+ )
450
+ }
451
+
452
+ function renameGroup(item: SidebarItem) {
453
+ emit('rename-group', getGroupPayload(item))
454
+ closeMenu()
455
+ }
456
+
457
+ function deleteGroup(item: SidebarItem) {
458
+ emit('delete-group', getGroupPayload(item))
459
+ closeMenu()
460
+ }
461
+
462
+ function createNoteInGroup(item: SidebarItem) {
463
+ emit('create-note-in-group', getGroupPayload(item))
464
+ closeMenu()
465
+ }
466
+
467
+ function createNotesInGroup(item: SidebarItem) {
468
+ emit('create-notes-in-group', getGroupPayload(item))
469
+ closeMenu()
470
+ }
471
+
472
+ function renameNote(item: SidebarItem) {
473
+ emit('rename-note', getNotePayload(item))
474
+ closeMenu()
475
+ }
476
+
477
+ function deleteNote(item: SidebarItem) {
478
+ emit('delete-note', getNotePayload(item))
479
+ closeMenu()
480
+ }
481
+
482
+ function openNoteAbout(item: SidebarItem) {
483
+ emit('open-note-about', getNotePayload(item))
484
+ closeMenu()
485
+ }
486
+
487
+ function createNoteAround(item: SidebarItem, placement: 'before' | 'after') {
488
+ emit('create-note-around', {
489
+ ...getNotePayload(item),
490
+ placement,
491
+ })
492
+ closeMenu()
493
+ }
494
+
495
+ function setDragData(event: DragEvent, payload: DragPayload) {
496
+ if (!event.dataTransfer) return
497
+
498
+ event.dataTransfer.effectAllowed = 'move'
499
+ event.dataTransfer.setData(dragDataType, JSON.stringify(payload))
500
+ event.dataTransfer.setData(
501
+ payload.type === 'note' ? dragNoteType : dragGroupType,
502
+ '1',
503
+ )
504
+ }
505
+
506
+ function getDragData(event: DragEvent): DragPayload | null {
507
+ const raw = event.dataTransfer?.getData(dragDataType)
508
+ if (!raw) return null
509
+
510
+ try {
511
+ return JSON.parse(raw) as DragPayload
512
+ } catch {
513
+ return null
514
+ }
515
+ }
516
+
517
+ function getDragTypes(event: DragEvent): string[] {
518
+ return Array.from(event.dataTransfer?.types ?? [])
519
+ }
520
+
521
+ function getDraggedType(event: DragEvent): DragPayload['type'] | null {
522
+ const types = getDragTypes(event)
523
+
524
+ if (types.includes(dragNoteType)) return 'note'
525
+ if (types.includes(dragGroupType)) return 'group'
526
+
527
+ return getDragData(event)?.type ?? null
528
+ }
529
+
530
+ function setDropEffect(event: DragEvent) {
531
+ if (event.dataTransfer) {
532
+ event.dataTransfer.dropEffect = 'move'
533
+ }
534
+ }
535
+
536
+ function clearDropTarget() {
537
+ dropTarget.value = null
538
+ }
539
+
540
+ function isSamePath(left: string[], right: string[]): boolean {
541
+ return (
542
+ left.length === right.length &&
543
+ left.every((item, index) => item === right[index])
544
+ )
545
+ }
546
+
547
+ function getNoteDropPlacement(event: DragEvent): 'before' | 'after' {
548
+ const row = event.currentTarget as HTMLElement | null
549
+ if (!row) return 'before'
550
+
551
+ const rect = row.getBoundingClientRect()
552
+ return event.clientY < rect.top + rect.height / 2 ? 'before' : 'after'
553
+ }
554
+
555
+ function getGroupDropClass(item: SidebarItem): Record<string, boolean> {
556
+ const key = getGroupDropKey(item)
557
+ return {
558
+ 'drop-before':
559
+ dropTarget.value?.type === 'group' &&
560
+ dropTarget.value.key === key &&
561
+ dropTarget.value.placement === 'before',
562
+ 'drop-inside':
563
+ dropTarget.value?.type === 'group' &&
564
+ dropTarget.value.key === key &&
565
+ dropTarget.value.placement === 'inside',
566
+ }
567
+ }
568
+
569
+ function getNoteDropClass(item: SidebarItem): Record<string, boolean> {
570
+ const key = getNoteDropKey(item)
571
+ return {
572
+ 'drop-before':
573
+ dropTarget.value?.type === 'note' &&
574
+ dropTarget.value.key === key &&
575
+ dropTarget.value.placement === 'before',
576
+ 'drop-after':
577
+ dropTarget.value?.type === 'note' &&
578
+ dropTarget.value.key === key &&
579
+ dropTarget.value.placement === 'after',
580
+ }
581
+ }
582
+
583
+ function onGroupDragStart(event: DragEvent, item: SidebarItem) {
584
+ if (!props.isDev) return
585
+ closeMenu()
586
+ setDragData(event, {
587
+ type: 'group',
588
+ groupPath: getGroupPath(item),
589
+ })
590
+ }
591
+
592
+ function onNoteDragStart(event: DragEvent, item: SidebarItem) {
593
+ if (!props.isDev) return
594
+ const noteIndex = extractNoteIdFromLink(item.link)
595
+ if (!noteIndex) return
596
+
597
+ closeMenu()
598
+ setDragData(event, {
599
+ type: 'note',
600
+ noteIndex,
601
+ })
602
+ }
603
+
604
+ function onGroupDragOver(event: DragEvent, item: SidebarItem) {
605
+ if (!props.isDev) return
606
+
607
+ const draggedType = getDraggedType(event)
608
+ if (!draggedType) return
609
+
610
+ setDropEffect(event)
611
+
612
+ const targetGroupPath = getGroupPath(item)
613
+ const payload = getDragData(event)
614
+ if (
615
+ draggedType === 'group' &&
616
+ payload?.groupPath &&
617
+ isSamePath(payload.groupPath, targetGroupPath)
618
+ ) {
619
+ clearDropTarget()
620
+ return
621
+ }
622
+
623
+ dropTarget.value = {
624
+ type: 'group',
625
+ key: getGroupDropKey(item),
626
+ placement: draggedType === 'note' ? 'inside' : 'before',
627
+ }
628
+ }
629
+
630
+ function onGroupDrop(event: DragEvent, item: SidebarItem) {
631
+ if (!props.isDev) return
632
+
633
+ const payload = getDragData(event)
634
+ if (!payload) {
635
+ clearDropTarget()
636
+ return
637
+ }
638
+
639
+ const targetGroupPath = getGroupPath(item)
640
+
641
+ if (payload.type === 'note' && payload.noteIndex) {
642
+ emit('reorder-sidebar', {
643
+ dragType: 'note',
644
+ noteIndex: payload.noteIndex,
645
+ targetType: 'group',
646
+ targetGroupPath,
647
+ })
648
+ clearDropTarget()
649
+ return
650
+ }
651
+
652
+ if (payload.type === 'group' && payload.groupPath) {
653
+ if (isSamePath(payload.groupPath, targetGroupPath)) {
654
+ clearDropTarget()
655
+ return
656
+ }
657
+
658
+ emit('reorder-sidebar', {
659
+ dragType: 'group',
660
+ groupPath: payload.groupPath,
661
+ targetType: 'group',
662
+ targetGroupPath,
663
+ placement: 'before',
664
+ })
665
+ }
666
+
667
+ clearDropTarget()
668
+ }
669
+
670
+ function onNoteDragOver(event: DragEvent, item: SidebarItem) {
671
+ if (!props.isDev || getDraggedType(event) !== 'note') return
672
+
673
+ const targetNoteIndex = extractNoteIdFromLink(item.link)
674
+ const payload = getDragData(event)
675
+ if (!targetNoteIndex || payload?.noteIndex === targetNoteIndex) {
676
+ clearDropTarget()
677
+ return
678
+ }
679
+
680
+ setDropEffect(event)
681
+ dropTarget.value = {
682
+ type: 'note',
683
+ key: getNoteDropKey(item),
684
+ placement: getNoteDropPlacement(event),
685
+ }
686
+ }
687
+
688
+ function onNoteDrop(event: DragEvent, item: SidebarItem) {
689
+ if (!props.isDev) return
690
+
691
+ const payload = getDragData(event)
692
+ const targetNoteIndex = extractNoteIdFromLink(item.link)
693
+ if (
694
+ !payload ||
695
+ payload.type !== 'note' ||
696
+ !payload.noteIndex ||
697
+ !targetNoteIndex
698
+ ) {
699
+ clearDropTarget()
700
+ return
701
+ }
702
+
703
+ if (payload.noteIndex === targetNoteIndex) {
704
+ clearDropTarget()
705
+ return
706
+ }
707
+
708
+ const placement = getNoteDropPlacement(event)
709
+ emit('reorder-sidebar', {
710
+ dragType: 'note',
711
+ noteIndex: payload.noteIndex,
712
+ targetType: 'note',
713
+ targetNoteIndex,
714
+ placement,
715
+ })
716
+ clearDropTarget()
717
+ }
718
+
719
+ // 获取完整链接(包含 base)
720
+ function getFullLink(link?: string) {
721
+ if (!link) return '#'
722
+ const cleanLink = link.startsWith('/') ? link.slice(1) : link
723
+ return props.base + cleanLink
724
+ }
725
+
726
+ // 判断链接是否激活
727
+ function isActive(link?: string) {
728
+ if (!link) return false
729
+ const fullLink = getFullLink(link)
730
+ const decodedRoutePath = decodeURIComponent(props.currentPath)
731
+ const decodedFullLink = decodeURIComponent(fullLink)
732
+
733
+ return (
734
+ decodedRoutePath === decodedFullLink ||
735
+ decodedRoutePath === decodedFullLink + '.html'
736
+ )
737
+ }
738
+
739
+ // 从链接中提取笔记 ID
740
+ function extractNoteIdFromLink(link?: string): string | null {
741
+ if (!link) return null
742
+ const match = link.match(/\/notes\/(\d{4})\./)
743
+ return match ? match[1] : null
744
+ }
745
+
746
+ // 提取文本开头的 emoji
747
+ function extractEmoji(text: string): { emoji: string; rest: string } {
748
+ const emojiMatch = text.match(
749
+ /^([\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}✅❌⏰]+)\s*/u
750
+ )
751
+
752
+ if (emojiMatch) {
753
+ return {
754
+ emoji: emojiMatch[1],
755
+ rest: text.slice(emojiMatch[0].length),
756
+ }
757
+ }
758
+
759
+ return { emoji: '', rest: text }
760
+ }
761
+
762
+ function getNoteStatusAndText(text: string): {
763
+ status: NoteStatus
764
+ rest: string
765
+ } {
766
+ const { emoji, rest } = extractEmoji(text)
767
+
768
+ if (emoji.includes('✅')) {
769
+ return { status: 'done', rest }
770
+ }
771
+
772
+ if (emoji.includes('⏰')) {
773
+ return { status: 'undone', rest }
774
+ }
775
+
776
+ return { status: null, rest: text }
777
+ }
778
+
779
+ function getStatusPrefix(status: NoteStatus): string {
780
+ if (status === 'done') return props.donePrefix
781
+ if (status === 'undone') return props.undonePrefix
782
+
783
+ return ''
784
+ }
785
+
786
+ function withStatusPrefix(prefix: string, text: string): string {
787
+ return prefix ? `${prefix} ${text}` : text
788
+ }
789
+
790
+ // 获取笔记显示文本
791
+ function getNoteDisplayText(text: string, link?: string): string {
792
+ const { status, rest } = getNoteStatusAndText(text)
793
+ const statusPrefix = getStatusPrefix(status)
794
+
795
+ if (props.showNoteId) {
796
+ if (/^\d{4}\./.test(rest)) {
797
+ return withStatusPrefix(statusPrefix, rest)
798
+ }
799
+
800
+ const noteId = extractNoteIdFromLink(link)
801
+ if (noteId) {
802
+ return withStatusPrefix(statusPrefix, `${noteId}. ${rest}`)
803
+ }
804
+
805
+ return withStatusPrefix(statusPrefix, rest)
806
+ } else {
807
+ const cleanRest = rest.replace(/^\d{4}\.\s*/, '')
808
+ return withStatusPrefix(statusPrefix, cleanRest)
809
+ }
810
+ }
811
+ </script>
812
+
813
+ <style scoped>
814
+ .group {
815
+ margin-bottom: 2px;
816
+ }
817
+
818
+ .group.density-compact {
819
+ margin-bottom: 0;
820
+ }
821
+
822
+ .group.density-loose {
823
+ margin-bottom: 6px;
824
+ }
825
+
826
+ .sidebar-row {
827
+ position: relative;
828
+ display: flex;
829
+ align-items: center;
830
+ width: 100%;
831
+ min-height: 30px;
832
+ border-radius: 6px;
833
+ transition: background-color 0.18s ease;
834
+ }
835
+
836
+ .sidebar-row.density-compact {
837
+ min-height: 26px;
838
+ border-radius: 4px;
839
+ }
840
+
841
+ .sidebar-row.density-loose {
842
+ min-height: 36px;
843
+ border-radius: 8px;
844
+ }
845
+
846
+ .sidebar-row:hover {
847
+ background: var(--vp-c-default-soft);
848
+ }
849
+
850
+ .sidebar-row.drop-before::before,
851
+ .sidebar-row.drop-after::after {
852
+ position: absolute;
853
+ right: 8px;
854
+ left: 8px;
855
+ z-index: 4;
856
+ height: 2px;
857
+ background: var(--vp-c-brand-1);
858
+ border-radius: 999px;
859
+ content: '';
860
+ pointer-events: none;
861
+ }
862
+
863
+ .sidebar-row.drop-before::before {
864
+ top: -2px;
865
+ }
866
+
867
+ .sidebar-row.drop-after::after {
868
+ bottom: -2px;
869
+ }
870
+
871
+ .sidebar-row.drop-inside {
872
+ background: var(--vp-c-brand-soft);
873
+ outline: 1px solid var(--vp-c-brand-1);
874
+ outline-offset: -1px;
875
+ }
876
+
877
+ .row-main {
878
+ min-width: 0;
879
+ flex: 1;
880
+ }
881
+
882
+ .group-title {
883
+ display: flex;
884
+ align-items: center;
885
+ gap: 6px;
886
+ width: 100%;
887
+ padding: 4px 6px;
888
+ font-weight: 600;
889
+ color: var(--vp-c-text-1);
890
+ background: none;
891
+ border: none;
892
+ cursor: pointer;
893
+ text-align: left;
894
+ transition: color 0.25s;
895
+ }
896
+
897
+ .sidebar-row.density-compact .group-title,
898
+ .sidebar-row.density-compact .nav-item {
899
+ min-height: 26px;
900
+ padding: 2px 5px;
901
+ font-size: 13px;
902
+ line-height: 18px;
903
+ }
904
+
905
+ .sidebar-row.density-loose .group-title,
906
+ .sidebar-row.density-loose .nav-item {
907
+ min-height: 36px;
908
+ padding: 6px 8px;
909
+ font-size: 14px;
910
+ line-height: 22px;
911
+ }
912
+
913
+ .group-title:hover {
914
+ color: var(--vp-c-brand-1);
915
+ }
916
+
917
+ .group-title-text {
918
+ overflow: hidden;
919
+ text-overflow: ellipsis;
920
+ white-space: nowrap;
921
+ }
922
+
923
+ .arrow {
924
+ display: inline-flex;
925
+ width: 14px;
926
+ height: 14px;
927
+ flex: 0 0 14px;
928
+ transition: transform 0.25s;
929
+ }
930
+
931
+ .arrow:not(.collapsed) {
932
+ transform: rotate(90deg);
933
+ }
934
+
935
+ .arrow img {
936
+ width: 14px;
937
+ height: 14px;
938
+ }
939
+
940
+ .nav-item {
941
+ display: flex;
942
+ align-items: center;
943
+ min-height: 30px;
944
+ padding: 4px 6px;
945
+ color: var(--vp-c-text-2);
946
+ text-decoration: none;
947
+ border-radius: 4px;
948
+ font-size: 14px;
949
+ line-height: 20px;
950
+ transition: all 0.25s;
951
+ overflow: hidden;
952
+ text-overflow: ellipsis;
953
+ white-space: nowrap;
954
+ }
955
+
956
+ .nav-item:hover {
957
+ color: var(--vp-c-brand-1);
958
+ }
959
+
960
+ .nav-item.active {
961
+ color: var(--vp-c-brand-1);
962
+ font-weight: 600;
963
+ }
964
+
965
+ .row-actions {
966
+ position: relative;
967
+ display: flex;
968
+ align-items: center;
969
+ gap: 0;
970
+ flex: 0 0 auto;
971
+ width: 0;
972
+ padding-right: 0;
973
+ opacity: 0;
974
+ overflow: visible;
975
+ pointer-events: none;
976
+ transition:
977
+ width 0.18s ease,
978
+ padding-right 0.18s ease,
979
+ gap 0.18s ease,
980
+ opacity 0.18s ease;
981
+ }
982
+
983
+ .sidebar-row:hover .row-actions,
984
+ .row-actions:focus-within,
985
+ .row-actions.menu-open {
986
+ gap: 4px;
987
+ width: 52px;
988
+ padding-right: 6px;
989
+ opacity: 1;
990
+ pointer-events: auto;
991
+ }
992
+
993
+ .action-btn {
994
+ display: inline-flex;
995
+ align-items: center;
996
+ justify-content: center;
997
+ width: 22px;
998
+ height: 22px;
999
+ padding: 0;
1000
+ color: var(--vp-c-text-2);
1001
+ background: transparent;
1002
+ border: none;
1003
+ border-radius: 4px;
1004
+ cursor: pointer;
1005
+ font-size: 11px;
1006
+ line-height: 1;
1007
+ }
1008
+
1009
+ .action-btn:hover {
1010
+ color: var(--vp-c-brand-1);
1011
+ background: var(--vp-c-bg-soft);
1012
+ }
1013
+
1014
+ .menu-trigger {
1015
+ font-weight: 600;
1016
+ font-family: var(--vp-font-family-base);
1017
+ }
1018
+
1019
+ .ellipsis {
1020
+ display: inline-block;
1021
+ transform: rotate(90deg);
1022
+ transform-origin: center;
1023
+ letter-spacing: 1px;
1024
+ }
1025
+
1026
+ .add-trigger {
1027
+ font-size: 15px;
1028
+ }
1029
+
1030
+ .action-menu {
1031
+ position: absolute;
1032
+ top: calc(100% + 4px);
1033
+ right: 4px;
1034
+ z-index: 5;
1035
+ display: flex;
1036
+ flex-direction: column;
1037
+ min-width: 128px;
1038
+ padding: 4px;
1039
+ background: var(--vp-c-bg-elv);
1040
+ border: 1px solid var(--vp-c-divider);
1041
+ border-radius: 6px;
1042
+ box-shadow: var(--vp-shadow-2);
1043
+ }
1044
+
1045
+ .menu-item {
1046
+ display: block;
1047
+ width: 100%;
1048
+ padding: 6px 8px;
1049
+ color: var(--vp-c-text-1);
1050
+ background: transparent;
1051
+ border: none;
1052
+ border-radius: 4px;
1053
+ cursor: pointer;
1054
+ font-size: 12px;
1055
+ line-height: 18px;
1056
+ text-align: left;
1057
+ white-space: nowrap;
1058
+ }
1059
+
1060
+ .menu-item:hover {
1061
+ color: var(--vp-c-brand-1);
1062
+ background: var(--vp-c-default-soft);
1063
+ }
1064
+
1065
+ .menu-item.danger:hover {
1066
+ color: var(--vp-c-danger-1, #ed3c50);
1067
+ }
1068
+
1069
+ /* 聚焦高亮动画 */
1070
+ .nav-item.focus-highlight {
1071
+ animation: focusPulse 1s ease-in-out;
1072
+ }
1073
+
1074
+ @keyframes focusPulse {
1075
+ 0%,
1076
+ 100% {
1077
+ background-color: transparent;
1078
+ }
1079
+ 50% {
1080
+ background-color: var(--vp-c-brand-soft);
1081
+ }
1082
+ }
1083
+ </style>