@notis_ai/cli 0.2.14 → 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 (296) hide show
  1. package/README.md +82 -20
  2. package/dist/agent-hooks/notis-agent-hook.mjs +19750 -0
  3. package/{skills → dist/base-skills}/notis-apps/SKILL.md +84 -53
  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 +38 -17
  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 +871 -387
  16. package/src/command-specs/auth.js +63 -10
  17. package/src/command-specs/index.js +6 -0
  18. package/src/command-specs/onboarding.js +69 -5
  19. package/src/command-specs/skills.js +56 -0
  20. package/src/runtime/agent-memory-state.js +126 -0
  21. package/src/runtime/agent-setup.js +383 -0
  22. package/src/runtime/app-dev-consumers.js +154 -0
  23. package/src/runtime/app-dev-host-lock.js +80 -0
  24. package/src/runtime/app-dev-roots.js +284 -0
  25. package/src/runtime/app-dev-server.js +212 -22
  26. package/src/runtime/app-dev-sessions.js +105 -145
  27. package/src/runtime/app-platform.js +1291 -165
  28. package/src/runtime/app-registry-scaffolds.js +367 -0
  29. package/src/runtime/base-skills.d.ts +20 -0
  30. package/src/runtime/base-skills.js +167 -0
  31. package/src/runtime/login-listener.js +15 -0
  32. package/src/runtime/oauth.js +1506 -141
  33. package/src/runtime/output.js +7 -0
  34. package/src/runtime/ports.js +16 -0
  35. package/src/runtime/profiles.js +0 -5
  36. package/src/runtime/skill-sync/cloud-client.ts +96 -0
  37. package/src/runtime/skill-sync/index.ts +644 -0
  38. package/src/runtime/skill-sync/local-scanner.ts +1046 -0
  39. package/src/runtime/skill-sync/symlink-manager.ts +383 -0
  40. package/src/runtime/skill-sync/sync-plan.ts +22 -0
  41. package/src/runtime/skill-sync/types.ts +103 -0
  42. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  43. package/src/runtime/store-screenshot.js +6 -1
  44. package/src/runtime/sync-skills.d.ts +37 -0
  45. package/src/runtime/sync-skills.js +215 -0
  46. package/template/app/layout.tsx +5 -1
  47. package/template/notis.config.ts +1 -0
  48. package/template/packages/sdk/package.json +2 -1
  49. package/template/packages/sdk/src/components/DocumentEditor.tsx +13 -3
  50. package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
  51. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +37 -119
  52. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
  53. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
  54. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
  55. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  56. package/template/packages/sdk/src/config.ts +19 -0
  57. package/template/packages/sdk/src/documents.ts +7 -1
  58. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  59. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +709 -0
  60. package/template/packages/sdk/src/hooks/useHandover.ts +3 -0
  61. package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
  62. package/template/packages/sdk/src/hooks/useTool.ts +5 -4
  63. package/template/packages/sdk/src/index.ts +47 -2
  64. package/template/packages/sdk/src/interactions/actions.ts +46 -0
  65. package/template/packages/sdk/src/interactions/shortcuts.tsx +628 -0
  66. package/template/packages/sdk/src/interactions.ts +41 -0
  67. package/template/packages/sdk/src/provider.tsx +2 -1
  68. package/template/packages/sdk/src/runtime.ts +89 -4
  69. package/dist/scaffolds/notis-database/CHANGELOG.md +0 -5
  70. package/dist/scaffolds/notis-database/app/globals.css +0 -44
  71. package/dist/scaffolds/notis-database/app/layout.tsx +0 -6
  72. package/dist/scaffolds/notis-database/app/page.tsx +0 -1088
  73. package/dist/scaffolds/notis-database/components/ui/badge.tsx +0 -28
  74. package/dist/scaffolds/notis-database/components/ui/button.tsx +0 -53
  75. package/dist/scaffolds/notis-database/components/ui/card.tsx +0 -56
  76. package/dist/scaffolds/notis-database/components/ui/table.tsx +0 -120
  77. package/dist/scaffolds/notis-database/components.json +0 -20
  78. package/dist/scaffolds/notis-database/index.html +0 -12
  79. package/dist/scaffolds/notis-database/lib/types.ts +0 -132
  80. package/dist/scaffolds/notis-database/lib/utils.ts +0 -6
  81. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  82. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  83. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  84. package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
  85. package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
  86. package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +0 -1839
  87. package/dist/scaffolds/notis-database/notis.config.ts +0 -73
  88. package/dist/scaffolds/notis-database/package-lock.json +0 -3935
  89. package/dist/scaffolds/notis-database/package.json +0 -32
  90. package/dist/scaffolds/notis-database/packages/sdk/package.json +0 -36
  91. package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  92. package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +0 -60
  93. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  94. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  95. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  96. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +0 -234
  97. package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -250
  98. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
  99. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  100. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  101. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  102. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +0 -78
  103. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +0 -121
  104. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useHandover.ts +0 -75
  105. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  106. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +0 -34
  107. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  108. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +0 -64
  109. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +0 -56
  110. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  111. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  112. package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +0 -100
  113. package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
  114. package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -351
  115. package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +0 -186
  116. package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +0 -15
  117. package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +0 -56
  118. package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +0 -15
  119. package/dist/scaffolds/notis-database/postcss.config.mjs +0 -8
  120. package/dist/scaffolds/notis-database/src/dev-main.tsx +0 -23
  121. package/dist/scaffolds/notis-database/src/mock-runtime.ts +0 -561
  122. package/dist/scaffolds/notis-database/tailwind.config.ts +0 -59
  123. package/dist/scaffolds/notis-database/tsconfig.json +0 -23
  124. package/dist/scaffolds/notis-database/vite.config.ts +0 -22
  125. package/dist/scaffolds/notis-journal/CHANGELOG.md +0 -29
  126. package/dist/scaffolds/notis-journal/app/globals.css +0 -37
  127. package/dist/scaffolds/notis-journal/app/insights/page.tsx +0 -513
  128. package/dist/scaffolds/notis-journal/app/journal-core.tsx +0 -362
  129. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +0 -337
  130. package/dist/scaffolds/notis-journal/app/layout.tsx +0 -6
  131. package/dist/scaffolds/notis-journal/app/page.tsx +0 -486
  132. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +0 -28
  133. package/dist/scaffolds/notis-journal/components/ui/button.tsx +0 -53
  134. package/dist/scaffolds/notis-journal/components/ui/card.tsx +0 -56
  135. package/dist/scaffolds/notis-journal/components.json +0 -20
  136. package/dist/scaffolds/notis-journal/index.html +0 -12
  137. package/dist/scaffolds/notis-journal/lib/utils.ts +0 -6
  138. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  139. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  140. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  141. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  142. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  143. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  144. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +0 -132
  145. package/dist/scaffolds/notis-journal/notis.config.ts +0 -93
  146. package/dist/scaffolds/notis-journal/package-lock.json +0 -4615
  147. package/dist/scaffolds/notis-journal/package.json +0 -34
  148. package/dist/scaffolds/notis-journal/packages/sdk/package.json +0 -36
  149. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  150. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +0 -60
  151. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  152. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  153. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  154. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +0 -234
  155. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -250
  156. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
  157. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  158. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  159. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  160. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +0 -78
  161. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +0 -121
  162. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useHandover.ts +0 -75
  163. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  164. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +0 -34
  165. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  166. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +0 -64
  167. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +0 -56
  168. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  169. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  170. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +0 -100
  171. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
  172. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -351
  173. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +0 -186
  174. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +0 -15
  175. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +0 -56
  176. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +0 -15
  177. package/dist/scaffolds/notis-journal/postcss.config.mjs +0 -8
  178. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +0 -120
  179. package/dist/scaffolds/notis-journal/src/dev-main.tsx +0 -58
  180. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +0 -199
  181. package/dist/scaffolds/notis-journal/tailwind.config.ts +0 -58
  182. package/dist/scaffolds/notis-journal/tsconfig.json +0 -23
  183. package/dist/scaffolds/notis-journal/vite.config.ts +0 -10
  184. package/dist/scaffolds/notis-notes/CHANGELOG.md +0 -5
  185. package/dist/scaffolds/notis-notes/app/globals.css +0 -3
  186. package/dist/scaffolds/notis-notes/app/layout.tsx +0 -6
  187. package/dist/scaffolds/notis-notes/app/page.tsx +0 -1943
  188. package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +0 -596
  189. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +0 -28
  190. package/dist/scaffolds/notis-notes/components/ui/button.tsx +0 -53
  191. package/dist/scaffolds/notis-notes/components/ui/card.tsx +0 -56
  192. package/dist/scaffolds/notis-notes/components.json +0 -20
  193. package/dist/scaffolds/notis-notes/lib/utils.ts +0 -6
  194. package/dist/scaffolds/notis-notes/lib/visible-properties.ts +0 -144
  195. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  196. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  197. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  198. package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
  199. package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
  200. package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +0 -752
  201. package/dist/scaffolds/notis-notes/notis.config.ts +0 -80
  202. package/dist/scaffolds/notis-notes/package-lock.json +0 -4636
  203. package/dist/scaffolds/notis-notes/package.json +0 -35
  204. package/dist/scaffolds/notis-notes/packages/sdk/package.json +0 -36
  205. package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  206. package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +0 -60
  207. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  208. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  209. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  210. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +0 -234
  211. package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -250
  212. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
  213. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  214. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  215. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  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/useHandover.ts +0 -75
  219. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  220. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +0 -34
  221. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  222. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +0 -64
  223. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +0 -56
  224. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  225. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  226. package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +0 -100
  227. package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
  228. package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -351
  229. package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +0 -186
  230. package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +0 -15
  231. package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +0 -56
  232. package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +0 -15
  233. package/dist/scaffolds/notis-notes/postcss.config.mjs +0 -8
  234. package/dist/scaffolds/notis-notes/tailwind.config.ts +0 -58
  235. package/dist/scaffolds/notis-notes/tsconfig.json +0 -23
  236. package/dist/scaffolds/notis-notes/vite.config.ts +0 -10
  237. package/dist/scaffolds/notis-random/CHANGELOG.md +0 -15
  238. package/dist/scaffolds/notis-random/README.md +0 -33
  239. package/dist/scaffolds/notis-random/app/globals.css +0 -11
  240. package/dist/scaffolds/notis-random/app/history/page.tsx +0 -67
  241. package/dist/scaffolds/notis-random/app/layout.tsx +0 -7
  242. package/dist/scaffolds/notis-random/app/page.tsx +0 -289
  243. package/dist/scaffolds/notis-random/components/ui/button.tsx +0 -50
  244. package/dist/scaffolds/notis-random/components/ui/card.tsx +0 -16
  245. package/dist/scaffolds/notis-random/components/ui/input.tsx +0 -23
  246. package/dist/scaffolds/notis-random/components.json +0 -20
  247. package/dist/scaffolds/notis-random/index.html +0 -12
  248. package/dist/scaffolds/notis-random/lib/notis-tools.ts +0 -128
  249. package/dist/scaffolds/notis-random/lib/rng.ts +0 -202
  250. package/dist/scaffolds/notis-random/lib/roll-record.ts +0 -189
  251. package/dist/scaffolds/notis-random/lib/utils.ts +0 -25
  252. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  253. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  254. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  255. package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
  256. package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
  257. package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +0 -753
  258. package/dist/scaffolds/notis-random/notis.config.ts +0 -86
  259. package/dist/scaffolds/notis-random/package-lock.json +0 -4513
  260. package/dist/scaffolds/notis-random/package.json +0 -36
  261. package/dist/scaffolds/notis-random/packages/sdk/package.json +0 -36
  262. package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  263. package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +0 -60
  264. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  265. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  266. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  267. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +0 -234
  268. package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -250
  269. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
  270. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  271. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  272. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  273. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +0 -78
  274. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +0 -121
  275. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useHandover.ts +0 -75
  276. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  277. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +0 -34
  278. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  279. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +0 -64
  280. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +0 -56
  281. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  282. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  283. package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +0 -100
  284. package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
  285. package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -351
  286. package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +0 -186
  287. package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +0 -15
  288. package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +0 -56
  289. package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +0 -15
  290. package/dist/scaffolds/notis-random/postcss.config.mjs +0 -6
  291. package/dist/scaffolds/notis-random/src/dev-main.tsx +0 -70
  292. package/dist/scaffolds/notis-random/src/mock-runtime.ts +0 -129
  293. package/dist/scaffolds/notis-random/tailwind.config.ts +0 -50
  294. package/dist/scaffolds/notis-random/tsconfig.json +0 -23
  295. package/dist/scaffolds/notis-random/vite.config.ts +0 -11
  296. package/dist/scaffolds.json +0 -49
@@ -0,0 +1,215 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+
6
+ import { reconcileBaseSkills } from './base-skills.js';
7
+
8
+ const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
9
+ const DEFAULT_LOCK_STALE_MS = 10 * 60_000;
10
+ const DEFAULT_LOCK_POLL_MS = 50;
11
+
12
+ async function lockSnapshot(lockDirectory) {
13
+ try {
14
+ const [raw, metadata] = await Promise.all([
15
+ readFile(join(lockDirectory, 'owner'), 'utf8').catch(() => ''),
16
+ stat(lockDirectory),
17
+ ]);
18
+ let owner = {};
19
+ try {
20
+ owner = raw ? JSON.parse(raw) : {};
21
+ } catch {
22
+ // A process can die between mkdir and the atomic owner write. Preserve
23
+ // the directory mtime as a reclaimable ownerless lease.
24
+ }
25
+ return {
26
+ id: typeof owner.id === 'string' ? owner.id : null,
27
+ pid: Number.isInteger(Number(owner.pid)) ? Number(owner.pid) : null,
28
+ at: Number.isFinite(Number(owner.at)) ? Number(owner.at) : metadata.mtimeMs,
29
+ mtimeMs: metadata.mtimeMs,
30
+ };
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function sameLockSnapshot(left, right) {
37
+ return Boolean(
38
+ left
39
+ && right
40
+ && left.id === right.id
41
+ && left.pid === right.pid
42
+ && left.at === right.at
43
+ && left.mtimeMs === right.mtimeMs,
44
+ );
45
+ }
46
+
47
+ function processIsAlive(pid) {
48
+ if (!Number.isInteger(pid) || pid <= 0) return false;
49
+ try {
50
+ process.kill(pid, 0);
51
+ return true;
52
+ } catch (error) {
53
+ return error?.code === 'EPERM';
54
+ }
55
+ }
56
+
57
+ function delay(milliseconds) {
58
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
59
+ }
60
+
61
+ async function quarantineStaleLock(lockDirectory, snapshot) {
62
+ const quarantineRoot = join(dirname(lockDirectory), '.stale-operation-locks');
63
+ const snapshotToken = createHash('sha256')
64
+ .update(JSON.stringify(snapshot))
65
+ .digest('hex');
66
+ await mkdir(quarantineRoot, { recursive: true, mode: 0o700 });
67
+ try {
68
+ // The deterministic destination is the compare-and-swap guard. If two
69
+ // waiters observed the same stale owner, only one can move it here. The
70
+ // retained non-empty tombstone prevents the loser from ever moving a new
71
+ // live lock that appeared at the shared pathname in the meantime.
72
+ await rename(lockDirectory, join(quarantineRoot, snapshotToken));
73
+ return true;
74
+ } catch (error) {
75
+ if (['EEXIST', 'ENOTEMPTY', 'ENOENT'].includes(error?.code)) return false;
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ async function writeLockOwnerAtomically(lockDirectory, owner) {
81
+ const temporaryOwnerPath = join(lockDirectory, `.owner.${owner.id}.tmp`);
82
+ const ownerPath = join(lockDirectory, 'owner');
83
+ await writeFile(temporaryOwnerPath, JSON.stringify(owner), { mode: 0o600 });
84
+ await rename(temporaryOwnerPath, ownerPath);
85
+ }
86
+
87
+ /** Serialize Desktop and terminal skill sync across processes on one Mac. */
88
+ export async function withSkillSyncLock(callback, {
89
+ home = homedir(),
90
+ timeoutMs = DEFAULT_LOCK_TIMEOUT_MS,
91
+ staleMs = DEFAULT_LOCK_STALE_MS,
92
+ pollMs = DEFAULT_LOCK_POLL_MS,
93
+ now = () => Date.now(),
94
+ } = {}) {
95
+ const lockDirectory = join(home, '.notis', 'skills', '.operation-lock');
96
+ const ownerId = `${process.pid}.${randomUUID()}`;
97
+ const deadline = now() + timeoutMs;
98
+ await mkdir(dirname(lockDirectory), { recursive: true, mode: 0o700 });
99
+
100
+ for (;;) {
101
+ try {
102
+ await mkdir(lockDirectory, { mode: 0o700 });
103
+ try {
104
+ await writeLockOwnerAtomically(lockDirectory, {
105
+ id: ownerId,
106
+ pid: process.pid,
107
+ at: now(),
108
+ });
109
+ } catch (error) {
110
+ // This process exclusively created the directory and has not yet
111
+ // published an owner, so it is safe to undo a failed acquisition.
112
+ await rm(lockDirectory, { recursive: true, force: true });
113
+ throw error;
114
+ }
115
+ break;
116
+ } catch (error) {
117
+ if (error?.code !== 'EEXIST') throw error;
118
+ }
119
+
120
+ const observed = await lockSnapshot(lockDirectory);
121
+ if (
122
+ observed
123
+ && now() - observed.at > staleMs
124
+ && !processIsAlive(observed.pid)
125
+ ) {
126
+ // Require an unchanged observation interval before stealing. This avoids
127
+ // racing a holder that is publishing or refreshing its owner lease.
128
+ await delay(Math.max(pollMs, 10));
129
+ const current = await lockSnapshot(lockDirectory);
130
+ if (sameLockSnapshot(observed, current) && !processIsAlive(current?.pid)) {
131
+ if (await quarantineStaleLock(lockDirectory, current)) continue;
132
+ }
133
+ }
134
+ if (now() >= deadline) {
135
+ throw new Error('Timed out waiting for another Notis skill sync to finish.');
136
+ }
137
+ await delay(pollMs);
138
+ }
139
+
140
+ const heartbeatMs = Math.max(10, Math.min(30_000, Math.floor(staleMs / 3)));
141
+ let heartbeatStopped = false;
142
+ let heartbeatInFlight = Promise.resolve();
143
+ const refreshHeartbeat = async () => {
144
+ if (heartbeatStopped) return;
145
+ const temporaryOwnerPath = join(lockDirectory, `.owner.${ownerId}.tmp`);
146
+ await writeFile(
147
+ temporaryOwnerPath,
148
+ JSON.stringify({ id: ownerId, pid: process.pid, at: now() }),
149
+ { mode: 0o600 },
150
+ );
151
+ const owner = await lockSnapshot(lockDirectory);
152
+ if (owner?.id !== ownerId) {
153
+ await rm(temporaryOwnerPath, { force: true });
154
+ return;
155
+ }
156
+ // If a stale-lock reclaimer moved this directory after the ownership
157
+ // check, the source path disappears and rename fails instead of
158
+ // overwriting the owner of a newly acquired lock at the shared path.
159
+ await rename(temporaryOwnerPath, join(lockDirectory, 'owner'));
160
+ };
161
+ const heartbeat = setInterval(() => {
162
+ // Serialize refreshes and retain the active promise so cleanup cannot race
163
+ // a temporary owner write that was already in flight when the interval was
164
+ // cleared.
165
+ heartbeatInFlight = heartbeatInFlight
166
+ .then(refreshHeartbeat)
167
+ .catch(() => undefined);
168
+ }, heartbeatMs);
169
+ heartbeat.unref?.();
170
+
171
+ try {
172
+ return await callback();
173
+ } finally {
174
+ heartbeatStopped = true;
175
+ clearInterval(heartbeat);
176
+ await heartbeatInFlight;
177
+ const owner = await lockSnapshot(lockDirectory);
178
+ if (owner?.id === ownerId) {
179
+ await rm(lockDirectory, { recursive: true, force: true });
180
+ }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Shared CLI-owned orchestration for base-skill installation plus account sync.
186
+ * Desktop injects the source engine that webpack bundles; the CLI command
187
+ * injects the generated dist engine used by the published package.
188
+ */
189
+ export async function reconcileAllSkills({
190
+ serverUrl,
191
+ jwt,
192
+ honorSyncEnabled,
193
+ userId = null,
194
+ home,
195
+ runAccountSync,
196
+ reconcileBase = reconcileBaseSkills,
197
+ lockOptions = {},
198
+ }) {
199
+ return withSkillSyncLock(async () => {
200
+ const base = reconcileBase({ userId, ...(home ? { home } : {}) });
201
+ const account = await runAccountSync(
202
+ serverUrl,
203
+ jwt,
204
+ {},
205
+ { honorSyncEnabled },
206
+ );
207
+ return {
208
+ ...account,
209
+ baseSkills: base.skills,
210
+ baseInstalled: base.installed,
211
+ baseLinked: base.linked,
212
+ baseBackups: base.backups,
213
+ };
214
+ }, { ...lockOptions, ...(home ? { home } : {}) });
215
+ }
@@ -1,6 +1,10 @@
1
1
  import '@notis/sdk/styles.css';
2
+ import { ShortcutProvider } from '@notis/sdk/interactions';
2
3
  import './globals.css';
3
4
 
4
5
  export default function AppShell({ children }: { children: React.ReactNode }) {
5
- return children;
6
+ // NotisProvider supplies this in the product host. Keeping the shell-level
7
+ // provider makes the same scope arbitration available in the local harness;
8
+ // ShortcutProvider is nesting-safe, so the hosted app still has one registry.
9
+ return <ShortcutProvider>{children}</ShortcutProvider>;
6
10
  }
@@ -2,6 +2,7 @@ import { defineNotisApp } from '@notis/sdk/config';
2
2
 
3
3
  export default defineNotisApp({
4
4
  name: 'my-notis-app',
5
+ devSlug: 'my-notis-app',
5
6
  title: 'My Notis App',
6
7
  description: 'A focused Notis app with one route and one database-backed workflow.',
7
8
  icon: 'phosphor:squares-four',
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@notis/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "exports": {
7
7
  ".": "./src/index.ts",
8
+ "./interactions": "./src/interactions.ts",
8
9
  "./config": "./src/config.ts",
9
10
  "./vite": "./src/vite.ts",
10
11
  "./ui": "./src/ui.ts",
@@ -51,7 +51,7 @@ function DocumentEditorFallback({ documentId, variant = 'full', className }: Not
51
51
  );
52
52
  }
53
53
 
54
- const isMarkdown = document.contentType !== 'file';
54
+ const isMarkdown = document.contentType === 'markdown' || !document.contentType;
55
55
  return (
56
56
  <div className={className}>
57
57
  {variant === 'full' ? <h1 style={fallbackTitleStyle}>{document.title}</h1> : null}
@@ -62,6 +62,15 @@ function DocumentEditorFallback({ documentId, variant = 'full', className }: Not
62
62
  <p style={fallbackNoticeStyle}>Read-only preview — editing is available inside the Notis portal.</p>
63
63
  )}
64
64
  </>
65
+ ) : document.contentType === 'view' ? (
66
+ <div style={fallbackFrameStyle}>
67
+ {document.viewType === 'report'
68
+ ? 'Interactive report'
69
+ : document.viewType === 'html'
70
+ ? 'Interactive HTML document'
71
+ : 'View document'} — open it in Notis Documents for
72
+ the full native experience.
73
+ </div>
65
74
  ) : (
66
75
  <div style={fallbackFrameStyle}>
67
76
  {document.fileType ? `${document.fileType.toUpperCase()} document` : 'File document'} — open it in
@@ -76,8 +85,9 @@ function DocumentEditorFallback({ documentId, variant = 'full', className }: Not
76
85
  * Embeds the document editor for a document anywhere in an app view. The host
77
86
  * (portal) provides the implementation through `runtime.ui.DocumentEditor` and
78
87
  * dispatches on the document's content type (markdown -> rich text editor,
79
- * files -> matching viewer). Outside the portal this falls back to a read-only
80
- * preview.
88
+ * files -> matching viewer, views -> their trusted native Documents surface).
89
+ * Outside the portal this falls back to a read-only preview or an explicit
90
+ * open-in-Documents notice for native views.
81
91
  *
82
92
  * ```tsx
83
93
  * <DocumentEditor documentId={entry.id} variant="body" className="min-h-[320px]" />
@@ -0,0 +1,121 @@
1
+ import type { ChangeEvent, KeyboardEvent, ReactElement } from 'react';
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { useNotisRuntime } from '../provider';
4
+ import type { NotisMarkdownEditorProps } from '../runtime';
5
+
6
+ function MarkdownEditorFallback({
7
+ value,
8
+ revision,
9
+ readOnly,
10
+ autosaveMs = 1500,
11
+ placeholder = 'Write in Markdown…',
12
+ className,
13
+ onChange,
14
+ onSave,
15
+ onDirtyChange,
16
+ onSavingChange,
17
+ }: NotisMarkdownEditorProps) {
18
+ const [draft, setDraft] = useState(value);
19
+ const [saveAttempt, setSaveAttempt] = useState(0);
20
+ const draftRef = useRef(value);
21
+ const dirtyRef = useRef(false);
22
+ const savingRef = useRef(false);
23
+ const revisionRef = useRef(revision);
24
+ const onSaveRef = useRef(onSave);
25
+ const readOnlyRef = useRef(readOnly);
26
+ const unmountedRef = useRef(false);
27
+ const saveRef = useRef<() => Promise<void>>(async () => undefined);
28
+
29
+ useEffect(() => {
30
+ onSaveRef.current = onSave;
31
+ readOnlyRef.current = readOnly;
32
+ }, [onSave, readOnly]);
33
+
34
+ useEffect(() => {
35
+ if (dirtyRef.current) return;
36
+ draftRef.current = value;
37
+ revisionRef.current = revision;
38
+ setDraft(value);
39
+ }, [revision, value]);
40
+
41
+ const save = useCallback(async () => {
42
+ if (!onSave || !dirtyRef.current || readOnly || savingRef.current) return;
43
+ const markdown = draftRef.current;
44
+ savingRef.current = true;
45
+ if (!unmountedRef.current) onSavingChange?.(true);
46
+ try {
47
+ const result = await onSave({ markdown, expectedRevision: revisionRef.current });
48
+ if (result?.revision) revisionRef.current = result.revision;
49
+ if (draftRef.current === markdown) {
50
+ dirtyRef.current = false;
51
+ if (!unmountedRef.current) onDirtyChange?.(false);
52
+ }
53
+ } finally {
54
+ savingRef.current = false;
55
+ if (!unmountedRef.current) onSavingChange?.(false);
56
+ if (dirtyRef.current && draftRef.current !== markdown) {
57
+ if (unmountedRef.current) {
58
+ void saveRef.current().catch((error) => console.error('Could not save Markdown on unmount', error));
59
+ } else {
60
+ setSaveAttempt((attempt) => attempt + 1);
61
+ }
62
+ }
63
+ }
64
+ }, [onDirtyChange, onSave, onSavingChange, readOnly]);
65
+ saveRef.current = save;
66
+
67
+ useEffect(() => {
68
+ unmountedRef.current = false;
69
+ return () => {
70
+ unmountedRef.current = true;
71
+ if (!savingRef.current) {
72
+ void saveRef.current().catch((error) => console.error('Could not save Markdown on unmount', error));
73
+ }
74
+ };
75
+ }, []);
76
+
77
+ useEffect(() => {
78
+ if (!dirtyRef.current || !onSave || readOnly) return;
79
+ const timer = window.setTimeout(() => void save(), Math.max(250, autosaveMs));
80
+ return () => window.clearTimeout(timer);
81
+ }, [autosaveMs, draft, onSave, readOnly, save, saveAttempt]);
82
+
83
+ const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
84
+ const next = event.currentTarget.value;
85
+ draftRef.current = next;
86
+ setDraft(next);
87
+ if (!dirtyRef.current) {
88
+ dirtyRef.current = true;
89
+ onDirtyChange?.(true);
90
+ }
91
+ onChange?.(next);
92
+ };
93
+
94
+ const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
95
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') {
96
+ event.preventDefault();
97
+ void save();
98
+ }
99
+ };
100
+
101
+ return (
102
+ <textarea
103
+ value={draft}
104
+ readOnly={readOnly}
105
+ placeholder={placeholder}
106
+ className={className}
107
+ onChange={handleChange}
108
+ onKeyDown={handleKeyDown}
109
+ style={{ minHeight: 360, width: '100%', resize: 'vertical' }}
110
+ />
111
+ );
112
+ }
113
+
114
+ /** Use Notis' editor while leaving persistence entirely under app control. */
115
+ export function MarkdownEditor(props: NotisMarkdownEditorProps): ReactElement {
116
+ const runtime = useNotisRuntime();
117
+ const HostEditor = runtime?.ui?.MarkdownEditor;
118
+ return HostEditor
119
+ ? <HostEditor key={props.resourceKey} {...props} />
120
+ : <MarkdownEditorFallback key={props.resourceKey} {...props} />;
121
+ }
@@ -1,25 +1,16 @@
1
1
  'use client';
2
2
 
3
- import {
4
- useEffect,
3
+ import React, {
5
4
  useMemo,
6
- useRef,
7
5
  useState,
8
6
  type CSSProperties,
9
7
  type ReactElement,
10
- type ReactNode,
11
8
  } from 'react';
9
+ import type { ResolvedCollectionAction } from '../interactions/actions';
10
+ import { shortcutDisplay, useShortcuts, type ShortcutDefinition } from '../interactions/shortcuts';
11
+ import type { ShortcutScope } from '../interactions/shortcuts';
12
12
 
13
- export interface MultiSelectAction {
14
- id: string;
15
- label: string;
16
- icon: ReactNode;
17
- /** Single-key (e.g. "E", "#") or "Mod+K" shortcut. Bound while the bar is mounted with count > 0. */
18
- shortcut?: string;
19
- onRun: () => void | Promise<void>;
20
- /** Hint for future destructive styling. Currently unused visually. */
21
- destructive?: boolean;
22
- }
13
+ export type MultiSelectAction = ResolvedCollectionAction;
23
14
 
24
15
  export interface MultiSelectActionBarProps {
25
16
  selectedCount: number;
@@ -30,73 +21,10 @@ export interface MultiSelectActionBarProps {
30
21
  className?: string;
31
22
  /** Optional override for the bar's positioning style. Defaults to bottom-center floating. */
32
23
  style?: CSSProperties;
33
- }
34
-
35
- interface ParsedShortcut {
36
- key: string;
37
- needsMod: boolean;
38
- needsShift: boolean;
39
- }
40
-
41
- function parseShortcut(raw: string): ParsedShortcut | null {
42
- const parts = raw.split('+').map((p) => p.trim()).filter(Boolean);
43
- if (parts.length === 0) return null;
44
- let needsMod = false;
45
- let needsShift = false;
46
- let key = '';
47
- for (const part of parts) {
48
- const lower = part.toLowerCase();
49
- if (lower === 'mod' || lower === 'cmd' || lower === 'ctrl' || lower === 'meta') {
50
- needsMod = true;
51
- } else if (lower === 'shift') {
52
- needsShift = true;
53
- } else {
54
- key = lower;
55
- }
56
- }
57
- if (!key) return null;
58
- return { key, needsMod, needsShift };
59
- }
60
-
61
- function shortcutMatches(shortcut: ParsedShortcut, event: KeyboardEvent): boolean {
62
- const eventKey = event.key.toLowerCase();
63
- // Special-case "#" so Shift+3 matches without the consumer specifying Shift.
64
- if (shortcut.key === '#') {
65
- if (eventKey === '#') return true;
66
- if (event.shiftKey && eventKey === '3') return true;
67
- return false;
68
- }
69
- if (eventKey !== shortcut.key) return false;
70
- if (shortcut.needsMod !== Boolean(event.metaKey || event.ctrlKey)) return false;
71
- if (shortcut.needsShift !== event.shiftKey) return false;
72
- if (event.altKey) return false;
73
- return true;
74
- }
75
-
76
- function isEditableEvent(event: Event): boolean {
77
- // Notis apps mount inside a Shadow DOM, so a document-level listener sees
78
- // `event.target` retargeted to the shadow host. `composedPath()[0]` pierces
79
- // the shadow boundary to the real focused element, so action-bar shortcuts
80
- // don't fire while the user is typing in an app input.
81
- const target = event.composedPath?.()[0] ?? event.target;
82
- if (!(target instanceof HTMLElement)) return false;
83
- if (target.isContentEditable) return true;
84
- const tag = target.tagName;
85
- return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
86
- }
87
-
88
- function shortcutDisplay(raw: string): string {
89
- const parts = raw.split('+').map((p) => p.trim()).filter(Boolean);
90
- return parts
91
- .map((part) => {
92
- const lower = part.toLowerCase();
93
- if (lower === 'mod' || lower === 'cmd' || lower === 'meta') return '⌘';
94
- if (lower === 'ctrl') return '⌃';
95
- if (lower === 'shift') return '⇧';
96
- if (lower === 'alt' || lower === 'option') return '⌥';
97
- return part.length === 1 ? part.toUpperCase() : part;
98
- })
99
- .join('');
24
+ /** Disable action key bindings while leaving the visible toolbar mounted. */
25
+ shortcutsEnabled?: boolean;
26
+ /** Override the default collection shortcut scope. */
27
+ shortcutScope?: ShortcutScope;
100
28
  }
101
29
 
102
30
  const containerBaseStyle: CSSProperties = {
@@ -180,43 +108,25 @@ export function MultiSelectActionBar({
180
108
  itemLabel,
181
109
  className,
182
110
  style,
111
+ shortcutsEnabled = true,
112
+ shortcutScope = 'collection',
183
113
  }: MultiSelectActionBarProps): ReactElement | null {
184
- const actionsRef = useRef(actions);
185
- actionsRef.current = actions;
186
-
187
- const parsedShortcuts = useMemo(() => {
188
- return actions
189
- .map((action) => {
190
- if (!action.shortcut) return null;
191
- const parsed = parseShortcut(action.shortcut);
192
- return parsed ? { action, parsed } : null;
193
- })
194
- .filter((entry): entry is { action: MultiSelectAction; parsed: ParsedShortcut } => entry !== null);
114
+ const actionShortcuts = useMemo<ShortcutDefinition[]>(() => {
115
+ return actions.flatMap((action): ShortcutDefinition[] => {
116
+ if (!action.shortcut || action.disabled || action.pending) return [];
117
+ return [{
118
+ id: `collection.action.${action.id}`,
119
+ keys: action.shortcut,
120
+ label: action.label,
121
+ onTrigger: () => action.onRun(),
122
+ }];
123
+ });
195
124
  }, [actions]);
196
-
197
- useEffect(() => {
198
- if (selectedCount === 0 || parsedShortcuts.length === 0) return;
199
-
200
- const handleKeyDown = (event: KeyboardEvent) => {
201
- if (isEditableEvent(event)) return;
202
- for (const { action, parsed } of parsedShortcuts) {
203
- if (shortcutMatches(parsed, event)) {
204
- event.preventDefault();
205
- event.stopPropagation();
206
- if (typeof event.stopImmediatePropagation === 'function') {
207
- event.stopImmediatePropagation();
208
- }
209
- void action.onRun();
210
- return;
211
- }
212
- }
213
- };
214
-
215
- document.addEventListener('keydown', handleKeyDown, true);
216
- return () => {
217
- document.removeEventListener('keydown', handleKeyDown, true);
218
- };
219
- }, [parsedShortcuts, selectedCount]);
125
+ useShortcuts(actionShortcuts, {
126
+ enabled: shortcutsEnabled && selectedCount > 0,
127
+ scope: shortcutScope,
128
+ priority: 25,
129
+ });
220
130
 
221
131
  if (selectedCount === 0) return null;
222
132
 
@@ -258,21 +168,29 @@ function ActionButton({ action }: { action: MultiSelectAction }) {
258
168
  color: hover
259
169
  ? 'hsl(var(--background))'
260
170
  : baseButtonStyle.color,
171
+ opacity: action.disabled ? 0.45 : 1,
172
+ cursor: action.disabled || action.pending ? 'not-allowed' : 'pointer',
261
173
  };
262
174
 
263
175
  return (
264
176
  <button
265
177
  type="button"
266
- onClick={() => void action.onRun()}
178
+ onClick={() => {
179
+ if (!action.disabled && !action.pending) void action.onRun();
180
+ }}
181
+ disabled={action.disabled || action.pending}
182
+ aria-busy={action.pending || undefined}
183
+ aria-keyshortcuts={action.shortcut || undefined}
184
+ data-destructive={action.destructive ? 'true' : 'false'}
267
185
  onMouseEnter={() => setHover(true)}
268
186
  onMouseLeave={() => setHover(false)}
269
187
  onFocus={() => setHover(true)}
270
188
  onBlur={() => setHover(false)}
271
189
  style={buttonStyle}
272
190
  >
273
- <span aria-hidden style={iconSlotStyle}>{action.icon}</span>
191
+ {action.icon ? <span aria-hidden style={iconSlotStyle}>{action.icon}</span> : null}
274
192
  {display ? <kbd aria-hidden style={keycapStyle}>{display}</kbd> : null}
275
- <span>{action.label}</span>
193
+ <span>{action.pending ? `${action.label}…` : action.label}</span>
276
194
  </button>
277
195
  );
278
196
  }
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
 
3
- import { type CSSProperties, type MouseEvent as ReactMouseEvent, type ReactElement } from 'react';
3
+ import React, { type CSSProperties, type MouseEvent as ReactMouseEvent, type ReactElement } from 'react';
4
4
 
5
5
  export interface MultiSelectCheckboxProps {
6
6
  isSelected: boolean;
7
7
  onClick: (event: ReactMouseEvent) => void;
8
+ disabled?: boolean;
8
9
  /** When true, the checkbox is always visible. Default false (hover/focus reveal via the parent's :hover state). */
9
10
  alwaysVisible?: boolean;
10
11
  /** Optional aria-label override. Defaults to "Select item" / "Deselect item". */
@@ -40,6 +41,7 @@ const checkPath = 'M3.5 7.5l2.5 2.5 6.5-6.5';
40
41
  export function MultiSelectCheckbox({
41
42
  isSelected,
42
43
  onClick,
44
+ disabled = false,
43
45
  alwaysVisible = false,
44
46
  ariaLabel,
45
47
  className,
@@ -49,6 +51,7 @@ export function MultiSelectCheckbox({
49
51
  ...baseStyle,
50
52
  ...(isSelected ? selectedStyle : null),
51
53
  ...(alwaysVisible || isSelected ? { opacity: 1 } : null),
54
+ ...(disabled ? { opacity: 0.45, cursor: 'not-allowed' } : null),
52
55
  ...(style || null),
53
56
  };
54
57
 
@@ -62,6 +65,7 @@ export function MultiSelectCheckbox({
62
65
  type="button"
63
66
  role="checkbox"
64
67
  aria-checked={isSelected}
68
+ disabled={disabled}
65
69
  aria-label={ariaLabel ?? (isSelected ? 'Deselect item' : 'Select item')}
66
70
  onClick={onClick}
67
71
  onMouseDown={(e) => e.stopPropagation()}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { type CSSProperties, type ReactElement } from 'react';
3
+ import React, { type CSSProperties, type ReactElement } from 'react';
4
4
  import type { DragRect } from '../hooks/useMultiSelect';
5
5
 
6
6
  export interface MultiSelectDragOverlayProps {