@notis_ai/cli 0.2.13 → 0.2.15

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 (295) hide show
  1. package/README.md +125 -21
  2. package/dist/agent-hooks/notis-agent-hook.mjs +19750 -0
  3. package/{skills → dist/base-skills}/notis-apps/SKILL.md +93 -58
  4. package/{skills → dist/base-skills}/notis-cli/SKILL.md +9 -9
  5. package/dist/base-skills/notis-query/SKILL.md +705 -0
  6. package/dist/skill-sync/index.js +1528 -0
  7. package/dist/skill-sync/index.js.map +7 -0
  8. package/package.json +5 -2
  9. package/skills/notis-apps/cli.md +44 -18
  10. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  11. package/skills/notis-onboarding/BRIEF.md +16 -0
  12. package/src/agent-hook-entry.js +5 -0
  13. package/src/cli.js +23 -14
  14. package/src/command-specs/agents.js +392 -0
  15. package/src/command-specs/apps.js +1090 -418
  16. package/src/command-specs/auth.js +63 -10
  17. package/src/command-specs/handover.js +374 -0
  18. package/src/command-specs/index.js +9 -0
  19. package/src/command-specs/onboarding.js +69 -5
  20. package/src/command-specs/skills.js +56 -0
  21. package/src/command-specs/tools.js +6 -0
  22. package/src/runtime/agent-memory-state.js +126 -0
  23. package/src/runtime/agent-setup.js +383 -0
  24. package/src/runtime/app-dev-consumers.js +154 -0
  25. package/src/runtime/app-dev-host-lock.js +80 -0
  26. package/src/runtime/app-dev-roots.js +284 -0
  27. package/src/runtime/app-dev-server.js +229 -30
  28. package/src/runtime/app-dev-sessions.js +105 -145
  29. package/src/runtime/app-platform.js +1499 -161
  30. package/src/runtime/app-registry-scaffolds.js +367 -0
  31. package/src/runtime/base-skills.d.ts +20 -0
  32. package/src/runtime/base-skills.js +167 -0
  33. package/src/runtime/delegated-context.js +68 -0
  34. package/src/runtime/git.js +233 -0
  35. package/src/runtime/login-listener.js +15 -0
  36. package/src/runtime/oauth.js +1506 -141
  37. package/src/runtime/output.js +7 -0
  38. package/src/runtime/ports.js +16 -0
  39. package/src/runtime/profiles.js +0 -5
  40. package/src/runtime/skill-sync/cloud-client.ts +96 -0
  41. package/src/runtime/skill-sync/index.ts +644 -0
  42. package/src/runtime/skill-sync/local-scanner.ts +1046 -0
  43. package/src/runtime/skill-sync/symlink-manager.ts +383 -0
  44. package/src/runtime/skill-sync/sync-plan.ts +22 -0
  45. package/src/runtime/skill-sync/types.ts +103 -0
  46. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  47. package/src/runtime/store-screenshot.js +6 -1
  48. package/src/runtime/sync-skills.d.ts +37 -0
  49. package/src/runtime/sync-skills.js +215 -0
  50. package/src/runtime/transport.js +19 -2
  51. package/template/.harness/index.html.tmpl +116 -47
  52. package/template/app/layout.tsx +5 -1
  53. package/template/notis.config.ts +1 -0
  54. package/template/packages/sdk/package.json +2 -1
  55. package/template/packages/sdk/src/components/DocumentEditor.tsx +13 -3
  56. package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
  57. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +37 -119
  58. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
  59. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
  60. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
  61. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  62. package/template/packages/sdk/src/config.ts +71 -0
  63. package/template/packages/sdk/src/documents.ts +28 -1
  64. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  65. package/template/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
  66. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +709 -0
  67. package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  68. package/template/packages/sdk/src/hooks/useHandover.ts +78 -0
  69. package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
  70. package/template/packages/sdk/src/hooks/useTool.ts +5 -4
  71. package/template/packages/sdk/src/index.ts +64 -2
  72. package/template/packages/sdk/src/interactions/actions.ts +46 -0
  73. package/template/packages/sdk/src/interactions/shortcuts.tsx +628 -0
  74. package/template/packages/sdk/src/interactions.ts +41 -0
  75. package/template/packages/sdk/src/provider.tsx +2 -1
  76. package/template/packages/sdk/src/runtime.ts +219 -3
  77. package/dist/scaffolds/notis-database/CHANGELOG.md +0 -5
  78. package/dist/scaffolds/notis-database/app/globals.css +0 -44
  79. package/dist/scaffolds/notis-database/app/layout.tsx +0 -6
  80. package/dist/scaffolds/notis-database/app/page.tsx +0 -1088
  81. package/dist/scaffolds/notis-database/components/ui/badge.tsx +0 -28
  82. package/dist/scaffolds/notis-database/components/ui/button.tsx +0 -53
  83. package/dist/scaffolds/notis-database/components/ui/card.tsx +0 -56
  84. package/dist/scaffolds/notis-database/components/ui/table.tsx +0 -120
  85. package/dist/scaffolds/notis-database/components.json +0 -20
  86. package/dist/scaffolds/notis-database/index.html +0 -12
  87. package/dist/scaffolds/notis-database/lib/types.ts +0 -132
  88. package/dist/scaffolds/notis-database/lib/utils.ts +0 -6
  89. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  90. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  91. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  92. package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
  93. package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
  94. package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +0 -1839
  95. package/dist/scaffolds/notis-database/notis.config.ts +0 -73
  96. package/dist/scaffolds/notis-database/package-lock.json +0 -3935
  97. package/dist/scaffolds/notis-database/package.json +0 -32
  98. package/dist/scaffolds/notis-database/packages/sdk/package.json +0 -36
  99. package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  100. package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +0 -60
  101. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  102. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  103. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  104. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +0 -196
  105. package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -229
  106. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
  107. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  108. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +0 -78
  109. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +0 -121
  110. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  111. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +0 -34
  112. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  113. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +0 -64
  114. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +0 -56
  115. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  116. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  117. package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +0 -83
  118. package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
  119. package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -220
  120. package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +0 -186
  121. package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +0 -15
  122. package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +0 -56
  123. package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +0 -15
  124. package/dist/scaffolds/notis-database/postcss.config.mjs +0 -8
  125. package/dist/scaffolds/notis-database/src/dev-main.tsx +0 -23
  126. package/dist/scaffolds/notis-database/src/mock-runtime.ts +0 -561
  127. package/dist/scaffolds/notis-database/tailwind.config.ts +0 -59
  128. package/dist/scaffolds/notis-database/tsconfig.json +0 -23
  129. package/dist/scaffolds/notis-database/vite.config.ts +0 -22
  130. package/dist/scaffolds/notis-journal/CHANGELOG.md +0 -29
  131. package/dist/scaffolds/notis-journal/app/globals.css +0 -37
  132. package/dist/scaffolds/notis-journal/app/insights/page.tsx +0 -513
  133. package/dist/scaffolds/notis-journal/app/journal-core.tsx +0 -362
  134. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +0 -337
  135. package/dist/scaffolds/notis-journal/app/layout.tsx +0 -6
  136. package/dist/scaffolds/notis-journal/app/page.tsx +0 -486
  137. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +0 -28
  138. package/dist/scaffolds/notis-journal/components/ui/button.tsx +0 -53
  139. package/dist/scaffolds/notis-journal/components/ui/card.tsx +0 -56
  140. package/dist/scaffolds/notis-journal/components.json +0 -20
  141. package/dist/scaffolds/notis-journal/index.html +0 -12
  142. package/dist/scaffolds/notis-journal/lib/utils.ts +0 -6
  143. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  144. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  145. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  146. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  147. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  148. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  149. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +0 -132
  150. package/dist/scaffolds/notis-journal/notis.config.ts +0 -93
  151. package/dist/scaffolds/notis-journal/package-lock.json +0 -4615
  152. package/dist/scaffolds/notis-journal/package.json +0 -34
  153. package/dist/scaffolds/notis-journal/packages/sdk/package.json +0 -36
  154. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  155. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +0 -60
  156. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  157. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  158. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  159. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +0 -196
  160. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -229
  161. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
  162. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  163. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +0 -78
  164. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +0 -121
  165. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  166. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +0 -34
  167. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  168. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +0 -64
  169. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +0 -56
  170. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  171. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  172. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +0 -83
  173. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
  174. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -220
  175. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +0 -186
  176. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +0 -15
  177. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +0 -56
  178. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +0 -15
  179. package/dist/scaffolds/notis-journal/postcss.config.mjs +0 -8
  180. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +0 -120
  181. package/dist/scaffolds/notis-journal/src/dev-main.tsx +0 -58
  182. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +0 -197
  183. package/dist/scaffolds/notis-journal/tailwind.config.ts +0 -58
  184. package/dist/scaffolds/notis-journal/tsconfig.json +0 -23
  185. package/dist/scaffolds/notis-journal/vite.config.ts +0 -10
  186. package/dist/scaffolds/notis-notes/CHANGELOG.md +0 -5
  187. package/dist/scaffolds/notis-notes/app/globals.css +0 -3
  188. package/dist/scaffolds/notis-notes/app/layout.tsx +0 -6
  189. package/dist/scaffolds/notis-notes/app/page.tsx +0 -1943
  190. package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +0 -596
  191. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +0 -28
  192. package/dist/scaffolds/notis-notes/components/ui/button.tsx +0 -53
  193. package/dist/scaffolds/notis-notes/components/ui/card.tsx +0 -56
  194. package/dist/scaffolds/notis-notes/components.json +0 -20
  195. package/dist/scaffolds/notis-notes/lib/utils.ts +0 -6
  196. package/dist/scaffolds/notis-notes/lib/visible-properties.ts +0 -144
  197. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  198. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  199. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  200. package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
  201. package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
  202. package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +0 -752
  203. package/dist/scaffolds/notis-notes/notis.config.ts +0 -80
  204. package/dist/scaffolds/notis-notes/package-lock.json +0 -4636
  205. package/dist/scaffolds/notis-notes/package.json +0 -35
  206. package/dist/scaffolds/notis-notes/packages/sdk/package.json +0 -36
  207. package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  208. package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +0 -60
  209. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  210. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  211. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  212. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +0 -196
  213. package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -229
  214. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
  215. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  216. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +0 -78
  217. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +0 -121
  218. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  219. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +0 -34
  220. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  221. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +0 -64
  222. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +0 -56
  223. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  224. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  225. package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +0 -83
  226. package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
  227. package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -220
  228. package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +0 -186
  229. package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +0 -15
  230. package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +0 -56
  231. package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +0 -15
  232. package/dist/scaffolds/notis-notes/postcss.config.mjs +0 -8
  233. package/dist/scaffolds/notis-notes/tailwind.config.ts +0 -58
  234. package/dist/scaffolds/notis-notes/tsconfig.json +0 -23
  235. package/dist/scaffolds/notis-notes/vite.config.ts +0 -10
  236. package/dist/scaffolds/notis-random/CHANGELOG.md +0 -15
  237. package/dist/scaffolds/notis-random/README.md +0 -33
  238. package/dist/scaffolds/notis-random/app/globals.css +0 -11
  239. package/dist/scaffolds/notis-random/app/history/page.tsx +0 -67
  240. package/dist/scaffolds/notis-random/app/layout.tsx +0 -7
  241. package/dist/scaffolds/notis-random/app/page.tsx +0 -289
  242. package/dist/scaffolds/notis-random/components/ui/button.tsx +0 -50
  243. package/dist/scaffolds/notis-random/components/ui/card.tsx +0 -16
  244. package/dist/scaffolds/notis-random/components/ui/input.tsx +0 -23
  245. package/dist/scaffolds/notis-random/components.json +0 -20
  246. package/dist/scaffolds/notis-random/index.html +0 -12
  247. package/dist/scaffolds/notis-random/lib/notis-tools.ts +0 -128
  248. package/dist/scaffolds/notis-random/lib/rng.ts +0 -202
  249. package/dist/scaffolds/notis-random/lib/roll-record.ts +0 -189
  250. package/dist/scaffolds/notis-random/lib/utils.ts +0 -25
  251. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  252. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  253. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  254. package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
  255. package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
  256. package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +0 -753
  257. package/dist/scaffolds/notis-random/notis.config.ts +0 -86
  258. package/dist/scaffolds/notis-random/package-lock.json +0 -4513
  259. package/dist/scaffolds/notis-random/package.json +0 -36
  260. package/dist/scaffolds/notis-random/packages/sdk/package.json +0 -36
  261. package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  262. package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +0 -60
  263. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  264. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  265. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  266. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +0 -196
  267. package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -229
  268. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
  269. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  270. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +0 -78
  271. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +0 -121
  272. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  273. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +0 -34
  274. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  275. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +0 -64
  276. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +0 -56
  277. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  278. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  279. package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +0 -83
  280. package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
  281. package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -220
  282. package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +0 -186
  283. package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +0 -15
  284. package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +0 -56
  285. package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +0 -15
  286. package/dist/scaffolds/notis-random/postcss.config.mjs +0 -6
  287. package/dist/scaffolds/notis-random/src/dev-main.tsx +0 -70
  288. package/dist/scaffolds/notis-random/src/mock-runtime.ts +0 -129
  289. package/dist/scaffolds/notis-random/tailwind.config.ts +0 -50
  290. package/dist/scaffolds/notis-random/tsconfig.json +0 -23
  291. package/dist/scaffolds/notis-random/vite.config.ts +0 -11
  292. package/dist/scaffolds.json +0 -49
  293. package/template/metadata/screenshot-1.png +0 -0
  294. package/template/metadata/screenshot-2.png +0 -0
  295. package/template/metadata/screenshot-3.png +0 -0
@@ -0,0 +1,709 @@
1
+ 'use client';
2
+
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type KeyboardEvent as ReactKeyboardEvent,
10
+ type MouseEvent as ReactMouseEvent,
11
+ } from 'react';
12
+ import {
13
+ collectionActionDefaults,
14
+ type CollectionAction,
15
+ type CollectionActionContext,
16
+ type ResolvedCollectionAction,
17
+ } from '../interactions/actions';
18
+ import {
19
+ activateShortcutCollection,
20
+ createShortcutCollectionOwner,
21
+ useShortcuts,
22
+ type ShortcutDefinition,
23
+ type ShortcutScope,
24
+ } from '../interactions/shortcuts';
25
+
26
+ export const COLLECTION_ITEM_ATTRIBUTE = 'data-notis-collection-item-id';
27
+ const LEGACY_ROW_ATTRIBUTE = 'data-notis-row-id';
28
+ const INTERACTIVE_SELECTOR = 'button, a, input, textarea, select, [role="button"], [contenteditable]:not([contenteditable="false"])';
29
+
30
+ export interface SelectionMarqueeRect {
31
+ left: number;
32
+ top: number;
33
+ width: number;
34
+ height: number;
35
+ }
36
+
37
+ export type CollectionInteractionReason =
38
+ | 'activate'
39
+ | 'clear'
40
+ | 'drag'
41
+ | 'items-changed'
42
+ | 'navigate'
43
+ | 'range'
44
+ | 'select-all'
45
+ | 'toggle';
46
+
47
+ export interface CollectionSelectionChange<T> {
48
+ reason: CollectionInteractionReason;
49
+ selectedIds: ReadonlySet<string>;
50
+ selectedItems: T[];
51
+ anchorId: string | null;
52
+ }
53
+
54
+ export type CollectionNavigationDirection = 'next' | 'previous' | 'up' | 'down' | 'left' | 'right';
55
+
56
+ export interface CollectionNavigationContext<T> {
57
+ direction: CollectionNavigationDirection;
58
+ currentId: string | null;
59
+ items: T[];
60
+ orderedIds: string[];
61
+ }
62
+
63
+ export interface CollectionKeyboardShortcuts {
64
+ clear: string | false;
65
+ selectAll: string | false;
66
+ toggle: string | false;
67
+ extendNext: string | false;
68
+ extendPrevious: string | false;
69
+ next: string | string[] | false;
70
+ previous: string | string[] | false;
71
+ up: string | false;
72
+ down: string | false;
73
+ left: string | false;
74
+ right: string | false;
75
+ activate: string | false;
76
+ }
77
+
78
+ export interface UseCollectionInteractionsOptions<T> {
79
+ items: T[];
80
+ getId: (item: T) => string;
81
+ selectionMode?: 'multiple' | 'single' | 'none';
82
+ selectedIds?: ReadonlySet<string>;
83
+ defaultSelectedIds?: Iterable<string>;
84
+ onSelectionChange?: (change: CollectionSelectionChange<T>) => void;
85
+ activeId?: string | null;
86
+ defaultActiveId?: string | null;
87
+ onActiveIdChange?: (id: string | null, reason: CollectionInteractionReason) => void;
88
+ anchorId?: string | null;
89
+ defaultAnchorId?: string | null;
90
+ onAnchorIdChange?: (id: string | null, reason: CollectionInteractionReason) => void;
91
+ onActivate?: (item: T, id: string) => void;
92
+ isItemDisabled?: (item: T) => boolean;
93
+ enabled?: boolean;
94
+ bindKeyboardShortcuts?: boolean;
95
+ enableDragSelect?: boolean;
96
+ /** Plain item clicks activate the item and clear checkbox selection by default. */
97
+ clearSelectionOnPlainClick?: boolean;
98
+ dragThreshold?: number;
99
+ shortcuts?: Partial<CollectionKeyboardShortcuts>;
100
+ shortcutScope?: ShortcutScope;
101
+ shortcutPriority?: number;
102
+ resolveNextId?: (context: CollectionNavigationContext<T>) => string | null;
103
+ onActiveItemChange?: (id: string | null) => void;
104
+ actions?: CollectionAction<T>[];
105
+ }
106
+
107
+ export interface CollectionInteractionController<T> {
108
+ selectedIds: ReadonlySet<string>;
109
+ selectedCount: number;
110
+ activeId: string | null;
111
+ anchorId: string | null;
112
+ /** Compatibility aliases used by the original multi-select API. */
113
+ headId: string | null;
114
+ lastClickedId: string | null;
115
+ dragRect: SelectionMarqueeRect | null;
116
+ actions: ResolvedCollectionAction[];
117
+
118
+ isSelected: (id: string) => boolean;
119
+ getSelectedItems: () => T[];
120
+ setActive: (id: string | null, reason?: CollectionInteractionReason) => void;
121
+ activate: (id: string) => void;
122
+ toggle: (id: string) => void;
123
+ select: (ids: string[]) => void;
124
+ clear: () => void;
125
+ selectAll: () => void;
126
+ selectRange: (anchorId: string, targetId: string) => void;
127
+
128
+ onCheckboxClick: (id: string) => (event: ReactMouseEvent) => void;
129
+ onRowMouseDown: (id: string) => (event: ReactMouseEvent) => void;
130
+ getRowProps: (id: string) => Record<string, string>;
131
+ getItemProps: (id: string) => {
132
+ [COLLECTION_ITEM_ATTRIBUTE]: string;
133
+ [LEGACY_ROW_ATTRIBUTE]: string;
134
+ tabIndex: number;
135
+ 'aria-selected': boolean;
136
+ 'aria-disabled': boolean;
137
+ 'data-notis-active': 'true' | 'false';
138
+ onFocus: () => void;
139
+ onMouseDown: (event: ReactMouseEvent) => void;
140
+ onClick: (event: ReactMouseEvent) => void;
141
+ onKeyDown: (event: ReactKeyboardEvent) => void;
142
+ };
143
+ getCheckboxProps: (id: string) => {
144
+ isSelected: boolean;
145
+ disabled: boolean;
146
+ onClick: (event: ReactMouseEvent) => void;
147
+ };
148
+ getContainerProps: () => {
149
+ ref: (node: HTMLElement | null) => void;
150
+ onMouseDown: (event: ReactMouseEvent) => void;
151
+ onFocusCapture: () => void;
152
+ onClickCapture: (event: ReactMouseEvent) => void;
153
+ style: { userSelect: 'none' };
154
+ };
155
+ }
156
+
157
+ const DEFAULT_SHORTCUTS: CollectionKeyboardShortcuts = {
158
+ clear: 'Escape',
159
+ selectAll: 'Mod+A',
160
+ toggle: 'X',
161
+ extendNext: 'Shift+ArrowDown',
162
+ extendPrevious: 'Shift+ArrowUp',
163
+ next: 'J',
164
+ previous: 'K',
165
+ up: 'ArrowUp',
166
+ down: 'ArrowDown',
167
+ left: false,
168
+ right: false,
169
+ activate: 'Enter',
170
+ };
171
+
172
+ function setsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
173
+ if (a.size !== b.size) return false;
174
+ for (const id of a) if (!b.has(id)) return false;
175
+ return true;
176
+ }
177
+
178
+ function rectsIntersect(
179
+ a: { left: number; top: number; right: number; bottom: number },
180
+ b: { left: number; top: number; right: number; bottom: number },
181
+ ): boolean {
182
+ return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
183
+ }
184
+
185
+ export function useCollectionInteractions<T>(
186
+ options: UseCollectionInteractionsOptions<T>,
187
+ ): CollectionInteractionController<T> {
188
+ const {
189
+ items,
190
+ getId,
191
+ selectionMode = 'multiple',
192
+ selectedIds: controlledSelectedIds,
193
+ defaultSelectedIds,
194
+ onSelectionChange,
195
+ activeId: controlledActiveId,
196
+ defaultActiveId = null,
197
+ onActiveIdChange,
198
+ anchorId: controlledAnchorId,
199
+ defaultAnchorId,
200
+ onAnchorIdChange,
201
+ onActivate,
202
+ isItemDisabled,
203
+ enabled = true,
204
+ bindKeyboardShortcuts = true,
205
+ enableDragSelect = selectionMode === 'multiple',
206
+ clearSelectionOnPlainClick = true,
207
+ dragThreshold = 5,
208
+ shortcuts: shortcutOverrides,
209
+ shortcutScope = 'collection',
210
+ shortcutPriority = 0,
211
+ resolveNextId,
212
+ onActiveItemChange,
213
+ actions: actionDefinitions = [],
214
+ } = options;
215
+
216
+ const [uncontrolledSelectedIds, setUncontrolledSelectedIds] = useState<Set<string>>(
217
+ () => new Set(defaultSelectedIds ?? []),
218
+ );
219
+ const selectedIds = controlledSelectedIds ?? uncontrolledSelectedIds;
220
+ const [uncontrolledActiveId, setUncontrolledActiveId] = useState<string | null>(defaultActiveId);
221
+ const activeId = controlledActiveId === undefined ? uncontrolledActiveId : controlledActiveId;
222
+ const [uncontrolledAnchorId, setUncontrolledAnchorId] = useState<string | null>(
223
+ defaultAnchorId === undefined ? defaultActiveId : defaultAnchorId,
224
+ );
225
+ const anchorId = controlledAnchorId === undefined ? uncontrolledAnchorId : controlledAnchorId;
226
+ const [dragRect, setDragRect] = useState<SelectionMarqueeRect | null>(null);
227
+
228
+ const itemsRef = useRef(items);
229
+ const getIdRef = useRef(getId);
230
+ const selectedIdsRef = useRef(selectedIds);
231
+ const activeIdRef = useRef(activeId);
232
+ const anchorIdRef = useRef(anchorId);
233
+ const onSelectionChangeRef = useRef(onSelectionChange);
234
+ const onActiveIdChangeRef = useRef(onActiveIdChange);
235
+ const onAnchorIdChangeRef = useRef(onAnchorIdChange);
236
+ const onActiveItemChangeRef = useRef(onActiveItemChange);
237
+ const onActivateRef = useRef(onActivate);
238
+ const resolveNextIdRef = useRef(resolveNextId);
239
+ const containerRef = useRef<HTMLElement | null>(null);
240
+ const shortcutCollectionOwnerRef = useRef<string | null>(null);
241
+ if (!shortcutCollectionOwnerRef.current) {
242
+ shortcutCollectionOwnerRef.current = createShortcutCollectionOwner();
243
+ }
244
+
245
+ itemsRef.current = items;
246
+ getIdRef.current = getId;
247
+ selectedIdsRef.current = selectedIds;
248
+ activeIdRef.current = activeId;
249
+ anchorIdRef.current = anchorId;
250
+ onSelectionChangeRef.current = onSelectionChange;
251
+ onActiveIdChangeRef.current = onActiveIdChange;
252
+ onAnchorIdChangeRef.current = onAnchorIdChange;
253
+ onActiveItemChangeRef.current = onActiveItemChange;
254
+ onActivateRef.current = onActivate;
255
+ resolveNextIdRef.current = resolveNextId;
256
+
257
+ const itemById = useMemo(() => new Map(items.map((item) => [getId(item), item])), [getId, items]);
258
+ const selectableIds = useMemo(
259
+ () => items.filter((item) => !isItemDisabled?.(item)).map((item) => getId(item)),
260
+ [getId, isItemDisabled, items],
261
+ );
262
+ const selectableIdsRef = useRef(selectableIds);
263
+ selectableIdsRef.current = selectableIds;
264
+
265
+ const getItemsForIds = useCallback((ids: ReadonlySet<string>) => {
266
+ return itemsRef.current.filter((item) => ids.has(getIdRef.current(item)));
267
+ }, []);
268
+
269
+ const commitSelection = useCallback((next: Set<string>, reason: CollectionInteractionReason) => {
270
+ const allowed = new Set(selectableIdsRef.current);
271
+ const normalized = selectionMode === 'none'
272
+ ? new Set<string>()
273
+ : new Set(Array.from(next).filter((id) => allowed.has(id)));
274
+ if (selectionMode === 'single' && normalized.size > 1) {
275
+ const first = normalized.values().next().value as string | undefined;
276
+ normalized.clear();
277
+ if (first) normalized.add(first);
278
+ }
279
+ if (setsEqual(normalized, selectedIdsRef.current)) return;
280
+ selectedIdsRef.current = normalized;
281
+ if (controlledSelectedIds === undefined) setUncontrolledSelectedIds(normalized);
282
+ onSelectionChangeRef.current?.({
283
+ reason,
284
+ selectedIds: normalized,
285
+ selectedItems: getItemsForIds(normalized),
286
+ anchorId: anchorIdRef.current,
287
+ });
288
+ }, [controlledSelectedIds, getItemsForIds, selectionMode]);
289
+
290
+ const setActive = useCallback((id: string | null, reason: CollectionInteractionReason = 'activate') => {
291
+ const next = id && selectableIdsRef.current.includes(id) ? id : null;
292
+ if (activeIdRef.current === next) return;
293
+ activeIdRef.current = next;
294
+ if (controlledActiveId === undefined) setUncontrolledActiveId(next);
295
+ onActiveIdChangeRef.current?.(next, reason);
296
+ onActiveItemChangeRef.current?.(next);
297
+ }, [controlledActiveId]);
298
+
299
+ const setAnchor = useCallback((id: string | null, reason: CollectionInteractionReason) => {
300
+ if (anchorIdRef.current === id) return;
301
+ anchorIdRef.current = id;
302
+ if (controlledAnchorId === undefined) setUncontrolledAnchorId(id);
303
+ onAnchorIdChangeRef.current?.(id, reason);
304
+ }, [controlledAnchorId]);
305
+
306
+ const activate = useCallback((id: string) => {
307
+ const item = itemsRef.current.find((candidate) => getIdRef.current(candidate) === id);
308
+ if (!item || !selectableIdsRef.current.includes(id)) return;
309
+ setActive(id, 'activate');
310
+ setAnchor(id, 'activate');
311
+ onActivateRef.current?.(item, id);
312
+ }, [setActive, setAnchor]);
313
+
314
+ const toggle = useCallback((id: string) => {
315
+ if (selectionMode === 'none' || !selectableIdsRef.current.includes(id)) return;
316
+ const next = new Set(selectedIdsRef.current);
317
+ if (next.has(id)) next.delete(id);
318
+ else {
319
+ if (selectionMode === 'single') next.clear();
320
+ next.add(id);
321
+ }
322
+ setAnchor(id, 'toggle');
323
+ commitSelection(next, 'toggle');
324
+ }, [commitSelection, selectionMode, setAnchor]);
325
+
326
+ const select = useCallback((ids: string[]) => {
327
+ const next = new Set(ids);
328
+ const last = ids[ids.length - 1] ?? null;
329
+ setAnchor(last, 'toggle');
330
+ commitSelection(next, 'toggle');
331
+ }, [commitSelection, setAnchor]);
332
+
333
+ const clear = useCallback(() => {
334
+ setAnchor(null, 'clear');
335
+ commitSelection(new Set(), 'clear');
336
+ }, [commitSelection, setAnchor]);
337
+
338
+ const selectAll = useCallback(() => {
339
+ if (selectionMode === 'none') return;
340
+ const ids = selectionMode === 'single' ? selectableIdsRef.current.slice(0, 1) : selectableIdsRef.current;
341
+ const last = ids[ids.length - 1] ?? null;
342
+ setAnchor(last, 'select-all');
343
+ commitSelection(new Set(ids), 'select-all');
344
+ }, [commitSelection, selectionMode, setAnchor]);
345
+
346
+ const replaceRange = useCallback((fromId: string, toId: string, reason: CollectionInteractionReason) => {
347
+ const ids = selectableIdsRef.current;
348
+ const from = ids.indexOf(fromId);
349
+ const to = ids.indexOf(toId);
350
+ if (from === -1 || to === -1) {
351
+ commitSelection(new Set([toId]), reason);
352
+ return;
353
+ }
354
+ commitSelection(new Set(ids.slice(Math.min(from, to), Math.max(from, to) + 1)), reason);
355
+ }, [commitSelection]);
356
+
357
+ const selectRange = useCallback((fromId: string, toId: string) => {
358
+ const ids = selectableIdsRef.current;
359
+ if (selectionMode === 'none' || !ids.includes(toId)) return;
360
+ const resolvedFromId = ids.includes(fromId)
361
+ ? fromId
362
+ : activeIdRef.current && ids.includes(activeIdRef.current)
363
+ ? activeIdRef.current
364
+ : toId;
365
+ setAnchor(toId, 'range');
366
+ if (selectionMode === 'single') {
367
+ commitSelection(new Set([toId]), 'range');
368
+ return;
369
+ }
370
+ const from = ids.indexOf(resolvedFromId);
371
+ const to = ids.indexOf(toId);
372
+ if (from === -1 || to === -1) {
373
+ commitSelection(new Set([toId]), 'range');
374
+ } else {
375
+ const next = new Set(selectedIdsRef.current);
376
+ for (const id of ids.slice(Math.min(from, to), Math.max(from, to) + 1)) next.add(id);
377
+ commitSelection(next, 'range');
378
+ }
379
+ }, [commitSelection, selectionMode, setAnchor]);
380
+
381
+ useEffect(() => {
382
+ const allowed = new Set(selectableIds);
383
+ const pruned = new Set(Array.from(selectedIdsRef.current).filter((id) => allowed.has(id)));
384
+ if (!setsEqual(pruned, selectedIdsRef.current)) commitSelection(pruned, 'items-changed');
385
+ if (activeIdRef.current && !allowed.has(activeIdRef.current)) setActive(null, 'items-changed');
386
+ if (anchorIdRef.current && !allowed.has(anchorIdRef.current)) {
387
+ setAnchor(null, 'items-changed');
388
+ }
389
+ }, [commitSelection, selectableIds, setActive, setAnchor]);
390
+
391
+ const moveActive = useCallback((direction: CollectionNavigationDirection, extend = false) => {
392
+ const ids = selectableIdsRef.current;
393
+ if (ids.length === 0) return;
394
+ const current = activeIdRef.current;
395
+ let next = resolveNextIdRef.current?.({
396
+ direction,
397
+ currentId: current,
398
+ items: itemsRef.current,
399
+ orderedIds: ids,
400
+ }) ?? null;
401
+ if (!next) {
402
+ const currentIndex = current ? ids.indexOf(current) : -1;
403
+ const forward = direction === 'next' || direction === 'down' || direction === 'right';
404
+ const nextIndex = currentIndex === -1
405
+ ? forward ? 0 : ids.length - 1
406
+ : Math.min(ids.length - 1, Math.max(0, currentIndex + (forward ? 1 : -1)));
407
+ next = ids[nextIndex] ?? null;
408
+ }
409
+ if (!next || next === current) return;
410
+ const stableAnchor = anchorIdRef.current ?? current ?? next;
411
+ if (extend) {
412
+ if (!anchorIdRef.current) {
413
+ setAnchor(stableAnchor, 'range');
414
+ }
415
+ replaceRange(stableAnchor, next, 'range');
416
+ } else {
417
+ setAnchor(next, 'navigate');
418
+ }
419
+ setActive(next, extend ? 'range' : 'navigate');
420
+ const nextId = next;
421
+ requestAnimationFrame(() => {
422
+ const nodes = containerRef.current?.querySelectorAll<HTMLElement>(`[${COLLECTION_ITEM_ATTRIBUTE}]`);
423
+ const node = nodes ? Array.from(nodes).find(
424
+ (candidate) => candidate.getAttribute(COLLECTION_ITEM_ATTRIBUTE) === nextId,
425
+ ) : null;
426
+ node?.focus({ preventScroll: true });
427
+ });
428
+ }, [replaceRange, setActive, setAnchor]);
429
+
430
+ const keyboard = useMemo<CollectionKeyboardShortcuts>(
431
+ () => ({ ...DEFAULT_SHORTCUTS, ...(shortcutOverrides || {}) }),
432
+ [shortcutOverrides],
433
+ );
434
+ const keyDefinitions = useMemo<ShortcutDefinition[]>(() => {
435
+ const definitions: ShortcutDefinition[] = [];
436
+ const add = (
437
+ id: string,
438
+ label: string,
439
+ keys: string | string[] | false,
440
+ onTrigger: () => void,
441
+ ) => {
442
+ if (!keys) return;
443
+ definitions.push({ id, label, keys, onTrigger });
444
+ };
445
+ add('collection.clear', 'Clear selection', keyboard.clear, clear);
446
+ add('collection.select-all', 'Select all visible items', selectionMode === 'none' ? false : keyboard.selectAll, selectAll);
447
+ add('collection.toggle', 'Toggle active item', selectionMode === 'none' ? false : keyboard.toggle, () => {
448
+ const id = activeIdRef.current ?? anchorIdRef.current;
449
+ if (id) toggle(id);
450
+ });
451
+ add('collection.extend-next', 'Extend selection down', selectionMode !== 'multiple' ? false : keyboard.extendNext, () => moveActive('down', true));
452
+ add('collection.extend-previous', 'Extend selection up', selectionMode !== 'multiple' ? false : keyboard.extendPrevious, () => moveActive('up', true));
453
+ add('collection.next', 'Next item', keyboard.next, () => moveActive('next'));
454
+ add('collection.previous', 'Previous item', keyboard.previous, () => moveActive('previous'));
455
+ add('collection.up', 'Move up', keyboard.up, () => moveActive('up'));
456
+ add('collection.down', 'Move down', keyboard.down, () => moveActive('down'));
457
+ add('collection.left', 'Move left', keyboard.left, () => moveActive('left'));
458
+ add('collection.right', 'Move right', keyboard.right, () => moveActive('right'));
459
+ add('collection.activate', 'Open active item', keyboard.activate, () => {
460
+ const id = activeIdRef.current ?? selectableIdsRef.current[0];
461
+ if (id) activate(id);
462
+ });
463
+ return definitions;
464
+ }, [activate, clear, keyboard, moveActive, selectAll, selectionMode, toggle]);
465
+ useShortcuts(keyDefinitions, {
466
+ enabled: enabled && bindKeyboardShortcuts,
467
+ scope: shortcutScope,
468
+ priority: shortcutPriority,
469
+ collectionOwnerId: shortcutCollectionOwnerRef.current,
470
+ });
471
+
472
+ const activateCollectionShortcuts = useCallback(() => {
473
+ activateShortcutCollection(shortcutCollectionOwnerRef.current!);
474
+ }, []);
475
+
476
+ const dragPendingRef = useRef(false);
477
+ const dragActiveRef = useRef(false);
478
+ const dragStartRef = useRef({ x: 0, y: 0 });
479
+ const dragCurrentRef = useRef({ x: 0, y: 0 });
480
+ const clickShouldClearRef = useRef(false);
481
+ const suppressNextClickRef = useRef(false);
482
+
483
+ const armSuppressNextClick = useCallback(() => {
484
+ suppressNextClickRef.current = true;
485
+ setTimeout(() => { suppressNextClickRef.current = false; }, 0);
486
+ }, []);
487
+
488
+ const computeDragRect = useCallback((): SelectionMarqueeRect => ({
489
+ left: Math.min(dragStartRef.current.x, dragCurrentRef.current.x),
490
+ top: Math.min(dragStartRef.current.y, dragCurrentRef.current.y),
491
+ width: Math.abs(dragCurrentRef.current.x - dragStartRef.current.x),
492
+ height: Math.abs(dragCurrentRef.current.y - dragStartRef.current.y),
493
+ }), []);
494
+
495
+ const computeIntersectingIds = useCallback((rect: {
496
+ left: number; top: number; right: number; bottom: number;
497
+ }) => {
498
+ const nodes = containerRef.current?.querySelectorAll<HTMLElement>(`[${COLLECTION_ITEM_ATTRIBUTE}]`);
499
+ if (!nodes) return [];
500
+ const ids: string[] = [];
501
+ nodes.forEach((node) => {
502
+ if (!rectsIntersect(rect, node.getBoundingClientRect())) return;
503
+ const id = node.getAttribute(COLLECTION_ITEM_ATTRIBUTE);
504
+ if (id && selectableIdsRef.current.includes(id)) ids.push(id);
505
+ });
506
+ return ids;
507
+ }, []);
508
+
509
+ const handleContainerMouseDown = useCallback((event: ReactMouseEvent) => {
510
+ if (!enabled || event.button !== 0) return;
511
+ const target = event.target as HTMLElement | null;
512
+ if (!target) return;
513
+ const itemTarget = target.closest(`[${COLLECTION_ITEM_ATTRIBUTE}]`);
514
+ const interactiveTarget = target.closest(INTERACTIVE_SELECTOR);
515
+ if (interactiveTarget && interactiveTarget !== itemTarget) {
516
+ clickShouldClearRef.current = false;
517
+ return;
518
+ }
519
+ clickShouldClearRef.current = !itemTarget;
520
+ if (itemTarget) return;
521
+ if (!enableDragSelect || selectionMode !== 'multiple') return;
522
+ dragPendingRef.current = true;
523
+ dragStartRef.current = { x: event.clientX, y: event.clientY };
524
+ dragCurrentRef.current = { x: event.clientX, y: event.clientY };
525
+ }, [enableDragSelect, enabled, selectionMode]);
526
+
527
+ const handleContainerClickCapture = useCallback((event: ReactMouseEvent) => {
528
+ if (!suppressNextClickRef.current) return;
529
+ suppressNextClickRef.current = false;
530
+ event.preventDefault();
531
+ event.stopPropagation();
532
+ }, []);
533
+
534
+ useEffect(() => {
535
+ if (!enabled || !enableDragSelect || selectionMode !== 'multiple') return;
536
+ const handleMouseMove = (event: MouseEvent) => {
537
+ if (dragPendingRef.current) {
538
+ const dx = event.clientX - dragStartRef.current.x;
539
+ const dy = event.clientY - dragStartRef.current.y;
540
+ if (Math.abs(dx) > dragThreshold || Math.abs(dy) > dragThreshold) {
541
+ dragPendingRef.current = false;
542
+ dragActiveRef.current = true;
543
+ window.getSelection()?.removeAllRanges();
544
+ } else return;
545
+ }
546
+ if (!dragActiveRef.current) return;
547
+ event.preventDefault();
548
+ dragCurrentRef.current = { x: event.clientX, y: event.clientY };
549
+ const rect = computeDragRect();
550
+ setDragRect(rect);
551
+ const ids = computeIntersectingIds({
552
+ left: rect.left,
553
+ top: rect.top,
554
+ right: rect.left + rect.width,
555
+ bottom: rect.top + rect.height,
556
+ });
557
+ commitSelection(new Set(ids), 'drag');
558
+ const last = ids[ids.length - 1] ?? null;
559
+ setAnchor(last, 'drag');
560
+ };
561
+ const handleMouseUp = () => {
562
+ const wasPending = dragPendingRef.current;
563
+ const wasActive = dragActiveRef.current;
564
+ dragPendingRef.current = false;
565
+ dragActiveRef.current = false;
566
+ if (wasActive) {
567
+ setDragRect(null);
568
+ clickShouldClearRef.current = false;
569
+ armSuppressNextClick();
570
+ } else if (wasPending && clickShouldClearRef.current) {
571
+ clickShouldClearRef.current = false;
572
+ clear();
573
+ }
574
+ };
575
+ document.addEventListener('mousemove', handleMouseMove);
576
+ document.addEventListener('mouseup', handleMouseUp);
577
+ return () => {
578
+ document.removeEventListener('mousemove', handleMouseMove);
579
+ document.removeEventListener('mouseup', handleMouseUp);
580
+ };
581
+ }, [armSuppressNextClick, clear, commitSelection, computeDragRect, computeIntersectingIds, dragThreshold, enableDragSelect, enabled, selectionMode, setAnchor]);
582
+
583
+ const onRowMouseDown = useCallback((id: string) => (event: ReactMouseEvent) => {
584
+ if (!enabled || event.button !== 0 || event.shiftKey || !(event.metaKey || event.ctrlKey)) return;
585
+ const target = event.target as HTMLElement | null;
586
+ const interactiveTarget = target?.closest(INTERACTIVE_SELECTOR);
587
+ if (interactiveTarget && interactiveTarget !== event.currentTarget) return;
588
+ event.preventDefault();
589
+ event.stopPropagation();
590
+ toggle(id);
591
+ armSuppressNextClick();
592
+ }, [armSuppressNextClick, enabled, toggle]);
593
+
594
+ const onCheckboxClick = useCallback((id: string) => (event: ReactMouseEvent) => {
595
+ event.stopPropagation();
596
+ if (event.shiftKey && selectionMode === 'multiple') {
597
+ const rangeAnchor = anchorIdRef.current ?? activeIdRef.current ?? id;
598
+ selectRange(rangeAnchor, id);
599
+ } else toggle(id);
600
+ }, [selectRange, selectionMode, toggle]);
601
+
602
+ const getRowProps = useCallback((id: string) => ({
603
+ [COLLECTION_ITEM_ATTRIBUTE]: id,
604
+ [LEGACY_ROW_ATTRIBUTE]: id,
605
+ }), []);
606
+
607
+ const getItemProps = useCallback((id: string) => ({
608
+ [COLLECTION_ITEM_ATTRIBUTE]: id,
609
+ [LEGACY_ROW_ATTRIBUTE]: id,
610
+ tabIndex: activeIdRef.current === id || (!activeIdRef.current && selectableIdsRef.current[0] === id) ? 0 : -1,
611
+ 'aria-selected': selectedIdsRef.current.has(id),
612
+ 'aria-disabled': !selectableIdsRef.current.includes(id),
613
+ 'data-notis-active': (activeIdRef.current === id ? 'true' : 'false') as 'true' | 'false',
614
+ onFocus: () => {
615
+ activateCollectionShortcuts();
616
+ setActive(id, 'activate');
617
+ },
618
+ onMouseDown: onRowMouseDown(id),
619
+ onClick: (event: ReactMouseEvent) => {
620
+ if (event.defaultPrevented) return;
621
+ if (!selectableIdsRef.current.includes(id)) return;
622
+ const target = event.target as HTMLElement | null;
623
+ const interactiveTarget = target?.closest(INTERACTIVE_SELECTOR);
624
+ if (interactiveTarget && interactiveTarget !== event.currentTarget) return;
625
+ if (event.shiftKey && selectionMode === 'multiple') {
626
+ event.preventDefault();
627
+ const rangeAnchor = anchorIdRef.current ?? activeIdRef.current ?? id;
628
+ selectRange(rangeAnchor, id);
629
+ return;
630
+ }
631
+ if (event.metaKey || event.ctrlKey) return;
632
+ if (clearSelectionOnPlainClick) clear();
633
+ activate(id);
634
+ },
635
+ onKeyDown: (event: ReactKeyboardEvent) => {
636
+ if (event.key !== ' ' || event.metaKey || event.ctrlKey || event.altKey) return;
637
+ event.preventDefault();
638
+ if (selectionMode === 'none') activate(id);
639
+ else toggle(id);
640
+ },
641
+ }), [activate, activateCollectionShortcuts, clear, clearSelectionOnPlainClick, onRowMouseDown, selectRange, selectionMode, setActive, toggle]);
642
+
643
+ const getCheckboxProps = useCallback((id: string) => ({
644
+ isSelected: selectedIdsRef.current.has(id),
645
+ disabled: !selectableIdsRef.current.includes(id),
646
+ onClick: onCheckboxClick(id),
647
+ }), [onCheckboxClick]);
648
+
649
+ const getSelectedItems = useCallback(() => getItemsForIds(selectedIdsRef.current), [getItemsForIds]);
650
+ const isSelected = useCallback((id: string) => selectedIds.has(id), [selectedIds]);
651
+
652
+ const actionContext = useMemo<CollectionActionContext<T>>(() => ({
653
+ selectedIds: Array.from(selectedIds),
654
+ selectedItems: getItemsForIds(selectedIds),
655
+ clearSelection: clear,
656
+ }), [clear, getItemsForIds, selectedIds]);
657
+ const resolvedActions = useMemo<ResolvedCollectionAction[]>(() => actionDefinitions.map((action) => {
658
+ const defaults = collectionActionDefaults(action.intent);
659
+ const disabled = typeof action.disabled === 'function' ? action.disabled(actionContext) : action.disabled;
660
+ const shortcut = action.shortcut === false ? undefined : action.shortcut ?? defaults.shortcut;
661
+ return {
662
+ id: action.id,
663
+ label: action.label ?? defaults.label ?? action.id,
664
+ icon: action.icon,
665
+ shortcut,
666
+ destructive: action.destructive ?? action.intent === 'delete',
667
+ disabled,
668
+ pending: action.pending,
669
+ onRun: () => action.onRun(actionContext),
670
+ };
671
+ }), [actionContext, actionDefinitions]);
672
+
673
+ const getContainerProps = useCallback(() => ({
674
+ ref: (node: HTMLElement | null) => { containerRef.current = node; },
675
+ onMouseDown: (event: ReactMouseEvent) => {
676
+ activateCollectionShortcuts();
677
+ handleContainerMouseDown(event);
678
+ },
679
+ onFocusCapture: activateCollectionShortcuts,
680
+ onClickCapture: handleContainerClickCapture,
681
+ style: { userSelect: 'none' as const },
682
+ }), [activateCollectionShortcuts, handleContainerClickCapture, handleContainerMouseDown]);
683
+
684
+ return {
685
+ selectedIds,
686
+ selectedCount: selectedIds.size,
687
+ activeId,
688
+ anchorId,
689
+ headId: activeId,
690
+ lastClickedId: anchorId,
691
+ dragRect,
692
+ actions: resolvedActions,
693
+ isSelected,
694
+ getSelectedItems,
695
+ setActive,
696
+ activate,
697
+ toggle,
698
+ select,
699
+ clear,
700
+ selectAll,
701
+ selectRange,
702
+ onCheckboxClick,
703
+ onRowMouseDown,
704
+ getRowProps,
705
+ getItemProps,
706
+ getCheckboxProps,
707
+ getContainerProps,
708
+ };
709
+ }