@tnotesjs/core 0.1.27 → 0.2.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 (71) hide show
  1. package/README.md +2 -1
  2. package/package.json +3 -1
  3. package/templates/sub-repo/.gitattributes +30 -0
  4. package/templates/sub-repo/.github/workflows/deploy.yml +87 -0
  5. package/templates/sub-repo/.vitepress/config.mts +12 -0
  6. package/templates/sub-repo/.vitepress/env.d.ts +10 -0
  7. package/templates/sub-repo/.vitepress/theme/index.ts +12 -0
  8. package/templates/sub-repo/.vscode/settings.json +330 -0
  9. package/templates/sub-repo/.vscode/tnotes.code-snippets +110 -0
  10. package/templates/sub-repo/public/favicon.ico +0 -0
  11. package/templates/sub-repo/public/logo.png +0 -0
  12. package/templates/sub-repo/tsconfig.json +45 -0
  13. package/vitepress/assets/icons/icon__setting_dark.svg +4 -0
  14. package/vitepress/assets/icons/icon__setting_light.svg +4 -0
  15. package/vitepress/assets/icons/index.ts +2 -2
  16. package/vitepress/client/localSearchIndexBridge.ts +24 -0
  17. package/vitepress/components/Discussions/Discussions.vue +1 -1
  18. package/vitepress/components/Footprints/Footprints.vue +90 -25
  19. package/vitepress/components/Layout/CustomSidebar.vue +489 -117
  20. package/vitepress/components/Layout/Layout.vue +5 -0
  21. package/vitepress/components/Layout/NavBarSettingsTrigger.vue +10 -2
  22. package/vitepress/components/Layout/SidebarItems.vue +517 -342
  23. package/vitepress/components/Layout/SidebarNavBefore.vue +1 -50
  24. package/vitepress/components/Layout/SidebarResizeHandle.vue +6 -23
  25. package/vitepress/components/Layout/composables/docLayoutLogic.ts +59 -0
  26. package/vitepress/components/Layout/composables/useDocLayout.test.ts +69 -0
  27. package/vitepress/components/Layout/composables/useDocLayout.ts +128 -0
  28. package/vitepress/components/Layout/composables/useSidebarDrag.ts +514 -0
  29. package/vitepress/components/Layout/composables/useSidebarLayout.ts +25 -8
  30. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +8 -3
  31. package/vitepress/components/Layout/sidebarCollapsedState.test.ts +183 -0
  32. package/vitepress/components/Layout/sidebarCollapsedState.ts +241 -0
  33. package/vitepress/components/Layout/sidebarDragLogic.test.ts +1146 -0
  34. package/vitepress/components/Layout/sidebarDragLogic.ts +1307 -0
  35. package/vitepress/components/Layout/sidebarHitTest.test.ts +572 -0
  36. package/vitepress/components/Layout/sidebarHitTest.ts +801 -0
  37. package/vitepress/components/Layout/sidebarItemRenderLogic.test.ts +53 -0
  38. package/vitepress/components/Layout/sidebarItemRenderLogic.ts +50 -0
  39. package/vitepress/components/MarkMap/MarkMap.vue +67 -10
  40. package/vitepress/components/Settings/Settings.vue +78 -74
  41. package/vitepress/components/SidebarCard/FolderTreeItems.vue +268 -0
  42. package/vitepress/components/SidebarCard/SidebarCard.vue +125 -274
  43. package/vitepress/components/constants.ts +18 -6
  44. package/vitepress/components/sidebar.data.ts +13 -11
  45. package/vitepress/components/sidebarTreeHelpers.test.ts +142 -0
  46. package/vitepress/components/sidebarTreeHelpers.ts +168 -0
  47. package/vitepress/components/utils/vscodePaths.test.ts +41 -0
  48. package/vitepress/components/utils/vscodePaths.ts +52 -0
  49. package/vitepress/config/index.ts +2 -0
  50. package/vitepress/plugins/localSearchIndexBuilder.ts +209 -0
  51. package/vitepress/plugins/localSearchReindex.test.ts +46 -0
  52. package/vitepress/plugins/localSearchReindexLogic.ts +50 -0
  53. package/vitepress/plugins/localSearchReindexPatch.test.ts +30 -0
  54. package/vitepress/plugins/localSearchReindexPatch.ts +37 -0
  55. package/vitepress/plugins/localSearchReindexPlugin.ts +318 -0
  56. package/vitepress/plugins/sidebarStructurePlugin.ts +209 -63
  57. package/vitepress/theme/index.ts +8 -0
  58. package/vitepress/theme/styles/base.scss +0 -8
  59. package/vitepress/theme/styles/components/markmap.scss +23 -2
  60. package/vitepress/theme/styles/components/swiper.scss +1 -1
  61. package/vitepress/theme/styles/doc-layout.scss +212 -0
  62. package/vitepress/theme/styles/index.scss +3 -0
  63. package/vitepress/theme/styles/layout.scss +60 -10
  64. package/dist/chunk-4KG6RXFS.js +0 -4637
  65. package/dist/chunk-AGSL3VZE.js +0 -193
  66. package/dist/cli/index.js +0 -683
  67. package/dist/index.js +0 -8
  68. package/dist/vitepress/config/index.js +0 -1178
  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,572 @@
1
+ /**
2
+ * vitepress/components/Layout/sidebarHitTest.test.ts
3
+ */
4
+
5
+ import { describe, expect, it } from 'vitest'
6
+
7
+ import {
8
+ buildFlatForHitTest,
9
+ createHitTestState,
10
+ dropIntentFingerprint,
11
+ resolveDropIntent,
12
+ type RowMetric,
13
+ } from './sidebarHitTest'
14
+
15
+ import type { SidebarTreeItem } from './sidebarDragLogic'
16
+
17
+ function note(
18
+ index: string,
19
+ title: string,
20
+ children: SidebarTreeItem[] = [],
21
+ collapsed = false,
22
+ tocLineIndex = 0,
23
+ ): SidebarTreeItem {
24
+ const nodeId = `note:${index}`
25
+ return {
26
+ text: `${index}. ${title}`,
27
+ link: `/notes/${index}. ${title}/README`,
28
+ nodeId,
29
+ items: children.length ? children : undefined,
30
+ collapsed,
31
+ tocLineIndex,
32
+ }
33
+ }
34
+
35
+ function mockRect(top: number, height = 32): DOMRect {
36
+ return {
37
+ top,
38
+ bottom: top + height,
39
+ height,
40
+ left: 0,
41
+ right: 200,
42
+ width: 200,
43
+ x: 0,
44
+ y: top,
45
+ toJSON: () => ({}),
46
+ } as DOMRect
47
+ }
48
+
49
+ describe('sidebarHitTest', () => {
50
+ it('resolves moveAfter when hovering lower half of sibling', () => {
51
+ const tree: SidebarTreeItem[] = [
52
+ note('0034', 'A', [], false, 0),
53
+ note('0035', 'B', [], false, 1),
54
+ ]
55
+ const flat = buildFlatForHitTest(tree, 0)
56
+ const rowMetrics: RowMetric[] = [
57
+ { nodeId: 'note:0034', tocLineIndex: 0, rect: mockRect(100) },
58
+ { nodeId: 'note:0035', tocLineIndex: 1, rect: mockRect(132) },
59
+ ]
60
+ const state = createHitTestState()
61
+
62
+ const intent = resolveDropIntent(
63
+ {
64
+ tree,
65
+ flat,
66
+ dragSource: { kind: 'note', tocLineIndex: 0, noteIndex: '0034' },
67
+ sourceNodeId: 'note:0034',
68
+ clientX: 120,
69
+ clientY: 150,
70
+ navLeft: 0,
71
+ navTop: 0,
72
+ navWidth: 240,
73
+ rowMetrics,
74
+ maxDepth: 0,
75
+ },
76
+ state,
77
+ )
78
+
79
+ expect(intent?.action).toBe('moveAfter')
80
+ expect(intent?.targetNodeId).toBe('note:0035')
81
+ })
82
+
83
+ it('resolves root prependChild when hovering upper half of first root row', () => {
84
+ function folder(
85
+ title: string,
86
+ children: SidebarTreeItem[] = [],
87
+ collapsed = true,
88
+ tocLineIndex = 0,
89
+ ): SidebarTreeItem {
90
+ return {
91
+ text: title,
92
+ folderPath: [title],
93
+ nodeId: `folder:${title}`,
94
+ tocLineIndex,
95
+ items: children.length ? children : undefined,
96
+ collapsed,
97
+ }
98
+ }
99
+
100
+ const tree: SidebarTreeItem[] = [
101
+ folder('TNotes.introduction', [note('0015', 'Intro', [], false, 1)], true, 0),
102
+ folder('TNotes 组件', [note('0006', 'Comp', [], false, 3)], true, 2),
103
+ note('0039', 'new', [], false, 4),
104
+ ]
105
+ const flat = buildFlatForHitTest(tree, 4)
106
+ const rowMetrics: RowMetric[] = [
107
+ {
108
+ nodeId: 'folder:TNotes.introduction',
109
+ tocLineIndex: 0,
110
+ rect: mockRect(100),
111
+ },
112
+ { nodeId: 'folder:TNotes 组件', tocLineIndex: 2, rect: mockRect(132) },
113
+ { nodeId: 'note:0039', tocLineIndex: 4, rect: mockRect(164) },
114
+ ]
115
+ const state = createHitTestState()
116
+
117
+ const intent = resolveDropIntent(
118
+ {
119
+ tree,
120
+ flat,
121
+ dragSource: { kind: 'note', tocLineIndex: 4, noteIndex: '0039' },
122
+ sourceNodeId: 'note:0039',
123
+ clientX: 120,
124
+ clientY: 108,
125
+ navLeft: 0,
126
+ navTop: 0,
127
+ navWidth: 240,
128
+ rowMetrics,
129
+ maxDepth: 0,
130
+ },
131
+ state,
132
+ )
133
+
134
+ expect(intent?.action).toBe('prependChild')
135
+ expect(intent?.isRootPrepend).toBe(true)
136
+ expect(intent?.targetNodeId).toBeNull()
137
+ expect(intent?.indicator.mode).toBe('root-prepend')
138
+ })
139
+
140
+ it('uses line after folder when hovering upper half of next sibling', () => {
141
+ function folder(
142
+ title: string,
143
+ children: SidebarTreeItem[] = [],
144
+ collapsed = true,
145
+ tocLineIndex = 0,
146
+ ): SidebarTreeItem {
147
+ return {
148
+ text: title,
149
+ folderPath: [title],
150
+ nodeId: `folder:${title}`,
151
+ tocLineIndex,
152
+ items: children.length ? children : undefined,
153
+ collapsed,
154
+ }
155
+ }
156
+
157
+ const tree: SidebarTreeItem[] = [
158
+ folder('TNotes 组件', [note('0006', 'Comp', [], false, 1)], true, 0),
159
+ folder('TNotes 笔记书写规范', [], true, 2),
160
+ note('0039', 'new', [], false, 3),
161
+ ]
162
+ const flat = buildFlatForHitTest(tree, 3)
163
+ const rowMetrics: RowMetric[] = [
164
+ { nodeId: 'folder:TNotes 组件', tocLineIndex: 0, rect: mockRect(100) },
165
+ {
166
+ nodeId: 'folder:TNotes 笔记书写规范',
167
+ tocLineIndex: 2,
168
+ rect: mockRect(132),
169
+ },
170
+ { nodeId: 'note:0039', tocLineIndex: 3, rect: mockRect(164) },
171
+ ]
172
+ const state = createHitTestState()
173
+
174
+ const intent = resolveDropIntent(
175
+ {
176
+ tree,
177
+ flat,
178
+ dragSource: { kind: 'note', tocLineIndex: 3, noteIndex: '0039' },
179
+ sourceNodeId: 'note:0039',
180
+ clientX: 120,
181
+ clientY: 140,
182
+ navLeft: 0,
183
+ navTop: 0,
184
+ navWidth: 240,
185
+ rowMetrics,
186
+ maxDepth: 0,
187
+ },
188
+ state,
189
+ )
190
+
191
+ expect(intent?.action).toBe('moveAfter')
192
+ expect(intent?.targetNodeId).toBe('folder:TNotes 组件')
193
+ expect(intent?.linePlacement).toBe('after')
194
+ })
195
+
196
+ it('resolves prependChild when hovering collapsed folder row without visible children', () => {
197
+ function folder(
198
+ title: string,
199
+ children: SidebarTreeItem[] = [],
200
+ collapsed = true,
201
+ tocLineIndex = 0,
202
+ ): SidebarTreeItem {
203
+ return {
204
+ text: title,
205
+ folderPath: [title],
206
+ tocLineIndex,
207
+ items: children.length ? children : undefined,
208
+ collapsed,
209
+ }
210
+ }
211
+
212
+ const tree: SidebarTreeItem[] = [
213
+ folder(
214
+ 'TNotes 笔记书写规范',
215
+ [note('0027', 'emoji', [], false, 42), note('0037', 'Writing', [], false, 43)],
216
+ true,
217
+ 41,
218
+ ),
219
+ note('0039', 'new', [], false, 45),
220
+ ]
221
+ const flat = buildFlatForHitTest(tree, 45)
222
+ const rowMetrics: RowMetric[] = [
223
+ {
224
+ nodeId: 'line:41',
225
+ tocLineIndex: 41,
226
+ rect: mockRect(132),
227
+ },
228
+ { nodeId: 'note:0039', tocLineIndex: 45, rect: mockRect(164) },
229
+ ]
230
+ const state = createHitTestState()
231
+
232
+ const intent = resolveDropIntent(
233
+ {
234
+ tree,
235
+ flat,
236
+ dragSource: { kind: 'note', tocLineIndex: 45, noteIndex: '0039' },
237
+ sourceNodeId: 'note:0039',
238
+ clientX: 120,
239
+ clientY: 148,
240
+ navLeft: 0,
241
+ navTop: 0,
242
+ navWidth: 240,
243
+ rowMetrics,
244
+ maxDepth: 0,
245
+ },
246
+ state,
247
+ )
248
+
249
+ expect(intent?.action).toBe('prependChild')
250
+ expect(intent?.targetNodeId).toBe('line:41')
251
+ expect(intent?.indicator.mode).toBe('inside')
252
+ })
253
+
254
+ it('resolves prependChild when folder hovers external note row body', () => {
255
+ function folder(
256
+ title: string,
257
+ children: SidebarTreeItem[] = [],
258
+ collapsed = true,
259
+ tocLineIndex = 0,
260
+ ): SidebarTreeItem {
261
+ return {
262
+ text: title,
263
+ folderPath: [title],
264
+ tocLineIndex,
265
+ items: children.length ? children : undefined,
266
+ collapsed,
267
+ }
268
+ }
269
+
270
+ const tree: SidebarTreeItem[] = [
271
+ folder(
272
+ 'TNotes 笔记书写规范',
273
+ [note('0027', 'emoji', [], false, 42)],
274
+ true,
275
+ 41,
276
+ ),
277
+ note('0039', 'new', [], false, 45),
278
+ ]
279
+ const flat = buildFlatForHitTest(tree, 45)
280
+ const rowMetrics: RowMetric[] = [
281
+ {
282
+ nodeId: 'line:41',
283
+ tocLineIndex: 41,
284
+ rect: mockRect(132),
285
+ },
286
+ { nodeId: 'note:0039', tocLineIndex: 45, rect: mockRect(164) },
287
+ ]
288
+ const state = createHitTestState()
289
+
290
+ const intent = resolveDropIntent(
291
+ {
292
+ tree,
293
+ flat,
294
+ dragSource: { kind: 'folder', tocLineIndex: 41 },
295
+ sourceNodeId: 'line:41',
296
+ clientX: 150,
297
+ clientY: 180,
298
+ navLeft: 0,
299
+ navTop: 0,
300
+ navWidth: 240,
301
+ rowMetrics,
302
+ maxDepth: 0,
303
+ },
304
+ state,
305
+ )
306
+
307
+ expect(intent?.action).toBe('prependChild')
308
+ expect(intent?.targetNodeId).toBe('note:0039')
309
+ expect(intent?.indicator.mode).toBe('inside')
310
+ })
311
+
312
+ it('resolves prependChild when note hovers external note row body', () => {
313
+ const tree: SidebarTreeItem[] = [
314
+ note('0027', 'emoji', [], false, 44),
315
+ note('0039', 'new', [], false, 45),
316
+ ]
317
+ const flat = buildFlatForHitTest(tree, 45)
318
+ const rowMetrics: RowMetric[] = [
319
+ { nodeId: 'note:0027', tocLineIndex: 44, rect: mockRect(132) },
320
+ { nodeId: 'note:0039', tocLineIndex: 45, rect: mockRect(164) },
321
+ ]
322
+ const state = createHitTestState()
323
+
324
+ const intent = resolveDropIntent(
325
+ {
326
+ tree,
327
+ flat,
328
+ dragSource: { kind: 'note', tocLineIndex: 45, noteIndex: '0039' },
329
+ sourceNodeId: 'note:0039',
330
+ clientX: 150,
331
+ clientY: 148,
332
+ navLeft: 0,
333
+ navTop: 0,
334
+ navWidth: 240,
335
+ rowMetrics,
336
+ maxDepth: 0,
337
+ },
338
+ state,
339
+ )
340
+
341
+ expect(intent?.action).toBe('prependChild')
342
+ expect(intent?.targetNodeId).toBe('note:0027')
343
+ expect(intent?.indicator.mode).toBe('inside')
344
+ })
345
+
346
+ it('resolves prependChild when note above hovers upper half of next note row', () => {
347
+ const tree: SidebarTreeItem[] = [
348
+ note('0023', 'git log', [], false, 43),
349
+ note('0027', 'emoji', [], false, 44),
350
+ ]
351
+ const flat = buildFlatForHitTest(tree, 43)
352
+ const rowMetrics: RowMetric[] = [
353
+ { nodeId: 'note:0023', tocLineIndex: 43, rect: mockRect(132) },
354
+ { nodeId: 'note:0027', tocLineIndex: 44, rect: mockRect(164) },
355
+ ]
356
+ const state = createHitTestState()
357
+
358
+ const intent = resolveDropIntent(
359
+ {
360
+ tree,
361
+ flat,
362
+ dragSource: { kind: 'note', tocLineIndex: 43, noteIndex: '0023' },
363
+ sourceNodeId: 'note:0023',
364
+ clientX: 150,
365
+ clientY: 172,
366
+ navLeft: 0,
367
+ navTop: 0,
368
+ navWidth: 240,
369
+ rowMetrics,
370
+ maxDepth: 0,
371
+ },
372
+ state,
373
+ )
374
+
375
+ expect(intent?.action).toBe('prependChild')
376
+ expect(intent?.targetNodeId).toBe('note:0027')
377
+ expect(intent?.indicator.mode).toBe('inside')
378
+ })
379
+
380
+ it('resolves prependChild when hovering empty collapsed folder row', () => {
381
+ const tree: SidebarTreeItem[] = [
382
+ {
383
+ text: '评论功能的技术实现(Giscus)',
384
+ folderPath: ['评论功能的技术实现(Giscus)'],
385
+ items: [],
386
+ collapsed: true,
387
+ tocLineIndex: 20,
388
+ },
389
+ note('0039', 'new', [], false, 45),
390
+ ]
391
+ const flat = buildFlatForHitTest(tree, 45)
392
+ const rowMetrics: RowMetric[] = [
393
+ {
394
+ nodeId: 'line:20',
395
+ tocLineIndex: 20,
396
+ rect: mockRect(132),
397
+ },
398
+ { nodeId: 'note:0039', tocLineIndex: 45, rect: mockRect(164) },
399
+ ]
400
+ const state = createHitTestState()
401
+
402
+ const intent = resolveDropIntent(
403
+ {
404
+ tree,
405
+ flat,
406
+ dragSource: { kind: 'note', tocLineIndex: 45, noteIndex: '0039' },
407
+ sourceNodeId: 'note:0039',
408
+ clientX: 120,
409
+ clientY: 148,
410
+ navLeft: 0,
411
+ navTop: 0,
412
+ navWidth: 240,
413
+ rowMetrics,
414
+ maxDepth: 0,
415
+ },
416
+ state,
417
+ )
418
+
419
+ expect(intent?.action).toBe('prependChild')
420
+ expect(intent?.targetNodeId).toBe('line:20')
421
+ expect(intent?.indicator.mode).toBe('inside')
422
+ })
423
+
424
+ it('resolves prependChild to parent when hovering upper half of first nested child', () => {
425
+ function folder(
426
+ title: string,
427
+ children: SidebarTreeItem[] = [],
428
+ collapsed = false,
429
+ tocLineIndex = 0,
430
+ ): SidebarTreeItem {
431
+ return {
432
+ text: title,
433
+ folderPath: [title],
434
+ nodeId: `folder:${title}`,
435
+ tocLineIndex,
436
+ items: children.length ? children : undefined,
437
+ collapsed,
438
+ }
439
+ }
440
+
441
+ const tree: SidebarTreeItem[] = [
442
+ folder(
443
+ 'TNotes 笔记书写规范',
444
+ [
445
+ note('0037', '规范', [], false, 1),
446
+ note('0038', '问题驱动', [], false, 2),
447
+ note('0027', 'emoji', [], false, 3),
448
+ ],
449
+ false,
450
+ 0,
451
+ ),
452
+ ]
453
+ const flat = buildFlatForHitTest(tree, 3)
454
+ const rowMetrics: RowMetric[] = [
455
+ {
456
+ nodeId: 'folder:TNotes 笔记书写规范',
457
+ tocLineIndex: 0,
458
+ rect: mockRect(100),
459
+ },
460
+ { nodeId: 'note:0037', tocLineIndex: 1, rect: mockRect(132) },
461
+ { nodeId: 'note:0038', tocLineIndex: 2, rect: mockRect(164) },
462
+ { nodeId: 'note:0027', tocLineIndex: 3, rect: mockRect(196) },
463
+ ]
464
+ const state = createHitTestState()
465
+
466
+ const intent = resolveDropIntent(
467
+ {
468
+ tree,
469
+ flat,
470
+ dragSource: { kind: 'note', tocLineIndex: 3, noteIndex: '0027' },
471
+ sourceNodeId: 'note:0027',
472
+ clientX: 140,
473
+ clientY: 140,
474
+ navLeft: 0,
475
+ navTop: 0,
476
+ navWidth: 240,
477
+ rowMetrics,
478
+ maxDepth: 0,
479
+ },
480
+ state,
481
+ )
482
+
483
+ expect(intent?.action).toBe('prependChild')
484
+ expect(intent?.targetNodeId).toBe('folder:TNotes 笔记书写规范')
485
+ expect(intent?.isRootPrepend).toBeFalsy()
486
+ expect(intent?.indicator.mode).toBe('inside')
487
+ })
488
+
489
+ it('keeps prependChild to parent in sticky zone on lower part of first child row', () => {
490
+ function folder(
491
+ title: string,
492
+ children: SidebarTreeItem[] = [],
493
+ collapsed = false,
494
+ tocLineIndex = 0,
495
+ ): SidebarTreeItem {
496
+ return {
497
+ text: title,
498
+ folderPath: [title],
499
+ nodeId: `folder:${title}`,
500
+ tocLineIndex,
501
+ items: children.length ? children : undefined,
502
+ collapsed,
503
+ }
504
+ }
505
+
506
+ const tree: SidebarTreeItem[] = [
507
+ folder(
508
+ 'Group',
509
+ [note('0037', 'A', [], false, 1), note('0027', 'B', [], false, 2)],
510
+ false,
511
+ 0,
512
+ ),
513
+ ]
514
+ const flat = buildFlatForHitTest(tree, 2)
515
+ const rowMetrics: RowMetric[] = [
516
+ { nodeId: 'folder:Group', tocLineIndex: 0, rect: mockRect(100) },
517
+ { nodeId: 'note:0037', tocLineIndex: 1, rect: mockRect(132) },
518
+ { nodeId: 'note:0027', tocLineIndex: 2, rect: mockRect(164) },
519
+ ]
520
+ const state = createHitTestState()
521
+
522
+ resolveDropIntent(
523
+ {
524
+ tree,
525
+ flat,
526
+ dragSource: { kind: 'note', tocLineIndex: 2, noteIndex: '0027' },
527
+ sourceNodeId: 'note:0027',
528
+ clientX: 140,
529
+ clientY: 140,
530
+ navLeft: 0,
531
+ navTop: 0,
532
+ navWidth: 240,
533
+ rowMetrics,
534
+ maxDepth: 0,
535
+ },
536
+ state,
537
+ )
538
+
539
+ const intent = resolveDropIntent(
540
+ {
541
+ tree,
542
+ flat,
543
+ dragSource: { kind: 'note', tocLineIndex: 2, noteIndex: '0027' },
544
+ sourceNodeId: 'note:0027',
545
+ clientX: 140,
546
+ clientY: 158,
547
+ navLeft: 0,
548
+ navTop: 0,
549
+ navWidth: 240,
550
+ rowMetrics,
551
+ maxDepth: 0,
552
+ },
553
+ state,
554
+ )
555
+
556
+ expect(intent?.action).toBe('prependChild')
557
+ expect(intent?.targetNodeId).toBe('folder:Group')
558
+ })
559
+
560
+ it('fingerprints stable intents', () => {
561
+ const fp = dropIntentFingerprint({
562
+ action: 'prependChild',
563
+ targetNodeId: 'note:0019',
564
+ sourceNodeId: 'note:0039',
565
+ indicator: { mode: 'inside', depthLevels: [0, 1], activeDepth: 1 },
566
+ linePlacement: 'after',
567
+ hoverRowIndex: 0,
568
+ })
569
+ expect(fp).toContain('prependChild')
570
+ expect(fp).toContain('note:0019')
571
+ })
572
+ })