@allbluecn/web-app 0.22.4 → 0.23.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 (500) hide show
  1. package/package.json +4 -14
  2. package/src/__tests__/integration/login-email-acceptance.test.ts +16 -0
  3. package/src/__tests__/story-view.test.ts +33 -1
  4. package/src/components/agents/__tests__/agent-bindings-tab.test.tsx +221 -0
  5. package/src/components/agents/__tests__/agents-page.test.tsx +284 -0
  6. package/src/components/agents/agent-bindings-tab.tsx +59 -45
  7. package/src/components/agents/agent-form-dialog.tsx +3 -3
  8. package/src/components/agents/agent-selector.tsx +2 -3
  9. package/src/components/agents/agents-page.tsx +66 -31
  10. package/src/components/agents/slash-skill-menu.tsx +2 -3
  11. package/src/components/auth/__tests__/session-guard.test.tsx +74 -0
  12. package/src/components/auth/session-guard.tsx +8 -0
  13. package/src/components/billing/login-prompt-dialog.tsx +1 -1
  14. package/src/components/billing/subscription-provider.tsx +2 -16
  15. package/src/components/bug/__tests__/peek-modes.test.tsx +8 -0
  16. package/src/components/bug/__tests__/subgroup-header.test.tsx +202 -40
  17. package/src/components/bug/peek-panel.tsx +5 -3
  18. package/src/components/bug/views/bug-list/group.tsx +12 -2
  19. package/src/components/bug/views/bug-list/index.tsx +94 -67
  20. package/src/components/command/command-palette.tsx +10 -9
  21. package/src/components/design/canvas/document/frame-custom-presets.ts +9 -1
  22. package/src/components/design/canvas/panels/toolbar.tsx +21 -22
  23. package/src/components/design/canvas/react/__tests__/canvas-context-load-echo.test.tsx +199 -0
  24. package/src/components/design/canvas/react/__tests__/hooks.test.tsx +17 -3
  25. package/src/components/design/canvas/react/canvas-context.tsx +47 -26
  26. package/src/components/design/canvas/react/hooks.ts +39 -34
  27. package/src/components/design/editor/layout/__tests__/collab-param-wiring.test.tsx +17 -0
  28. package/src/components/design/editor/layout/__tests__/context-menu.test.tsx +17 -1
  29. package/src/components/design/editor/layout/__tests__/no-page-save-toast.test.tsx +17 -0
  30. package/src/components/design/editor/layout/__tests__/persistence-restore.test.tsx +17 -1
  31. package/src/components/design/editor/layout/__tests__/ssr-localstorage-smoke.test.tsx +17 -1
  32. package/src/components/design/editor/layout/design-editor-layout.tsx +13 -2
  33. package/src/components/design/editor/layout/hooks/use-design-auto-save.ts +3 -19
  34. package/src/components/devtools/devtools-panel.tsx +56 -0
  35. package/src/components/forms/__tests__/create-app-form.test.tsx +131 -0
  36. package/src/components/forms/app-field-error.tsx +21 -1
  37. package/src/components/forms/app-field.tsx +8 -8
  38. package/src/components/forms/app-submit.tsx +12 -9
  39. package/src/components/forms/create-app-form.tsx +124 -0
  40. package/src/components/forms/index.ts +2 -3
  41. package/src/components/harness/__tests__/gallery-behavior.test.tsx +37 -1
  42. package/src/components/harness/__tests__/harness-workbench-first-send.test.tsx +4 -1
  43. package/src/components/harness/__tests__/message-actions.test.tsx +7 -6
  44. package/src/components/harness/__tests__/use-video-generation.test.tsx +165 -0
  45. package/src/components/harness/ai-chat-history/index.tsx +5 -3
  46. package/src/components/harness/gallery/gallery-card.tsx +57 -35
  47. package/src/components/harness/gallery/gallery-grid.tsx +32 -15
  48. package/src/components/harness/gallery/gallery-types.ts +19 -0
  49. package/src/components/harness/gallery/gallery-video-card.tsx +54 -0
  50. package/src/components/harness/gallery/types.ts +2 -0
  51. package/src/components/harness/gallery/use-video-generation.ts +95 -0
  52. package/src/components/harness/harness-workbench-page.tsx +2 -3
  53. package/src/components/harness/message-actions.tsx +5 -3
  54. package/src/components/hotkeys/shortcuts-panel.tsx +28 -13
  55. package/src/components/iteration/__tests__/burndown-chart.test.tsx +118 -0
  56. package/src/components/iteration/burndown-chart.tsx +54 -23
  57. package/src/components/knowledge/editor/__tests__/knowledge-doc-editor-collab.test.tsx +96 -0
  58. package/src/components/knowledge/editor/knowledge-doc-editor.tsx +56 -1
  59. package/src/components/knowledge/editor/mindmap-doc-editor.tsx +12 -0
  60. package/src/components/knowledge/editor/use-auto-save-channels.ts +3 -1
  61. package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +0 -4
  62. package/src/components/layout/sidebar-03/nav-knowledge.tsx +11 -11
  63. package/src/components/layout/sidebar-03/nav-main.tsx +12 -15
  64. package/src/components/layout/sidebar-03/nav-notifications.tsx +5 -8
  65. package/src/components/layout/sidebar-03/nav-workspace.tsx +11 -9
  66. package/src/components/notifications/__tests__/settings-notifications.test.tsx +158 -0
  67. package/src/components/notifications/settings-notifications.tsx +37 -13
  68. package/src/components/notifications/team-notifications.tsx +10 -14
  69. package/src/components/settings/personal-team/account/__tests__/account-client.test.tsx +170 -0
  70. package/src/components/settings/personal-team/account/account-client.tsx +44 -7
  71. package/src/components/settings/personal-team/team/team-client.tsx +17 -19
  72. package/src/components/shared-kit/list-kit/__tests__/frozen-header.test.ts +210 -41
  73. package/src/components/shared-kit/list-kit/frozen-header.ts +134 -21
  74. package/src/components/shared-kit/list-kit/kanban/frozen-header.ts +20 -27
  75. package/src/components/story/__tests__/peek-panel-patch-sync.test.tsx +8 -0
  76. package/src/components/story/__tests__/story-list-table.test.tsx +214 -0
  77. package/src/components/story/columns.tsx +11 -2
  78. package/src/components/story/peek-panel.tsx +4 -2
  79. package/src/components/story/story-list-table.tsx +16 -1
  80. package/src/components/story/views/story-list/__tests__/frozen-headers.test.tsx +186 -0
  81. package/src/components/story/views/story-list/group.tsx +12 -1
  82. package/src/components/story/views/story-list/index.tsx +101 -68
  83. package/src/components/testcase/__tests__/case-row.test.tsx +134 -2
  84. package/src/components/testcase/__tests__/peek-panel.test.tsx +8 -0
  85. package/src/components/testcase/__tests__/step-editor.test.tsx +106 -0
  86. package/src/components/testcase/list/grouped-list.tsx +50 -22
  87. package/src/components/testcase/peek-panel.tsx +7 -4
  88. package/src/components/testcase/step-editor.tsx +196 -150
  89. package/src/components/tiptap/notion-like/editor-extensions.ts +0 -3
  90. package/src/components/tiptap/notion-like/notion-like-editor-header.tsx +1 -31
  91. package/src/components/tiptap/notion-like/notion-like-editor.tsx +32 -1
  92. package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +2 -28
  93. package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +2 -28
  94. package/src/components/workspace/dialogs/create-workspace-dialog.tsx +45 -43
  95. package/src/components/workspace/dialogs/edit-workspace-dialog.tsx +38 -36
  96. package/src/design-engine/engine/engine-decor.ts +2 -0
  97. package/src/design-engine/engine/engine-path.ts +2 -0
  98. package/src/hooks/__tests__/use-unsaved-guard.test.tsx +180 -0
  99. package/src/hooks/use-notifications.ts +9 -16
  100. package/src/hooks/use-unsaved-guard.tsx +87 -0
  101. package/src/lib/__tests__/pacer.test.ts +33 -96
  102. package/src/lib/__tests__/query-defaults.test.ts +169 -0
  103. package/src/lib/__tests__/query-global-error.test.ts +256 -0
  104. package/src/lib/anyhub/anyhub-store.ts +2 -2
  105. package/src/lib/changelog/sdk-versions.ts +2 -10
  106. package/src/lib/collab/presence-store.ts +5 -5
  107. package/src/lib/collab/use-presence-list-refresh.ts +2 -2
  108. package/src/lib/collab/use-share-presence.ts +2 -2
  109. package/src/lib/collections/query-keys.ts +7 -2
  110. package/src/lib/commands/__tests__/command-palette-state.test.ts +12 -14
  111. package/src/lib/commands/__tests__/registry.test.ts +6 -3
  112. package/src/lib/commands/command-palette-state.ts +8 -31
  113. package/src/lib/hotkeys/__tests__/registry.test.ts +71 -18
  114. package/src/lib/hotkeys/registry.ts +188 -85
  115. package/src/lib/hotkeys/types.ts +1 -13
  116. package/src/lib/pacer.ts +1 -79
  117. package/src/lib/queries/README.md +67 -0
  118. package/src/lib/queries/__tests__/agents-queries.test.ts +101 -0
  119. package/src/lib/queries/__tests__/compliance-queries.test.ts +136 -0
  120. package/src/lib/queries/__tests__/notifications-queries.test.ts +175 -0
  121. package/src/lib/queries/__tests__/team-queries.test.ts +87 -0
  122. package/src/lib/queries/agents.ts +40 -0
  123. package/src/lib/queries/compliance.ts +69 -0
  124. package/src/lib/queries/notifications.ts +63 -0
  125. package/src/lib/queries/team.ts +39 -0
  126. package/src/lib/query-defaults.ts +8 -0
  127. package/src/lib/query-global-error.ts +128 -0
  128. package/src/lib/remote-auth-client.ts +16 -0
  129. package/src/paraglide/README.md +1 -1
  130. package/src/paraglide/messages/_index.d.ts +11 -0
  131. package/src/paraglide/messages/_index.js +5659 -88285
  132. package/src/paraglide/messages/common_error_generic.d.ts +16 -0
  133. package/src/paraglide/messages/common_error_generic.js +29 -0
  134. package/src/paraglide/messages/common_unsaved_body.d.ts +16 -0
  135. package/src/paraglide/messages/common_unsaved_body.js +29 -0
  136. package/src/paraglide/messages/common_unsaved_leave.d.ts +16 -0
  137. package/src/paraglide/messages/common_unsaved_leave.js +29 -0
  138. package/src/paraglide/messages/common_unsaved_stay.d.ts +16 -0
  139. package/src/paraglide/messages/common_unsaved_stay.js +29 -0
  140. package/src/paraglide/messages/common_unsaved_title.d.ts +16 -0
  141. package/src/paraglide/messages/common_unsaved_title.js +29 -0
  142. package/src/paraglide/messages/gallery_videofailed1.d.ts +17 -0
  143. package/src/paraglide/messages/gallery_videofailed1.js +30 -0
  144. package/src/paraglide/messages/gallery_videogenerating1.d.ts +17 -0
  145. package/src/paraglide/messages/gallery_videogenerating1.js +30 -0
  146. package/src/paraglide/messages/gallery_videomissingprompt2.d.ts +17 -0
  147. package/src/paraglide/messages/gallery_videomissingprompt2.js +30 -0
  148. package/src/paraglide/messages/gallery_videoprogress1.d.ts +19 -0
  149. package/src/paraglide/messages/gallery_videoprogress1.js +30 -0
  150. package/src/paraglide/messages/gallery_videostop1.d.ts +17 -0
  151. package/src/paraglide/messages/gallery_videostop1.js +30 -0
  152. package/src/paraglide/messages/story_table_resizecolumn1.d.ts +17 -0
  153. package/src/paraglide/messages/story_table_resizecolumn1.js +30 -0
  154. package/src/providers/index.tsx +5 -50
  155. package/src/routeTree.gen.ts +21 -0
  156. package/src/router.tsx +21 -8
  157. package/src/routes/__root.tsx +2 -1
  158. package/src/routes/__tests__/-edition-routing.test.tsx +6 -1
  159. package/src/routes/_login/forgot-password/route.lazy.tsx +7 -6
  160. package/src/routes/_login/login/route.lazy.tsx +10 -9
  161. package/src/routes/_login/register/route.lazy.tsx +19 -18
  162. package/src/routes/_login/reset-password/route.lazy.tsx +10 -9
  163. package/src/routes/_nav/knowledge/compliance/$id.lazy.tsx +9 -12
  164. package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +16 -13
  165. package/src/routes/_nav/knowledge/laws/$id.lazy.tsx +1 -1
  166. package/src/routes/_nav/notifications/route.tsx +6 -16
  167. package/src/routes/_nav/settings/-settings-tabs.tsx +9 -12
  168. package/src/routes/api/generate/__tests__/media.test.ts +378 -0
  169. package/src/routes/api/generate/media.ts +179 -0
  170. package/src/server/__tests__/agent-client-graph.test.ts +0 -1
  171. package/src/server/__tests__/agent-crud.test.ts +37 -4
  172. package/src/server/__tests__/bug-groups-two-level.test.ts +34 -1
  173. package/src/server/__tests__/chat-conversations.test.ts +37 -4
  174. package/src/server/__tests__/chat-list-messages.test.ts +40 -7
  175. package/src/server/__tests__/csv-export.test.ts +36 -3
  176. package/src/server/__tests__/desktop-bootstrap.test.ts +189 -0
  177. package/src/server/__tests__/remote-auth.test.ts +72 -0
  178. package/src/server/__tests__/skill-crud.test.ts +37 -4
  179. package/src/server/__tests__/sso-domain.test.ts +1 -1
  180. package/src/server/__tests__/sso-rate-limit-singleton.test.ts +123 -0
  181. package/src/server/__tests__/task-serverfn-crud.test.ts +35 -2
  182. package/src/server/__tests__/task-serverfn-queries.test.ts +35 -2
  183. package/src/server/__tests__/testcase-board.test.ts +38 -5
  184. package/src/server/__tests__/testcase-bulk.test.ts +38 -5
  185. package/src/server/__tests__/testcase-list-scope.test.ts +38 -5
  186. package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +126 -2
  187. package/src/server/ai-gateway/__tests__/chat-stream-builder-code-mode.test.ts +4 -0
  188. package/src/server/ai-gateway/__tests__/chat-stream-builder-memory.test.ts +4 -0
  189. package/src/server/ai-gateway/__tests__/chat-stream-builder-sandbox.test.ts +4 -0
  190. package/src/server/ai-gateway/__tests__/chat-stream-builder-skills.test.ts +4 -0
  191. package/src/server/ai-gateway/__tests__/pgvector-memory-adapter.test.ts +30 -0
  192. package/src/server/ai-gateway/__tests__/pgvector-memory-conformance.test.ts +92 -0
  193. package/src/server/ai-gateway/__tests__/source-mode.test.ts +49 -8
  194. package/src/server/ai-gateway/ai-output-guard.ts +50 -2
  195. package/src/server/ai-gateway/chat-stream-builder.ts +3 -0
  196. package/src/server/ai-gateway/media/index.ts +36 -29
  197. package/src/server/ai-gateway/media/trim-input.ts +52 -0
  198. package/src/server/ai-gateway/memory/pgvector-adapter.ts +27 -13
  199. package/src/server/ai-gateway/sandbox/__tests__/harness-sandbox.test.ts +252 -41
  200. package/src/server/ai-gateway/sandbox/harness-sandbox.ts +6 -67
  201. package/src/server/ai-gateway/source-mode.ts +24 -8
  202. package/src/server/desktop-bootstrap.ts +74 -0
  203. package/src/server/lib/__tests__/rate-limit.test.ts +0 -1
  204. package/src/server/lib/rate-limit.ts +0 -2
  205. package/src/server/middlewares/__tests__/auth-context.test.ts +156 -0
  206. package/src/server/middlewares/auth-context.ts +62 -0
  207. package/src/server/remote-auth.ts +58 -31
  208. package/src/server/serverFns/__tests__/ai-hosted-models.test.ts +47 -15
  209. package/src/server/serverFns/__tests__/ai-source-mode.test.ts +45 -15
  210. package/src/server/serverFns/__tests__/billing.test.ts +37 -2
  211. package/src/server/serverFns/__tests__/collab-snapshot.test.ts +41 -14
  212. package/src/server/serverFns/__tests__/collab.test.ts +41 -14
  213. package/src/server/serverFns/__tests__/compliance.test.ts +33 -1
  214. package/src/server/serverFns/__tests__/email-verification.test.ts +35 -1
  215. package/src/server/serverFns/__tests__/knowledge-breadcrumb.test.ts +46 -6
  216. package/src/server/serverFns/__tests__/knowledge-delete.test.ts +42 -14
  217. package/src/server/serverFns/__tests__/knowledge-kb-projects.test.ts +46 -6
  218. package/src/server/serverFns/__tests__/knowledge-public-compliance.test.ts +36 -2
  219. package/src/server/serverFns/__tests__/knowledge-readable-kb.test.ts +42 -10
  220. package/src/server/serverFns/__tests__/knowledge-search.test.ts +45 -9
  221. package/src/server/serverFns/__tests__/legal-code-canonical.test.ts +36 -2
  222. package/src/server/serverFns/__tests__/link-preview.test.ts +38 -4
  223. package/src/server/serverFns/__tests__/notifications-list.test.ts +36 -3
  224. package/src/server/serverFns/__tests__/ownership-merge.test.ts +38 -4
  225. package/src/server/serverFns/__tests__/project-delete-transaction.test.ts +39 -2
  226. package/src/server/serverFns/__tests__/project-kb.test.ts +34 -1
  227. package/src/server/serverFns/__tests__/project-member-idor.test.ts +34 -1
  228. package/src/server/serverFns/__tests__/project-team-assign.test.ts +34 -1
  229. package/src/server/serverFns/__tests__/read-write-merge.test.ts +38 -4
  230. package/src/server/serverFns/__tests__/story-comments.test.ts +38 -4
  231. package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +38 -4
  232. package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +37 -3
  233. package/src/server/serverFns/__tests__/story-crud.test.ts +36 -3
  234. package/src/server/serverFns/__tests__/story-links.test.ts +38 -4
  235. package/src/server/serverFns/__tests__/story-reactions.test.ts +38 -4
  236. package/src/server/serverFns/__tests__/story-tree.test.ts +38 -4
  237. package/src/server/serverFns/__tests__/team-accept.test.ts +34 -1
  238. package/src/server/serverFns/__tests__/team-invitation.test.ts +34 -1
  239. package/src/server/serverFns/__tests__/team-join-request.test.ts +34 -1
  240. package/src/server/serverFns/__tests__/team-members.test.ts +34 -1
  241. package/src/server/serverFns/__tests__/team-ownership-merge.test.ts +34 -1
  242. package/src/server/serverFns/__tests__/update-bug-guard.test.ts +37 -3
  243. package/src/server/serverFns/__tests__/update-document-guard.test.ts +38 -4
  244. package/src/server/serverFns/__tests__/update-page-guard.test.ts +40 -4
  245. package/src/server/serverFns/__tests__/update-seats.test.ts +33 -1
  246. package/src/server/serverFns/__tests__/update-story-guard.test.ts +36 -3
  247. package/src/server/serverFns/__tests__/update-testcase-guard.test.ts +37 -3
  248. package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +36 -2
  249. package/src/server/serverFns/__tests__/workspace-kb.test.ts +36 -2
  250. package/src/server/serverFns/__tests__/workspace.test.ts +34 -1
  251. package/src/server/serverFns/activity.ts +10 -14
  252. package/src/server/serverFns/agents/agent-crud.ts +16 -15
  253. package/src/server/serverFns/agents/bindings.ts +12 -13
  254. package/src/server/serverFns/agents/skill-crud.ts +16 -15
  255. package/src/server/serverFns/ai/code-mode-readiness.ts +3 -4
  256. package/src/server/serverFns/ai/search-capability.ts +3 -4
  257. package/src/server/serverFns/anyhub/app.ts +10 -14
  258. package/src/server/serverFns/anyhub/index.ts +7 -10
  259. package/src/server/serverFns/anyhub/install.ts +4 -4
  260. package/src/server/serverFns/anyhub/permissions.ts +10 -14
  261. package/src/server/serverFns/anyhub/proxy.ts +4 -6
  262. package/src/server/serverFns/auth/email-verification.ts +4 -5
  263. package/src/server/serverFns/auth/session.ts +9 -0
  264. package/src/server/serverFns/billing.ts +18 -28
  265. package/src/server/serverFns/blob-upload.ts +6 -9
  266. package/src/server/serverFns/bug/__tests__/bug-access-tokens.test.ts +34 -1
  267. package/src/server/serverFns/bug/__tests__/bug-activities.test.ts +34 -1
  268. package/src/server/serverFns/bug/__tests__/bug-comments.test.ts +34 -1
  269. package/src/server/serverFns/bug/__tests__/bug-crud.test.ts +34 -1
  270. package/src/server/serverFns/bug/__tests__/bug-fix.test.ts +34 -1
  271. package/src/server/serverFns/bug/__tests__/bug-groups.test.ts +34 -1
  272. package/src/server/serverFns/bug/__tests__/bug-reactions.test.ts +34 -1
  273. package/src/server/serverFns/bug/__tests__/bug-reorder.test.ts +39 -5
  274. package/src/server/serverFns/bug/__tests__/bug-repro.test.ts +34 -1
  275. package/src/server/serverFns/bug/__tests__/bug-story.test.ts +34 -1
  276. package/src/server/serverFns/bug/__tests__/bug-webhooks.test.ts +34 -1
  277. package/src/server/serverFns/bug/bug-access-tokens.ts +16 -22
  278. package/src/server/serverFns/bug/bug-activities.ts +4 -6
  279. package/src/server/serverFns/bug/bug-collect-tokens.ts +13 -18
  280. package/src/server/serverFns/bug/bug-comments.ts +13 -18
  281. package/src/server/serverFns/bug/bug-crud/detail.ts +10 -14
  282. package/src/server/serverFns/bug/bug-crud.ts +28 -38
  283. package/src/server/serverFns/bug/bug-display-options.ts +8 -11
  284. package/src/server/serverFns/bug/bug-export.ts +5 -7
  285. package/src/server/serverFns/bug/bug-fix.ts +10 -14
  286. package/src/server/serverFns/bug/bug-groups.ts +10 -14
  287. package/src/server/serverFns/bug/bug-reactions.ts +10 -14
  288. package/src/server/serverFns/bug/bug-reorder.ts +4 -6
  289. package/src/server/serverFns/bug/bug-repro.ts +8 -11
  290. package/src/server/serverFns/bug/bug-story-bulk.ts +13 -18
  291. package/src/server/serverFns/bug/bug-story.ts +10 -14
  292. package/src/server/serverFns/bug/bug-views.ts +12 -18
  293. package/src/server/serverFns/bug/bug-webhooks.ts +13 -18
  294. package/src/server/serverFns/bug/test-bug-confirm.ts +7 -10
  295. package/src/server/serverFns/changelog/__tests__/fetch.test.ts +40 -1
  296. package/src/server/serverFns/changelog/fetch.ts +4 -6
  297. package/src/server/serverFns/chat/conversations.ts +17 -28
  298. package/src/server/serverFns/chat/list-messages.ts +4 -7
  299. package/src/server/serverFns/collab-presence.ts +11 -13
  300. package/src/server/serverFns/collab-snapshot.ts +6 -7
  301. package/src/server/serverFns/collab.ts +7 -7
  302. package/src/server/serverFns/compliance.ts +8 -8
  303. package/src/server/serverFns/design/__tests__/comments.test.ts +54 -10
  304. package/src/server/serverFns/design/__tests__/designs.test.ts +39 -16
  305. package/src/server/serverFns/design/__tests__/page-versions.test.ts +52 -14
  306. package/src/server/serverFns/design/__tests__/shares-password.test.ts +44 -14
  307. package/src/server/serverFns/design/__tests__/shares.test.ts +44 -14
  308. package/src/server/serverFns/design/__tests__/versions.test.ts +44 -14
  309. package/src/server/serverFns/design/ai/__tests__/agent-conversations.test.ts +50 -21
  310. package/src/server/serverFns/design/ai/__tests__/providers.test.ts +42 -8
  311. package/src/server/serverFns/design/ai/agent-conversations.ts +19 -26
  312. package/src/server/serverFns/design/ai/fill-annotation.ts +5 -7
  313. package/src/server/serverFns/design/ai/generate-prototype.ts +5 -7
  314. package/src/server/serverFns/design/ai/hosted-models.ts +4 -5
  315. package/src/server/serverFns/design/ai/providers.ts +30 -39
  316. package/src/server/serverFns/design/ai/source-mode.ts +10 -12
  317. package/src/server/serverFns/design/comments.ts +14 -13
  318. package/src/server/serverFns/design/designs.ts +13 -19
  319. package/src/server/serverFns/design/page-versions.ts +17 -18
  320. package/src/server/serverFns/design/pages.ts +17 -18
  321. package/src/server/serverFns/design/reviews.ts +8 -9
  322. package/src/server/serverFns/design/shares-password.ts +13 -19
  323. package/src/server/serverFns/design/shares.ts +22 -31
  324. package/src/server/serverFns/design/versions.ts +8 -9
  325. package/src/server/serverFns/harness/__tests__/attach-prototype.spec.ts +52 -2
  326. package/src/server/serverFns/harness/__tests__/attach-to-kb.spec.ts +52 -2
  327. package/src/server/serverFns/harness/__tests__/attach-to-story-ssrf.spec.ts +52 -2
  328. package/src/server/serverFns/harness/__tests__/attach-to-story.spec.ts +52 -2
  329. package/src/server/serverFns/harness/__tests__/cancel-run.test.ts +40 -15
  330. package/src/server/serverFns/harness/__tests__/extract-attachment.spec.ts +52 -2
  331. package/src/server/serverFns/harness/__tests__/gallery.spec.ts +76 -12
  332. package/src/server/serverFns/harness/__tests__/github-workspace.spec.ts +38 -6
  333. package/src/server/serverFns/harness/__tests__/media-prototype.spec.ts +50 -6
  334. package/src/server/serverFns/harness/__tests__/media.spec.ts +55 -11
  335. package/src/server/serverFns/harness/__tests__/sandbox-policy.test.ts +41 -16
  336. package/src/server/serverFns/harness/__tests__/workspace-files.test.ts +41 -16
  337. package/src/server/serverFns/harness/attach-prototype.ts +4 -4
  338. package/src/server/serverFns/harness/attach-to-kb.ts +4 -4
  339. package/src/server/serverFns/harness/attach-to-story.ts +4 -4
  340. package/src/server/serverFns/harness/cancel-run.ts +4 -6
  341. package/src/server/serverFns/harness/extract-attachment.ts +3 -4
  342. package/src/server/serverFns/harness/gallery.ts +19 -23
  343. package/src/server/serverFns/harness/github-workspace.ts +14 -21
  344. package/src/server/serverFns/harness/media.ts +18 -117
  345. package/src/server/serverFns/harness/sandbox-policy.ts +11 -10
  346. package/src/server/serverFns/harness/search-mention-candidates.ts +4 -6
  347. package/src/server/serverFns/harness/workspace-files.ts +19 -26
  348. package/src/server/serverFns/iteration/__tests__/iteration-auto-schedule.test.ts +35 -3
  349. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +35 -3
  350. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +35 -3
  351. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +35 -3
  352. package/src/server/serverFns/iteration/iteration-auto-schedule.ts +7 -10
  353. package/src/server/serverFns/iteration/iteration-burndown.ts +4 -6
  354. package/src/server/serverFns/iteration/iteration-crud.ts +26 -35
  355. package/src/server/serverFns/iteration/iteration-stories.ts +10 -14
  356. package/src/server/serverFns/iteration/iteration-transfer.ts +4 -6
  357. package/src/server/serverFns/knowledge/public/bookmarks/categories.ts +11 -15
  358. package/src/server/serverFns/knowledge/public/bookmarks/create.ts +4 -6
  359. package/src/server/serverFns/knowledge/public/bookmarks/delete.ts +5 -7
  360. package/src/server/serverFns/knowledge/public/bookmarks/detail.ts +4 -6
  361. package/src/server/serverFns/knowledge/public/bookmarks/favorite.ts +4 -6
  362. package/src/server/serverFns/knowledge/public/bookmarks/list.ts +4 -6
  363. package/src/server/serverFns/knowledge/public/bookmarks/refresh-favicon.ts +4 -6
  364. package/src/server/serverFns/knowledge/public/bookmarks/share.ts +4 -6
  365. package/src/server/serverFns/knowledge/public/bookmarks/sync-github.ts +5 -7
  366. package/src/server/serverFns/knowledge/public/bookmarks/unshare.ts +4 -6
  367. package/src/server/serverFns/knowledge/public/bookmarks/update.ts +6 -8
  368. package/src/server/serverFns/knowledge/public/compliance/create.ts +4 -6
  369. package/src/server/serverFns/knowledge/public/compliance/delete.ts +5 -7
  370. package/src/server/serverFns/knowledge/public/compliance/detail.ts +4 -6
  371. package/src/server/serverFns/knowledge/public/compliance/list.ts +4 -6
  372. package/src/server/serverFns/knowledge/public/compliance/share.ts +4 -6
  373. package/src/server/serverFns/knowledge/public/compliance/unshare.ts +4 -6
  374. package/src/server/serverFns/knowledge/public/compliance/update.ts +5 -7
  375. package/src/server/serverFns/knowledge/public/dictionary/create.ts +4 -6
  376. package/src/server/serverFns/knowledge/public/dictionary/delete.ts +5 -7
  377. package/src/server/serverFns/knowledge/public/dictionary/detail.ts +4 -6
  378. package/src/server/serverFns/knowledge/public/dictionary/list.ts +4 -6
  379. package/src/server/serverFns/knowledge/public/dictionary/share.ts +4 -6
  380. package/src/server/serverFns/knowledge/public/dictionary/unshare.ts +4 -6
  381. package/src/server/serverFns/knowledge/public/dictionary/update.ts +5 -7
  382. package/src/server/serverFns/knowledge/public/entity-tags.ts +4 -6
  383. package/src/server/serverFns/knowledge/public/favorite.ts +7 -10
  384. package/src/server/serverFns/knowledge/public/legal/create.ts +4 -6
  385. package/src/server/serverFns/knowledge/public/legal/delete.ts +5 -7
  386. package/src/server/serverFns/knowledge/public/legal/detail.ts +4 -6
  387. package/src/server/serverFns/knowledge/public/legal/list.ts +4 -6
  388. package/src/server/serverFns/knowledge/public/legal/share.ts +4 -6
  389. package/src/server/serverFns/knowledge/public/legal/unshare.ts +4 -6
  390. package/src/server/serverFns/knowledge/public/legal/update.ts +5 -7
  391. package/src/server/serverFns/knowledge-index.ts +12 -16
  392. package/src/server/serverFns/knowledge.ts +50 -67
  393. package/src/server/serverFns/notifications.ts +25 -34
  394. package/src/server/serverFns/plugins/harness-plugins.ts +16 -23
  395. package/src/server/serverFns/project/module-overview.ts +7 -10
  396. package/src/server/serverFns/project.ts +48 -68
  397. package/src/server/serverFns/references/create.ts +4 -6
  398. package/src/server/serverFns/references/resolve.ts +4 -6
  399. package/src/server/serverFns/references/search.ts +4 -6
  400. package/src/server/serverFns/share/design/[designId]/events.ts +4 -6
  401. package/src/server/serverFns/sso.ts +1 -1
  402. package/src/server/serverFns/story/__tests__/bulk.test.ts +34 -1
  403. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +34 -1
  404. package/src/server/serverFns/story/__tests__/story-attachments.test.ts +34 -1
  405. package/src/server/serverFns/story/__tests__/story-crud-date-key.test.ts +42 -17
  406. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +34 -1
  407. package/src/server/serverFns/story/__tests__/story-gantt-deps.test.ts +34 -1
  408. package/src/server/serverFns/story/__tests__/story-groups.test.ts +34 -1
  409. package/src/server/serverFns/story/__tests__/story-read-path-authz.test.ts +40 -4
  410. package/src/server/serverFns/story/__tests__/story-search.test.ts +34 -1
  411. package/src/server/serverFns/story/__tests__/story-transfer.test.ts +34 -1
  412. package/src/server/serverFns/story/link-preview.ts +5 -7
  413. package/src/server/serverFns/story/story-assoc.ts +28 -38
  414. package/src/server/serverFns/story/story-attachments.ts +7 -10
  415. package/src/server/serverFns/story/story-children.ts +4 -6
  416. package/src/server/serverFns/story/story-comments.ts +13 -18
  417. package/src/server/serverFns/story/story-crud/activities.ts +4 -6
  418. package/src/server/serverFns/story/story-crud/archive.ts +7 -10
  419. package/src/server/serverFns/story/story-crud/bulk.ts +10 -14
  420. package/src/server/serverFns/story/story-crud/detail.ts +7 -10
  421. package/src/server/serverFns/story/story-crud/label-suggest.ts +4 -6
  422. package/src/server/serverFns/story/story-crud/labels.ts +6 -9
  423. package/src/server/serverFns/story/story-crud/user-brief.ts +4 -6
  424. package/src/server/serverFns/story/story-crud.ts +13 -18
  425. package/src/server/serverFns/story/story-dependencies.ts +16 -22
  426. package/src/server/serverFns/story/story-groups.ts +10 -14
  427. package/src/server/serverFns/story/story-links.ts +13 -18
  428. package/src/server/serverFns/story/story-reactions.ts +13 -18
  429. package/src/server/serverFns/story/story-search.ts +7 -10
  430. package/src/server/serverFns/story/story-transfer.ts +10 -14
  431. package/src/server/serverFns/story/story-tree.ts +7 -10
  432. package/src/server/serverFns/story-display-options.ts +7 -8
  433. package/src/server/serverFns/story-view.ts +12 -18
  434. package/src/server/serverFns/tasks/task-crud.ts +13 -17
  435. package/src/server/serverFns/tasks/task-queries.ts +12 -18
  436. package/src/server/serverFns/team.ts +46 -66
  437. package/src/server/serverFns/testcase/__tests__/automation.test.ts +38 -5
  438. package/src/server/serverFns/testcase/__tests__/evidence-upload.test.ts +37 -3
  439. package/src/server/serverFns/testcase/__tests__/generation.test.ts +38 -5
  440. package/src/server/serverFns/testcase/__tests__/plans.test.ts +36 -3
  441. package/src/server/serverFns/testcase/__tests__/regression.test.ts +36 -3
  442. package/src/server/serverFns/testcase/__tests__/run-cases.test.ts +36 -3
  443. package/src/server/serverFns/testcase/__tests__/runs.test.ts +36 -3
  444. package/src/server/serverFns/testcase/__tests__/stats.test.ts +36 -3
  445. package/src/server/serverFns/testcase/__tests__/testcase-board.test.ts +39 -5
  446. package/src/server/serverFns/testcase/__tests__/testcase-crud.test.ts +36 -3
  447. package/src/server/serverFns/testcase/__tests__/testcase-folders.test.ts +36 -3
  448. package/src/server/serverFns/testcase/__tests__/testcase-groups.test.ts +38 -5
  449. package/src/server/serverFns/testcase/automation.ts +13 -17
  450. package/src/server/serverFns/testcase/bulk.ts +10 -14
  451. package/src/server/serverFns/testcase/evidence-upload.ts +5 -7
  452. package/src/server/serverFns/testcase/generation.ts +10 -13
  453. package/src/server/serverFns/testcase/plans.ts +23 -31
  454. package/src/server/serverFns/testcase/regression.ts +7 -10
  455. package/src/server/serverFns/testcase/run-cases.ts +13 -18
  456. package/src/server/serverFns/testcase/runs.ts +13 -18
  457. package/src/server/serverFns/testcase/stats.ts +13 -18
  458. package/src/server/serverFns/testcase/testcase-activities.ts +4 -6
  459. package/src/server/serverFns/testcase/testcase-board.ts +4 -6
  460. package/src/server/serverFns/testcase/testcase-crud.ts +20 -27
  461. package/src/server/serverFns/testcase/testcase-display-options.ts +8 -11
  462. package/src/server/serverFns/testcase/testcase-export.ts +5 -7
  463. package/src/server/serverFns/testcase/testcase-filter-views.ts +12 -18
  464. package/src/server/serverFns/testcase/testcase-folders.ts +16 -22
  465. package/src/server/serverFns/testcase/testcase-groups.ts +4 -6
  466. package/src/server/serverFns/testcase/testcase-reorder.ts +4 -6
  467. package/src/server/serverFns/translation.ts +4 -6
  468. package/src/server/serverFns/user/avatar.ts +4 -6
  469. package/src/server/serverFns/user/settings.ts +4 -6
  470. package/src/server/serverFns/user/update-locale.ts +4 -6
  471. package/src/server/serverFns/workspace-kb.ts +26 -35
  472. package/src/server/serverFns/workspace.ts +43 -59
  473. package/src/server/serverFns/ws.ts +4 -6
  474. package/src/server/serverfn-public-allowlist.ts +117 -0
  475. package/src/server/story/types.ts +0 -1
  476. package/src/server/sync/__tests__/scopes.test.ts +64 -0
  477. package/src/server/sync/scopes.ts +7 -0
  478. package/src/server/testcase/__tests__/auto-bug.test.ts +44 -12
  479. package/src/__tests__/bench/baselines/format-timestamp.json +0 -44
  480. package/src/__typeprobe__.d.ts +0 -4
  481. package/src/components/forms/app-form-dialog.tsx +0 -126
  482. package/src/components/forms/hooks/use-form-submit.ts +0 -57
  483. package/src/components/forms/types.ts +0 -34
  484. package/src/components/harness/code/workspace-picker.tsx +0 -239
  485. package/src/components/tiptap/extensions/local-file/insert-local-file.ts +0 -35
  486. package/src/components/tiptap/extensions/local-file/local-file-node.ts +0 -71
  487. package/src/devframe-docks/ai-chat-iframe.tsx +0 -29
  488. package/src/devframe-docks/session-iframe.tsx +0 -90
  489. package/src/lib/__tests__/desktop-detect.test.ts +0 -19
  490. package/src/lib/collections/index.ts +0 -22
  491. package/src/lib/collections/knowledge/index.ts +0 -18
  492. package/src/lib/collections/knowledge/knowledge-bases-collection.ts +0 -34
  493. package/src/lib/collections/reference/bookmark-categories-collection.ts +0 -31
  494. package/src/lib/collections/reference/labels-collection.ts +0 -31
  495. package/src/lib/collections/reference/projects-collection.ts +0 -31
  496. package/src/lib/collections/reference/users-brief-collection.ts +0 -35
  497. package/src/lib/collections/reference/workspaces-collection.ts +0 -34
  498. package/src/lib/desktop-detect.ts +0 -29
  499. package/src/paraglide/messages/en.js +0 -28260
  500. package/src/paraglide/messages/zh.js +0 -28258
@@ -0,0 +1,186 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ // story 列表吸附组头(任务 22):旧「滚动容器外常驻冻结层」(固定 top 覆盖渲染、
19
+ // threshold+1 容差、无顶出)已迁离,判定走共享 computeFrozenHeader——等价原生
20
+ // sticky:活跃才渲染副本、被下个组头逐像素顶出、边界不留容差、满宽实色底色盒。
21
+ // 公式本体在 list-kit/__tests__/frozen-header.test.ts 覆盖;此处断言 story 接线
22
+ // (jsdom:行高走 estimateSize 精确值,scrollTop 直写 + rAF 节流,bug 同款)。
23
+ import { afterEach, describe, expect, it, vi } from "vitest";
24
+ import { render, fireEvent, cleanup, act } from "@testing-library/react";
25
+
26
+ // createServerFn 需 tanstackStart 插件编译(vitest 无该插件),显式空实现保模块求值
27
+ vi.mock("@/server/serverFns/story", () => ({
28
+ createStoryFn: vi.fn(),
29
+ bulkArchiveStoriesFn: vi.fn(),
30
+ bulkDeleteStoriesFn: vi.fn(),
31
+ bulkUpdateStoriesFn: vi.fn(),
32
+ reorderStoriesFn: vi.fn(),
33
+ }));
34
+ vi.mock("../row", () => ({ StoryListRow: () => null }));
35
+ vi.mock("../batch-toolbar", () => ({ BatchToolbar: () => null }));
36
+ vi.mock("../../inline-story-creator", () => ({
37
+ InlineStoryCreator: () => null,
38
+ }));
39
+
40
+ import { StoryList } from "../index";
41
+ import { DEFAULT_DISPLAY_OPTIONS } from "@/shared/list-kit/display-values";
42
+ import type { PagedGroupNode } from "../../../display/use-paged-story-groups";
43
+ import type { StoryListItem } from "@/shared/story/types";
44
+
45
+ const testUser = {
46
+ id: "u1",
47
+ username: "alice",
48
+ email: "alice@example.com",
49
+ avatarConfig: null,
50
+ bgShape: null,
51
+ bgColor: null,
52
+ };
53
+
54
+ function makeStory(id: string): StoryListItem {
55
+ return {
56
+ id,
57
+ identifier: "STORY-1",
58
+ name: `需求 ${id}`,
59
+ description: null,
60
+ state: "todo",
61
+ priority: "none",
62
+ archived: false,
63
+ startDate: null,
64
+ targetDate: null,
65
+ boardOrder: 0,
66
+ projectId: "p1",
67
+ parentId: null,
68
+ parent: null,
69
+ assignee: null,
70
+ createdBy: testUser,
71
+ labels: [],
72
+ createdAt: "2026-01-01T00:00:00.000Z",
73
+ updatedAt: "2026-01-01T00:00:00.000Z",
74
+ };
75
+ }
76
+
77
+ /** 双层分组夹具(estimateSize 估高:header/subHeader 36、story 44、tail 40):
78
+ * 0 G1(待处理) 0
79
+ * 1 S(严重) 36
80
+ * 2 story s1 72
81
+ * 3 tail 116
82
+ * 4 S(较低) 156
83
+ * 5 story s2 192
84
+ * 6 tail 236
85
+ * 7 G2(已修复) 276
86
+ * 8 S(严重) 312
87
+ * 9 story s3 356
88
+ * 10 tail 400 */
89
+ const groups: PagedGroupNode[] = [
90
+ {
91
+ key: "todo",
92
+ label: "待处理",
93
+ totalCount: 2,
94
+ items: [makeStory("s1"), makeStory("s2")],
95
+ subGroups: [
96
+ { key: "high", label: "严重", totalCount: 1, items: [makeStory("s1")] },
97
+ { key: "low", label: "较低", totalCount: 1, items: [makeStory("s2")] },
98
+ ],
99
+ },
100
+ {
101
+ key: "done",
102
+ label: "已修复",
103
+ totalCount: 1,
104
+ items: [makeStory("s3")],
105
+ subGroups: [{ key: "high", label: "严重", totalCount: 1, items: [makeStory("s3")] }],
106
+ },
107
+ ];
108
+
109
+ const baseProps = {
110
+ groups,
111
+ displayFilters: DEFAULT_DISPLAY_OPTIONS,
112
+ users: [],
113
+ labels: [],
114
+ projects: [],
115
+ onRefresh: () => {},
116
+ onPatchStory: () => {},
117
+ onOpenStory: () => {},
118
+ };
119
+
120
+ const frozenLayer = (container: HTMLElement) =>
121
+ container.querySelector<HTMLElement>('[data-slot="story-frozen-headers"]');
122
+
123
+ async function scrollStoryList(container: HTMLElement, top: number) {
124
+ const el = container.querySelector<HTMLElement>(".overflow-auto");
125
+ if (!el) throw new Error("scroll container not found");
126
+ Object.defineProperty(el, "scrollTop", { configurable: true, value: top });
127
+ await act(async () => {
128
+ fireEvent.scroll(el);
129
+ // 组件内 rAF 节流:排进同帧队列,等下一帧回调执行完
130
+ await new Promise((r) => requestAnimationFrame(r));
131
+ });
132
+ }
133
+
134
+ describe("StoryList 吸附组头(共享 computeFrozenHeader)", () => {
135
+ afterEach(() => {
136
+ cleanup();
137
+ vi.clearAllMocks();
138
+ });
139
+
140
+ it("首屏即吸附(首个组头容器坐标恰为 0):主/子双层满宽实色底色盒 offset 0", () => {
141
+ const { container } = render(<StoryList {...baseProps} />);
142
+ const layer = frozenLayer(container);
143
+ expect(layer).toBeTruthy();
144
+ const bands = layer!.querySelectorAll<HTMLElement>(":scope > div");
145
+ expect(bands.length).toBe(2);
146
+ // 子组头带在下(top 36 = HEADER_ROW_HEIGHT)、主组头带在上(top 0)
147
+ expect(bands[0]!.textContent).toContain("严重");
148
+ expect(bands[0]!.style.transform).toBe("translateY(0px)");
149
+ expect(bands[0]!.style.top).toBe("36px");
150
+ expect(bands[1]!.textContent).toContain("待处理");
151
+ expect(bands[1]!.style.transform).toBe("translateY(0px)");
152
+ // 满宽实色底色盒(与在流行/组头 sticky 变体同源,任务 22 单一来源)
153
+ expect(bands[0]!.className).toContain("bg-[color-mix(in_oklch,var(--muted)_35%,var(--card)_65%)]");
154
+ expect(bands[1]!.className).toContain("bg-[color-mix(in_oklch,var(--muted)_50%,var(--card)_50%)]");
155
+ });
156
+
157
+ it("下个组头逼近:主组头被顶出负值、子组头随区段结束滑出", async () => {
158
+ const { container } = render(<StoryList {...baseProps} />);
159
+ await scrollStoryList(container, 275);
160
+ const bands = frozenLayer(container)!.querySelectorAll<HTMLElement>(":scope > div");
161
+ expect(bands[1]!.textContent).toContain("待处理");
162
+ expect(bands[1]!.style.transform).toBe("translateY(-35px)");
163
+ expect(bands[0]!.textContent).toContain("较低");
164
+ expect(bands[0]!.style.transform).toBe("translateY(-71px)");
165
+ });
166
+
167
+ it("组切换边界零跳变:新组头 offset 0 就位,子组头切换为新组首个子组", async () => {
168
+ const { container } = render(<StoryList {...baseProps} />);
169
+ await scrollStoryList(container, 276);
170
+ const bands = frozenLayer(container)!.querySelectorAll<HTMLElement>(":scope > div");
171
+ expect(bands[1]!.textContent).toContain("已修复");
172
+ expect(bands[1]!.style.transform).toBe("translateY(0px)");
173
+ expect(bands[0]!.textContent).toContain("严重");
174
+ expect(bands[0]!.style.transform).toBe("translateY(0px)");
175
+ });
176
+
177
+ it("子组头被下个同级子组头顶出(组内滚动)", async () => {
178
+ const { container } = render(<StoryList {...baseProps} />);
179
+ await scrollStoryList(container, 100);
180
+ const bands = frozenLayer(container)!.querySelectorAll<HTMLElement>(":scope > div");
181
+ expect(bands[1]!.textContent).toContain("待处理");
182
+ expect(bands[1]!.style.transform).toBe("translateY(0px)");
183
+ expect(bands[0]!.textContent).toContain("严重");
184
+ expect(bands[0]!.style.transform).toBe("translateY(-16px)");
185
+ });
186
+ });
@@ -53,6 +53,17 @@ interface VirtualGroupHeaderProps {
53
53
  showRoleLabel: boolean;
54
54
  }
55
55
 
56
+ /**
57
+ * 组头吸附层底色单一来源(任务 22):在流行(index.tsx 虚拟化行 wrapper)、
58
+ * 吸附层满宽底色盒与 VirtualGroupHeader 的 sticky 变体三处共用,防漂移。
59
+ * 必须是实色(color-mix 两实色变量):吸附层要完全遮挡其下滚动的行,
60
+ * 半透明底会透出下层内容(AGENTS.md「底色 columnSurface 单一来源」同规)。
61
+ */
62
+ export const groupHeaderSurfaceClass = (depth: number) =>
63
+ depth === 0
64
+ ? "bg-[color-mix(in_oklch,var(--muted)_50%,var(--card)_50%)]"
65
+ : "bg-[color-mix(in_oklch,var(--muted)_35%,var(--card)_65%)]";
66
+
56
67
  export const VirtualGroupHeader = memo(function VirtualGroupHeader({ node, depth, collapsed, collapseKey, onToggle, onCreate, onRetry, sticky, hideCreate, selectState, onToggleSelect, showSelect, groupDimension, showRoleLabel }: VirtualGroupHeaderProps) {
57
68
  const textColor =
58
69
  node.color?.split(" ").find((c) => c.startsWith("text")) ?? "text-muted-foreground";
@@ -63,7 +74,7 @@ export const VirtualGroupHeader = memo(function VirtualGroupHeader({ node, depth
63
74
  : undefined;
64
75
 
65
76
  return (
66
- <div className={cn("flex h-full items-center", sticky ? (depth === 0 ? "bg-[color-mix(in_oklch,var(--muted)_50%,var(--card)_50%)]" : "bg-[color-mix(in_oklch,var(--muted)_35%,var(--card)_65%)]") : depth === 0 ? "bg-muted/50" : "bg-muted/35", depth > 0 && "pl-4")}>
77
+ <div className={cn("flex h-full items-center", sticky ? groupHeaderSurfaceClass(depth) : depth === 0 ? "bg-muted/50" : "bg-muted/35", depth > 0 && "pl-4")}>
67
78
  {showSelect && (
68
79
  <Checkbox
69
80
  checked={selectState === true}
@@ -16,7 +16,7 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
19
- import { defaultRangeExtractor, useVirtualizer } from "@tanstack/react-virtual";
19
+ import { useVirtualizer } from "@tanstack/react-virtual";
20
20
  import { Plus } from "lucide-react";
21
21
  import {
22
22
  createStoryFn,
@@ -31,10 +31,13 @@ import type { StoryState, StoryPriority } from "@/shared/story/constants";
31
31
  import type { DisplayOptions } from "../../display/types";
32
32
  import type { PagedGroupNode } from "../../display/use-paged-story-groups";
33
33
  import { BUTTON_ROW_HEIGHT, useFlatRows } from "./use-flat-rows";
34
- import { VirtualGroupHeader, GroupTailRow } from "./group";
34
+ import { VirtualGroupHeader, GroupTailRow, groupHeaderSurfaceClass } from "./group";
35
35
  import { StoryListRow } from "./row";
36
36
  import { BatchToolbar } from "./batch-toolbar";
37
- import { computeFrozenHeader, type FrozenHeader } from "@/components/shared-kit/list-kit/frozen-header";
37
+ // 任务 22:吸附组头判定统一走共享 computeFrozenHeader(等价原生 sticky:
38
+ // 活跃才渲染副本、被下个组头逐像素顶出、边界不留容差),替代旧的
39
+ // 「滚动容器外常驻冻结层」(固定 top、无顶出、threshold+1 容差——违规存量)。
40
+ import { computeFrozenHeader, type FrozenHeaderResult } from "@/components/shared-kit/list-kit/frozen-header";
38
41
  import { computeMove, canMove } from "@/components/shared-kit/list-kit/move";
39
42
  import { computeGroupCheckState, toggleGroupSelection } from "./group-select";
40
43
  import { GROUP_FIELD, toGroupFieldPatch, type GroupField, type StoryPatch } from "../../display/group-fields";
@@ -43,6 +46,7 @@ import { WavePhysicsLoader } from "@/components/shared-kit/loading/wave-physics-
43
46
  import { useDeferredFlag } from "@/components/shared-kit/loading/use-deferred-flag";
44
47
  import { m } from "@/paraglide/messages.js";
45
48
  import { toastError } from "@/lib/toast-error";
49
+ import { cn } from "@/lib/utils";
46
50
 
47
51
  const HEADER_ROW_HEIGHT = 36;
48
52
  const STORY_ROW_HEIGHT = 44;
@@ -148,12 +152,13 @@ export function StoryList({ groups, isLoading = false, displayFilters, users, la
148
152
  }, [groupBy, subGroupBy, onRefresh, projectId]);
149
153
 
150
154
  const scrollRef = useRef<HTMLDivElement>(null);
151
- // 冻结组头渲染在滚动容器外层(容器外天然不受横滚影响),位置固定(组头 top:0、
152
- // 子组头 top:36),滚动时唯一变化的是「当前活跃组头索引」——仅在跨越分组边界时改变。
153
- // 因此不存每帧变化的 scrollTop,只存派生结果,并用 ref 去重:组内连续滚动零 setState
154
- // → 零整表重渲染,仅跨边界帧更新一次。
155
- const [frozenHeader, setFrozenHeader] = useState<FrozenHeader | null>(null);
156
- const frozenHeaderRef = useRef<FrozenHeader | null>(null);
155
+ // 吸附组头(任务 22 前为滚动容器外常驻冻结层):computeFrozenHeader 派生活跃
156
+ // (主/子)组头及顶出位移。副本仍渲染在滚动容器外层(容器外天然不受横滚影响),
157
+ // 但仅在组头越过吸附位后才出现,并被下一个组头逐像素顶出——与原生 sticky 等价。
158
+ // 不存每帧变化的 scrollTop,只存派生结果(index+offset 双值),ref 去重:
159
+ // 组内连续滚动零 setState → 零整表重渲染,仅顶出/切换帧更新。
160
+ const [frozen, setFrozen] = useState<FrozenHeaderResult | null>(null);
161
+ const frozenRef = useRef<FrozenHeaderResult | null>(null);
157
162
  const frozenRafRef = useRef<number | null>(null);
158
163
  // 滚动层纵向滚动条占位宽:冻结层右缘对齐滚动层可视右缘(非卡片右缘),
159
164
  // 否则冻结层加号比原位组头加号(sticky right-0 钉滚动条内侧)偏右一个滚动条宽。
@@ -371,8 +376,6 @@ export function StoryList({ groups, isLoading = false, displayFilters, users, la
371
376
  return map;
372
377
  }, [flatRows]);
373
378
 
374
- const activeStickyRef = useRef<{ groupIdx: number; subIdx: number | null }>({ groupIdx: 0, subIdx: null });
375
-
376
379
  const estimateSize = useCallback((i: number) => {
377
380
  const row = flatRows[i];
378
381
  if (!row) return HEADER_ROW_HEIGHT;
@@ -386,46 +389,40 @@ export function StoryList({ groups, isLoading = false, displayFilters, users, la
386
389
  getScrollElement: () => scrollRef.current,
387
390
  estimateSize,
388
391
  overscan: OVERSCAN,
389
- rangeExtractor: useCallback(
390
- (range) => {
391
- const activeGroupIdx = [...groupHeaderIndexes].reverse().find((i) => range.startIndex >= i);
392
- let activeSubIdx: number | null = null;
393
- if (activeGroupIdx != null) {
394
- const candidate = [...subGroupHeaderIndexes].reverse().find((i) => i <= range.startIndex);
395
- if (candidate != null && parentGroupOfSub.get(candidate) === activeGroupIdx) {
396
- activeSubIdx = candidate;
397
- }
398
- }
399
- activeStickyRef.current = { groupIdx: activeGroupIdx ?? 0, subIdx: activeSubIdx };
400
- const idx = new Set(defaultRangeExtractor(range));
401
- if (activeGroupIdx != null) idx.add(activeGroupIdx);
402
- if (activeSubIdx != null) idx.add(activeSubIdx);
403
- return [...idx].sort((a, b) => a - b);
404
- },
405
- [groupHeaderIndexes, subGroupHeaderIndexes, parentGroupOfSub],
406
- ),
392
+ // 任务 22:不再用 rangeExtractor 强留活跃组头行——吸附层从 flatRows 数据直接
393
+ // 渲染(与 kanban 同构),在流行按虚拟窗口自然进出(越顶的活跃行被吸附副本遮挡)
407
394
  enabled: flatRows.length > 0,
408
395
  });
409
396
  const virtualRows = rowVirtualizer.getVirtualItems();
410
397
  const totalHeight = rowVirtualizer.getTotalSize();
411
398
  const remeasureRows = useCallback(() => rowVirtualizer.measure(), [rowVirtualizer]);
412
399
 
413
- // 冻结组头派生:读取当前滚动位置算出活跃(主/子)组头,仅在结果变化时提交 state
400
+ // 吸附组头派生:读取当前滚动位置算出活跃(主/子)组头与顶出位移,仅在结果
401
+ // (index+offset 双值)变化时提交 state
414
402
  const recomputeFrozenHeader = useCallback(() => {
415
403
  frozenRafRef.current = null;
416
404
  const el = scrollRef.current;
417
405
  if (!el) return;
418
- const next = computeFrozenHeader(
419
- el.scrollTop,
420
- rowVirtualizer.measurementsCache,
421
- groupHeaderIndexes,
422
- subGroupHeaderIndexes,
406
+ const next = computeFrozenHeader(el.scrollTop, rowVirtualizer.measurementsCache, {
407
+ groupIndexes: groupHeaderIndexes,
408
+ subIndexes: subGroupHeaderIndexes,
423
409
  parentGroupOfSub,
424
- );
425
- const prev = frozenHeaderRef.current;
426
- if (prev?.groupIdx === next?.groupIdx && prev?.subIdx === next?.subIdx) return;
427
- frozenHeaderRef.current = next;
428
- setFrozenHeader(next);
410
+ // 滚动容器 overflow-auto 无上内边距:测量原点 = 滚动口顶
411
+ pad: 0,
412
+ // 子组头吸附在吸附主组头正下方(组头行高 36)
413
+ subPinTop: HEADER_ROW_HEIGHT,
414
+ });
415
+ const prev = frozenRef.current;
416
+ if (
417
+ prev?.group?.index === next.group?.index &&
418
+ prev?.group?.offset === next.group?.offset &&
419
+ prev?.sub?.index === next.sub?.index &&
420
+ prev?.sub?.offset === next.sub?.offset
421
+ ) {
422
+ return;
423
+ }
424
+ frozenRef.current = next;
425
+ setFrozen(next);
429
426
  }, [rowVirtualizer, groupHeaderIndexes, subGroupHeaderIndexes, parentGroupOfSub]);
430
427
 
431
428
  // rAF 节流:每帧最多重算一次(滚动事件可能一帧多次触发)
@@ -467,7 +464,7 @@ export function StoryList({ groups, isLoading = false, displayFilters, users, la
467
464
  <div
468
465
  key={row.key}
469
466
  style={{ ...baseStyle, height: HEADER_ROW_HEIGHT }}
470
- className={`flex ${row.depth === 0 ? "bg-[color-mix(in_oklch,var(--muted)_50%,var(--card)_50%)]" : "bg-[color-mix(in_oklch,var(--muted)_35%,var(--card)_65%)]"}`}
467
+ className={cn("flex", groupHeaderSurfaceClass(row.depth))}
471
468
  >
472
469
  <div className="sticky left-0 h-full w-fit">
473
470
  <VirtualGroupHeader
@@ -546,31 +543,27 @@ export function StoryList({ groups, isLoading = false, displayFilters, users, la
546
543
  return null;
547
544
  };
548
545
 
549
- const renderFrozenHeader = (idx: number, top: number, depth: number, zIndex: number) => {
546
+ // 吸附层组头内容渲染:与在流组头同一渲染单元(VirtualGroupHeader 同 props 形态,
547
+ // sticky 实色变体),保证吸附态与在流态视觉与交互完全一致(kanban renderSubHeaderButton 同规)
548
+ const renderFrozenHeaderRow = (idx: number, depth: 0 | 1) => {
550
549
  const row = flatRows[idx];
551
550
  if (!row || row.kind !== "header") return null;
552
551
  return (
553
- <div
554
- key={row.key}
555
- className="absolute overflow-hidden"
556
- style={{ top, left: 0, right: scrollbarW, zIndex, height: HEADER_ROW_HEIGHT }}
557
- >
558
- <VirtualGroupHeader
559
- node={row.node}
560
- onRetry={onRefresh}
561
- depth={row.depth}
562
- collapsed={row.isCollapsed}
563
- collapseKey={row.key}
564
- onCreate={onCreateByKey.get(row.node.key)}
565
- onToggle={toggleCollapse}
566
- sticky
567
- selectState={computeGroupCheckState(groupStoryIds.get(row.key) ?? [], selectedIds)}
568
- onToggleSelect={() => toggleGroup(row.key)}
569
- showSelect={selectedIds.size > 0}
570
- groupDimension={depth === 0 ? groupBy : subGroupBy}
571
- showRoleLabel={showRoleLabel}
572
- />
573
- </div>
552
+ <VirtualGroupHeader
553
+ node={row.node}
554
+ onRetry={onRefresh}
555
+ depth={row.depth}
556
+ collapsed={row.isCollapsed}
557
+ collapseKey={row.key}
558
+ onCreate={onCreateByKey.get(row.node.key)}
559
+ onToggle={toggleCollapse}
560
+ sticky
561
+ selectState={computeGroupCheckState(groupStoryIds.get(row.key) ?? [], selectedIds)}
562
+ onToggleSelect={() => toggleGroup(row.key)}
563
+ showSelect={selectedIds.size > 0}
564
+ groupDimension={depth === 0 ? groupBy : subGroupBy}
565
+ showRoleLabel={showRoleLabel}
566
+ />
574
567
  );
575
568
  };
576
569
 
@@ -587,11 +580,51 @@ export function StoryList({ groups, isLoading = false, displayFilters, users, la
587
580
  </div>
588
581
  )}
589
582
  </div>
590
- {frozenHeader && (
591
- <>
592
- {frozenHeader.subIdx != null && renderFrozenHeader(frozenHeader.subIdx, HEADER_ROW_HEIGHT, 1, 9)}
593
- {renderFrozenHeader(frozenHeader.groupIdx, 0, 0, 10)}
594
- </>
583
+ {/* 吸附组头副本(任务 22,替代旧常驻冻结层):仅在组头越过吸附位后渲染
584
+ (computeFrozenHeader),并被下一个组头逐像素顶出(translateY 负值),
585
+ 与原生 sticky 等价。结构照 kanban 吸附层同款:
586
+ - 外层 pointer-events-none + overflow-hidden:顶出越界部分裁在滚动口顶
587
+ - 满宽实色底色盒(groupHeaderSurfaceClass,与在流行同源)无纵向内边距
588
+ ——纵向范围 = 组头行自身盒子(高 HEADER_ROW_HEIGHT),多留内边距会把
589
+ 紧邻首行顶部一起盖住;上方无需遮挡带(未越过时本层根本不渲染)
590
+ - 内容包 pointer-events-auto:组头可点(折叠/勾选/创建),带外点击穿透
591
+ - right 预留纵向滚动条宽:与在流组头 sticky right-0 加号对齐
592
+ - 子组头层在下、主组头层在上(z 9/10):组切换顶出阶段两层同带滑出,
593
+ 露出主组头(旧常驻层 z 序,行为保持) */}
594
+ {frozen && (frozen.group || frozen.sub) && (
595
+ <div
596
+ data-slot="story-frozen-headers"
597
+ className="pointer-events-none absolute inset-0 z-10 overflow-hidden"
598
+ style={{ right: scrollbarW }}
599
+ >
600
+ {frozen.sub && (
601
+ <div
602
+ className={cn("absolute inset-x-0 z-[9]", groupHeaderSurfaceClass(1))}
603
+ style={{
604
+ top: HEADER_ROW_HEIGHT,
605
+ height: HEADER_ROW_HEIGHT,
606
+ transform: `translateY(${frozen.sub.offset}px)`,
607
+ }}
608
+ >
609
+ <div className="pointer-events-auto h-full">
610
+ {renderFrozenHeaderRow(frozen.sub.index, 1)}
611
+ </div>
612
+ </div>
613
+ )}
614
+ {frozen.group && (
615
+ <div
616
+ className={cn("absolute inset-x-0 top-0 z-10", groupHeaderSurfaceClass(0))}
617
+ style={{
618
+ height: HEADER_ROW_HEIGHT,
619
+ transform: `translateY(${frozen.group.offset}px)`,
620
+ }}
621
+ >
622
+ <div className="pointer-events-auto h-full">
623
+ {renderFrozenHeaderRow(frozen.group.index, 0)}
624
+ </div>
625
+ </div>
626
+ )}
627
+ </div>
595
628
  )}
596
629
  {!isLoading && groups.length === 0 && (
597
630
  <div className="flex h-40 items-center justify-center text-sm text-muted-foreground">
@@ -28,8 +28,12 @@ vi.mock("@tanstack/react-start", () => ({
28
28
  useServerFn: (fn: unknown) => fn,
29
29
  }));
30
30
 
31
- // 虚拟化桩:默认全量 items(断言行渲染契约);窗口化测试置 stub=false 走真实实现
32
- const virtualState = vi.hoisted(() => ({ stub: true }));
31
+ // 虚拟化桩:默认全量 items(断言行渲染契约);窗口化测试置 stub=false 走真实实现。
32
+ // scrollToIndex 桩透传 virtualState.scrollToIndex(默认 no-op),供 ↑↓ 定位断言注入。
33
+ const virtualState = vi.hoisted(() => ({
34
+ stub: true,
35
+ scrollToIndex: null as null | ((index: number, options?: { align?: string }) => void),
36
+ }));
33
37
  vi.mock("@tanstack/react-virtual", async (importOriginal) => {
34
38
  const actual = await importOriginal<typeof import("@tanstack/react-virtual")>();
35
39
  return {
@@ -46,6 +50,8 @@ vi.mock("@tanstack/react-virtual", async (importOriginal) => {
46
50
  })),
47
51
  getTotalSize: () => opts.count * 44,
48
52
  measure: () => {},
53
+ scrollToIndex: (index: number, options?: { align?: string }) =>
54
+ virtualState.scrollToIndex?.(index, options),
49
55
  };
50
56
  }
51
57
  return actual.useVirtualizer(opts);
@@ -164,6 +170,7 @@ beforeEach(() => {
164
170
  vi.clearAllMocks();
165
171
  updateTestCaseFn.mockResolvedValue({ ...baseCase });
166
172
  intersectionFire = null;
173
+ virtualState.scrollToIndex = null;
167
174
  vi.stubGlobal("IntersectionObserver", MockIntersectionObserver);
168
175
  });
169
176
 
@@ -447,6 +454,39 @@ describe("GroupedList 分组虚拟列表", () => {
447
454
  expect(onSelect).toHaveBeenLastCalledWith("c1");
448
455
  });
449
456
 
457
+ it("↑↓ 定位行走 scrollToIndex(任务 21):选中照旧,定位传扁平行 index(含组头偏移)+ align auto", () => {
458
+ const onSelect = vi.fn();
459
+ const scrollToIndex = vi.fn();
460
+ virtualState.scrollToIndex = scrollToIndex;
461
+ // groupBy none:扁平行 = 用例行,c2 的 flat index = 1
462
+ renderList({ selectedId: "c1", onSelect });
463
+ const scrollEl = screen.getByTestId("grouped-list-scroll");
464
+ fireEvent.keyDown(scrollEl, { key: "ArrowDown" });
465
+ expect(onSelect).toHaveBeenLastCalledWith("c2");
466
+ expect(scrollToIndex).toHaveBeenLastCalledWith(1, { align: "auto" });
467
+ fireEvent.keyDown(scrollEl, { key: "ArrowUp" });
468
+ expect(scrollToIndex).toHaveBeenLastCalledWith(0, { align: "auto" });
469
+ // 首行上移保持:选中停在 c1,仍定位 index 0
470
+ fireEvent.keyDown(scrollEl, { key: "ArrowUp" });
471
+ expect(scrollToIndex).toHaveBeenLastCalledWith(0, { align: "auto" });
472
+ });
473
+
474
+ it("↑↓ 定位 flat index 含组头偏移:groupBy state 下目标行 index 计入其组头", () => {
475
+ const onSelect = vi.fn();
476
+ const scrollToIndex = vi.fn();
477
+ virtualState.scrollToIndex = scrollToIndex;
478
+ const groups: TestCaseGroupInfo[] = [
479
+ { key: "approved", label: "approved", count: 2 },
480
+ { key: "draft", label: "draft", count: 1 },
481
+ ];
482
+ // 扁平顺序:header(approved)=0, c1=1, c3=2, header(draft)=3, c2=4
483
+ // caseIds=[c1,c3,c2];从 c3 下移到 c2:caseIds index 2 ≠ flat index 4(差两个组头)
484
+ renderList({ groupBy: "state", groups, selectedId: "c3", onSelect });
485
+ fireEvent.keyDown(screen.getByTestId("grouped-list-scroll"), { key: "ArrowDown" });
486
+ expect(onSelect).toHaveBeenLastCalledWith("c2");
487
+ expect(scrollToIndex).toHaveBeenLastCalledWith(4, { align: "auto" });
488
+ });
489
+
450
490
  it("窗口化渲染计数断言(1k 性能口径替代):真实虚拟化下 DOM 行数远小于总行数", () => {
451
491
  virtualState.stub = false;
452
492
  // virtual-core 的 getRect 读视口尺寸(非 gBCR):3.19.5 及以前读 offsetWidth/
@@ -484,4 +524,96 @@ describe("GroupedList 分组虚拟列表", () => {
484
524
  virtualState.stub = true;
485
525
  }
486
526
  });
527
+
528
+ it("定位行虚拟窗口外一步到位(任务 21 验收,旧 querySelector 实现在此静默失效):scrollToIndex 按测量值写 scrollTop", () => {
529
+ virtualState.stub = false;
530
+ let fireResize: () => void = () => {};
531
+ class CapturableResizeObserver {
532
+ constructor(cb: ResizeObserverCallback) {
533
+ fireResize = () => cb([], this as unknown as ResizeObserver);
534
+ }
535
+ observe() {}
536
+ unobserve() {}
537
+ disconnect() {}
538
+ }
539
+ vi.stubGlobal("ResizeObserver", CapturableResizeObserver);
540
+ try {
541
+ const big = Array.from({ length: 1000 }, (_, i) =>
542
+ makeCase({ id: `c${i}`, identifier: `PERF-TC${i + 1}` }),
543
+ );
544
+ const { container } = render(<GroupedList groupBy="none" groups={[]} cases={big} folders={folders} users={users} hasMore={false} selectedId="c799" onSelect={vi.fn()} onPatchCase={vi.fn()} onLoadMore={vi.fn()} />);
545
+ const scrollEl = container.querySelector<HTMLElement>('[data-testid="grouped-list-scroll"]')!;
546
+ Object.defineProperty(scrollEl, "offsetHeight", { configurable: true, value: 600 });
547
+ Object.defineProperty(scrollEl, "offsetWidth", { configurable: true, value: 800 });
548
+ Object.defineProperty(scrollEl, "clientHeight", { configurable: true, value: 600 });
549
+ Object.defineProperty(scrollEl, "clientWidth", { configurable: true, value: 800 });
550
+ // scrollPaddingStart 落点会被 getMaxScrollOffset(scrollHeight − clientHeight)钳制,
551
+ // jsdom 无布局 scrollHeight 恒 0——注入内容总高,令钳制上限真实(44000 − 600)
552
+ Object.defineProperty(scrollEl, "scrollHeight", { configurable: true, value: big.length * 44 });
553
+ // jsdom 无 Element.scrollTo(virtual-core elementScroll 静默 no-op):桩捕获 scrollTop 写入
554
+ //(scrollTo 带 (x, y) 重载,桩按 options 签名注入需收窄赋值)
555
+ const scrollToSpy = vi.fn<(options?: ScrollToOptions) => void>();
556
+ scrollEl.scrollTo = scrollToSpy as unknown as typeof scrollEl.scrollTo;
557
+ act(() => {
558
+ fireResize();
559
+ });
560
+ // 前置:目标行 c800(flat index 800)在窗口外,DOM 节点不存在——
561
+ // 旧实现 querySelector 命中 null 即失效,新实现不依赖 DOM 节点
562
+ expect(container.querySelector('[data-case-id="c800"]')).toBeNull();
563
+ const callsBefore = scrollToSpy.mock.calls.length;
564
+ fireEvent.keyDown(scrollEl, { key: "ArrowDown" });
565
+ // 一次 scrollTo 到位(无 rAF 二跳):align auto 向下越界 → 行底对齐视口底
566
+ //(scrollPaddingEnd 0),top = 行 end − 视口高 = 801*44 − 600
567
+ expect(scrollToSpy.mock.calls.length).toBe(callsBefore + 1);
568
+ expect(scrollToSpy).toHaveBeenLastCalledWith({ top: 801 * 44 - 600, behavior: "auto" });
569
+ } finally {
570
+ virtualState.stub = true;
571
+ }
572
+ });
573
+
574
+ it("定位行窗口内零滚动(等价旧 nearest,裁决 T21-2):行已可见时仅写当前 offset 无位移", () => {
575
+ virtualState.stub = false;
576
+ let fireResize: () => void = () => {};
577
+ class CapturableResizeObserver {
578
+ constructor(cb: ResizeObserverCallback) {
579
+ fireResize = () => cb([], this as unknown as ResizeObserver);
580
+ }
581
+ observe() {}
582
+ unobserve() {}
583
+ disconnect() {}
584
+ }
585
+ vi.stubGlobal("ResizeObserver", CapturableResizeObserver);
586
+ try {
587
+ const big = Array.from({ length: 1000 }, (_, i) =>
588
+ makeCase({ id: `c${i}`, identifier: `PERF-TC${i + 1}` }),
589
+ );
590
+ const onSelect = vi.fn();
591
+ const { container } = render(<GroupedList groupBy="none" groups={[]} cases={big} folders={folders} users={users} hasMore={false} selectedId="c29" onSelect={onSelect} onPatchCase={vi.fn()} onLoadMore={vi.fn()} />);
592
+ const scrollEl = container.querySelector<HTMLElement>('[data-testid="grouped-list-scroll"]')!;
593
+ Object.defineProperty(scrollEl, "offsetHeight", { configurable: true, value: 600 });
594
+ Object.defineProperty(scrollEl, "offsetWidth", { configurable: true, value: 800 });
595
+ Object.defineProperty(scrollEl, "clientHeight", { configurable: true, value: 600 });
596
+ Object.defineProperty(scrollEl, "clientWidth", { configurable: true, value: 800 });
597
+ Object.defineProperty(scrollEl, "scrollHeight", { configurable: true, value: big.length * 44 });
598
+ const scrollToSpy = vi.fn<(options?: ScrollToOptions) => void>();
599
+ scrollEl.scrollTo = scrollToSpy as unknown as typeof scrollEl.scrollTo;
600
+ act(() => {
601
+ fireResize();
602
+ });
603
+ // 模拟滚到 1000:jsdom 赋 scrollTop 不派发 scroll 事件,手动派发让 virtualizer 读回
604
+ scrollEl.scrollTop = 1000;
605
+ act(() => {
606
+ fireEvent.scroll(scrollEl);
607
+ });
608
+ const callsBefore = scrollToSpy.mock.calls.length;
609
+ // 目标行 c30 [1320, 1364] 完全落在可视区(顶部组头补偿线 1036 之下、视口底 1600 之上):
610
+ // 旧 nearest 不滚动;auto 解析为当前 offset,写入无位移
611
+ fireEvent.keyDown(scrollEl, { key: "ArrowDown" });
612
+ expect(onSelect).toHaveBeenLastCalledWith("c30");
613
+ expect(scrollToSpy.mock.calls.length).toBe(callsBefore + 1);
614
+ expect(scrollToSpy).toHaveBeenLastCalledWith({ top: 1000, behavior: "auto" });
615
+ } finally {
616
+ virtualState.stub = true;
617
+ }
618
+ });
487
619
  });
@@ -171,6 +171,14 @@ class MockIntersectionObserver {
171
171
  disconnect() {}
172
172
  }
173
173
 
174
+ // useAppHotkeys 需要 HotkeysProvider appHotkeys 挂载;单测无全局 Provider,
175
+ import { appHotkeys } from "@/lib/hotkeys/registry"
176
+ // 局部替换该 hook 返回应用声明,其余(useHotkey/useHotkeys)保持官方真实实现
177
+ vi.mock("@tanstack/react-hotkeys", async (importOriginal) => {
178
+ const actual = await importOriginal<typeof import("@tanstack/react-hotkeys")>()
179
+ return { ...actual, useAppHotkeys: () => appHotkeys }
180
+ })
181
+
174
182
  import { TestCasePeekPanel } from "../peek-panel";
175
183
  import { TestCaseListView } from "../list-view";
176
184
  import type { TestCaseSummary } from "@/server/serverFns/testcase/types";