@notis_ai/cli 0.2.18 → 0.2.19

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 (395) hide show
  1. package/README.md +542 -146
  2. package/bin/check-runtime.js +15 -0
  3. package/bin/notis.js +2 -0
  4. package/config/notis_app_design_rules.json +320 -0
  5. package/config/portal-route-roots.json +45 -0
  6. package/dist/agent-hooks/notis-agent-hook.mjs +42325 -18273
  7. package/dist/base-skills/notis-cli/.notis-base-skill.json +1 -0
  8. package/dist/base-skills/notis-cli/SKILL.md +51 -280
  9. package/dist/base-skills/notis-cli/references/app-delivery.md +17 -0
  10. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  11. package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
  12. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  13. package/dist/base-skills/notis-cli/references/troubleshooting.md +67 -0
  14. package/dist/base-skills/notis-query/.notis-base-skill.json +1 -0
  15. package/dist/base-skills/notis-query/SKILL.md +35 -699
  16. package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
  17. package/dist/base-skills/notis-query/references/documents.md +74 -0
  18. package/dist/base-skills/notis-query/references/query.md +76 -0
  19. package/dist/base-skills/notis-spaces/.notis-base-skill.json +1 -0
  20. package/dist/base-skills/notis-spaces/SKILL.md +218 -0
  21. package/dist/base-skills/notis-spaces/references/architecture.md +116 -0
  22. package/dist/base-skills/notis-spaces/references/context.md +81 -0
  23. package/dist/base-skills/notis-spaces/references/design.md +141 -0
  24. package/dist/base-skills/notis-spaces/references/fixtures.md +29 -0
  25. package/dist/base-skills/notis-spaces/references/memory.md +58 -0
  26. package/dist/base-skills/notis-spaces/references/reading.md +73 -0
  27. package/dist/base-skills/notis-spaces/references/release.md +120 -0
  28. package/dist/base-skills/notis-spaces/references/reports.md +77 -0
  29. package/dist/base-skills/notis-spaces/references/sdk.md +273 -0
  30. package/dist/base-skills/notis-spaces/references/store.md +118 -0
  31. package/dist/base-skills/notis-spaces/references/troubleshooting.md +28 -0
  32. package/dist/base-skills/notis-spaces/references/views.md +229 -0
  33. package/{template/packages → dist}/sdk/package.json +9 -10
  34. package/dist/sdk/src/agentContext.ts +36 -0
  35. package/dist/sdk/src/bundleModule.ts +175 -0
  36. package/dist/sdk/src/components/Dialog.tsx +103 -0
  37. package/dist/sdk/src/components/DocumentEditor.tsx +113 -0
  38. package/dist/sdk/src/components/DocumentPage.tsx +184 -0
  39. package/{template/packages → dist}/sdk/src/components/Markdown.tsx +9 -3
  40. package/{template/packages → dist}/sdk/src/components/MarkdownEditor.tsx +26 -5
  41. package/{template/packages → dist}/sdk/src/components/MultiSelectActionBar.tsx +95 -12
  42. package/{template/packages → dist}/sdk/src/components/MultiSelectCheckbox.tsx +4 -2
  43. package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  44. package/{template/packages → dist}/sdk/src/components/NotisSelectionBoundary.tsx +9 -5
  45. package/dist/sdk/src/components/RecordComponents.tsx +111 -0
  46. package/dist/sdk/src/components/Skeleton.tsx +38 -0
  47. package/dist/sdk/src/config.ts +31 -0
  48. package/{template/packages → dist}/sdk/src/documents.ts +21 -6
  49. package/dist/sdk/src/hooks/useAction.ts +28 -0
  50. package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
  51. package/{template/packages → dist}/sdk/src/hooks/useBackend.ts +3 -2
  52. package/dist/sdk/src/hooks/useCloudComputer.ts +94 -0
  53. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  54. package/{template/packages → dist}/sdk/src/hooks/useCollectionInteractions.ts +157 -147
  55. package/dist/sdk/src/hooks/useDatabaseSchema.ts +50 -0
  56. package/{template/packages → dist}/sdk/src/hooks/useDatabaseSubscription.ts +2 -2
  57. package/dist/sdk/src/hooks/useDocument.ts +51 -0
  58. package/{template/packages → dist}/sdk/src/hooks/useDocuments.ts +25 -59
  59. package/{template/packages → dist}/sdk/src/hooks/useHandover.ts +7 -7
  60. package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
  61. package/dist/sdk/src/hooks/useMarqueeSelection.ts +194 -0
  62. package/{template/packages → dist}/sdk/src/hooks/useMultiSelect.ts +2 -8
  63. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  64. package/dist/sdk/src/hooks/useNativeDocuments.ts +143 -0
  65. package/{template/packages → dist}/sdk/src/hooks/useNotis.ts +6 -1
  66. package/dist/sdk/src/hooks/useNotisNavigation.ts +70 -0
  67. package/dist/sdk/src/hooks/useQuery.ts +81 -0
  68. package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
  69. package/dist/sdk/src/hooks/useTopBarSearch.ts +102 -0
  70. package/dist/sdk/src/hooks/useTopBarTitleLeaf.ts +75 -0
  71. package/{template/packages → dist}/sdk/src/hooks/useUpsertDocument.ts +1 -1
  72. package/dist/sdk/src/hooks/useViewParams.ts +195 -0
  73. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  74. package/{template/packages → dist}/sdk/src/index.ts +64 -1
  75. package/{template/packages → dist}/sdk/src/interactions/actions.ts +14 -1
  76. package/{template/packages → dist}/sdk/src/interactions/shortcuts.tsx +129 -23
  77. package/dist/sdk/src/interactions/visibility.ts +13 -0
  78. package/{template/packages → dist}/sdk/src/interactions.ts +5 -1
  79. package/dist/sdk/src/nativeData.ts +39 -0
  80. package/{template/packages → dist}/sdk/src/provider.tsx +9 -4
  81. package/dist/sdk/src/queryCache.ts +292 -0
  82. package/dist/sdk/src/queryErrors.ts +51 -0
  83. package/dist/sdk/src/runtime.ts +675 -0
  84. package/dist/sdk/src/runtimeModules.ts +33 -0
  85. package/dist/sdk/src/space.ts +442 -0
  86. package/dist/sdk/src/standardViewParams.ts +36 -0
  87. package/{template/packages → dist}/sdk/src/styles.css +81 -16
  88. package/dist/sdk/src/tailwind.ts +66 -0
  89. package/dist/sdk/src/ui/charts/RenderChartData.tsx +25 -0
  90. package/dist/sdk/src/ui/engine/core.ts +392 -0
  91. package/dist/sdk/src/ui/engine/dom.ts +183 -0
  92. package/dist/sdk/src/ui/engine/hooks.ts +195 -0
  93. package/dist/sdk/src/ui/engine/instance.ts +63 -0
  94. package/dist/sdk/src/ui/engine/keycapContext.ts +2 -0
  95. package/dist/sdk/src/ui/engine/registry.ts +262 -0
  96. package/dist/sdk/src/ui/engine/types.ts +62 -0
  97. package/dist/sdk/src/ui/icons.tsx +91 -0
  98. package/dist/sdk/src/ui/util.ts +83 -0
  99. package/dist/sdk/src/viewPeriod.ts +26 -0
  100. package/dist/sdk/src/vite.ts +92 -0
  101. package/dist/sdk/src/words/actions.ts +295 -0
  102. package/dist/sdk/src/words/clashes.ts +158 -0
  103. package/dist/sdk/src/words/format.ts +136 -0
  104. package/dist/sdk/src/words/index.ts +13 -0
  105. package/dist/sdk/src/words/keymap.ts +72 -0
  106. package/dist/sdk/src/words/keys.ts +297 -0
  107. package/dist/sdk/src/words/shell.ts +46 -0
  108. package/dist/sdk/src/words/types.ts +65 -0
  109. package/dist/skill-sync/index.js +832 -151
  110. package/dist/skill-sync/index.js.map +4 -4
  111. package/dist/skill-sync-worker.mjs +3941 -0
  112. package/dist/space-harness/host.js +1460 -0
  113. package/dist/space-sdk/dist/chunk-ZSN57ATH.js +5 -0
  114. package/dist/space-sdk/dist/index.js +15 -0
  115. package/dist/space-sdk/dist/ui.js +18 -0
  116. package/dist/space-sdk/package.json +33 -0
  117. package/dist/space-sdk/src/agentContext.d.ts +36 -0
  118. package/dist/space-sdk/src/components/Dialog.d.ts +20 -0
  119. package/dist/space-sdk/src/components/DocumentEditor.d.ts +46 -0
  120. package/dist/space-sdk/src/components/DocumentPage.d.ts +53 -0
  121. package/dist/space-sdk/src/components/Markdown.d.ts +18 -0
  122. package/dist/space-sdk/src/components/MarkdownEditor.d.ts +11 -0
  123. package/dist/space-sdk/src/components/MultiSelectActionBar.d.ts +31 -0
  124. package/dist/space-sdk/src/components/MultiSelectCheckbox.d.ts +14 -0
  125. package/dist/space-sdk/src/components/MultiSelectDragOverlay.d.ts +8 -0
  126. package/dist/space-sdk/src/components/NotisCommentBoundary.d.ts +36 -0
  127. package/dist/space-sdk/src/components/NotisSelectionBoundary.d.ts +16 -0
  128. package/dist/space-sdk/src/components/RecordComponents.d.ts +64 -0
  129. package/dist/space-sdk/src/components/ShortcutHints.d.ts +12 -0
  130. package/dist/space-sdk/src/components/Skeleton.d.ts +23 -0
  131. package/dist/space-sdk/src/config.ts +31 -0
  132. package/dist/space-sdk/src/documents.d.ts +59 -0
  133. package/dist/space-sdk/src/hooks/useAction.d.ts +8 -0
  134. package/dist/space-sdk/src/hooks/useActiveResource.d.ts +3 -0
  135. package/dist/space-sdk/src/hooks/useAgentContext.d.ts +7 -0
  136. package/dist/space-sdk/src/hooks/useBackend.d.ts +23 -0
  137. package/dist/space-sdk/src/hooks/useCloudComputer.d.ts +45 -0
  138. package/dist/space-sdk/src/hooks/useCloudComputerShell.d.ts +49 -0
  139. package/dist/space-sdk/src/hooks/useCollectionInteractions.d.ts +118 -0
  140. package/dist/space-sdk/src/hooks/useDatabaseSchema.d.ts +14 -0
  141. package/dist/space-sdk/src/hooks/useDatabaseSubscription.d.ts +26 -0
  142. package/dist/space-sdk/src/hooks/useDocument.d.ts +22 -0
  143. package/dist/space-sdk/src/hooks/useDocuments.d.ts +31 -0
  144. package/dist/space-sdk/src/hooks/useHandover.d.ts +43 -0
  145. package/dist/space-sdk/src/hooks/useLongPressSelection.d.ts +9 -0
  146. package/dist/space-sdk/src/hooks/useMarqueeSelection.d.ts +57 -0
  147. package/dist/space-sdk/src/hooks/useMultiSelect.d.ts +51 -0
  148. package/dist/space-sdk/src/hooks/useNativeDocumentBody.d.ts +24 -0
  149. package/dist/space-sdk/src/hooks/useNativeDocuments.d.ts +55 -0
  150. package/dist/space-sdk/src/hooks/useNotis.d.ts +25 -0
  151. package/dist/space-sdk/src/hooks/useNotisNavigation.d.ts +33 -0
  152. package/dist/space-sdk/src/hooks/useQuery.d.ts +22 -0
  153. package/dist/space-sdk/src/hooks/useTool.d.ts +20 -0
  154. package/dist/space-sdk/src/hooks/useToolQuery.d.ts +3 -0
  155. package/dist/space-sdk/src/hooks/useTools.d.ts +15 -0
  156. package/dist/space-sdk/src/hooks/useTopBarSearch.d.ts +48 -0
  157. package/dist/space-sdk/src/hooks/useTopBarTitleLeaf.d.ts +24 -0
  158. package/dist/space-sdk/src/hooks/useUpsertDocument.d.ts +27 -0
  159. package/dist/space-sdk/src/hooks/useViewParams.d.ts +76 -0
  160. package/dist/space-sdk/src/hooks/useViewerReads.d.ts +38 -0
  161. package/dist/space-sdk/src/index.d.ts +98 -0
  162. package/dist/space-sdk/src/interactions/actions.d.ts +33 -0
  163. package/dist/space-sdk/src/interactions/shortcuts.d.ts +42 -0
  164. package/dist/space-sdk/src/interactions/visibility.d.ts +2 -0
  165. package/dist/space-sdk/src/nativeData.d.ts +66 -0
  166. package/dist/space-sdk/src/provider.d.ts +17 -0
  167. package/dist/space-sdk/src/queryCache.d.ts +45 -0
  168. package/dist/space-sdk/src/queryErrors.ts +51 -0
  169. package/dist/space-sdk/src/runtime.d.ts +615 -0
  170. package/dist/space-sdk/src/runtimeModules.ts +33 -0
  171. package/dist/space-sdk/src/space.ts +442 -0
  172. package/dist/space-sdk/src/standardViewParams.d.ts +15 -0
  173. package/dist/space-sdk/src/tailwind-candidates.d.ts +3418 -0
  174. package/dist/space-sdk/src/tailwind.ts +66 -0
  175. package/dist/space-sdk/src/ui/actions/ActionToast.d.ts +15 -0
  176. package/dist/space-sdk/src/ui/actions/actionWords.d.ts +16 -0
  177. package/dist/space-sdk/src/ui/actions/index.d.ts +7 -0
  178. package/dist/space-sdk/src/ui/actions/useActions.d.ts +69 -0
  179. package/dist/space-sdk/src/ui/actions/useToast.d.ts +41 -0
  180. package/dist/space-sdk/src/ui/blocks/Tabs.d.ts +139 -0
  181. package/dist/space-sdk/src/ui/blocks/blocks.d.ts +217 -0
  182. package/dist/space-sdk/src/ui/blocks/index.d.ts +4 -0
  183. package/dist/space-sdk/src/ui/board/BoardView.d.ts +36 -0
  184. package/dist/space-sdk/src/ui/board/boardModel.d.ts +46 -0
  185. package/dist/space-sdk/src/ui/board/boardWords.d.ts +9 -0
  186. package/dist/space-sdk/src/ui/board/index.d.ts +5 -0
  187. package/dist/space-sdk/src/ui/calendar/CalendarView.d.ts +49 -0
  188. package/dist/space-sdk/src/ui/calendar/DayPicker.d.ts +23 -0
  189. package/dist/space-sdk/src/ui/calendar/calendarMath.d.ts +81 -0
  190. package/dist/space-sdk/src/ui/charts/Chart.d.ts +203 -0
  191. package/dist/space-sdk/src/ui/charts/ChartTable.d.ts +37 -0
  192. package/dist/space-sdk/src/ui/charts/RenderChartData.d.ts +16 -0
  193. package/dist/space-sdk/src/ui/charts/StatRow.d.ts +59 -0
  194. package/dist/space-sdk/src/ui/charts/chartData.d.ts +84 -0
  195. package/dist/space-sdk/src/ui/comments/CommentPane.d.ts +58 -0
  196. package/dist/space-sdk/src/ui/comments/commentModel.d.ts +157 -0
  197. package/dist/space-sdk/src/ui/comments/commentWords.d.ts +60 -0
  198. package/dist/space-sdk/src/ui/comments/index.d.ts +4 -0
  199. package/dist/space-sdk/src/ui/comments/useComments.d.ts +146 -0
  200. package/dist/space-sdk/src/ui/engine/HelpSheet.d.ts +12 -0
  201. package/dist/space-sdk/src/ui/engine/KeyboardEngine.d.ts +27 -0
  202. package/dist/space-sdk/src/ui/engine/core.d.ts +73 -0
  203. package/dist/space-sdk/src/ui/engine/hooks.d.ts +64 -0
  204. package/dist/space-sdk/src/ui/engine/index.d.ts +14 -0
  205. package/dist/space-sdk/src/ui/engine/instance.d.ts +21 -0
  206. package/dist/space-sdk/src/ui/engine/keycapContext.d.ts +2 -0
  207. package/dist/space-sdk/src/ui/engine/registry.d.ts +77 -0
  208. package/dist/space-sdk/src/ui/engine/step.d.ts +11 -0
  209. package/dist/space-sdk/src/ui/engine/types.d.ts +54 -0
  210. package/dist/space-sdk/src/ui/fields/fieldWords.d.ts +56 -0
  211. package/dist/space-sdk/src/ui/fields/fields.d.ts +162 -0
  212. package/dist/space-sdk/src/ui/fields/index.d.ts +5 -0
  213. package/dist/space-sdk/src/ui/fields/useFieldDraft.d.ts +22 -0
  214. package/dist/space-sdk/src/ui/gallery/AttachmentGallery.d.ts +53 -0
  215. package/dist/space-sdk/src/ui/gallery/ImageViewer.d.ts +27 -0
  216. package/dist/space-sdk/src/ui/gallery/galleryWords.d.ts +95 -0
  217. package/dist/space-sdk/src/ui/gallery/index.d.ts +5 -0
  218. package/dist/space-sdk/src/ui/icons.d.ts +13 -0
  219. package/dist/space-sdk/src/ui/index.d.ts +54 -0
  220. package/dist/space-sdk/src/ui/keycap/Keycap.d.ts +22 -0
  221. package/dist/space-sdk/src/ui/keycap/index.d.ts +2 -0
  222. package/dist/space-sdk/src/ui/layout/Panels.d.ts +50 -0
  223. package/dist/space-sdk/src/ui/layout/chatPanelEdge.d.ts +77 -0
  224. package/dist/space-sdk/src/ui/layout/index.d.ts +5 -0
  225. package/dist/space-sdk/src/ui/layout/panelMath.d.ts +91 -0
  226. package/dist/space-sdk/src/ui/list/ListReader.d.ts +380 -0
  227. package/dist/space-sdk/src/ui/list/Row.d.ts +145 -0
  228. package/dist/space-sdk/src/ui/list/SelectionBar.d.ts +65 -0
  229. package/dist/space-sdk/src/ui/list/index.d.ts +14 -0
  230. package/dist/space-sdk/src/ui/list/rowState.d.ts +22 -0
  231. package/dist/space-sdk/src/ui/list/useListKeys.d.ts +89 -0
  232. package/dist/space-sdk/src/ui/list/useReorder.d.ts +86 -0
  233. package/dist/space-sdk/src/ui/list/useSelection.d.ts +90 -0
  234. package/dist/space-sdk/src/ui/open/OpenAsMenu.d.ts +15 -0
  235. package/dist/space-sdk/src/ui/open/index.d.ts +6 -0
  236. package/dist/space-sdk/src/ui/open/openModeStore.d.ts +39 -0
  237. package/dist/space-sdk/src/ui/open/useOpenMode.d.ts +17 -0
  238. package/dist/space-sdk/src/ui/overlay/Menu.d.ts +88 -0
  239. package/dist/space-sdk/src/ui/overlay/Popover.d.ts +28 -0
  240. package/dist/space-sdk/src/ui/overlay/Tooltip.d.ts +19 -0
  241. package/dist/space-sdk/src/ui/overlay/index.d.ts +9 -0
  242. package/dist/space-sdk/src/ui/overlay/layerRoot.d.ts +24 -0
  243. package/dist/space-sdk/src/ui/overlay/position.d.ts +31 -0
  244. package/dist/space-sdk/src/ui/period/PeriodPicker.d.ts +30 -0
  245. package/dist/space-sdk/src/ui/period/dateFormat.d.ts +50 -0
  246. package/dist/space-sdk/src/ui/period/dateWords.d.ts +88 -0
  247. package/dist/space-sdk/src/ui/period/periods.d.ts +70 -0
  248. package/dist/space-sdk/src/ui/period/usePeriod.d.ts +23 -0
  249. package/dist/space-sdk/src/ui/present/index.d.ts +3 -0
  250. package/dist/space-sdk/src/ui/present/presentWords.d.ts +27 -0
  251. package/dist/space-sdk/src/ui/present/screens.d.ts +36 -0
  252. package/dist/space-sdk/src/ui/present/useAudienceScreen.d.ts +38 -0
  253. package/dist/space-sdk/src/ui/styles.d.ts +21 -0
  254. package/dist/space-sdk/src/ui/timeline/TimelineView.d.ts +44 -0
  255. package/dist/space-sdk/src/ui/timeline/timelineMath.d.ts +123 -0
  256. package/dist/space-sdk/src/viewPeriod.d.ts +8 -0
  257. package/dist/space-sdk/src/vite.ts +92 -0
  258. package/dist/space-sdk/src/words/actions.ts +295 -0
  259. package/dist/space-sdk/src/words/clashes.ts +158 -0
  260. package/dist/space-sdk/src/words/format.ts +136 -0
  261. package/dist/space-sdk/src/words/index.ts +13 -0
  262. package/dist/space-sdk/src/words/keymap.ts +72 -0
  263. package/dist/space-sdk/src/words/keys.ts +297 -0
  264. package/dist/space-sdk/src/words/shell.ts +46 -0
  265. package/dist/space-sdk/src/words/types.ts +65 -0
  266. package/dist/view-renderer/dist/host.css +1 -0
  267. package/dist/view-renderer/dist/host.js +2374 -0
  268. package/dist/view-renderer/dist/theme.css +73 -0
  269. package/dist/view-renderer/src/dom.js +212 -0
  270. package/dist/view-renderer/src/errors.js +8 -0
  271. package/dist/view-renderer/src/host.tsx +250 -0
  272. package/dist/view-renderer/src/index.js +4 -0
  273. package/dist/view-renderer/src/png.js +213 -0
  274. package/dist/view-renderer/src/pool.js +110 -0
  275. package/dist/view-renderer/src/remote-broker.js +59 -0
  276. package/dist/view-renderer/src/renderer.js +271 -0
  277. package/package.json +21 -11
  278. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +63 -12
  279. package/skills/notis-onboarding/BRIEF.md +14 -9
  280. package/skills/notis-query/cli.md +1 -1
  281. package/skills/notis-spaces/cli.md +540 -0
  282. package/src/cli.js +8 -8
  283. package/src/command-specs/agents.js +121 -8
  284. package/src/command-specs/auth.js +3 -3
  285. package/src/command-specs/diagnostics.js +97 -20
  286. package/src/command-specs/documents.js +158 -0
  287. package/src/command-specs/files.js +29 -0
  288. package/src/command-specs/handover.js +331 -79
  289. package/src/command-specs/helpers.js +50 -40
  290. package/src/command-specs/index.js +14 -5
  291. package/src/command-specs/meta.js +38 -9
  292. package/src/command-specs/onboarding.js +1 -1
  293. package/src/command-specs/profile.js +42 -1
  294. package/src/command-specs/skills.js +314 -9
  295. package/src/command-specs/smoke.js +16 -4
  296. package/src/command-specs/spaces.js +783 -0
  297. package/src/command-specs/tools.js +118 -21
  298. package/src/command-specs/version-history.js +20 -0
  299. package/src/command-specs/views.js +69 -0
  300. package/src/runtime/agent-browser.js +178 -267
  301. package/src/runtime/agent-setup.js +172 -13
  302. package/src/runtime/app-boundary-validator.js +338 -2
  303. package/src/runtime/app-data-literals.js +82 -0
  304. package/src/runtime/app-platform.js +750 -1570
  305. package/src/runtime/auth-recovery.js +17 -0
  306. package/src/runtime/base-skills.d.ts +11 -0
  307. package/src/runtime/base-skills.js +100 -3
  308. package/src/runtime/computer-identity.d.ts +30 -0
  309. package/src/runtime/computer-identity.js +151 -0
  310. package/src/runtime/config-lock.js +135 -0
  311. package/src/runtime/confined-space-build.js +74 -0
  312. package/src/runtime/confined-space-verify.js +49 -0
  313. package/src/runtime/delegated-context.js +5 -21
  314. package/src/runtime/native-record-files.js +47 -0
  315. package/src/runtime/oauth.js +342 -31
  316. package/src/runtime/profiles.js +19 -112
  317. package/src/runtime/retired-sdk-files.js +24 -0
  318. package/src/runtime/retry.js +150 -0
  319. package/src/runtime/skill-sync/cloud-client.ts +33 -8
  320. package/src/runtime/skill-sync/index.ts +443 -106
  321. package/src/runtime/skill-sync/local-scanner.ts +248 -32
  322. package/src/runtime/skill-sync/native-identity.ts +102 -0
  323. package/src/runtime/skill-sync/native-plan.ts +70 -0
  324. package/src/runtime/skill-sync/symlink-manager.ts +66 -16
  325. package/src/runtime/skill-sync/sync-plan.ts +34 -2
  326. package/src/runtime/skill-sync/types.ts +54 -0
  327. package/src/runtime/skill-sync/write-cloud-skill.ts +4 -4
  328. package/src/runtime/skill-sync-service.js +109 -0
  329. package/src/runtime/source-root.js +32 -0
  330. package/src/runtime/space-collection-scaffold.js +107 -0
  331. package/src/runtime/space-database-structures.js +229 -0
  332. package/src/runtime/space-fixture-broker.js +68 -0
  333. package/src/runtime/space-lock.js +337 -0
  334. package/src/runtime/space-platform.js +133 -0
  335. package/src/runtime/space-source.js +290 -0
  336. package/src/runtime/space-test-server.js +354 -0
  337. package/src/runtime/space-verification.js +94 -0
  338. package/src/runtime/space-verify-browser-worker.js +51 -0
  339. package/src/runtime/space-view-manifest.js +368 -0
  340. package/src/runtime/sync-skills.d.ts +5 -1
  341. package/src/runtime/sync-skills.js +11 -2
  342. package/src/runtime/tool-confirmation.js +138 -0
  343. package/src/runtime/transport.js +118 -24
  344. package/src/runtime/upsell.js +219 -0
  345. package/src/runtime/view-render.js +49 -0
  346. package/src/skill-sync-worker-entry.js +2 -0
  347. package/src/skill-sync-worker.js +114 -0
  348. package/src/space-harness-host.ts +122 -0
  349. package/template/.harness/index.html.tmpl +9 -352
  350. package/template/app/globals.css +29 -3
  351. package/template/app/layout.tsx +2 -6
  352. package/template/components/ui/badge.tsx +7 -4
  353. package/template/components/ui/button.tsx +1 -1
  354. package/template/components/ui/native-select.tsx +24 -0
  355. package/template/notis.config.ts +4 -36
  356. package/template/package-lock.json +1800 -1867
  357. package/template/package.json +16 -14
  358. package/template/postcss.config.mjs +1 -1
  359. package/template/tailwind.config.ts +1 -6
  360. package/template/tsconfig.json +1 -0
  361. package/template/vite.config.ts +2 -2
  362. package/dist/base-skills/notis-apps/SKILL.md +0 -613
  363. package/skills/notis-apps/cli.md +0 -285
  364. package/src/command-specs/apps.js +0 -3011
  365. package/src/runtime/app-changelog.js +0 -79
  366. package/src/runtime/app-dev-consumers.js +0 -154
  367. package/src/runtime/app-dev-host-lock.js +0 -80
  368. package/src/runtime/app-dev-process-identity.js +0 -111
  369. package/src/runtime/app-dev-roots.js +0 -284
  370. package/src/runtime/app-dev-server.js +0 -1140
  371. package/src/runtime/app-dev-sessions.js +0 -185
  372. package/src/runtime/app-registry-scaffolds.js +0 -367
  373. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  374. package/src/runtime/cli-mode.generated.js +0 -5
  375. package/src/runtime/cli-mode.js +0 -34
  376. package/src/runtime/store-screenshot.js +0 -143
  377. package/template/CHANGELOG.md +0 -5
  378. package/template/app/page.tsx +0 -60
  379. package/template/components/ui/card.tsx +0 -56
  380. package/template/packages/sdk/src/components/DocumentEditor.tsx +0 -103
  381. package/template/packages/sdk/src/config.ts +0 -259
  382. package/template/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
  383. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
  384. package/template/packages/sdk/src/hooks/useDocument.ts +0 -78
  385. package/template/packages/sdk/src/hooks/useNotisNavigation.ts +0 -52
  386. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
  387. package/template/packages/sdk/src/runtime.ts +0 -439
  388. package/template/packages/sdk/src/vite.ts +0 -56
  389. package/template/packages/sdk/tsconfig.json +0 -15
  390. /package/{template/packages → dist}/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
  391. /package/{template/packages → dist}/sdk/src/components/ShortcutHints.tsx +0 -0
  392. /package/{template/packages → dist}/sdk/src/hooks/useActiveResource.ts +0 -0
  393. /package/{template/packages → dist}/sdk/src/hooks/useTool.ts +0 -0
  394. /package/{template/packages → dist}/sdk/src/hooks/useTools.ts +0 -0
  395. /package/{template/packages → dist}/sdk/src/ui.ts +0 -0
@@ -17,22 +17,23 @@ import {
17
17
  } from '../interactions/actions';
18
18
  import {
19
19
  activateShortcutCollection,
20
+ releaseShortcutCollection,
20
21
  createShortcutCollectionOwner,
21
22
  useShortcuts,
22
23
  type ShortcutDefinition,
23
24
  type ShortcutScope,
24
25
  } from '../interactions/shortcuts';
25
26
 
27
+ import type { MultiSelectActionBarProps } from '../components/MultiSelectActionBar';
28
+ import { isInteractionElementVisible } from '../interactions/visibility';
29
+ import { useLongPressSelection } from './useLongPressSelection';
30
+ import { useMarqueeSelection, type SelectionMarqueeRect } from './useMarqueeSelection';
31
+
26
32
  export const COLLECTION_ITEM_ATTRIBUTE = 'data-notis-collection-item-id';
27
33
  const LEGACY_ROW_ATTRIBUTE = 'data-notis-row-id';
28
34
  const INTERACTIVE_SELECTOR = 'button, a, input, textarea, select, [role="button"], [contenteditable]:not([contenteditable="false"])';
29
35
 
30
- export interface SelectionMarqueeRect {
31
- left: number;
32
- top: number;
33
- width: number;
34
- height: number;
35
- }
36
+ export type { SelectionMarqueeRect };
36
37
 
37
38
  export type CollectionInteractionReason =
38
39
  | 'activate'
@@ -93,6 +94,8 @@ export interface UseCollectionInteractionsOptions<T> {
93
94
  enabled?: boolean;
94
95
  bindKeyboardShortcuts?: boolean;
95
96
  enableDragSelect?: boolean;
97
+ /** Touch-only long press; optional per view. */
98
+ enableLongPressSelection?: boolean;
96
99
  /** Plain item clicks activate the item and clear checkbox selection by default. */
97
100
  clearSelectionOnPlainClick?: boolean;
98
101
  dragThreshold?: number;
@@ -114,6 +117,7 @@ export interface CollectionInteractionController<T> {
114
117
  lastClickedId: string | null;
115
118
  dragRect: SelectionMarqueeRect | null;
116
119
  actions: ResolvedCollectionAction[];
120
+ getActionBarProps: () => Pick<MultiSelectActionBarProps, 'selectedCount' | 'actions' | 'collectionOwnerId' | 'isAvailable' | 'shortcutsEnabled' | 'onClearSelection'>;
117
121
 
118
122
  isSelected: (id: string) => boolean;
119
123
  getSelectedItems: () => T[];
@@ -128,7 +132,7 @@ export interface CollectionInteractionController<T> {
128
132
  onCheckboxClick: (id: string) => (event: ReactMouseEvent) => void;
129
133
  onRowMouseDown: (id: string) => (event: ReactMouseEvent) => void;
130
134
  getRowProps: (id: string) => Record<string, string>;
131
- getItemProps: (id: string) => {
135
+ getItemProps: (id: string) => Partial<ReturnType<ReturnType<typeof useLongPressSelection>>> & {
132
136
  [COLLECTION_ITEM_ATTRIBUTE]: string;
133
137
  [LEGACY_ROW_ATTRIBUTE]: string;
134
138
  tabIndex: number;
@@ -185,13 +189,6 @@ function setsEqual(a: ReadonlySet<string>, b: ReadonlySet<string>): boolean {
185
189
  return true;
186
190
  }
187
191
 
188
- function rectsIntersect(
189
- a: { left: number; top: number; right: number; bottom: number },
190
- b: { left: number; top: number; right: number; bottom: number },
191
- ): boolean {
192
- return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
193
- }
194
-
195
192
  export function useCollectionInteractions<T>(
196
193
  options: UseCollectionInteractionsOptions<T>,
197
194
  ): CollectionInteractionController<T> {
@@ -213,6 +210,7 @@ export function useCollectionInteractions<T>(
213
210
  enabled = true,
214
211
  bindKeyboardShortcuts = true,
215
212
  enableDragSelect = selectionMode === 'multiple',
213
+ enableLongPressSelection = false,
216
214
  clearSelectionOnPlainClick = true,
217
215
  dragThreshold = 5,
218
216
  shortcuts: shortcutOverrides,
@@ -233,13 +231,14 @@ export function useCollectionInteractions<T>(
233
231
  defaultAnchorId === undefined ? defaultActiveId : defaultAnchorId,
234
232
  );
235
233
  const anchorId = controlledAnchorId === undefined ? uncontrolledAnchorId : controlledAnchorId;
236
- const [dragRect, setDragRect] = useState<SelectionMarqueeRect | null>(null);
237
234
 
238
235
  const itemsRef = useRef(items);
239
236
  const getIdRef = useRef(getId);
240
237
  const selectedIdsRef = useRef(selectedIds);
241
238
  const activeIdRef = useRef(activeId);
242
239
  const anchorIdRef = useRef(anchorId);
240
+ // Keep manually selected rows outside the current keyboard range on reversal.
241
+ const keyboardRangeBaseRef = useRef<Set<string> | null>(null);
243
242
  const onSelectionChangeRef = useRef(onSelectionChange);
244
243
  const onActiveIdChangeRef = useRef(onActiveIdChange);
245
244
  const onAnchorIdChangeRef = useRef(onAnchorIdChange);
@@ -247,6 +246,11 @@ export function useCollectionInteractions<T>(
247
246
  const onActivateRef = useRef(onActivate);
248
247
  const resolveNextIdRef = useRef(resolveNextId);
249
248
  const containerRef = useRef<HTMLElement | null>(null);
249
+ const [containerElement, setContainerElement] = useState<HTMLElement | null>(null);
250
+ const setContainerRef = useCallback((node: HTMLElement | null) => {
251
+ containerRef.current = node;
252
+ setContainerElement(node);
253
+ }, []);
250
254
  const shortcutCollectionOwnerRef = useRef<string | null>(null);
251
255
  if (!shortcutCollectionOwnerRef.current) {
252
256
  shortcutCollectionOwnerRef.current = createShortcutCollectionOwner();
@@ -254,6 +258,9 @@ export function useCollectionInteractions<T>(
254
258
 
255
259
  itemsRef.current = items;
256
260
  getIdRef.current = getId;
261
+ if (!setsEqual(selectedIdsRef.current, selectedIds) || anchorIdRef.current !== anchorId) {
262
+ keyboardRangeBaseRef.current = null;
263
+ }
257
264
  selectedIdsRef.current = selectedIds;
258
265
  activeIdRef.current = activeId;
259
266
  anchorIdRef.current = anchorId;
@@ -316,6 +323,7 @@ export function useCollectionInteractions<T>(
316
323
  const activate = useCallback((id: string) => {
317
324
  const item = itemsRef.current.find((candidate) => getIdRef.current(candidate) === id);
318
325
  if (!item || !selectableIdsRef.current.includes(id)) return;
326
+ keyboardRangeBaseRef.current = null;
319
327
  setActive(id, 'activate');
320
328
  setAnchor(id, 'activate');
321
329
  onActivateRef.current?.(item, id);
@@ -323,6 +331,7 @@ export function useCollectionInteractions<T>(
323
331
 
324
332
  const toggle = useCallback((id: string) => {
325
333
  if (selectionMode === 'none' || !selectableIdsRef.current.includes(id)) return;
334
+ keyboardRangeBaseRef.current = null;
326
335
  const next = new Set(selectedIdsRef.current);
327
336
  if (next.has(id)) next.delete(id);
328
337
  else {
@@ -334,6 +343,7 @@ export function useCollectionInteractions<T>(
334
343
  }, [commitSelection, selectionMode, setAnchor]);
335
344
 
336
345
  const select = useCallback((ids: string[]) => {
346
+ keyboardRangeBaseRef.current = null;
337
347
  const next = new Set(ids);
338
348
  const last = ids[ids.length - 1] ?? null;
339
349
  setAnchor(last, 'toggle');
@@ -341,12 +351,14 @@ export function useCollectionInteractions<T>(
341
351
  }, [commitSelection, setAnchor]);
342
352
 
343
353
  const clear = useCallback(() => {
354
+ keyboardRangeBaseRef.current = null;
344
355
  setAnchor(null, 'clear');
345
356
  commitSelection(new Set(), 'clear');
346
357
  }, [commitSelection, setAnchor]);
347
358
 
348
359
  const selectAll = useCallback(() => {
349
360
  if (selectionMode === 'none') return;
361
+ keyboardRangeBaseRef.current = null;
350
362
  const ids = selectionMode === 'single' ? selectableIdsRef.current.slice(0, 1) : selectableIdsRef.current;
351
363
  const last = ids[ids.length - 1] ?? null;
352
364
  setAnchor(last, 'select-all');
@@ -361,12 +373,16 @@ export function useCollectionInteractions<T>(
361
373
  commitSelection(new Set([toId]), reason);
362
374
  return;
363
375
  }
364
- commitSelection(new Set(ids.slice(Math.min(from, to), Math.max(from, to) + 1)), reason);
376
+ commitSelection(new Set([
377
+ ...(keyboardRangeBaseRef.current ?? []),
378
+ ...ids.slice(Math.min(from, to), Math.max(from, to) + 1),
379
+ ]), reason);
365
380
  }, [commitSelection]);
366
381
 
367
382
  const selectRange = useCallback((fromId: string, toId: string) => {
368
383
  const ids = selectableIdsRef.current;
369
384
  if (selectionMode === 'none' || !ids.includes(toId)) return;
385
+ keyboardRangeBaseRef.current = null;
370
386
  const resolvedFromId = ids.includes(fromId)
371
387
  ? fromId
372
388
  : activeIdRef.current && ids.includes(activeIdRef.current)
@@ -390,6 +406,9 @@ export function useCollectionInteractions<T>(
390
406
 
391
407
  useEffect(() => {
392
408
  const allowed = new Set(selectableIds);
409
+ if (keyboardRangeBaseRef.current) {
410
+ keyboardRangeBaseRef.current = new Set(Array.from(keyboardRangeBaseRef.current).filter((id) => allowed.has(id)));
411
+ }
393
412
  const pruned = new Set(Array.from(selectedIdsRef.current).filter((id) => allowed.has(id)));
394
413
  if (!setsEqual(pruned, selectedIdsRef.current)) commitSelection(pruned, 'items-changed');
395
414
  if (activeIdRef.current && !allowed.has(activeIdRef.current)) setActive(null, 'items-changed');
@@ -398,11 +417,39 @@ export function useCollectionInteractions<T>(
398
417
  }
399
418
  }, [commitSelection, selectableIds, setActive, setAnchor]);
400
419
 
420
+ const focusItem = useCallback((id: string) => {
421
+ const nodes = containerRef.current?.querySelectorAll<HTMLElement>(`[${COLLECTION_ITEM_ATTRIBUTE}]`);
422
+ const node = nodes ? Array.from(nodes).find(
423
+ (candidate) => candidate.getAttribute(COLLECTION_ITEM_ATTRIBUTE) === id && isInteractionElementVisible(candidate),
424
+ ) : null;
425
+ node?.focus({ preventScroll: true });
426
+ node?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
427
+ }, []);
428
+
429
+ const focusPointerItem = useCallback((id: string) => {
430
+ setActive(id, 'activate');
431
+ focusItem(id);
432
+ }, [focusItem, setActive]);
433
+
434
+ const selectPointerRange = useCallback((id: string) => {
435
+ const rangeAnchor = anchorIdRef.current ?? activeIdRef.current ?? id;
436
+ const ids = selectableIdsRef.current;
437
+ const from = ids.indexOf(rangeAnchor);
438
+ const to = ids.indexOf(id);
439
+ const range = new Set(ids.slice(Math.min(from, to), Math.max(from, to) + 1));
440
+ const base = keyboardRangeBaseRef.current ?? new Set(Array.from(selectedIdsRef.current).filter((selected) => !range.has(selected)));
441
+ keyboardRangeBaseRef.current = base;
442
+ setAnchor(rangeAnchor, 'range');
443
+ replaceRange(rangeAnchor, id, 'range');
444
+ focusPointerItem(id);
445
+ }, [focusPointerItem, replaceRange, setAnchor]);
446
+
401
447
  const moveActive = useCallback((direction: CollectionNavigationDirection, extend = false) => {
402
448
  const ids = selectableIdsRef.current;
403
449
  if (ids.length === 0) return;
404
450
  const current = activeIdRef.current;
405
- let next = resolveNextIdRef.current?.({
451
+ // Range gestures follow the displayed order, independent of asymmetric grid geometry.
452
+ let next = extend ? null : resolveNextIdRef.current?.({
406
453
  direction,
407
454
  currentId: current,
408
455
  items: itemsRef.current,
@@ -419,23 +466,21 @@ export function useCollectionInteractions<T>(
419
466
  if (!next || next === current) return;
420
467
  const stableAnchor = anchorIdRef.current ?? current ?? next;
421
468
  if (extend) {
469
+ keyboardRangeBaseRef.current ??= new Set(
470
+ Array.from(selectedIdsRef.current).filter((id) => id !== stableAnchor),
471
+ );
422
472
  if (!anchorIdRef.current) {
423
473
  setAnchor(stableAnchor, 'range');
424
474
  }
425
475
  replaceRange(stableAnchor, next, 'range');
426
476
  } else {
477
+ keyboardRangeBaseRef.current = new Set();
427
478
  setAnchor(next, 'navigate');
428
479
  }
429
480
  setActive(next, extend ? 'range' : 'navigate');
430
481
  const nextId = next;
431
- requestAnimationFrame(() => {
432
- const nodes = containerRef.current?.querySelectorAll<HTMLElement>(`[${COLLECTION_ITEM_ATTRIBUTE}]`);
433
- const node = nodes ? Array.from(nodes).find(
434
- (candidate) => candidate.getAttribute(COLLECTION_ITEM_ATTRIBUTE) === nextId,
435
- ) : null;
436
- node?.focus({ preventScroll: true });
437
- });
438
- }, [replaceRange, setActive, setAnchor]);
482
+ requestAnimationFrame(() => focusItem(nextId));
483
+ }, [focusItem, replaceRange, setActive, setAnchor]);
439
484
 
440
485
  const keyboard = useMemo<CollectionKeyboardShortcuts>(
441
486
  () => ({ ...DEFAULT_SHORTCUTS, ...(shortcutOverrides || {}) }),
@@ -456,7 +501,9 @@ export function useCollectionInteractions<T>(
456
501
  const extendDown = filterShortcutKeys(keyboard.extendNext, (key) => !key.toLowerCase().endsWith('arrowright'));
457
502
  const extendLeft = filterShortcutKeys(keyboard.extendPrevious, (key) => key.toLowerCase().endsWith('arrowleft'));
458
503
  const extendUp = filterShortcutKeys(keyboard.extendPrevious, (key) => !key.toLowerCase().endsWith('arrowleft'));
459
- add('collection.clear', 'Clear selection', keyboard.clear, clear);
504
+ // An empty collection has nothing to dismiss. Let the enclosing surface
505
+ // own Escape (for example Desktop's final Inbox navigation layer).
506
+ add('collection.clear', 'Clear selection', selectedIds.size > 0 ? keyboard.clear : false, clear);
460
507
  add('collection.select-all', 'Select all visible items', selectionMode === 'none' ? false : keyboard.selectAll, selectAll);
461
508
  add('collection.toggle', 'Toggle active item', selectionMode === 'none' ? false : keyboard.toggle, () => {
462
509
  const id = activeIdRef.current ?? anchorIdRef.current;
@@ -477,124 +524,52 @@ export function useCollectionInteractions<T>(
477
524
  if (id) activate(id);
478
525
  });
479
526
  return definitions;
480
- }, [activate, clear, keyboard, moveActive, selectAll, selectionMode, toggle]);
527
+ }, [activate, clear, keyboard, moveActive, selectAll, selectedIds.size, selectionMode, toggle]);
528
+ const isAvailable = useCallback(() => isInteractionElementVisible(containerRef.current), []);
529
+ useEffect(() => {
530
+ const owner = shortcutCollectionOwnerRef.current!;
531
+ const container = containerElement;
532
+ const Observer = container?.ownerDocument.defaultView?.MutationObserver;
533
+ const observer = Observer ? new Observer(() => {
534
+ if (!isAvailable()) releaseShortcutCollection(owner);
535
+ }) : null;
536
+ let element: Element | null = container;
537
+ while (element) {
538
+ observer?.observe(element, { attributes: true, attributeFilter: ['hidden', 'inert', 'aria-hidden', 'style', 'class'] });
539
+ const root: Node = element.getRootNode();
540
+ element = element.parentElement ?? ('host' in root ? (root as ShadowRoot).host : null);
541
+ }
542
+ return () => { observer?.disconnect(); releaseShortcutCollection(owner); };
543
+ }, [containerElement, isAvailable]);
544
+
481
545
  useShortcuts(keyDefinitions, {
482
546
  enabled: enabled && bindKeyboardShortcuts,
483
547
  scope: shortcutScope,
484
548
  priority: shortcutPriority,
485
549
  collectionOwnerId: shortcutCollectionOwnerRef.current,
550
+ isAvailable,
486
551
  });
487
552
 
488
553
  const activateCollectionShortcuts = useCallback(() => {
489
554
  activateShortcutCollection(shortcutCollectionOwnerRef.current!);
490
555
  }, []);
491
556
 
492
- const dragPendingRef = useRef(false);
493
- const dragActiveRef = useRef(false);
494
- const dragStartRef = useRef({ x: 0, y: 0 });
495
- const dragCurrentRef = useRef({ x: 0, y: 0 });
496
- const clickShouldClearRef = useRef(false);
497
- const suppressNextClickRef = useRef(false);
498
-
499
- const armSuppressNextClick = useCallback(() => {
500
- suppressNextClickRef.current = true;
501
- setTimeout(() => { suppressNextClickRef.current = false; }, 0);
502
- }, []);
503
-
504
- const computeDragRect = useCallback((): SelectionMarqueeRect => ({
505
- left: Math.min(dragStartRef.current.x, dragCurrentRef.current.x),
506
- top: Math.min(dragStartRef.current.y, dragCurrentRef.current.y),
507
- width: Math.abs(dragCurrentRef.current.x - dragStartRef.current.x),
508
- height: Math.abs(dragCurrentRef.current.y - dragStartRef.current.y),
509
- }), []);
510
-
511
- const computeIntersectingIds = useCallback((rect: {
512
- left: number; top: number; right: number; bottom: number;
513
- }) => {
514
- const nodes = containerRef.current?.querySelectorAll<HTMLElement>(`[${COLLECTION_ITEM_ATTRIBUTE}]`);
515
- if (!nodes) return [];
516
- const ids: string[] = [];
517
- nodes.forEach((node) => {
518
- if (!rectsIntersect(rect, node.getBoundingClientRect())) return;
519
- const id = node.getAttribute(COLLECTION_ITEM_ATTRIBUTE);
520
- if (id && selectableIdsRef.current.includes(id)) ids.push(id);
521
- });
522
- return ids;
523
- }, []);
524
-
525
- const handleContainerMouseDown = useCallback((event: ReactMouseEvent) => {
526
- if (!enabled || event.button !== 0) return;
527
- const target = event.target as HTMLElement | null;
528
- if (!target) return;
529
- const itemTarget = target.closest(`[${COLLECTION_ITEM_ATTRIBUTE}]`);
530
- const interactiveTarget = target.closest(INTERACTIVE_SELECTOR);
531
- if (interactiveTarget && interactiveTarget !== itemTarget) {
532
- clickShouldClearRef.current = false;
533
- return;
534
- }
535
- clickShouldClearRef.current = !itemTarget;
536
- if (itemTarget) return;
537
- if (!enableDragSelect || selectionMode !== 'multiple') return;
538
- dragPendingRef.current = true;
539
- dragStartRef.current = { x: event.clientX, y: event.clientY };
540
- dragCurrentRef.current = { x: event.clientX, y: event.clientY };
541
- }, [enableDragSelect, enabled, selectionMode]);
542
-
543
- const handleContainerClickCapture = useCallback((event: ReactMouseEvent) => {
544
- if (!suppressNextClickRef.current) return;
545
- suppressNextClickRef.current = false;
546
- event.preventDefault();
547
- event.stopPropagation();
548
- }, []);
549
-
550
- useEffect(() => {
551
- if (!enabled || !enableDragSelect || selectionMode !== 'multiple') return;
552
- const handleMouseMove = (event: MouseEvent) => {
553
- if (dragPendingRef.current) {
554
- const dx = event.clientX - dragStartRef.current.x;
555
- const dy = event.clientY - dragStartRef.current.y;
556
- if (Math.abs(dx) > dragThreshold || Math.abs(dy) > dragThreshold) {
557
- dragPendingRef.current = false;
558
- dragActiveRef.current = true;
559
- window.getSelection()?.removeAllRanges();
560
- } else return;
561
- }
562
- if (!dragActiveRef.current) return;
563
- event.preventDefault();
564
- dragCurrentRef.current = { x: event.clientX, y: event.clientY };
565
- const rect = computeDragRect();
566
- setDragRect(rect);
567
- const ids = computeIntersectingIds({
568
- left: rect.left,
569
- top: rect.top,
570
- right: rect.left + rect.width,
571
- bottom: rect.top + rect.height,
572
- });
557
+ // The marquee (drag from empty space) is the shared one the toolkit list uses too.
558
+ const marquee = useMarqueeSelection({
559
+ enabled: enabled && enableDragSelect && selectionMode === 'multiple',
560
+ itemAttribute: COLLECTION_ITEM_ATTRIBUTE,
561
+ isSelectable: (id) => selectableIdsRef.current.includes(id),
562
+ onSelect: (ids) => {
573
563
  commitSelection(new Set(ids), 'drag');
574
564
  const last = ids[ids.length - 1] ?? null;
575
565
  setAnchor(last, 'drag');
576
- };
577
- const handleMouseUp = () => {
578
- const wasPending = dragPendingRef.current;
579
- const wasActive = dragActiveRef.current;
580
- dragPendingRef.current = false;
581
- dragActiveRef.current = false;
582
- if (wasActive) {
583
- setDragRect(null);
584
- clickShouldClearRef.current = false;
585
- armSuppressNextClick();
586
- } else if (wasPending && clickShouldClearRef.current) {
587
- clickShouldClearRef.current = false;
588
- clear();
589
- }
590
- };
591
- document.addEventListener('mousemove', handleMouseMove);
592
- document.addEventListener('mouseup', handleMouseUp);
593
- return () => {
594
- document.removeEventListener('mousemove', handleMouseMove);
595
- document.removeEventListener('mouseup', handleMouseUp);
596
- };
597
- }, [armSuppressNextClick, clear, commitSelection, computeDragRect, computeIntersectingIds, dragThreshold, enableDragSelect, enabled, selectionMode, setAnchor]);
566
+ },
567
+ onClear: clear,
568
+ threshold: dragThreshold,
569
+ interactiveSelector: INTERACTIVE_SELECTOR,
570
+ });
571
+ const dragRect = marquee.rect;
572
+ const { armSuppressNextClick, onMouseDown: handleContainerMouseDown, onClickCapture: handleContainerClickCapture } = marquee;
598
573
 
599
574
  const onRowMouseDown = useCallback((id: string) => (event: ReactMouseEvent) => {
600
575
  if (!enabled || event.button !== 0 || event.shiftKey || !(event.metaKey || event.ctrlKey)) return;
@@ -604,23 +579,34 @@ export function useCollectionInteractions<T>(
604
579
  event.preventDefault();
605
580
  event.stopPropagation();
606
581
  toggle(id);
582
+ focusPointerItem(id);
607
583
  armSuppressNextClick();
608
- }, [armSuppressNextClick, enabled, toggle]);
584
+ }, [armSuppressNextClick, enabled, focusPointerItem, toggle]);
609
585
 
610
586
  const onCheckboxClick = useCallback((id: string) => (event: ReactMouseEvent) => {
611
587
  event.stopPropagation();
588
+ if (!enabled) return;
612
589
  if (event.shiftKey && selectionMode === 'multiple') {
613
- const rangeAnchor = anchorIdRef.current ?? activeIdRef.current ?? id;
614
- selectRange(rangeAnchor, id);
615
- } else toggle(id);
616
- }, [selectRange, selectionMode, toggle]);
590
+ selectPointerRange(id);
591
+ } else {
592
+ toggle(id);
593
+ focusPointerItem(id);
594
+ }
595
+ }, [enabled, focusPointerItem, selectPointerRange, selectionMode, toggle]);
617
596
 
618
597
  const getRowProps = useCallback((id: string) => ({
619
598
  [COLLECTION_ITEM_ATTRIBUTE]: id,
620
599
  [LEGACY_ROW_ATTRIBUTE]: id,
621
600
  }), []);
622
601
 
602
+ const longPress = useLongPressSelection((id) => {
603
+ if (!enabled || selectionMode === 'none') return;
604
+ activateCollectionShortcuts();
605
+ toggle(id);
606
+ focusPointerItem(id);
607
+ });
623
608
  const getItemProps = useCallback((id: string) => ({
609
+ ...(enableLongPressSelection ? longPress(id) : {}),
624
610
  [COLLECTION_ITEM_ATTRIBUTE]: id,
625
611
  [LEGACY_ROW_ATTRIBUTE]: id,
626
612
  tabIndex: activeIdRef.current === id || (!activeIdRef.current && selectableIdsRef.current[0] === id) ? 0 : -1,
@@ -633,15 +619,14 @@ export function useCollectionInteractions<T>(
633
619
  },
634
620
  onMouseDown: onRowMouseDown(id),
635
621
  onClick: (event: ReactMouseEvent) => {
636
- if (event.defaultPrevented) return;
622
+ if (!enabled || event.defaultPrevented) return;
637
623
  if (!selectableIdsRef.current.includes(id)) return;
638
624
  const target = event.target as HTMLElement | null;
639
625
  const interactiveTarget = target?.closest(INTERACTIVE_SELECTOR);
640
626
  if (interactiveTarget && interactiveTarget !== event.currentTarget) return;
641
627
  if (event.shiftKey && selectionMode === 'multiple') {
642
628
  event.preventDefault();
643
- const rangeAnchor = anchorIdRef.current ?? activeIdRef.current ?? id;
644
- selectRange(rangeAnchor, id);
629
+ selectPointerRange(id);
645
630
  return;
646
631
  }
647
632
  if (event.metaKey || event.ctrlKey) return;
@@ -649,19 +634,19 @@ export function useCollectionInteractions<T>(
649
634
  activate(id);
650
635
  },
651
636
  onKeyDown: (event: ReactKeyboardEvent) => {
652
- if (event.key !== ' ' || event.metaKey || event.ctrlKey || event.altKey) return;
637
+ if (!enabled || event.key !== ' ' || event.metaKey || event.ctrlKey || event.altKey) return;
653
638
  if (event.target !== event.currentTarget) return;
654
639
  event.preventDefault();
655
640
  if (selectionMode === 'none') activate(id);
656
641
  else toggle(id);
657
642
  },
658
- }), [activate, activateCollectionShortcuts, clear, clearSelectionOnPlainClick, onRowMouseDown, selectRange, selectionMode, setActive, toggle]);
643
+ }), [activate, activateCollectionShortcuts, clear, clearSelectionOnPlainClick, enabled, enableLongPressSelection, longPress, onRowMouseDown, selectPointerRange, selectionMode, setActive, toggle]);
659
644
 
660
645
  const getCheckboxProps = useCallback((id: string) => ({
661
646
  isSelected: selectedIdsRef.current.has(id),
662
- disabled: !selectableIdsRef.current.includes(id),
647
+ disabled: !enabled || !selectableIdsRef.current.includes(id),
663
648
  onClick: onCheckboxClick(id),
664
- }), [onCheckboxClick]);
649
+ }), [enabled, onCheckboxClick]);
665
650
 
666
651
  const getSelectedItems = useCallback(() => getItemsForIds(selectedIdsRef.current), [getItemsForIds]);
667
652
  const isSelected = useCallback((id: string) => selectedIds.has(id), [selectedIds]);
@@ -681,14 +666,38 @@ export function useCollectionInteractions<T>(
681
666
  icon: action.icon,
682
667
  shortcut,
683
668
  destructive: action.destructive ?? action.intent === 'delete',
684
- disabled,
669
+ disabled: !enabled || disabled,
685
670
  pending: action.pending,
686
- onRun: () => action.onRun(actionContext),
671
+ onRun: () => { if (!enabled || disabled || action.pending) return; return action.onRun(actionContext); },
687
672
  };
688
- }), [actionContext, actionDefinitions]);
673
+ }), [actionContext, actionDefinitions, enabled]);
674
+
675
+ // Optimistic filtering can remove the last selected row before a request settles.
676
+ // Keep pending keys on the visible controller, independently of toolbar mounting.
677
+ useShortcuts(resolvedActions.flatMap((action): ShortcutDefinition[] => (
678
+ action.shortcut && (action.pending || action.disabled)
679
+ ? [{ id: `collection.pending.${action.id}`, keys: action.shortcut, allowRepeat: true, onTrigger: () => undefined }]
680
+ : []
681
+ )), {
682
+ enabled: bindKeyboardShortcuts && resolvedActions.some((action) => action.pending),
683
+ scope: shortcutScope,
684
+ priority: shortcutPriority + 25,
685
+ collectionOwnerId: shortcutCollectionOwnerRef.current!,
686
+ isAvailable,
687
+ });
688
+
689
+ const getActionBarProps = useCallback(() => ({
690
+ selectedCount: selectedIds.size,
691
+ actions: resolvedActions,
692
+ collectionOwnerId: shortcutCollectionOwnerRef.current!,
693
+ isAvailable,
694
+ // Pending work may disable collection gestures but must retain its action keys.
695
+ shortcutsEnabled: bindKeyboardShortcuts && (enabled || resolvedActions.some((action) => action.pending)),
696
+ onClearSelection: enabled ? clear : undefined,
697
+ }), [selectedIds.size, resolvedActions, isAvailable, enabled, bindKeyboardShortcuts, clear]);
689
698
 
690
699
  const getContainerProps = useCallback(() => ({
691
- ref: (node: HTMLElement | null) => { containerRef.current = node; },
700
+ ref: setContainerRef,
692
701
  onMouseDown: (event: ReactMouseEvent) => {
693
702
  activateCollectionShortcuts();
694
703
  handleContainerMouseDown(event);
@@ -696,7 +705,7 @@ export function useCollectionInteractions<T>(
696
705
  onFocusCapture: activateCollectionShortcuts,
697
706
  onClickCapture: handleContainerClickCapture,
698
707
  style: { userSelect: 'none' as const },
699
- }), [activateCollectionShortcuts, handleContainerClickCapture, handleContainerMouseDown]);
708
+ }), [activateCollectionShortcuts, handleContainerClickCapture, handleContainerMouseDown, setContainerRef]);
700
709
 
701
710
  return {
702
711
  selectedIds,
@@ -707,6 +716,7 @@ export function useCollectionInteractions<T>(
707
716
  lastClickedId: anchorId,
708
717
  dragRect,
709
718
  actions: resolvedActions,
719
+ getActionBarProps,
710
720
  isSelected,
711
721
  getSelectedItems,
712
722
  setActive,
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+
3
+ import { useQuery } from './useQuery';
4
+ import { useNotisRuntime } from '../provider';
5
+ import { normalizeDatabaseProperty, optionalString } from '../documents';
6
+ import type { DatabaseProperty } from '../runtime';
7
+
8
+ interface GetDatabaseResult {
9
+ database?: {
10
+ name?: string;
11
+ description?: string | null;
12
+ schema?: {
13
+ properties?: unknown[];
14
+ };
15
+ };
16
+ message?: string;
17
+ error?: string;
18
+ }
19
+
20
+ export interface UseDatabaseSchemaResult {
21
+ name: string | null;
22
+ description: string | null;
23
+ properties: DatabaseProperty[];
24
+ loading: boolean;
25
+ isFetching: boolean;
26
+ hasData: boolean;
27
+ error: Error | null;
28
+ /** Reads again; resolves to the read's error, or null when it succeeded. */
29
+ refetch: () => Promise<Error | null>;
30
+ }
31
+
32
+ /** Fetch a database's schema (normalized properties + metadata). */
33
+ export function useDatabaseSchema(databaseSlug: string): UseDatabaseSchemaResult {
34
+ const runtime = useNotisRuntime();
35
+ const query = useQuery(['database-schema', databaseSlug], async () => {
36
+ const result = await runtime!.callTool<GetDatabaseResult>('LOCAL_NOTIS_DATABASE_GET_DATABASE', {
37
+ database_slug: databaseSlug,
38
+ }, { dedupe: true, readOnly: true });
39
+ if (!result.database) throw new Error(result.error || result.message || 'Database not found');
40
+ return {
41
+ name: optionalString(result.database.name),
42
+ description: optionalString(result.database.description),
43
+ properties: (result.database.schema?.properties ?? []).map(normalizeDatabaseProperty)
44
+ .filter((property): property is DatabaseProperty => Boolean(property)),
45
+ };
46
+ }, { readOnly: true });
47
+ return { ...query, name: query.data?.name ?? null, description: query.data?.description ?? null,
48
+ properties: query.data?.properties ?? EMPTY_PROPERTIES };
49
+ }
50
+ const EMPTY_PROPERTIES: DatabaseProperty[] = [];
@@ -36,7 +36,7 @@ export function useDatabaseSubscription(
36
36
  ): UseDatabaseSubscriptionResult {
37
37
  const runtime = useNotisRuntime();
38
38
  const { subscribe = true, ...documentOptions } = options;
39
- const { documents, loading, error, refetch } = useDocuments(databaseSlug, documentOptions);
39
+ const { documents, loading, isFetching, hasData, error, refetch } = useDocuments(databaseSlug, documentOptions);
40
40
  const [live, setLive] = useState(false);
41
41
 
42
42
  const refetchRef = useRef(refetch);
@@ -72,5 +72,5 @@ export function useDatabaseSubscription(
72
72
  };
73
73
  }, [runtime, databaseSlug, enabled]);
74
74
 
75
- return { documents, rows: documents, loading, error, refetch, live };
75
+ return { documents, rows: documents, loading, isFetching, hasData, error, refetch, live };
76
76
  }
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+
3
+ import { useQuery } from './useQuery';
4
+ import { useNotisRuntime } from '../provider';
5
+ import { normalizeDocumentRecord } from '../documents';
6
+ import type { DocumentRecord } from '../runtime';
7
+ import { pick, type Words } from '../words/types';
8
+
9
+ /** The words of the document hook's own failures, in English and French (C8). */
10
+ export const DOCUMENT_HOOK_WORDS = Object.freeze({
11
+ notFound: { en: 'Document not found', fr: 'Document introuvable' },
12
+ }) satisfies Readonly<Record<string, Words>>;
13
+
14
+ interface GetDocumentResult {
15
+ status?: string;
16
+ document?: unknown;
17
+ message?: string;
18
+ error?: string;
19
+ }
20
+
21
+ export interface UseDocumentOptions {
22
+ enabled?: boolean;
23
+ }
24
+
25
+ export interface UseDocumentResult {
26
+ document: DocumentRecord | null;
27
+ loading: boolean;
28
+ isFetching: boolean;
29
+ hasData: boolean;
30
+ error: Error | null;
31
+ /** Reads again; resolves to the read's error, or null when it succeeded. */
32
+ refetch: () => Promise<Error | null>;
33
+ }
34
+
35
+ /** Fetch a single document (with content) by id, normalized. */
36
+ export function useDocument(
37
+ documentId: string | null | undefined,
38
+ options: UseDocumentOptions = {},
39
+ ): UseDocumentResult {
40
+ const runtime = useNotisRuntime();
41
+ const locale = runtime?.context?.locale === 'fr' ? 'fr' : 'en';
42
+ const query = useQuery<DocumentRecord>(['document', documentId ?? null, locale], async () => {
43
+ if (!runtime || !documentId) throw new Error(pick(DOCUMENT_HOOK_WORDS.notFound, locale));
44
+ const result = await runtime.callTool<GetDocumentResult>('LOCAL_NOTIS_DATABASE_GET_DOCUMENT', {
45
+ document_id: documentId,
46
+ }, { dedupe: true, readOnly: true });
47
+ if (!result.document) throw new Error(result.error ?? result.message ?? pick(DOCUMENT_HOOK_WORDS.notFound, locale));
48
+ return normalizeDocumentRecord(result.document, { locale });
49
+ }, { readOnly: true, enabled: options.enabled !== false && Boolean(documentId) });
50
+ return { document: query.data ?? null, loading: query.loading, isFetching: query.isFetching, hasData: query.hasData, error: query.error, refetch: query.refetch };
51
+ }