@notis_ai/cli 0.2.14 → 0.2.16

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 (299) hide show
  1. package/README.md +83 -21
  2. package/dist/agent-hooks/notis-agent-hook.mjs +20168 -0
  3. package/{skills → dist/base-skills}/notis-apps/SKILL.md +167 -72
  4. package/{skills → dist/base-skills}/notis-cli/SKILL.md +86 -28
  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 +39 -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 +1021 -424
  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-process-identity.js +111 -0
  25. package/src/runtime/app-dev-roots.js +284 -0
  26. package/src/runtime/app-dev-server.js +447 -29
  27. package/src/runtime/app-dev-sessions.js +105 -145
  28. package/src/runtime/app-platform.js +1314 -166
  29. package/src/runtime/app-registry-scaffolds.js +367 -0
  30. package/src/runtime/base-skills.d.ts +20 -0
  31. package/src/runtime/base-skills.js +167 -0
  32. package/src/runtime/login-listener.js +15 -0
  33. package/src/runtime/oauth.js +1506 -141
  34. package/src/runtime/output.js +7 -0
  35. package/src/runtime/ports.js +16 -0
  36. package/src/runtime/profiles.js +19 -16
  37. package/src/runtime/skill-sync/cloud-client.ts +96 -0
  38. package/src/runtime/skill-sync/index.ts +644 -0
  39. package/src/runtime/skill-sync/local-scanner.ts +1046 -0
  40. package/src/runtime/skill-sync/symlink-manager.ts +383 -0
  41. package/src/runtime/skill-sync/sync-plan.ts +22 -0
  42. package/src/runtime/skill-sync/types.ts +103 -0
  43. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  44. package/src/runtime/store-screenshot.js +6 -1
  45. package/src/runtime/sync-skills.d.ts +37 -0
  46. package/src/runtime/sync-skills.js +231 -0
  47. package/template/app/layout.tsx +5 -1
  48. package/template/notis.config.ts +1 -0
  49. package/template/packages/sdk/package.json +2 -1
  50. package/template/packages/sdk/src/components/DocumentEditor.tsx +13 -3
  51. package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
  52. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +43 -119
  53. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
  54. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
  55. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
  56. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  57. package/template/packages/sdk/src/config.ts +25 -0
  58. package/template/packages/sdk/src/documents.ts +7 -1
  59. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  60. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +726 -0
  61. package/template/packages/sdk/src/hooks/useHandover.ts +3 -0
  62. package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
  63. package/template/packages/sdk/src/hooks/useNotis.ts +3 -0
  64. package/template/packages/sdk/src/hooks/useNotisNavigation.ts +7 -4
  65. package/template/packages/sdk/src/hooks/useTool.ts +5 -4
  66. package/template/packages/sdk/src/index.ts +47 -2
  67. package/template/packages/sdk/src/interactions/actions.ts +46 -0
  68. package/template/packages/sdk/src/interactions/shortcuts.tsx +634 -0
  69. package/template/packages/sdk/src/interactions.ts +41 -0
  70. package/template/packages/sdk/src/provider.tsx +2 -1
  71. package/template/packages/sdk/src/runtime.ts +92 -4
  72. package/dist/scaffolds/notis-database/CHANGELOG.md +0 -5
  73. package/dist/scaffolds/notis-database/app/globals.css +0 -44
  74. package/dist/scaffolds/notis-database/app/layout.tsx +0 -6
  75. package/dist/scaffolds/notis-database/app/page.tsx +0 -1088
  76. package/dist/scaffolds/notis-database/components/ui/badge.tsx +0 -28
  77. package/dist/scaffolds/notis-database/components/ui/button.tsx +0 -53
  78. package/dist/scaffolds/notis-database/components/ui/card.tsx +0 -56
  79. package/dist/scaffolds/notis-database/components/ui/table.tsx +0 -120
  80. package/dist/scaffolds/notis-database/components.json +0 -20
  81. package/dist/scaffolds/notis-database/index.html +0 -12
  82. package/dist/scaffolds/notis-database/lib/types.ts +0 -132
  83. package/dist/scaffolds/notis-database/lib/utils.ts +0 -6
  84. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  85. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  86. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  87. package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
  88. package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
  89. package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +0 -1839
  90. package/dist/scaffolds/notis-database/notis.config.ts +0 -73
  91. package/dist/scaffolds/notis-database/package-lock.json +0 -3935
  92. package/dist/scaffolds/notis-database/package.json +0 -32
  93. package/dist/scaffolds/notis-database/packages/sdk/package.json +0 -36
  94. package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  95. package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +0 -60
  96. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  97. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  98. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  99. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +0 -234
  100. package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -250
  101. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
  102. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  103. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  104. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  105. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +0 -78
  106. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +0 -121
  107. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useHandover.ts +0 -75
  108. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  109. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +0 -34
  110. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  111. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +0 -64
  112. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +0 -56
  113. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  114. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  115. package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +0 -100
  116. package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
  117. package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -351
  118. package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +0 -186
  119. package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +0 -15
  120. package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +0 -56
  121. package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +0 -15
  122. package/dist/scaffolds/notis-database/postcss.config.mjs +0 -8
  123. package/dist/scaffolds/notis-database/src/dev-main.tsx +0 -23
  124. package/dist/scaffolds/notis-database/src/mock-runtime.ts +0 -561
  125. package/dist/scaffolds/notis-database/tailwind.config.ts +0 -59
  126. package/dist/scaffolds/notis-database/tsconfig.json +0 -23
  127. package/dist/scaffolds/notis-database/vite.config.ts +0 -22
  128. package/dist/scaffolds/notis-journal/CHANGELOG.md +0 -29
  129. package/dist/scaffolds/notis-journal/app/globals.css +0 -37
  130. package/dist/scaffolds/notis-journal/app/insights/page.tsx +0 -513
  131. package/dist/scaffolds/notis-journal/app/journal-core.tsx +0 -362
  132. package/dist/scaffolds/notis-journal/app/journal-ui.tsx +0 -337
  133. package/dist/scaffolds/notis-journal/app/layout.tsx +0 -6
  134. package/dist/scaffolds/notis-journal/app/page.tsx +0 -486
  135. package/dist/scaffolds/notis-journal/components/ui/badge.tsx +0 -28
  136. package/dist/scaffolds/notis-journal/components/ui/button.tsx +0 -53
  137. package/dist/scaffolds/notis-journal/components/ui/card.tsx +0 -56
  138. package/dist/scaffolds/notis-journal/components.json +0 -20
  139. package/dist/scaffolds/notis-journal/index.html +0 -12
  140. package/dist/scaffolds/notis-journal/lib/utils.ts +0 -6
  141. package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
  142. package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
  143. package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
  144. package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
  145. package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
  146. package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
  147. package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +0 -132
  148. package/dist/scaffolds/notis-journal/notis.config.ts +0 -93
  149. package/dist/scaffolds/notis-journal/package-lock.json +0 -4615
  150. package/dist/scaffolds/notis-journal/package.json +0 -34
  151. package/dist/scaffolds/notis-journal/packages/sdk/package.json +0 -36
  152. package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  153. package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +0 -60
  154. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  155. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  156. package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  157. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +0 -234
  158. package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -250
  159. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
  160. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  161. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  162. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  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/useHandover.ts +0 -75
  166. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  167. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +0 -34
  168. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  169. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +0 -64
  170. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +0 -56
  171. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  172. package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  173. package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +0 -100
  174. package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
  175. package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -351
  176. package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +0 -186
  177. package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +0 -15
  178. package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +0 -56
  179. package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +0 -15
  180. package/dist/scaffolds/notis-journal/postcss.config.mjs +0 -8
  181. package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +0 -120
  182. package/dist/scaffolds/notis-journal/src/dev-main.tsx +0 -58
  183. package/dist/scaffolds/notis-journal/src/mock-runtime.ts +0 -199
  184. package/dist/scaffolds/notis-journal/tailwind.config.ts +0 -58
  185. package/dist/scaffolds/notis-journal/tsconfig.json +0 -23
  186. package/dist/scaffolds/notis-journal/vite.config.ts +0 -10
  187. package/dist/scaffolds/notis-notes/CHANGELOG.md +0 -5
  188. package/dist/scaffolds/notis-notes/app/globals.css +0 -3
  189. package/dist/scaffolds/notis-notes/app/layout.tsx +0 -6
  190. package/dist/scaffolds/notis-notes/app/page.tsx +0 -1943
  191. package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +0 -596
  192. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +0 -28
  193. package/dist/scaffolds/notis-notes/components/ui/button.tsx +0 -53
  194. package/dist/scaffolds/notis-notes/components/ui/card.tsx +0 -56
  195. package/dist/scaffolds/notis-notes/components.json +0 -20
  196. package/dist/scaffolds/notis-notes/lib/utils.ts +0 -6
  197. package/dist/scaffolds/notis-notes/lib/visible-properties.ts +0 -144
  198. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  199. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  200. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  201. package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
  202. package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
  203. package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +0 -752
  204. package/dist/scaffolds/notis-notes/notis.config.ts +0 -80
  205. package/dist/scaffolds/notis-notes/package-lock.json +0 -4636
  206. package/dist/scaffolds/notis-notes/package.json +0 -35
  207. package/dist/scaffolds/notis-notes/packages/sdk/package.json +0 -36
  208. package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  209. package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +0 -60
  210. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  211. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  212. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  213. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +0 -234
  214. package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -250
  215. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
  216. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  217. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  218. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  219. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +0 -78
  220. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +0 -121
  221. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useHandover.ts +0 -75
  222. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  223. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +0 -34
  224. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  225. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +0 -64
  226. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +0 -56
  227. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  228. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  229. package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +0 -100
  230. package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
  231. package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -351
  232. package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +0 -186
  233. package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +0 -15
  234. package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +0 -56
  235. package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +0 -15
  236. package/dist/scaffolds/notis-notes/postcss.config.mjs +0 -8
  237. package/dist/scaffolds/notis-notes/tailwind.config.ts +0 -58
  238. package/dist/scaffolds/notis-notes/tsconfig.json +0 -23
  239. package/dist/scaffolds/notis-notes/vite.config.ts +0 -10
  240. package/dist/scaffolds/notis-random/CHANGELOG.md +0 -15
  241. package/dist/scaffolds/notis-random/README.md +0 -33
  242. package/dist/scaffolds/notis-random/app/globals.css +0 -11
  243. package/dist/scaffolds/notis-random/app/history/page.tsx +0 -67
  244. package/dist/scaffolds/notis-random/app/layout.tsx +0 -7
  245. package/dist/scaffolds/notis-random/app/page.tsx +0 -289
  246. package/dist/scaffolds/notis-random/components/ui/button.tsx +0 -50
  247. package/dist/scaffolds/notis-random/components/ui/card.tsx +0 -16
  248. package/dist/scaffolds/notis-random/components/ui/input.tsx +0 -23
  249. package/dist/scaffolds/notis-random/components.json +0 -20
  250. package/dist/scaffolds/notis-random/index.html +0 -12
  251. package/dist/scaffolds/notis-random/lib/notis-tools.ts +0 -128
  252. package/dist/scaffolds/notis-random/lib/rng.ts +0 -202
  253. package/dist/scaffolds/notis-random/lib/roll-record.ts +0 -189
  254. package/dist/scaffolds/notis-random/lib/utils.ts +0 -25
  255. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  256. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  257. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  258. package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
  259. package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
  260. package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +0 -753
  261. package/dist/scaffolds/notis-random/notis.config.ts +0 -86
  262. package/dist/scaffolds/notis-random/package-lock.json +0 -4513
  263. package/dist/scaffolds/notis-random/package.json +0 -36
  264. package/dist/scaffolds/notis-random/packages/sdk/package.json +0 -36
  265. package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +0 -93
  266. package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +0 -60
  267. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
  268. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
  269. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
  270. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +0 -234
  271. package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -250
  272. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
  273. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  274. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  275. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
  276. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +0 -78
  277. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +0 -121
  278. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useHandover.ts +0 -75
  279. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
  280. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +0 -34
  281. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
  282. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +0 -64
  283. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +0 -56
  284. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  285. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
  286. package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +0 -100
  287. package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
  288. package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -351
  289. package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +0 -186
  290. package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +0 -15
  291. package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +0 -56
  292. package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +0 -15
  293. package/dist/scaffolds/notis-random/postcss.config.mjs +0 -6
  294. package/dist/scaffolds/notis-random/src/dev-main.tsx +0 -70
  295. package/dist/scaffolds/notis-random/src/mock-runtime.ts +0 -129
  296. package/dist/scaffolds/notis-random/tailwind.config.ts +0 -50
  297. package/dist/scaffolds/notis-random/tsconfig.json +0 -23
  298. package/dist/scaffolds/notis-random/vite.config.ts +0 -11
  299. package/dist/scaffolds.json +0 -49
@@ -0,0 +1,231 @@
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
+ async function releaseOwnedLock(lockDirectory, ownerId) {
88
+ const owner = await lockSnapshot(lockDirectory);
89
+ if (owner?.id !== ownerId) return;
90
+
91
+ const quarantineRoot = join(dirname(lockDirectory), '.stale-operation-locks');
92
+ const releasedDirectory = join(quarantineRoot, `released.${ownerId}`);
93
+ await mkdir(quarantineRoot, { recursive: true, mode: 0o700 });
94
+ try {
95
+ // Moving the owned directory releases the shared pathname atomically.
96
+ // Delete only the private destination so a waiter can safely acquire a new
97
+ // lock without racing this owner's recursive cleanup.
98
+ await rename(lockDirectory, releasedDirectory);
99
+ } catch (error) {
100
+ if (error?.code === 'ENOENT') return;
101
+ throw error;
102
+ }
103
+ await rm(releasedDirectory, { recursive: true, force: true });
104
+ }
105
+
106
+ /** Serialize Desktop and terminal skill sync across processes on one Mac. */
107
+ export async function withSkillSyncLock(callback, {
108
+ home = homedir(),
109
+ timeoutMs = DEFAULT_LOCK_TIMEOUT_MS,
110
+ staleMs = DEFAULT_LOCK_STALE_MS,
111
+ pollMs = DEFAULT_LOCK_POLL_MS,
112
+ now = () => Date.now(),
113
+ } = {}) {
114
+ const lockDirectory = join(home, '.notis', 'skills', '.operation-lock');
115
+ const ownerId = `${process.pid}.${randomUUID()}`;
116
+ const deadline = now() + timeoutMs;
117
+ await mkdir(dirname(lockDirectory), { recursive: true, mode: 0o700 });
118
+
119
+ for (;;) {
120
+ try {
121
+ await mkdir(lockDirectory, { mode: 0o700 });
122
+ try {
123
+ await writeLockOwnerAtomically(lockDirectory, {
124
+ id: ownerId,
125
+ pid: process.pid,
126
+ at: now(),
127
+ });
128
+ } catch (error) {
129
+ // This process exclusively created the directory and has not yet
130
+ // published an owner, so it is safe to undo a failed acquisition.
131
+ await rm(lockDirectory, { recursive: true, force: true });
132
+ throw error;
133
+ }
134
+ break;
135
+ } catch (error) {
136
+ if (error?.code !== 'EEXIST') throw error;
137
+ }
138
+
139
+ const observed = await lockSnapshot(lockDirectory);
140
+ if (
141
+ observed
142
+ && now() - observed.at > staleMs
143
+ && !processIsAlive(observed.pid)
144
+ ) {
145
+ // Require an unchanged observation interval before stealing. This avoids
146
+ // racing a holder that is publishing or refreshing its owner lease.
147
+ await delay(Math.max(pollMs, 10));
148
+ const current = await lockSnapshot(lockDirectory);
149
+ if (sameLockSnapshot(observed, current) && !processIsAlive(current?.pid)) {
150
+ if (await quarantineStaleLock(lockDirectory, current)) continue;
151
+ }
152
+ }
153
+ if (now() >= deadline) {
154
+ throw new Error('Timed out waiting for another Notis skill sync to finish.');
155
+ }
156
+ await delay(pollMs);
157
+ }
158
+
159
+ const heartbeatMs = Math.max(10, Math.min(30_000, Math.floor(staleMs / 3)));
160
+ let heartbeatStopped = false;
161
+ let heartbeatInFlight = Promise.resolve();
162
+ const refreshHeartbeat = async () => {
163
+ if (heartbeatStopped) return;
164
+ const temporaryOwnerPath = join(lockDirectory, `.owner.${ownerId}.tmp`);
165
+ await writeFile(
166
+ temporaryOwnerPath,
167
+ JSON.stringify({ id: ownerId, pid: process.pid, at: now() }),
168
+ { mode: 0o600 },
169
+ );
170
+ const owner = await lockSnapshot(lockDirectory);
171
+ if (owner?.id !== ownerId) {
172
+ await rm(temporaryOwnerPath, { force: true });
173
+ return;
174
+ }
175
+ // If a stale-lock reclaimer moved this directory after the ownership
176
+ // check, the source path disappears and rename fails instead of
177
+ // overwriting the owner of a newly acquired lock at the shared path.
178
+ await rename(temporaryOwnerPath, join(lockDirectory, 'owner'));
179
+ };
180
+ const heartbeat = setInterval(() => {
181
+ // Serialize refreshes and retain the active promise so cleanup cannot race
182
+ // a temporary owner write that was already in flight when the interval was
183
+ // cleared.
184
+ heartbeatInFlight = heartbeatInFlight
185
+ .then(refreshHeartbeat)
186
+ .catch(() => undefined);
187
+ }, heartbeatMs);
188
+ heartbeat.unref?.();
189
+
190
+ try {
191
+ return await callback();
192
+ } finally {
193
+ heartbeatStopped = true;
194
+ clearInterval(heartbeat);
195
+ await heartbeatInFlight;
196
+ await releaseOwnedLock(lockDirectory, ownerId);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Shared CLI-owned orchestration for base-skill installation plus account sync.
202
+ * Desktop injects the source engine that webpack bundles; the CLI command
203
+ * injects the generated dist engine used by the published package.
204
+ */
205
+ export async function reconcileAllSkills({
206
+ serverUrl,
207
+ jwt,
208
+ honorSyncEnabled,
209
+ userId = null,
210
+ home,
211
+ runAccountSync,
212
+ reconcileBase = reconcileBaseSkills,
213
+ lockOptions = {},
214
+ }) {
215
+ return withSkillSyncLock(async () => {
216
+ const base = reconcileBase({ userId, ...(home ? { home } : {}) });
217
+ const account = await runAccountSync(
218
+ serverUrl,
219
+ jwt,
220
+ {},
221
+ { honorSyncEnabled },
222
+ );
223
+ return {
224
+ ...account,
225
+ baseSkills: base.skills,
226
+ baseInstalled: base.installed,
227
+ baseLinked: base.linked,
228
+ baseBackups: base.backups,
229
+ };
230
+ }, { ...lockOptions, ...(home ? { home } : {}) });
231
+ }
@@ -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 = {
@@ -121,10 +49,12 @@ const containerBaseStyle: CSSProperties = {
121
49
  };
122
50
 
123
51
  const countStyle: CSSProperties = {
52
+ flexShrink: 0,
124
53
  padding: '0 0.5rem',
125
54
  fontSize: '12px',
126
55
  fontWeight: 500,
127
56
  fontVariantNumeric: 'tabular-nums',
57
+ whiteSpace: 'nowrap',
128
58
  color: 'color-mix(in srgb, hsl(var(--background)) 70%, transparent)',
129
59
  };
130
60
 
@@ -136,6 +66,7 @@ const dividerStyle: CSSProperties = {
136
66
  };
137
67
 
138
68
  const baseButtonStyle: CSSProperties = {
69
+ flexShrink: 0,
139
70
  display: 'inline-flex',
140
71
  alignItems: 'center',
141
72
  gap: '0.375rem',
@@ -147,6 +78,7 @@ const baseButtonStyle: CSSProperties = {
147
78
  cursor: 'pointer',
148
79
  fontSize: '13px',
149
80
  fontFamily: 'inherit',
81
+ whiteSpace: 'nowrap',
150
82
  transition: 'background-color 120ms ease, color 120ms ease',
151
83
  };
152
84
 
@@ -180,43 +112,25 @@ export function MultiSelectActionBar({
180
112
  itemLabel,
181
113
  className,
182
114
  style,
115
+ shortcutsEnabled = true,
116
+ shortcutScope = 'collection',
183
117
  }: 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);
118
+ const actionShortcuts = useMemo<ShortcutDefinition[]>(() => {
119
+ return actions.flatMap((action): ShortcutDefinition[] => {
120
+ if (!action.shortcut || action.disabled || action.pending) return [];
121
+ return [{
122
+ id: `collection.action.${action.id}`,
123
+ keys: action.shortcut,
124
+ label: action.label,
125
+ onTrigger: () => action.onRun(),
126
+ }];
127
+ });
195
128
  }, [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]);
129
+ useShortcuts(actionShortcuts, {
130
+ enabled: shortcutsEnabled && selectedCount > 0,
131
+ scope: shortcutScope,
132
+ priority: 25,
133
+ });
220
134
 
221
135
  if (selectedCount === 0) return null;
222
136
 
@@ -258,21 +172,31 @@ function ActionButton({ action }: { action: MultiSelectAction }) {
258
172
  color: hover
259
173
  ? 'hsl(var(--background))'
260
174
  : baseButtonStyle.color,
175
+ opacity: action.disabled ? 0.45 : 1,
176
+ cursor: action.disabled || action.pending ? 'not-allowed' : 'pointer',
261
177
  };
262
178
 
263
179
  return (
264
180
  <button
265
181
  type="button"
266
- onClick={() => void action.onRun()}
182
+ onClick={() => {
183
+ if (!action.disabled && !action.pending) void action.onRun();
184
+ }}
185
+ disabled={action.disabled || action.pending}
186
+ aria-busy={action.pending || undefined}
187
+ aria-keyshortcuts={action.shortcut || undefined}
188
+ data-destructive={action.destructive ? 'true' : 'false'}
267
189
  onMouseEnter={() => setHover(true)}
268
190
  onMouseLeave={() => setHover(false)}
269
191
  onFocus={() => setHover(true)}
270
192
  onBlur={() => setHover(false)}
271
193
  style={buttonStyle}
272
194
  >
273
- <span aria-hidden style={iconSlotStyle}>{action.icon}</span>
195
+ {!action.shortcut && action.icon ? (
196
+ <span aria-hidden style={iconSlotStyle}>{action.icon}</span>
197
+ ) : null}
274
198
  {display ? <kbd aria-hidden style={keycapStyle}>{display}</kbd> : null}
275
- <span>{action.label}</span>
199
+ <span>{action.pending ? `${action.label}…` : action.label}</span>
276
200
  </button>
277
201
  );
278
202
  }
@@ -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()}