@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.
- package/README.md +542 -146
- package/bin/check-runtime.js +15 -0
- package/bin/notis.js +2 -0
- package/config/notis_app_design_rules.json +320 -0
- package/config/portal-route-roots.json +45 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +42323 -18258
- package/dist/base-skills/notis-cli/.notis-base-skill.json +1 -0
- package/dist/base-skills/notis-cli/SKILL.md +51 -280
- package/dist/base-skills/notis-cli/references/app-delivery.md +17 -0
- package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
- package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
- package/dist/base-skills/notis-cli/references/troubleshooting.md +67 -0
- package/dist/base-skills/notis-query/.notis-base-skill.json +1 -0
- package/dist/base-skills/notis-query/SKILL.md +35 -699
- package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
- package/dist/base-skills/notis-query/references/documents.md +74 -0
- package/dist/base-skills/notis-query/references/query.md +76 -0
- package/dist/base-skills/notis-spaces/.notis-base-skill.json +1 -0
- package/dist/base-skills/notis-spaces/SKILL.md +218 -0
- package/dist/base-skills/notis-spaces/references/architecture.md +116 -0
- package/dist/base-skills/notis-spaces/references/context.md +81 -0
- package/dist/base-skills/notis-spaces/references/design.md +141 -0
- package/dist/base-skills/notis-spaces/references/fixtures.md +29 -0
- package/dist/base-skills/notis-spaces/references/memory.md +58 -0
- package/dist/base-skills/notis-spaces/references/reading.md +73 -0
- package/dist/base-skills/notis-spaces/references/release.md +120 -0
- package/dist/base-skills/notis-spaces/references/reports.md +77 -0
- package/dist/base-skills/notis-spaces/references/sdk.md +273 -0
- package/dist/base-skills/notis-spaces/references/store.md +118 -0
- package/dist/base-skills/notis-spaces/references/troubleshooting.md +28 -0
- package/dist/base-skills/notis-spaces/references/views.md +229 -0
- package/{template/packages → dist}/sdk/package.json +9 -10
- package/dist/sdk/src/agentContext.ts +36 -0
- package/dist/sdk/src/bundleModule.ts +175 -0
- package/dist/sdk/src/components/Dialog.tsx +103 -0
- package/dist/sdk/src/components/DocumentEditor.tsx +113 -0
- package/dist/sdk/src/components/DocumentPage.tsx +184 -0
- package/{template/packages → dist}/sdk/src/components/Markdown.tsx +9 -3
- package/{template/packages → dist}/sdk/src/components/MarkdownEditor.tsx +26 -5
- package/{template/packages → dist}/sdk/src/components/MultiSelectActionBar.tsx +95 -12
- package/{template/packages → dist}/sdk/src/components/MultiSelectCheckbox.tsx +4 -2
- package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
- package/{template/packages → dist}/sdk/src/components/NotisSelectionBoundary.tsx +9 -5
- package/dist/sdk/src/components/RecordComponents.tsx +111 -0
- package/dist/sdk/src/components/Skeleton.tsx +38 -0
- package/dist/sdk/src/config.ts +31 -0
- package/{template/packages → dist}/sdk/src/documents.ts +21 -6
- package/dist/sdk/src/hooks/useAction.ts +28 -0
- package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
- package/{template/packages → dist}/sdk/src/hooks/useBackend.ts +3 -2
- package/dist/sdk/src/hooks/useCloudComputer.ts +94 -0
- package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
- package/{template/packages → dist}/sdk/src/hooks/useCollectionInteractions.ts +157 -147
- package/dist/sdk/src/hooks/useDatabaseSchema.ts +50 -0
- package/{template/packages → dist}/sdk/src/hooks/useDatabaseSubscription.ts +2 -2
- package/dist/sdk/src/hooks/useDocument.ts +51 -0
- package/{template/packages → dist}/sdk/src/hooks/useDocuments.ts +25 -59
- package/{template/packages → dist}/sdk/src/hooks/useHandover.ts +7 -7
- package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/dist/sdk/src/hooks/useMarqueeSelection.ts +194 -0
- package/{template/packages → dist}/sdk/src/hooks/useMultiSelect.ts +2 -8
- package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
- package/dist/sdk/src/hooks/useNativeDocuments.ts +143 -0
- package/{template/packages → dist}/sdk/src/hooks/useNotis.ts +6 -1
- package/dist/sdk/src/hooks/useNotisNavigation.ts +70 -0
- package/dist/sdk/src/hooks/useQuery.ts +81 -0
- package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
- package/dist/sdk/src/hooks/useTopBarSearch.ts +102 -0
- package/dist/sdk/src/hooks/useTopBarTitleLeaf.ts +75 -0
- package/{template/packages → dist}/sdk/src/hooks/useUpsertDocument.ts +1 -1
- package/dist/sdk/src/hooks/useViewParams.ts +195 -0
- package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
- package/{template/packages → dist}/sdk/src/index.ts +64 -1
- package/{template/packages → dist}/sdk/src/interactions/actions.ts +14 -1
- package/{template/packages → dist}/sdk/src/interactions/shortcuts.tsx +129 -23
- package/dist/sdk/src/interactions/visibility.ts +13 -0
- package/{template/packages → dist}/sdk/src/interactions.ts +5 -1
- package/dist/sdk/src/nativeData.ts +39 -0
- package/{template/packages → dist}/sdk/src/provider.tsx +9 -4
- package/dist/sdk/src/queryCache.ts +292 -0
- package/dist/sdk/src/queryErrors.ts +51 -0
- package/dist/sdk/src/runtime.ts +675 -0
- package/dist/sdk/src/runtimeModules.ts +33 -0
- package/dist/sdk/src/space.ts +442 -0
- package/dist/sdk/src/standardViewParams.ts +36 -0
- package/{template/packages → dist}/sdk/src/styles.css +81 -16
- package/dist/sdk/src/tailwind.ts +66 -0
- package/dist/sdk/src/ui/charts/RenderChartData.tsx +25 -0
- package/dist/sdk/src/ui/engine/core.ts +392 -0
- package/dist/sdk/src/ui/engine/dom.ts +183 -0
- package/dist/sdk/src/ui/engine/hooks.ts +195 -0
- package/dist/sdk/src/ui/engine/instance.ts +63 -0
- package/dist/sdk/src/ui/engine/keycapContext.ts +2 -0
- package/dist/sdk/src/ui/engine/registry.ts +262 -0
- package/dist/sdk/src/ui/engine/types.ts +62 -0
- package/dist/sdk/src/ui/icons.tsx +91 -0
- package/dist/sdk/src/ui/util.ts +83 -0
- package/dist/sdk/src/viewPeriod.ts +26 -0
- package/dist/sdk/src/vite.ts +92 -0
- package/dist/sdk/src/words/actions.ts +295 -0
- package/dist/sdk/src/words/clashes.ts +158 -0
- package/dist/sdk/src/words/format.ts +136 -0
- package/dist/sdk/src/words/index.ts +13 -0
- package/dist/sdk/src/words/keymap.ts +72 -0
- package/dist/sdk/src/words/keys.ts +297 -0
- package/dist/sdk/src/words/shell.ts +46 -0
- package/dist/sdk/src/words/types.ts +65 -0
- package/dist/skill-sync/index.js +832 -151
- package/dist/skill-sync/index.js.map +4 -4
- package/dist/skill-sync-worker.mjs +3941 -0
- package/dist/space-harness/host.js +1460 -0
- package/dist/space-sdk/dist/chunk-ZSN57ATH.js +5 -0
- package/dist/space-sdk/dist/index.js +15 -0
- package/dist/space-sdk/dist/ui.js +18 -0
- package/dist/space-sdk/package.json +33 -0
- package/dist/space-sdk/src/agentContext.d.ts +36 -0
- package/dist/space-sdk/src/components/Dialog.d.ts +20 -0
- package/dist/space-sdk/src/components/DocumentEditor.d.ts +46 -0
- package/dist/space-sdk/src/components/DocumentPage.d.ts +53 -0
- package/dist/space-sdk/src/components/Markdown.d.ts +18 -0
- package/dist/space-sdk/src/components/MarkdownEditor.d.ts +11 -0
- package/dist/space-sdk/src/components/MultiSelectActionBar.d.ts +31 -0
- package/dist/space-sdk/src/components/MultiSelectCheckbox.d.ts +14 -0
- package/dist/space-sdk/src/components/MultiSelectDragOverlay.d.ts +8 -0
- package/dist/space-sdk/src/components/NotisCommentBoundary.d.ts +36 -0
- package/dist/space-sdk/src/components/NotisSelectionBoundary.d.ts +16 -0
- package/dist/space-sdk/src/components/RecordComponents.d.ts +64 -0
- package/dist/space-sdk/src/components/ShortcutHints.d.ts +12 -0
- package/dist/space-sdk/src/components/Skeleton.d.ts +23 -0
- package/dist/space-sdk/src/config.ts +31 -0
- package/dist/space-sdk/src/documents.d.ts +59 -0
- package/dist/space-sdk/src/hooks/useAction.d.ts +8 -0
- package/dist/space-sdk/src/hooks/useActiveResource.d.ts +3 -0
- package/dist/space-sdk/src/hooks/useAgentContext.d.ts +7 -0
- package/dist/space-sdk/src/hooks/useBackend.d.ts +23 -0
- package/dist/space-sdk/src/hooks/useCloudComputer.d.ts +45 -0
- package/dist/space-sdk/src/hooks/useCloudComputerShell.d.ts +49 -0
- package/dist/space-sdk/src/hooks/useCollectionInteractions.d.ts +118 -0
- package/dist/space-sdk/src/hooks/useDatabaseSchema.d.ts +14 -0
- package/dist/space-sdk/src/hooks/useDatabaseSubscription.d.ts +26 -0
- package/dist/space-sdk/src/hooks/useDocument.d.ts +22 -0
- package/dist/space-sdk/src/hooks/useDocuments.d.ts +31 -0
- package/dist/space-sdk/src/hooks/useHandover.d.ts +43 -0
- package/dist/space-sdk/src/hooks/useLongPressSelection.d.ts +9 -0
- package/dist/space-sdk/src/hooks/useMarqueeSelection.d.ts +57 -0
- package/dist/space-sdk/src/hooks/useMultiSelect.d.ts +51 -0
- package/dist/space-sdk/src/hooks/useNativeDocumentBody.d.ts +24 -0
- package/dist/space-sdk/src/hooks/useNativeDocuments.d.ts +55 -0
- package/dist/space-sdk/src/hooks/useNotis.d.ts +25 -0
- package/dist/space-sdk/src/hooks/useNotisNavigation.d.ts +33 -0
- package/dist/space-sdk/src/hooks/useQuery.d.ts +22 -0
- package/dist/space-sdk/src/hooks/useTool.d.ts +20 -0
- package/dist/space-sdk/src/hooks/useToolQuery.d.ts +3 -0
- package/dist/space-sdk/src/hooks/useTools.d.ts +15 -0
- package/dist/space-sdk/src/hooks/useTopBarSearch.d.ts +48 -0
- package/dist/space-sdk/src/hooks/useTopBarTitleLeaf.d.ts +24 -0
- package/dist/space-sdk/src/hooks/useUpsertDocument.d.ts +27 -0
- package/dist/space-sdk/src/hooks/useViewParams.d.ts +76 -0
- package/dist/space-sdk/src/hooks/useViewerReads.d.ts +38 -0
- package/dist/space-sdk/src/index.d.ts +98 -0
- package/dist/space-sdk/src/interactions/actions.d.ts +33 -0
- package/dist/space-sdk/src/interactions/shortcuts.d.ts +42 -0
- package/dist/space-sdk/src/interactions/visibility.d.ts +2 -0
- package/dist/space-sdk/src/nativeData.d.ts +66 -0
- package/dist/space-sdk/src/provider.d.ts +17 -0
- package/dist/space-sdk/src/queryCache.d.ts +45 -0
- package/dist/space-sdk/src/queryErrors.ts +51 -0
- package/dist/space-sdk/src/runtime.d.ts +615 -0
- package/dist/space-sdk/src/runtimeModules.ts +33 -0
- package/dist/space-sdk/src/space.ts +442 -0
- package/dist/space-sdk/src/standardViewParams.d.ts +15 -0
- package/dist/space-sdk/src/tailwind-candidates.d.ts +3418 -0
- package/dist/space-sdk/src/tailwind.ts +66 -0
- package/dist/space-sdk/src/ui/actions/ActionToast.d.ts +15 -0
- package/dist/space-sdk/src/ui/actions/actionWords.d.ts +16 -0
- package/dist/space-sdk/src/ui/actions/index.d.ts +7 -0
- package/dist/space-sdk/src/ui/actions/useActions.d.ts +69 -0
- package/dist/space-sdk/src/ui/actions/useToast.d.ts +41 -0
- package/dist/space-sdk/src/ui/blocks/Tabs.d.ts +139 -0
- package/dist/space-sdk/src/ui/blocks/blocks.d.ts +217 -0
- package/dist/space-sdk/src/ui/blocks/index.d.ts +4 -0
- package/dist/space-sdk/src/ui/board/BoardView.d.ts +36 -0
- package/dist/space-sdk/src/ui/board/boardModel.d.ts +46 -0
- package/dist/space-sdk/src/ui/board/boardWords.d.ts +9 -0
- package/dist/space-sdk/src/ui/board/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/calendar/CalendarView.d.ts +49 -0
- package/dist/space-sdk/src/ui/calendar/DayPicker.d.ts +23 -0
- package/dist/space-sdk/src/ui/calendar/calendarMath.d.ts +81 -0
- package/dist/space-sdk/src/ui/charts/Chart.d.ts +203 -0
- package/dist/space-sdk/src/ui/charts/ChartTable.d.ts +37 -0
- package/dist/space-sdk/src/ui/charts/RenderChartData.d.ts +16 -0
- package/dist/space-sdk/src/ui/charts/StatRow.d.ts +59 -0
- package/dist/space-sdk/src/ui/charts/chartData.d.ts +84 -0
- package/dist/space-sdk/src/ui/comments/CommentPane.d.ts +58 -0
- package/dist/space-sdk/src/ui/comments/commentModel.d.ts +157 -0
- package/dist/space-sdk/src/ui/comments/commentWords.d.ts +60 -0
- package/dist/space-sdk/src/ui/comments/index.d.ts +4 -0
- package/dist/space-sdk/src/ui/comments/useComments.d.ts +146 -0
- package/dist/space-sdk/src/ui/engine/HelpSheet.d.ts +12 -0
- package/dist/space-sdk/src/ui/engine/KeyboardEngine.d.ts +27 -0
- package/dist/space-sdk/src/ui/engine/core.d.ts +73 -0
- package/dist/space-sdk/src/ui/engine/hooks.d.ts +64 -0
- package/dist/space-sdk/src/ui/engine/index.d.ts +14 -0
- package/dist/space-sdk/src/ui/engine/instance.d.ts +21 -0
- package/dist/space-sdk/src/ui/engine/keycapContext.d.ts +2 -0
- package/dist/space-sdk/src/ui/engine/registry.d.ts +77 -0
- package/dist/space-sdk/src/ui/engine/step.d.ts +11 -0
- package/dist/space-sdk/src/ui/engine/types.d.ts +54 -0
- package/dist/space-sdk/src/ui/fields/fieldWords.d.ts +56 -0
- package/dist/space-sdk/src/ui/fields/fields.d.ts +162 -0
- package/dist/space-sdk/src/ui/fields/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/fields/useFieldDraft.d.ts +22 -0
- package/dist/space-sdk/src/ui/gallery/AttachmentGallery.d.ts +53 -0
- package/dist/space-sdk/src/ui/gallery/ImageViewer.d.ts +27 -0
- package/dist/space-sdk/src/ui/gallery/galleryWords.d.ts +95 -0
- package/dist/space-sdk/src/ui/gallery/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/icons.d.ts +13 -0
- package/dist/space-sdk/src/ui/index.d.ts +54 -0
- package/dist/space-sdk/src/ui/keycap/Keycap.d.ts +22 -0
- package/dist/space-sdk/src/ui/keycap/index.d.ts +2 -0
- package/dist/space-sdk/src/ui/layout/Panels.d.ts +50 -0
- package/dist/space-sdk/src/ui/layout/chatPanelEdge.d.ts +77 -0
- package/dist/space-sdk/src/ui/layout/index.d.ts +5 -0
- package/dist/space-sdk/src/ui/layout/panelMath.d.ts +91 -0
- package/dist/space-sdk/src/ui/list/ListReader.d.ts +380 -0
- package/dist/space-sdk/src/ui/list/Row.d.ts +145 -0
- package/dist/space-sdk/src/ui/list/SelectionBar.d.ts +65 -0
- package/dist/space-sdk/src/ui/list/index.d.ts +14 -0
- package/dist/space-sdk/src/ui/list/rowState.d.ts +22 -0
- package/dist/space-sdk/src/ui/list/useListKeys.d.ts +89 -0
- package/dist/space-sdk/src/ui/list/useReorder.d.ts +86 -0
- package/dist/space-sdk/src/ui/list/useSelection.d.ts +90 -0
- package/dist/space-sdk/src/ui/open/OpenAsMenu.d.ts +15 -0
- package/dist/space-sdk/src/ui/open/index.d.ts +6 -0
- package/dist/space-sdk/src/ui/open/openModeStore.d.ts +39 -0
- package/dist/space-sdk/src/ui/open/useOpenMode.d.ts +17 -0
- package/dist/space-sdk/src/ui/overlay/Menu.d.ts +88 -0
- package/dist/space-sdk/src/ui/overlay/Popover.d.ts +28 -0
- package/dist/space-sdk/src/ui/overlay/Tooltip.d.ts +19 -0
- package/dist/space-sdk/src/ui/overlay/index.d.ts +9 -0
- package/dist/space-sdk/src/ui/overlay/layerRoot.d.ts +24 -0
- package/dist/space-sdk/src/ui/overlay/position.d.ts +31 -0
- package/dist/space-sdk/src/ui/period/PeriodPicker.d.ts +30 -0
- package/dist/space-sdk/src/ui/period/dateFormat.d.ts +50 -0
- package/dist/space-sdk/src/ui/period/dateWords.d.ts +88 -0
- package/dist/space-sdk/src/ui/period/periods.d.ts +70 -0
- package/dist/space-sdk/src/ui/period/usePeriod.d.ts +23 -0
- package/dist/space-sdk/src/ui/present/index.d.ts +3 -0
- package/dist/space-sdk/src/ui/present/presentWords.d.ts +27 -0
- package/dist/space-sdk/src/ui/present/screens.d.ts +36 -0
- package/dist/space-sdk/src/ui/present/useAudienceScreen.d.ts +38 -0
- package/dist/space-sdk/src/ui/styles.d.ts +21 -0
- package/dist/space-sdk/src/ui/timeline/TimelineView.d.ts +44 -0
- package/dist/space-sdk/src/ui/timeline/timelineMath.d.ts +123 -0
- package/dist/space-sdk/src/viewPeriod.d.ts +8 -0
- package/dist/space-sdk/src/vite.ts +92 -0
- package/dist/space-sdk/src/words/actions.ts +295 -0
- package/dist/space-sdk/src/words/clashes.ts +158 -0
- package/dist/space-sdk/src/words/format.ts +136 -0
- package/dist/space-sdk/src/words/index.ts +13 -0
- package/dist/space-sdk/src/words/keymap.ts +72 -0
- package/dist/space-sdk/src/words/keys.ts +297 -0
- package/dist/space-sdk/src/words/shell.ts +46 -0
- package/dist/space-sdk/src/words/types.ts +65 -0
- package/dist/view-renderer/dist/host.css +1 -0
- package/dist/view-renderer/dist/host.js +2374 -0
- package/dist/view-renderer/dist/theme.css +73 -0
- package/dist/view-renderer/src/dom.js +212 -0
- package/dist/view-renderer/src/errors.js +8 -0
- package/dist/view-renderer/src/host.tsx +250 -0
- package/dist/view-renderer/src/index.js +4 -0
- package/dist/view-renderer/src/png.js +213 -0
- package/dist/view-renderer/src/pool.js +110 -0
- package/dist/view-renderer/src/remote-broker.js +59 -0
- package/dist/view-renderer/src/renderer.js +271 -0
- package/package.json +21 -11
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +63 -12
- package/skills/notis-onboarding/BRIEF.md +14 -9
- package/skills/notis-query/cli.md +1 -1
- package/skills/notis-spaces/cli.md +540 -0
- package/src/cli.js +8 -8
- package/src/command-specs/agents.js +121 -8
- package/src/command-specs/auth.js +3 -3
- package/src/command-specs/diagnostics.js +97 -20
- package/src/command-specs/documents.js +158 -0
- package/src/command-specs/files.js +29 -0
- package/src/command-specs/handover.js +331 -79
- package/src/command-specs/helpers.js +50 -40
- package/src/command-specs/index.js +14 -5
- package/src/command-specs/meta.js +38 -9
- package/src/command-specs/onboarding.js +1 -1
- package/src/command-specs/profile.js +42 -1
- package/src/command-specs/skills.js +314 -9
- package/src/command-specs/smoke.js +16 -4
- package/src/command-specs/spaces.js +783 -0
- package/src/command-specs/tools.js +118 -21
- package/src/command-specs/version-history.js +20 -0
- package/src/command-specs/views.js +69 -0
- package/src/runtime/agent-browser.js +178 -267
- package/src/runtime/agent-setup.js +172 -13
- package/src/runtime/app-boundary-validator.js +338 -2
- package/src/runtime/app-data-literals.js +82 -0
- package/src/runtime/app-platform.js +750 -1568
- package/src/runtime/auth-recovery.js +17 -0
- package/src/runtime/base-skills.d.ts +11 -0
- package/src/runtime/base-skills.js +100 -3
- package/src/runtime/computer-identity.d.ts +30 -0
- package/src/runtime/computer-identity.js +151 -0
- package/src/runtime/config-lock.js +135 -0
- package/src/runtime/confined-space-build.js +74 -0
- package/src/runtime/confined-space-verify.js +49 -0
- package/src/runtime/delegated-context.js +5 -21
- package/src/runtime/native-record-files.js +47 -0
- package/src/runtime/oauth.js +342 -31
- package/src/runtime/profiles.js +19 -112
- package/src/runtime/retired-sdk-files.js +24 -0
- package/src/runtime/retry.js +150 -0
- package/src/runtime/skill-sync/cloud-client.ts +33 -8
- package/src/runtime/skill-sync/index.ts +443 -106
- package/src/runtime/skill-sync/local-scanner.ts +248 -32
- package/src/runtime/skill-sync/native-identity.ts +102 -0
- package/src/runtime/skill-sync/native-plan.ts +70 -0
- package/src/runtime/skill-sync/symlink-manager.ts +66 -16
- package/src/runtime/skill-sync/sync-plan.ts +34 -2
- package/src/runtime/skill-sync/types.ts +54 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +4 -4
- package/src/runtime/skill-sync-service.js +109 -0
- package/src/runtime/source-root.js +32 -0
- package/src/runtime/space-collection-scaffold.js +107 -0
- package/src/runtime/space-database-structures.js +229 -0
- package/src/runtime/space-fixture-broker.js +68 -0
- package/src/runtime/space-lock.js +337 -0
- package/src/runtime/space-platform.js +133 -0
- package/src/runtime/space-source.js +290 -0
- package/src/runtime/space-test-server.js +354 -0
- package/src/runtime/space-verification.js +94 -0
- package/src/runtime/space-verify-browser-worker.js +51 -0
- package/src/runtime/space-view-manifest.js +368 -0
- package/src/runtime/sync-skills.d.ts +5 -1
- package/src/runtime/sync-skills.js +11 -2
- package/src/runtime/tool-confirmation.js +138 -0
- package/src/runtime/transport.js +118 -24
- package/src/runtime/upsell.js +219 -0
- package/src/runtime/view-render.js +49 -0
- package/src/skill-sync-worker-entry.js +2 -0
- package/src/skill-sync-worker.js +114 -0
- package/src/space-harness-host.ts +122 -0
- package/template/.harness/index.html.tmpl +9 -352
- package/template/app/globals.css +29 -3
- package/template/app/layout.tsx +2 -6
- package/template/components/ui/badge.tsx +7 -4
- package/template/components/ui/button.tsx +1 -1
- package/template/components/ui/native-select.tsx +24 -0
- package/template/notis.config.ts +4 -36
- package/template/package-lock.json +1800 -1867
- package/template/package.json +16 -14
- package/template/postcss.config.mjs +1 -1
- package/template/tailwind.config.ts +1 -6
- package/template/tsconfig.json +1 -0
- package/template/vite.config.ts +2 -2
- package/dist/base-skills/notis-apps/SKILL.md +0 -613
- package/skills/notis-apps/cli.md +0 -285
- package/src/command-specs/apps.js +0 -3000
- package/src/runtime/app-changelog.js +0 -79
- package/src/runtime/app-dev-consumers.js +0 -154
- package/src/runtime/app-dev-host-lock.js +0 -80
- package/src/runtime/app-dev-process-identity.js +0 -111
- package/src/runtime/app-dev-roots.js +0 -284
- package/src/runtime/app-dev-server.js +0 -1140
- package/src/runtime/app-dev-sessions.js +0 -185
- package/src/runtime/app-registry-scaffolds.js +0 -367
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/cli-mode.generated.js +0 -5
- package/src/runtime/cli-mode.js +0 -34
- package/src/runtime/store-screenshot.js +0 -143
- package/template/CHANGELOG.md +0 -5
- package/template/app/page.tsx +0 -60
- package/template/components/ui/card.tsx +0 -56
- package/template/packages/sdk/src/components/DocumentEditor.tsx +0 -103
- package/template/packages/sdk/src/config.ts +0 -259
- package/template/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/template/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/template/packages/sdk/src/hooks/useNotisNavigation.ts +0 -52
- package/template/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/template/packages/sdk/src/runtime.ts +0 -439
- package/template/packages/sdk/src/vite.ts +0 -56
- package/template/packages/sdk/tsconfig.json +0 -15
- /package/{template/packages → dist}/sdk/src/components/MultiSelectDragOverlay.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/components/ShortcutHints.tsx +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useActiveResource.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useTool.ts +0 -0
- /package/{template/packages → dist}/sdk/src/hooks/useTools.ts +0 -0
- /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
|
-
"./
|
|
12
|
-
"./
|
|
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.
|
|
34
|
-
"@types/react-dom": "^19.
|
|
35
|
-
"typescript": "^
|
|
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
|
+
}
|