@tnotesjs/core 0.1.28 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +2 -1
  2. package/dist/{chunk-4KG6RXFS.js → chunk-5QN44ES5.js} +2946 -1533
  3. package/dist/{chunk-AGSL3VZE.js → chunk-5Y5IYS6C.js} +7 -0
  4. package/dist/cli/index.js +343 -40
  5. package/dist/index.js +1 -1
  6. package/dist/vitepress/config/index.js +573 -65
  7. package/package.json +4 -1
  8. package/templates/sub-repo/.gitattributes +30 -0
  9. package/templates/sub-repo/.github/workflows/deploy.yml +87 -0
  10. package/templates/sub-repo/.vitepress/config.mts +12 -0
  11. package/templates/sub-repo/.vitepress/env.d.ts +10 -0
  12. package/templates/sub-repo/.vitepress/theme/index.ts +12 -0
  13. package/templates/sub-repo/.vscode/settings.json +330 -0
  14. package/templates/sub-repo/.vscode/tnotes.code-snippets +110 -0
  15. package/templates/sub-repo/public/favicon.ico +0 -0
  16. package/templates/sub-repo/public/logo.png +0 -0
  17. package/templates/sub-repo/tsconfig.json +45 -0
  18. package/vitepress/assets/icons/icon__setting_dark.svg +4 -0
  19. package/vitepress/assets/icons/icon__setting_light.svg +4 -0
  20. package/vitepress/assets/icons/index.ts +2 -2
  21. package/vitepress/client/localSearchIndexBridge.ts +24 -0
  22. package/vitepress/components/Discussions/Discussions.vue +1 -1
  23. package/vitepress/components/Footprints/Footprints.vue +90 -25
  24. package/vitepress/components/Layout/CustomSidebar.vue +489 -117
  25. package/vitepress/components/Layout/Layout.vue +5 -0
  26. package/vitepress/components/Layout/NavBarSettingsTrigger.vue +10 -2
  27. package/vitepress/components/Layout/SidebarItems.vue +517 -342
  28. package/vitepress/components/Layout/SidebarNavBefore.vue +1 -50
  29. package/vitepress/components/Layout/SidebarResizeHandle.vue +6 -23
  30. package/vitepress/components/Layout/composables/docLayoutLogic.ts +59 -0
  31. package/vitepress/components/Layout/composables/useDocLayout.test.ts +69 -0
  32. package/vitepress/components/Layout/composables/useDocLayout.ts +128 -0
  33. package/vitepress/components/Layout/composables/useSidebarDrag.ts +514 -0
  34. package/vitepress/components/Layout/composables/useSidebarLayout.ts +25 -8
  35. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +8 -3
  36. package/vitepress/components/Layout/sidebarCollapsedState.test.ts +183 -0
  37. package/vitepress/components/Layout/sidebarCollapsedState.ts +241 -0
  38. package/vitepress/components/Layout/sidebarDragLogic.test.ts +1146 -0
  39. package/vitepress/components/Layout/sidebarDragLogic.ts +1307 -0
  40. package/vitepress/components/Layout/sidebarHitTest.test.ts +572 -0
  41. package/vitepress/components/Layout/sidebarHitTest.ts +801 -0
  42. package/vitepress/components/Layout/sidebarItemRenderLogic.test.ts +53 -0
  43. package/vitepress/components/Layout/sidebarItemRenderLogic.ts +50 -0
  44. package/vitepress/components/MarkMap/MarkMap.vue +67 -10
  45. package/vitepress/components/Settings/Settings.vue +78 -74
  46. package/vitepress/components/SidebarCard/FolderTreeItems.vue +268 -0
  47. package/vitepress/components/SidebarCard/SidebarCard.vue +125 -274
  48. package/vitepress/components/constants.ts +18 -6
  49. package/vitepress/components/sidebar.data.ts +13 -11
  50. package/vitepress/components/sidebarTreeHelpers.test.ts +142 -0
  51. package/vitepress/components/sidebarTreeHelpers.ts +168 -0
  52. package/vitepress/components/utils/vscodePaths.test.ts +41 -0
  53. package/vitepress/components/utils/vscodePaths.ts +52 -0
  54. package/vitepress/config/index.ts +2 -0
  55. package/vitepress/plugins/localSearchIndexBuilder.ts +209 -0
  56. package/vitepress/plugins/localSearchReindex.test.ts +46 -0
  57. package/vitepress/plugins/localSearchReindexLogic.ts +50 -0
  58. package/vitepress/plugins/localSearchReindexPatch.test.ts +30 -0
  59. package/vitepress/plugins/localSearchReindexPatch.ts +37 -0
  60. package/vitepress/plugins/localSearchReindexPlugin.ts +318 -0
  61. package/vitepress/plugins/sidebarStructurePlugin.ts +209 -63
  62. package/vitepress/theme/index.ts +8 -0
  63. package/vitepress/theme/styles/base.scss +0 -8
  64. package/vitepress/theme/styles/components/markmap.scss +23 -2
  65. package/vitepress/theme/styles/components/swiper.scss +1 -1
  66. package/vitepress/theme/styles/doc-layout.scss +212 -0
  67. package/vitepress/theme/styles/index.scss +3 -0
  68. package/vitepress/theme/styles/layout.scss +60 -10
  69. package/vitepress/assets/icons/icon__mindmap.svg +0 -1
  70. package/vitepress/assets/icons/icon__setting.svg +0 -1
  71. package/vitepress/components/SidebarCard/MindMapView.vue +0 -488
@@ -0,0 +1,183 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import {
4
+ applySidebarCollapsedState,
5
+ buildPostDropCollapsePatch,
6
+ collapseKeyForLine,
7
+ collapseKeyForNote,
8
+ collectSidebarCollapsedState,
9
+ mergeCollapseStore,
10
+ normalizeCollapseStore,
11
+ shouldSuppressActiveItemScroll,
12
+ suppressActiveItemScroll,
13
+ } from './sidebarCollapsedState'
14
+
15
+ describe('sidebarCollapsedState', () => {
16
+ const tree = [
17
+ {
18
+ link: '/notes/0037. Writing/README',
19
+ collapsed: false,
20
+ tocLineIndex: 10,
21
+ items: [
22
+ {
23
+ link: '/notes/0038. Mode/README',
24
+ collapsed: true,
25
+ items: [],
26
+ },
27
+ ],
28
+ },
29
+ {
30
+ link: '/notes/0019. Author/README',
31
+ collapsed: true,
32
+ tocLineIndex: 20,
33
+ items: [
34
+ {
35
+ link: '/notes/0036. Workflow/README',
36
+ collapsed: true,
37
+ items: [],
38
+ },
39
+ ],
40
+ },
41
+ {
42
+ text: 'Giscus',
43
+ collapsed: false,
44
+ tocLineIndex: 34,
45
+ items: [
46
+ {
47
+ link: '/notes/0003. Giscus/README',
48
+ collapsed: true,
49
+ items: [],
50
+ },
51
+ ],
52
+ },
53
+ ]
54
+
55
+ it('collectSidebarCollapsedState records note and line keys', () => {
56
+ expect(collectSidebarCollapsedState(tree)).toEqual({
57
+ [collapseKeyForNote('0037')]: false,
58
+ [collapseKeyForNote('0019')]: true,
59
+ [collapseKeyForLine(34)]: false,
60
+ })
61
+ })
62
+
63
+ it('applySidebarCollapsedState restores cached expansion', () => {
64
+ const restored = applySidebarCollapsedState(
65
+ tree.map((item) => ({ ...item, collapsed: true })),
66
+ { [collapseKeyForNote('0037')]: false },
67
+ )
68
+
69
+ expect(restored[0].collapsed).toBe(false)
70
+ expect(restored[1].collapsed).toBe(true)
71
+ expect(restored[2].collapsed).toBe(true)
72
+ })
73
+
74
+ it('applySidebarCollapsedState restores pure folder by line key', () => {
75
+ const restored = applySidebarCollapsedState(
76
+ [{ ...tree[2], collapsed: true }],
77
+ { [collapseKeyForLine(34)]: false },
78
+ )
79
+ expect(restored[0].collapsed).toBe(false)
80
+ })
81
+
82
+ it('normalizeCollapseStore migrates v1 note keys', () => {
83
+ expect(normalizeCollapseStore({ '0019': false, '0037': true })).toEqual({
84
+ [collapseKeyForNote('0019')]: false,
85
+ [collapseKeyForNote('0037')]: true,
86
+ })
87
+ })
88
+
89
+ it('normalizeCollapseStore reads v2 entries', () => {
90
+ expect(
91
+ normalizeCollapseStore({
92
+ v: 2,
93
+ entries: { [collapseKeyForLine(34)]: false },
94
+ }),
95
+ ).toEqual({
96
+ [collapseKeyForLine(34)]: false,
97
+ })
98
+ })
99
+
100
+ it('mergeCollapseStore applies patch', () => {
101
+ expect(
102
+ mergeCollapseStore(
103
+ { [collapseKeyForNote('0019')]: true },
104
+ { [collapseKeyForNote('0019')]: false },
105
+ ),
106
+ ).toEqual({ [collapseKeyForNote('0019')]: false })
107
+ })
108
+
109
+ it('buildPostDropCollapsePatch returns empty for moveAfter', () => {
110
+ expect(
111
+ buildPostDropCollapsePatch(
112
+ { action: 'moveAfter', target_uuid: 'note:0039' },
113
+ { sourceCollapseKey: null, wasExpanded: false },
114
+ ),
115
+ ).toEqual({})
116
+ })
117
+
118
+ it('buildPostDropCollapsePatch expands prependChild target folder', () => {
119
+ expect(
120
+ buildPostDropCollapsePatch(
121
+ { action: 'prependChild', target_uuid: 'line:41' },
122
+ { sourceCollapseKey: null, wasExpanded: false },
123
+ ),
124
+ ).toEqual({ [collapseKeyForLine(41)]: false })
125
+ })
126
+
127
+ it('buildPostDropCollapsePatch expands prependChild target note', () => {
128
+ expect(
129
+ buildPostDropCollapsePatch(
130
+ { action: 'prependChild', target_uuid: 'note:0019' },
131
+ { sourceCollapseKey: null, wasExpanded: false },
132
+ ),
133
+ ).toEqual({ [collapseKeyForNote('0019')]: false })
134
+ })
135
+
136
+ it('buildPostDropCollapsePatch expands folder target via targetCollapseKey', () => {
137
+ expect(
138
+ buildPostDropCollapsePatch(
139
+ {
140
+ action: 'prependChild',
141
+ target_uuid: 'folder:TNotes 笔记书写规范',
142
+ },
143
+ {
144
+ sourceCollapseKey: null,
145
+ wasExpanded: false,
146
+ targetCollapseKey: collapseKeyForLine(41),
147
+ },
148
+ ),
149
+ ).toEqual({ [collapseKeyForLine(41)]: false })
150
+ })
151
+
152
+ it('buildPostDropCollapsePatch preserves expanded source folder', () => {
153
+ expect(
154
+ buildPostDropCollapsePatch(
155
+ { action: 'moveAfter', target_uuid: 'note:0039' },
156
+ {
157
+ sourceCollapseKey: collapseKeyForNote('0019'),
158
+ wasExpanded: true,
159
+ },
160
+ ),
161
+ ).toEqual({ [collapseKeyForNote('0019')]: false })
162
+ })
163
+
164
+ it('shouldSuppressActiveItemScroll respects TTL', () => {
165
+ const key = 'test-suppress-active-scroll'
166
+ const storage = new Map<string, string>()
167
+
168
+ const sessionStorage = {
169
+ getItem: (name: string) => storage.get(name) ?? null,
170
+ setItem: (name: string, value: string) => {
171
+ storage.set(name, value)
172
+ },
173
+ }
174
+
175
+ vi.stubGlobal('window', { sessionStorage })
176
+
177
+ suppressActiveItemScroll(key, 2000, 1000)
178
+ expect(shouldSuppressActiveItemScroll(key, 1500)).toBe(true)
179
+ expect(shouldSuppressActiveItemScroll(key, 3500)).toBe(false)
180
+
181
+ vi.unstubAllGlobals()
182
+ })
183
+ })
@@ -0,0 +1,241 @@
1
+ /**
2
+ * 侧边栏文件夹展开/折叠状态(localStorage)
3
+ */
4
+
5
+ import { extractNoteIndexFromLink } from './sidebarDragLogic'
6
+
7
+ export interface SidebarCollapseNode {
8
+ link?: string
9
+ collapsed?: boolean
10
+ tocLineIndex?: number
11
+ items?: SidebarCollapseNode[]
12
+ }
13
+
14
+ export type CollapseStoreV2 = {
15
+ v: 2
16
+ entries: Record<string, boolean>
17
+ }
18
+
19
+ export function collapseKeyForNote(noteIndex: string): string {
20
+ return `note:${noteIndex}`
21
+ }
22
+
23
+ export function collapseKeyForLine(tocLineIndex: number): string {
24
+ return `line:${tocLineIndex}`
25
+ }
26
+
27
+ function getCollapseKey(item: SidebarCollapseNode): string | null {
28
+ if (!item.items?.length) return null
29
+
30
+ const noteIndex = extractNoteIndexFromLink(item.link)
31
+ if (noteIndex) return collapseKeyForNote(noteIndex)
32
+
33
+ if (item.tocLineIndex !== undefined) {
34
+ return collapseKeyForLine(item.tocLineIndex)
35
+ }
36
+
37
+ return null
38
+ }
39
+
40
+ export function collectSidebarCollapsedState(
41
+ items: SidebarCollapseNode[],
42
+ result: Record<string, boolean> = {},
43
+ ): Record<string, boolean> {
44
+ for (const item of items) {
45
+ const key = getCollapseKey(item)
46
+ if (key) {
47
+ result[key] = !!item.collapsed
48
+ }
49
+ if (item.items?.length) {
50
+ collectSidebarCollapsedState(item.items, result)
51
+ }
52
+ }
53
+ return result
54
+ }
55
+
56
+ export function normalizeCollapseStore(
57
+ raw: unknown,
58
+ ): Record<string, boolean> {
59
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
60
+ return {}
61
+ }
62
+
63
+ const obj = raw as Record<string, unknown>
64
+
65
+ if (obj.v === 2 && obj.entries && typeof obj.entries === 'object') {
66
+ const entries = obj.entries as Record<string, unknown>
67
+ const result: Record<string, boolean> = {}
68
+ for (const [key, value] of Object.entries(entries)) {
69
+ if (/^(note:\d{4}|line:\d+)$/.test(key)) {
70
+ result[key] = !!value
71
+ }
72
+ }
73
+ return result
74
+ }
75
+
76
+ const result: Record<string, boolean> = {}
77
+ for (const [key, value] of Object.entries(obj)) {
78
+ if (/^\d{4}$/.test(key)) {
79
+ result[collapseKeyForNote(key)] = !!value
80
+ }
81
+ }
82
+ return result
83
+ }
84
+
85
+ export function applySidebarCollapsedState<T extends SidebarCollapseNode>(
86
+ items: T[],
87
+ state: Record<string, boolean>,
88
+ ): T[] {
89
+ return items.map((item) => {
90
+ const key = getCollapseKey(item)
91
+ const next = { ...item }
92
+
93
+ if (key && key in state) {
94
+ next.collapsed = state[key]
95
+ }
96
+
97
+ if (item.items?.length) {
98
+ next.items = applySidebarCollapsedState(item.items, state)
99
+ }
100
+
101
+ return next
102
+ })
103
+ }
104
+
105
+ export function loadSidebarCollapsedState(
106
+ storageKey: string,
107
+ ): Record<string, boolean> {
108
+ if (typeof window === 'undefined') return {}
109
+
110
+ try {
111
+ const raw = window.localStorage.getItem(storageKey)
112
+ if (!raw) return {}
113
+
114
+ return normalizeCollapseStore(JSON.parse(raw) as unknown)
115
+ } catch {
116
+ return {}
117
+ }
118
+ }
119
+
120
+ export function saveSidebarCollapsedState(
121
+ storageKey: string,
122
+ items: SidebarCollapseNode[],
123
+ ): void {
124
+ if (typeof window === 'undefined') return
125
+
126
+ try {
127
+ const payload: CollapseStoreV2 = {
128
+ v: 2,
129
+ entries: collectSidebarCollapsedState(items),
130
+ }
131
+ window.localStorage.setItem(storageKey, JSON.stringify(payload))
132
+ } catch {
133
+ // ignore quota / privacy mode
134
+ }
135
+ }
136
+
137
+ export function mergeCollapseStore(
138
+ base: Record<string, boolean>,
139
+ patch: Record<string, boolean>,
140
+ ): Record<string, boolean> {
141
+ return { ...base, ...patch }
142
+ }
143
+
144
+ export function saveCollapseEntries(
145
+ storageKey: string,
146
+ entries: Record<string, boolean>,
147
+ ): void {
148
+ if (typeof window === 'undefined') return
149
+
150
+ try {
151
+ const payload: CollapseStoreV2 = { v: 2, entries }
152
+ window.localStorage.setItem(storageKey, JSON.stringify(payload))
153
+ } catch {
154
+ // ignore quota / privacy mode
155
+ }
156
+ }
157
+
158
+ export function getCollapseKeyForTreeItem(
159
+ item: SidebarCollapseNode,
160
+ ): string | null {
161
+ return getCollapseKey(item)
162
+ }
163
+
164
+ export interface PostDropCollapseInput {
165
+ action: 'moveAfter' | 'prependChild'
166
+ target_uuid?: string
167
+ }
168
+
169
+ export interface PostDropCollapseMeta {
170
+ sourceCollapseKey: string | null
171
+ wasExpanded: boolean
172
+ /** prependChild 目标为 folder:path 等时由调用方解析 */
173
+ targetCollapseKey?: string | null
174
+ }
175
+
176
+ export function buildPostDropCollapsePatch(
177
+ payload: PostDropCollapseInput,
178
+ meta: PostDropCollapseMeta,
179
+ ): Record<string, boolean> {
180
+ const patch: Record<string, boolean> = {}
181
+
182
+ if (payload.action === 'prependChild' && payload.target_uuid) {
183
+ if (payload.target_uuid.startsWith('note:')) {
184
+ patch[collapseKeyForNote(payload.target_uuid.slice(5))] = false
185
+ } else if (payload.target_uuid.startsWith('line:')) {
186
+ patch[collapseKeyForLine(Number(payload.target_uuid.slice(5)))] = false
187
+ } else if (
188
+ payload.target_uuid.startsWith('folder:') &&
189
+ meta.targetCollapseKey
190
+ ) {
191
+ patch[meta.targetCollapseKey] = false
192
+ }
193
+ }
194
+
195
+ if (
196
+ payload.action === 'prependChild' &&
197
+ meta.targetCollapseKey &&
198
+ !(meta.targetCollapseKey in patch)
199
+ ) {
200
+ patch[meta.targetCollapseKey] = false
201
+ }
202
+
203
+ if (meta.wasExpanded && meta.sourceCollapseKey) {
204
+ patch[meta.sourceCollapseKey] = false
205
+ }
206
+
207
+ return patch
208
+ }
209
+
210
+ const DEFAULT_SUPPRESS_ACTIVE_SCROLL_MS = 2000
211
+
212
+ export function shouldSuppressActiveItemScroll(
213
+ storageKey: string,
214
+ now = Date.now(),
215
+ ): boolean {
216
+ if (typeof window === 'undefined') return false
217
+
218
+ try {
219
+ const until = Number(window.sessionStorage.getItem(storageKey) || 0)
220
+ return now < until
221
+ } catch {
222
+ return false
223
+ }
224
+ }
225
+
226
+ export function suppressActiveItemScroll(
227
+ storageKey: string,
228
+ durationMs = DEFAULT_SUPPRESS_ACTIVE_SCROLL_MS,
229
+ now = Date.now(),
230
+ ): void {
231
+ if (typeof window === 'undefined') return
232
+
233
+ try {
234
+ window.sessionStorage.setItem(
235
+ storageKey,
236
+ String(now + durationMs),
237
+ )
238
+ } catch {
239
+ // ignore quota / privacy mode
240
+ }
241
+ }