@notis_ai/cli 0.2.17 → 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 +42323 -18258
  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 -1568
  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 -3000
  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
@@ -8,30 +8,29 @@
8
8
  "./interactions": "./src/interactions.ts",
9
9
  "./config": "./src/config.ts",
10
10
  "./vite": "./src/vite.ts",
11
- "./ui": "./src/ui.ts",
12
- "./styles.css": "./src/styles.css"
11
+ "./styles.css": "./src/styles.css",
12
+ "./ui": "./src/ui.ts"
13
13
  },
14
14
  "files": [
15
- "src",
16
- "template"
15
+ "src"
17
16
  ],
18
17
  "scripts": {
19
- "type-check": "tsc --noEmit"
18
+ "type-check": "tsc --noEmit && tsc --noEmit -p design-system"
20
19
  },
21
20
  "dependencies": {
22
21
  "react-markdown": "^10.1.0",
23
22
  "remark-gfm": "^4.0.1"
24
23
  },
25
24
  "peerDependencies": {
25
+ "@vitejs/plugin-react": ">=4.0.0",
26
26
  "react": ">=18.0.0",
27
27
  "react-dom": ">=18.0.0",
28
- "vite": ">=5.0.0",
29
- "@vitejs/plugin-react": ">=4.0.0"
28
+ "vite": ">=5.0.0"
30
29
  },
31
30
  "devDependencies": {
32
31
  "@types/node": "^20.0.0",
33
- "@types/react": "^19.0.0",
34
- "@types/react-dom": "^19.0.0",
35
- "typescript": "^5.0.0"
32
+ "@types/react": "^19.2.18",
33
+ "@types/react-dom": "^19.2.7",
34
+ "typescript": "^7.0.2"
36
35
  }
37
36
  }
@@ -0,0 +1,36 @@
1
+ /** Serializable context shared with the agent. Apps own annotation storage. */
2
+ export interface ContextAttachment {
3
+ /** Durable HTTPS URL supplied by the app. Sent through the host media pipeline. */
4
+ url: string;
5
+ name: string;
6
+ mimeType: string;
7
+ }
8
+
9
+ export interface AgentContextContent {
10
+ title?: string;
11
+ /** Emoji, Phosphor icon name, or HTTPS image URL. Never executable markup. */
12
+ icon?: string;
13
+ /** An app-defined kind, not a closed list of annotation types. */
14
+ kind?: string;
15
+ text?: string;
16
+ comment?: string;
17
+ preview?: { format: 'text' | 'markdown'; content: string };
18
+ /** Any JSON-serializable value: coordinates, chart state, loaded records, etc. */
19
+ data?: unknown;
20
+ attachments?: ContextAttachment[];
21
+ }
22
+
23
+ export interface AgentContextItem extends AgentContextContent {
24
+ /** Stable within this app. The host scopes IDs to the originating app. */
25
+ id: string;
26
+ resource?: import('./runtime').ContextResource | null;
27
+ }
28
+
29
+ /** Host-stamped origin. Apps describe resources; the host identifies the app/view. */
30
+ export interface AgentContextSource {
31
+ appId: string;
32
+ appName: string;
33
+ viewSlug: string;
34
+ viewName: string;
35
+ viewUrl?: string | null;
36
+ }
@@ -0,0 +1,175 @@
1
+ /** Framework-neutral ESM loading shared by Portal and credentialless verification. */
2
+ import type React from 'react';
3
+ import { NOTIS_SDK_HOST_MODULES } from './runtimeModules';
4
+
5
+ const SDK_SPECIFIER = String.raw`@notis\/sdk(?:\/[^"'\s]*)?`;
6
+ // `from "x"`, `import("x")` and the side-effect `import "x"`.
7
+ const SDK_REFERENCE = new RegExp(String.raw`(\bfrom\s*|\bimport\s*\(\s*|\bimport(?![\w$])\s*)(["'])(${SDK_SPECIFIER})\2`, 'g');
8
+ // The clause of a static `import ... from "x"` or `export ... from "x"`; quotes never appear in it.
9
+ const SDK_CLAUSE = new RegExp(String.raw`\b(?:import|export)(?![\w$])\s*([^;"'\x60()]*?)\s*from\s*(["'])(${SDK_SPECIFIER})\2`, 'g');
10
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
11
+
12
+ type HostGlobals = Record<string, unknown>;
13
+
14
+ function sdkError(code: string, message: string, extra: Record<string, unknown> = {}) {
15
+ return Object.assign(new Error(message), { code, ...extra });
16
+ }
17
+
18
+ /** The names a static import or re-export clause takes from its module ('default' for a default import). */
19
+ function importedNames(clause: string): string[] {
20
+ const names: string[] = [];
21
+ let rest = clause.trim();
22
+ const braces = rest.match(/\{([^}]*)\}/);
23
+ if (braces) {
24
+ for (const part of braces[1].split(',')) {
25
+ const name = part.trim().split(/\s+as\s+/)[0]?.trim();
26
+ if (name) names.push(name);
27
+ }
28
+ rest = rest.replace(braces[0], '');
29
+ }
30
+ for (const part of rest.split(',')) {
31
+ const name = part.trim();
32
+ // `* as N` and `export *` take the whole namespace; nothing to check by name.
33
+ if (name && !name.startsWith('*')) names.push('default');
34
+ }
35
+ return names;
36
+ }
37
+
38
+ function hostNamespace(specifier: string, globals: HostGlobals): Record<string, unknown> {
39
+ if (!Object.hasOwn(NOTIS_SDK_HOST_MODULES, specifier)) {
40
+ throw sdkError('client_update_required', `This Space needs ${specifier}, which this version of Notis does not provide.`,
41
+ { missing: [specifier] });
42
+ }
43
+ const globalName = NOTIS_SDK_HOST_MODULES[specifier as keyof typeof NOTIS_SDK_HOST_MODULES];
44
+ const namespace = globals[globalName];
45
+ if (!namespace || typeof namespace !== 'object') {
46
+ throw sdkError('notis_sdk_host_unavailable', `The host did not provide ${specifier} (window.${globalName}).`);
47
+ }
48
+ return namespace as Record<string, unknown>;
49
+ }
50
+
51
+ /** A module that re-exports every member of one host-provided SDK namespace, read when it evaluates. */
52
+ function sdkShim(globalName: string, namespace: Record<string, unknown>): string {
53
+ const names = Object.keys(namespace);
54
+ const body = [`const M = globalThis[${JSON.stringify(globalName)}];`];
55
+ const exported: string[] = [];
56
+ names.forEach((name, index) => {
57
+ body.push(`const e${index} = M[${JSON.stringify(name)}];`);
58
+ exported.push(`e${index} as ${IDENTIFIER.test(name) ? name : JSON.stringify(name)}`);
59
+ });
60
+ if (exported.length) body.push(`export { ${exported.join(', ')} };`);
61
+ return `data:text/javascript,${encodeURIComponent(body.join(' '))}`;
62
+ }
63
+
64
+ /**
65
+ * Points every `@notis/sdk` import of a Space bundle at the host's own SDK
66
+ * (`window.__NotisSDK`, `window.__NotisSDKUI`). A bundle that needs a module or
67
+ * a name this host does not have is refused with `client_update_required`
68
+ * before it loads, instead of failing to link.
69
+ */
70
+ export function rewriteSdkImports(source: string, globals: HostGlobals = globalThis as unknown as HostGlobals): string {
71
+ if (!source.includes('@notis/sdk')) return source;
72
+ const missing = new Set<string>();
73
+ for (const [, clause, , specifier] of source.matchAll(SDK_CLAUSE)) {
74
+ const namespace = hostNamespace(specifier, globals);
75
+ for (const name of importedNames(clause)) {
76
+ if (!Object.hasOwn(namespace, name)) missing.add(`${specifier}:${name}`);
77
+ }
78
+ }
79
+ if (missing.size) {
80
+ throw sdkError('client_update_required', `This Space needs a newer version of Notis (missing ${[...missing].join(', ')}).`,
81
+ { missing: [...missing] });
82
+ }
83
+ const shims = new Map<string, string>();
84
+ return source.replace(SDK_REFERENCE, (_match, lead: string, quote: string, specifier: string) => {
85
+ let shim = shims.get(specifier);
86
+ if (!shim) {
87
+ const namespace = hostNamespace(specifier, globals);
88
+ shim = sdkShim(NOTIS_SDK_HOST_MODULES[specifier as keyof typeof NOTIS_SDK_HOST_MODULES], namespace);
89
+ shims.set(specifier, shim);
90
+ }
91
+ return `${lead}${quote}${shim}${quote}`;
92
+ });
93
+ }
94
+
95
+ /** Every host-provided import of a bundle (React and the SDK) pointed at the host's globals. */
96
+ export function rewriteBundleImports(source: string, globals?: HostGlobals): string {
97
+ return rewriteSdkImports(rewriteReactImports(source), globals);
98
+ }
99
+
100
+ export function rewriteReactImports(source: string): string {
101
+ // Create inline data URLs that re-export from window globals.
102
+ // This avoids the "Cannot find module" error with blob URL imports.
103
+ const reactShim = `data:text/javascript,${encodeURIComponent(
104
+ 'const R = window.React; export default R; export const { createContext, createRef, useState, useEffect, useContext, useCallback, useMemo, useRef, useReducer, useLayoutEffect, useId, memo, forwardRef, createElement, Fragment, Children, cloneElement, isValidElement, lazy, Suspense, startTransition, useTransition, useDeferredValue, useSyncExternalStore, useInsertionEffect, useDebugValue, useImperativeHandle, Component, PureComponent } = R;'
105
+ )}`;
106
+ const jsxShim = `data:text/javascript,${encodeURIComponent(
107
+ 'const J = window.__ReactJSXRuntime || {}; const R = window.React; export const jsx = J.jsx || R.createElement; export const jsxs = J.jsxs || R.createElement; export const jsxDEV = J.jsxDEV || R.createElement; export const Fragment = J.Fragment || R.Fragment;'
108
+ )}`;
109
+ const reactDomShim = `data:text/javascript,${encodeURIComponent(
110
+ 'const RD = window.ReactDOM; export default RD; export const { createPortal, flushSync, unstable_batchedUpdates } = RD;'
111
+ )}`;
112
+ const reactDomClientShim = `data:text/javascript,${encodeURIComponent(
113
+ 'const RDC = window.ReactDOMClient; export default RDC; export const { createRoot, hydrateRoot } = RDC;'
114
+ )}`;
115
+
116
+ let result = source;
117
+ // Replace bare side-effect imports
118
+ result = result.replace(/import\s*["']react-dom\/client["'];?/g, `import "${reactDomClientShim}";`);
119
+ result = result.replace(/import\s*["']react-dom["'];?/g, `import "${reactDomShim}";`);
120
+ result = result.replace(/import\s*["']react\/jsx-runtime["'];?/g, `import "${jsxShim}";`);
121
+ result = result.replace(/import\s*["']react\/jsx-dev-runtime["'];?/g, `import "${jsxShim}";`);
122
+ result = result.replace(/import\s*["']react["'];?/g, `import "${reactShim}";`);
123
+
124
+ // Replace import statements with data URL shims
125
+ // Handle: import { ... } from "react/jsx-runtime"
126
+ result = result.replace(/from\s*["']react\/jsx-runtime["']/g, `from "${jsxShim}"`);
127
+ result = result.replace(/from\s*["']react\/jsx-dev-runtime["']/g, `from "${jsxShim}"`);
128
+ // Handle react-dom/client before react-dom
129
+ result = result.replace(/from\s*["']react-dom\/client["']/g, `from "${reactDomClientShim}"`);
130
+ // Handle: import ... from "react-dom"
131
+ result = result.replace(/from\s*["']react-dom["']/g, `from "${reactDomShim}"`);
132
+ // Handle: import ... from "react" (must come after react-dom and react/jsx-runtime)
133
+ result = result.replace(/from\s*["']react["']/g, `from "${reactShim}"`);
134
+
135
+ return result;
136
+ }
137
+
138
+ export function resolveBundleRouteExport(
139
+ moduleExports: Record<string, React.ComponentType>,
140
+ exportCandidates: string[],
141
+ strict = false,
142
+ ): { name: string; component: React.ComponentType } | null {
143
+ const namedExport = exportCandidates.find((name) => Boolean(moduleExports[name]));
144
+ if (namedExport) {
145
+ return { name: namedExport, component: moduleExports[namedExport] };
146
+ }
147
+ if (strict) return null;
148
+ if (moduleExports.default) {
149
+ return { name: 'default', component: moduleExports.default };
150
+ }
151
+
152
+ // Vite may minify an entry export even though the manifest still carries
153
+ // its source name. A bundle with one non-shell component is unambiguous, so
154
+ // keep Dev usable instead of showing "View not available" for that route.
155
+ const componentExports = Object.entries(moduleExports).filter(
156
+ ([name, value]) => name !== '__AppShell' && Boolean(value),
157
+ );
158
+ if (componentExports.length !== 1) {
159
+ return null;
160
+ }
161
+ const [name, component] = componentExports[0];
162
+ return { name, component };
163
+ }
164
+
165
+ export async function importRewrittenBundleSource(source: string): Promise<Record<string, React.ComponentType>> {
166
+ const blob = new Blob([source], { type: 'text/javascript' });
167
+ const url = URL.createObjectURL(blob);
168
+ try {
169
+ return await import(/* webpackIgnore: true */ url) as Record<string, React.ComponentType>;
170
+ } finally { URL.revokeObjectURL(url); }
171
+ }
172
+
173
+ export function importBundleSource(source: string) {
174
+ return importRewrittenBundleSource(rewriteBundleImports(source));
175
+ }
@@ -0,0 +1,103 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useId, useRef, useState, type ReactNode } from 'react';
4
+ import { useIsoLayoutEffect } from '../ui/util';
5
+
6
+ export interface DialogProps {
7
+ open: boolean;
8
+ /** Request dismissal. Keep open unchanged to prevent dismissal while saving. */
9
+ onClose: () => void;
10
+ title: ReactNode;
11
+ description?: ReactNode;
12
+ role?: 'dialog' | 'alertdialog';
13
+ /**
14
+ * A confirmation: on phones and tablets the operating system's own dialog asks instead of the card. OK clicks the
15
+ * button marked `data-dialog-confirm`; Cancel clicks the one marked `data-dialog-cancel`, else calls `onClose`.
16
+ * Desktop browsers and the Desktop app keep the card.
17
+ */
18
+ nativeOnTouch?: boolean;
19
+ children: ReactNode;
20
+ }
21
+
22
+ /** How long the card stays hidden after OK while the work runs; a slow or failed action shows it again. */
23
+ const NATIVE_CONFIRM_REVEAL_MS = 1500;
24
+ /** A Cancel faster than this came from a browser with no OS dialog (an embedded view): show the card instead. */
25
+ const NATIVE_CONFIRM_MISSING_MS = 30;
26
+
27
+ /** Touch-first devices confirm with the operating system's dialog, as the Portal's AlertDialog does. */
28
+ export function prefersNativeConfirm(): boolean {
29
+ if (typeof window === 'undefined' || typeof window.confirm !== 'function') return false;
30
+ if ((window as Window & { electronAPI?: { isElectron?: boolean } }).electronAPI?.isElectron) return false;
31
+ return window.matchMedia?.('(hover: none) and (pointer: coarse)').matches === true;
32
+ }
33
+
34
+ /** One OS dialog: the question first, then each line of the explanation as its own paragraph. */
35
+ function nativeConfirmMessage(title: string, description: string): string {
36
+ return [title, ...description.split(/\n+/)].map((part) => part.trim()).filter(Boolean).join('\n\n');
37
+ }
38
+
39
+ /** Native top-layer dialog; content and actions remain app-owned. */
40
+ export function Dialog({ open, onClose, title, description, role = 'dialog', nativeOnTouch = false, children }: DialogProps) {
41
+ const ref = useRef<HTMLDialogElement>(null);
42
+ const titleId = useId();
43
+ const descriptionId = useId();
44
+ const onCloseRef = useRef(onClose);
45
+ onCloseRef.current = onClose;
46
+ // Each opening asks the OS again.
47
+ const [native, setNative] = useState(false);
48
+
49
+ // Before paint, so a touch confirmation never flashes the card first.
50
+ useIsoLayoutEffect(() => {
51
+ if (!open) return;
52
+ const dialog = ref.current!;
53
+ dialog.showModal();
54
+ setNative(nativeOnTouch && prefersNativeConfirm());
55
+ return () => { if (dialog.open) dialog.close(); };
56
+ }, [open, nativeOnTouch]);
57
+
58
+ useEffect(() => {
59
+ if (!open || !native) return;
60
+ const dialog = ref.current!;
61
+ let reveal: ReturnType<typeof setTimeout> | undefined;
62
+ // Let the dialog finish opening (title, buttons) before the OS takes over.
63
+ const ask = setTimeout(() => {
64
+ const asked = performance.now();
65
+ const describedBy = dialog.querySelector('.notis-dialog-description') as HTMLElement | null;
66
+ const confirmed = window.confirm(nativeConfirmMessage(
67
+ dialog.querySelector('.notis-dialog-title')?.textContent ?? '',
68
+ describedBy ? (typeof describedBy.innerText === 'string' ? describedBy.innerText : describedBy.textContent ?? '') : '',
69
+ ));
70
+ if (!confirmed && performance.now() - asked < NATIVE_CONFIRM_MISSING_MS) {
71
+ setNative(false);
72
+ return;
73
+ }
74
+ if (confirmed) dialog.querySelector<HTMLButtonElement>('[data-dialog-confirm]')?.click();
75
+ else {
76
+ const cancel = dialog.querySelector<HTMLButtonElement>('[data-dialog-cancel]');
77
+ if (cancel) cancel.click(); else onCloseRef.current();
78
+ }
79
+ reveal = setTimeout(() => setNative(false), confirmed ? NATIVE_CONFIRM_REVEAL_MS : 0);
80
+ }, 0);
81
+ return () => {
82
+ clearTimeout(ask);
83
+ if (reveal !== undefined) clearTimeout(reveal);
84
+ };
85
+ }, [open, native]);
86
+
87
+ return (
88
+ <dialog
89
+ ref={ref}
90
+ className="notis-dialog"
91
+ role={role}
92
+ aria-labelledby={titleId}
93
+ aria-describedby={description == null ? undefined : descriptionId}
94
+ data-native-confirm={native ? '' : undefined}
95
+ onCancel={(event) => { event.preventDefault(); onClose(); }}
96
+ onKeyDown={(event) => event.stopPropagation()}
97
+ >
98
+ <h2 id={titleId} className="notis-dialog-title">{title}</h2>
99
+ {description != null && <p id={descriptionId} className="notis-dialog-description">{description}</p>}
100
+ {children}
101
+ </dialog>
102
+ );
103
+ }
@@ -0,0 +1,113 @@
1
+ 'use client';
2
+
3
+ import type { CSSProperties, ReactElement } from 'react';
4
+ import { useNotisRuntime } from '../provider';
5
+ import { DOCUMENT_HOOK_WORDS, useDocument } from '../hooks/useDocument';
6
+ import { fillWords, pick, type Locale, type Words } from '../words/types';
7
+ import { Markdown } from './Markdown';
8
+ import { RecordComponentUnavailable } from './RecordComponents';
9
+ import type { NotisDocumentEditorProps } from '../runtime';
10
+
11
+ /** The words of the read-only document preview, in English and French (C7). */
12
+ export const DOCUMENT_EDITOR_WORDS = Object.freeze({
13
+ loading: { en: 'Loading document', fr: 'Chargement du document' },
14
+ readOnly: { en: 'Read-only preview. Edit it in Notis.', fr: 'Aperçu en lecture seule. Modifie-le dans Notis.' },
15
+ report: { en: 'Interactive report. Open it in its Space to use it.', fr: 'Rapport interactif. Ouvre-le dans son Space pour l’utiliser.' },
16
+ html: { en: 'Interactive page. Open it in its Space to use it.', fr: 'Page interactive. Ouvre-la dans son Space pour l’utiliser.' },
17
+ view: { en: 'View. Open it in its Space to see it.', fr: 'Vue. Ouvre-la dans son Space pour la voir.' },
18
+ file: { en: '{type} file. Open it in Notis to see it.', fr: 'Fichier {type}. Ouvre-le dans Notis pour le voir.' },
19
+ anyFile: { en: 'File. Open it in Notis to see it.', fr: 'Fichier. Ouvre-le dans Notis pour le voir.' },
20
+ }) satisfies Readonly<Record<string, Words>>;
21
+
22
+ const fallbackNoticeStyle: CSSProperties = {
23
+ marginTop: '0.75rem',
24
+ fontSize: '12px',
25
+ color: 'hsl(var(--muted-foreground))',
26
+ };
27
+
28
+ // Flat, like a callout: a tinted block, no border (C7).
29
+ const fallbackFrameStyle: CSSProperties = {
30
+ borderRadius: '0.5rem',
31
+ background: 'hsl(var(--muted) / .5)',
32
+ padding: '12px 16px',
33
+ fontSize: '13px',
34
+ lineHeight: '20px',
35
+ color: 'hsl(var(--muted-foreground))',
36
+ };
37
+
38
+ const fallbackTitleStyle: CSSProperties = {
39
+ margin: '0 0 0.75rem',
40
+ fontSize: '1.5rem',
41
+ fontWeight: 700,
42
+ letterSpacing: '-0.01em',
43
+ color: 'hsl(var(--foreground))',
44
+ };
45
+
46
+ /**
47
+ * Read-only stand-in used when no host editor is available (standalone dev
48
+ * harness, snapshots). Markdown content renders via `Markdown`; other content
49
+ * types get a typed placeholder.
50
+ */
51
+ function DocumentEditorFallback({ documentId, variant = 'full', className }: NotisDocumentEditorProps) {
52
+ const { document, loading, error } = useDocument(documentId ?? null);
53
+ const locale: Locale = useNotisRuntime()?.context?.locale === 'fr' ? 'fr' : 'en';
54
+ const t = (words: Words) => pick(words, locale);
55
+
56
+ if (loading) {
57
+ return <div role="status" aria-busy="true" className={className} style={fallbackFrameStyle}>{t(DOCUMENT_EDITOR_WORDS.loading)}</div>;
58
+ }
59
+ if (error || !document) {
60
+ return (
61
+ <div role="status" className={className} style={fallbackFrameStyle}>
62
+ {error?.message ?? t(DOCUMENT_HOOK_WORDS.notFound)}
63
+ </div>
64
+ );
65
+ }
66
+
67
+ const isMarkdown = document.contentType === 'markdown' || !document.contentType;
68
+ return (
69
+ <div className={className}>
70
+ {variant === 'full' ? <h1 style={fallbackTitleStyle}>{document.title}</h1> : null}
71
+ {isMarkdown ? (
72
+ <>
73
+ <Markdown value={document.contentMarkdown ?? ''} />
74
+ <p style={fallbackNoticeStyle}>{t(DOCUMENT_EDITOR_WORDS.readOnly)}</p>
75
+ </>
76
+ ) : document.contentType === 'view' ? (
77
+ <div style={fallbackFrameStyle}>
78
+ {t(document.viewType === 'report' ? DOCUMENT_EDITOR_WORDS.report
79
+ : document.viewType === 'html' ? DOCUMENT_EDITOR_WORDS.html : DOCUMENT_EDITOR_WORDS.view)}
80
+ </div>
81
+ ) : (
82
+ <div style={fallbackFrameStyle}>
83
+ {document.fileType
84
+ ? t(fillWords(DOCUMENT_EDITOR_WORDS.file, { type: document.fileType.toUpperCase() }))
85
+ : t(DOCUMENT_EDITOR_WORDS.anyFile)}
86
+ </div>
87
+ )}
88
+ </div>
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Embeds the document editor for a document anywhere in an app view. The host
94
+ * (portal) provides the implementation through `runtime.ui.DocumentEditor` and
95
+ * dispatches on the document's content type (markdown -> rich text editor,
96
+ * files -> matching viewer, views -> their trusted native Documents surface).
97
+ * Outside the portal this falls back to a read-only preview or an explicit
98
+ * open-in-Space notice for native views.
99
+ *
100
+ * ```tsx
101
+ * <DocumentEditor documentId={entry.id} variant="body" className="min-h-[320px]" />
102
+ * ```
103
+ */
104
+ export function DocumentEditor(props: NotisDocumentEditorProps): ReactElement {
105
+ const runtime = useNotisRuntime();
106
+ const HostEditor = runtime?.ui?.DocumentEditor;
107
+ if (HostEditor) {
108
+ return <HostEditor {...props} />;
109
+ }
110
+ // A Space must never fall back to the legacy account-wide document reader.
111
+ if (runtime?.resource?.kind === 'space' || props.recordKey) return <RecordComponentUnavailable className={props.className} />;
112
+ return <DocumentEditorFallback {...props} />;
113
+ }
@@ -0,0 +1,184 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useRef, useState, type CSSProperties, type ReactElement } from 'react';
4
+ import { useNotisRuntime } from '../provider';
5
+ import { useTopBarTitleLeaf } from '../hooks/useTopBarTitleLeaf';
6
+ import { DocumentEditor } from './DocumentEditor';
7
+ import { HtmlFrame, ShareControl } from './RecordComponents';
8
+ import { Skeleton } from './Skeleton';
9
+ import { renderIcon } from '../ui/icons';
10
+ import type { NotisDocumentPageProps } from '../runtime';
11
+
12
+ const copy = {
13
+ en: { loading: 'Loading document', breadcrumb: 'Breadcrumb', untitled: 'Untitled' },
14
+ fr: { loading: 'Chargement du document', breadcrumb: "Fil d'Ariane", untitled: 'Sans titre' },
15
+ };
16
+
17
+ /** Column widths shared by the host page, its skeleton and the fallback, so nothing moves when the page arrives. */
18
+ export const DOCUMENT_PAGE_WIDTHS = { standard: 900, wide: 1200, full: undefined } as const;
19
+ /**
20
+ * The page's horizontal gutter: 16px, then 48px from 640px wide (the host page's px-4 sm:px-12), and its
21
+ * toolbar height (56px plus its 1px border, Beta's py-3 toolbar). The skeleton uses the same values.
22
+ */
23
+ export const DOCUMENT_PAGE_GUTTER = { narrow: 16, wide: 48, breakpoint: 640, toolbar: 57 } as const;
24
+
25
+ /** The widest a crumb before the last one gets; past it the words end in an ellipsis (C10). */
26
+ const CRUMB_MAX = 160;
27
+ // The room the fallback crumb keeps beside its buttons, and the one padding every crumb part has.
28
+ const CRUMB_MIN = 72;
29
+ const CRUMB_PADDING = '4px 6px';
30
+
31
+ const SKELETON = 'notis-document-page-skeleton';
32
+ // Inline styles cannot change with the width; this sheet (rendered with the skeleton, so it reaches a
33
+ // Space's ShadowRoot) gives the skeleton the page's own gutter and icon row at every width.
34
+ const skeletonCss = `.${SKELETON}__column{padding:8px ${DOCUMENT_PAGE_GUTTER.narrow}px 0}`
35
+ + `.${SKELETON}__icon{width:44px;height:44px;margin-bottom:4px}`
36
+ + `@media (min-width:${DOCUMENT_PAGE_GUTTER.breakpoint}px){.${SKELETON}__column{padding:8px ${DOCUMENT_PAGE_GUTTER.wide}px 0}`
37
+ + `.${SKELETON}__icon{width:56px;height:56px}}`;
38
+
39
+ function useCopy() {
40
+ return copy[useNotisRuntime()?.context?.locale === 'fr' ? 'fr' : 'en'];
41
+ }
42
+
43
+ function columnStyle(width: NotisDocumentPageProps['width'] = 'standard'): CSSProperties {
44
+ const max = DOCUMENT_PAGE_WIDTHS[width];
45
+ return { width: '100%', maxWidth: max ? `${max}px` : undefined, margin: '0 auto', boxSizing: 'border-box' };
46
+ }
47
+
48
+ /**
49
+ * Placeholder shaped like the document page (toolbar, icon row, title, meta strip, body lines) at the
50
+ * same column width and gutter, so the page replaces it without a layout shift. Hosts show it while the
51
+ * record and the editor load; a Space can show it while its own data decides which record opens.
52
+ */
53
+ export function DocumentPageSkeleton({ width = 'standard', className, label }: {
54
+ width?: NotisDocumentPageProps['width']; className?: string; label?: string;
55
+ }): ReactElement {
56
+ const t = useCopy();
57
+ return <div role="status" aria-busy="true" aria-label={label || t.loading} data-notis-skeleton="document-page" className={className}
58
+ style={{ width: '100%', display: 'flex', flexDirection: 'column' }}>
59
+ <style>{skeletonCss}</style>
60
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', height: DOCUMENT_PAGE_GUTTER.toolbar,
61
+ padding: `0 ${DOCUMENT_PAGE_GUTTER.narrow}px`, boxSizing: 'border-box' }}>
62
+ <Skeleton style={{ width: 180, height: 14 }} />
63
+ <Skeleton style={{ width: 48, height: 12 }} />
64
+ </div>
65
+ <div className={`${SKELETON}__column`} style={{ ...columnStyle(width), display: 'flex', flexDirection: 'column' }}>
66
+ <Skeleton className={`${SKELETON}__icon`} style={{ borderRadius: 12, height: undefined }} />
67
+ <Skeleton style={{ width: '58%', height: 36, margin: '2px 0 10px' }} />
68
+ <Skeleton style={{ width: '72%', height: 14, margin: '15px 0' }} />
69
+ <div style={{ display: 'grid', gap: 12, marginTop: 24 }}>
70
+ {[100, 96, 88, 100, 64].map((size, index) => <Skeleton key={index} style={{ width: `${size}%`, height: 16 }} />)}
71
+ </div>
72
+ </div>
73
+ </div>;
74
+ }
75
+
76
+ /**
77
+ * Shown by hosts that predate DocumentPage: the same record, with the editor these hosts already have.
78
+ * menuItems show as toolbar buttons (icon only when they carry one) and onSaved is called when a save finishes (title null);
79
+ * layout, header parts, meta and width options other than the column are not available there.
80
+ * With viewer 'html' the host's HtmlFrame replaces the editor.
81
+ */
82
+ // A 16px box for a menu item's icon, sized here because a host without the toolkit stylesheet gives the svg no size.
83
+ const ICON_BOX: CSSProperties = { display: 'inline-flex', width: 16, height: 16, overflow: 'hidden' };
84
+
85
+ function DocumentPageFallback(props: NotisDocumentPageProps) {
86
+ const t = useCopy();
87
+ const { recordKey, breadcrumb, actions, share = false, aboveBody, belowBody, width, readOnly, className, menuItems = [], onSaved, onSavingChange } = props;
88
+ const crumbs = breadcrumb === false ? [] : breadcrumb || [];
89
+ const showToolbar = crumbs.length > 0 || Boolean(actions) || share || menuItems.length > 0;
90
+ // DocumentEditor reports saving, not what it saved: a finished save tells the Space to refresh its lists.
91
+ const saving = useRef(false);
92
+ const savingChange = useCallback((value: boolean) => {
93
+ if (saving.current && !value) onSaved?.({ recordKey, title: null });
94
+ saving.current = value;
95
+ onSavingChange?.(value);
96
+ }, [onSaved, onSavingChange, recordKey]);
97
+ return <div className={className} style={{ width: '100%', display: 'flex', flexDirection: 'column' }}>
98
+ {showToolbar ? <div style={{ display: 'flex', alignItems: 'center', gap: 8, minHeight: 48, padding: '0 16px' }}>
99
+ {/* C10: the crumb shrinks (each part ends in an ellipsis, a slash only between parts) and never runs under the
100
+ buttons, which never wrap. The last part gets the room the others leave; every part has the same padding. When
101
+ the buttons alone outgrow the bar (longer French labels), their labels shorten too, down to CRUMB_MIN of crumb. */}
102
+ <nav aria-label={t.breadcrumb} style={{ display: 'flex', alignItems: 'center', gap: 4, flex: 1, minWidth: CRUMB_MIN, overflow: 'hidden', fontSize: 14 }}>
103
+ {crumbs.map((crumb, index) => {
104
+ const last = index === crumbs.length - 1;
105
+ const text: CSSProperties = { display: 'block', minWidth: 0, maxWidth: last ? '100%' : CRUMB_MAX, overflow: 'hidden',
106
+ textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: 'hsl(var(--muted-foreground))' };
107
+ return <span key={`${crumb.label}-${index}`} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, minWidth: 0, flex: '0 1 auto' }}>
108
+ {crumb.onSelect ? <button type="button" onClick={crumb.onSelect} title={crumb.label}
109
+ style={{ ...text, background: 'none', border: 0, padding: CRUMB_PADDING, borderRadius: 6, cursor: 'pointer', font: 'inherit' }}>
110
+ {crumb.label}</button>
111
+ : <span title={crumb.label} style={{ ...text, padding: CRUMB_PADDING }}>{crumb.label}</span>}
112
+ {last ? null : <span aria-hidden="true" style={{ flex: 'none', color: 'hsl(var(--muted-foreground))' }}>/</span>}
113
+ </span>;
114
+ })}
115
+ </nav>
116
+ <div data-notis-document-page-actions="" style={{ display: 'flex', alignItems: 'center', gap: 4, flex: '0 1 auto', minWidth: 0, whiteSpace: 'nowrap' }}>
117
+ {actions}
118
+ {share ? <ShareControl recordKey={recordKey} /> : null}
119
+ {/* The host's '...' menu is not available here: its entries show as buttons, icon only when they carry one. */}
120
+ {menuItems.map((item, index) => <button key={`${item.label}-${index}`} type="button" onClick={item.onSelect} disabled={item.disabled} title={item.label}
121
+ aria-label={item.icon ? item.label : undefined}
122
+ style={{ display: 'inline-flex', alignItems: 'center', background: 'none', border: 0, padding: item.icon ? 6 : '4px 8px', borderRadius: 6, cursor: item.disabled ? 'default' : 'pointer', font: 'inherit', fontSize: 13,
123
+ flex: '0 1 auto', minWidth: 0, maxWidth: CRUMB_MAX, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: item.destructive ? 'hsl(var(--destructive))' : 'hsl(var(--muted-foreground))', opacity: item.disabled ? 0.5 : 1 }}>{item.icon ? <span style={ICON_BOX}>{renderIcon(item.icon)}</span> : item.label}</button>)}
124
+ </div>
125
+ </div> : null}
126
+ {props.viewer === 'html' ? <HtmlFrame recordKey={recordKey} /> :
127
+ <div style={{ ...columnStyle(width), padding: '24px 16px 96px', display: 'flex', flexDirection: 'column', gap: 20 }}>
128
+ {aboveBody}
129
+ <DocumentEditor recordKey={recordKey} variant={props.header === false ? 'body' : 'full'} showProperties={props.properties !== 'hidden'}
130
+ readOnly={readOnly} onDirtyChange={props.onDirtyChange} onSavingChange={savingChange} />
131
+ {belowBody}
132
+ </div>}
133
+ </div>;
134
+ }
135
+
136
+ /**
137
+ * The full page of one record that is the main content of a Space view, such as a note: breadcrumb,
138
+ * Saving or Saved status, '...' menu, cover, icon, title, meta strip, properties that save
139
+ * automatically, and the collaborative body with presence. The host (Portal) draws it and keeps one
140
+ * save queue for the whole record. Files open in their viewer under the header; HTML and reports show
141
+ * only the toolbar (breadcrumb, actions, Share, '...' menu) above their frame, with no header, meta,
142
+ * properties, aboveBody or belowBody. `viewer: 'html'` opens a record whose one stored `.html` file is
143
+ * the page (for example a note that keeps a saved HTML report) the same way.
144
+ *
145
+ * ```tsx
146
+ * if (params.note) return <DocumentPage recordKey={params.note}
147
+ * breadcrumb={[{ label: 'All notes', onSelect: () => setParams({ note: null }) }]}
148
+ * actions={<TrashButton />} onSaved={() => notes.refetch()} />;
149
+ * ```
150
+ *
151
+ * Slots (`breadcrumb`, `actions`, `menuItems`, `aboveBody`, `belowBody`) and toggles (`share`,
152
+ * `header`, `meta`, `properties`, `width`) adjust it; `layout` rearranges its parts. Hosts without
153
+ * this component show the record with `DocumentEditor` instead.
154
+ */
155
+ export function DocumentPage(props: NotisDocumentPageProps): ReactElement {
156
+ const runtime = useNotisRuntime();
157
+ const Host = runtime?.ui?.DocumentPage;
158
+ const t = useCopy();
159
+ const { recordKey, onTitle } = props;
160
+ // A record opened from the view (it draws a way back) names the top bar leaf with its title, so the view's page
161
+ // becomes a parent crumb. The host reports the title; until it does, and on hosts that never do, there is no leaf.
162
+ const leaf = props.topBarTitle ?? (Array.isArray(props.breadcrumb) && props.breadcrumb.length > 0);
163
+ // The top bar is then the page's crumb: its view crumb goes back as the page's last crumb did, and the host draws
164
+ // its own breadcrumb only where the top bar shows none.
165
+ const inTopBar = Boolean(leaf && Host && runtime?.setTopBarTitleLeaf);
166
+ const back = Array.isArray(props.breadcrumb) ? [...props.breadcrumb].reverse().find(crumb => crumb.onSelect)?.onSelect : undefined;
167
+ const [reported, setReported] = useState<{ recordKey: string; title: string } | null>(null);
168
+ const reportTitle = useCallback((title: string) => {
169
+ setReported(current => current?.recordKey === recordKey && current.title === title ? current : { recordKey, title });
170
+ onTitle?.(title);
171
+ }, [recordKey, onTitle]);
172
+ const title = reported?.recordKey === recordKey ? reported.title : null;
173
+ useTopBarTitleLeaf(leaf && title !== null ? title.trim() || t.untitled : null, back, { recordKey });
174
+ return Host ? <Host {...props} topBarTitle={inTopBar} onTitle={reportTitle} /> : <DocumentPageFallback {...props} />;
175
+ }
176
+
177
+ /**
178
+ * Starts reading a record before it opens (for example on a list row's hover or focus), so its
179
+ * DocumentPage shows at once. Does nothing outside a host that supports it.
180
+ */
181
+ export function usePrefetchRecord(): (recordKey: string) => void {
182
+ const prefetch = useNotisRuntime()?.ui?.prefetchRecord;
183
+ return useCallback((recordKey: string) => { if (recordKey) prefetch?.(recordKey); }, [prefetch]);
184
+ }