@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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Build-only SDK content discovery. Author configuration and PostCSS plugins stay intact. */
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
export function notisTailwindContent() {
|
|
8
|
+
let root = process.cwd();
|
|
9
|
+
let temporary: string | undefined;
|
|
10
|
+
const wrappers = new Map<string, string>();
|
|
11
|
+
const cleanup = () => {
|
|
12
|
+
if (temporary) rmSync(temporary, { recursive: true, force: true });
|
|
13
|
+
temporary = undefined;
|
|
14
|
+
wrappers.clear();
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
name: 'notis-sdk-tailwind-content',
|
|
18
|
+
enforce: 'pre' as const,
|
|
19
|
+
configResolved(config: { root: string }) { root = config.root; },
|
|
20
|
+
transform(source: string, id: string) {
|
|
21
|
+
const stylesheet = id.split('?')[0];
|
|
22
|
+
if (!stylesheet.endsWith('.css')) return null;
|
|
23
|
+
// Tailwind 4 discovers dependency sources through CSS, not config.content.
|
|
24
|
+
// Keep the author's imports, configuration and PostCSS pipeline untouched.
|
|
25
|
+
if (/@import\s+(?:url\(\s*)?(['"])tailwindcss(?:\/[^'"]*)?\1/.test(source)) {
|
|
26
|
+
const sdkSource = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const relativeSource = './' + relative(dirname(stylesheet), sdkSource).replaceAll('\\', '/');
|
|
28
|
+
const directive = `@source ${JSON.stringify(relativeSource)};`;
|
|
29
|
+
return source.includes(directive) ? null : { code: `${source}\n${directive}\n`, map: null };
|
|
30
|
+
}
|
|
31
|
+
if (!/@tailwind\s/.test(source)) return null;
|
|
32
|
+
const explicit = source.match(/@config\s+(['"])(.*?)\1\s*;/);
|
|
33
|
+
const config = explicit
|
|
34
|
+
? resolve(dirname(id.split('?')[0]), explicit[2])
|
|
35
|
+
: ['ts', 'js', 'cjs', 'mjs'].map(ext => join(root, `tailwind.config.${ext}`)).find(existsSync);
|
|
36
|
+
if (!config) return null;
|
|
37
|
+
let wrapper = wrappers.get(config);
|
|
38
|
+
if (!wrapper) {
|
|
39
|
+
temporary ||= mkdtempSync(join(tmpdir(), 'notis-tailwind-'));
|
|
40
|
+
wrapper = join(temporary, `config-${wrappers.size}.ts`);
|
|
41
|
+
const sdkGlob = join(dirname(fileURLToPath(import.meta.url)), '**/*.{ts,tsx}').replaceAll('\\', '/');
|
|
42
|
+
writeFileSync(wrapper, `import config from ${JSON.stringify(config)};
|
|
43
|
+
import { resolve } from 'node:path';
|
|
44
|
+
const content = config.content || [];
|
|
45
|
+
const files = Array.isArray(content) ? content : (content.files || []);
|
|
46
|
+
const base = ${JSON.stringify(root)};
|
|
47
|
+
const relativeBase = ${JSON.stringify(dirname(config))};
|
|
48
|
+
export default { ...config, content: {
|
|
49
|
+
...(Array.isArray(content) ? {} : content),
|
|
50
|
+
relative: false,
|
|
51
|
+
files: [...files.map(file => typeof file !== 'string' ? file :
|
|
52
|
+
(file.startsWith('!') ? '!' : '') + resolve(content.relative ? relativeBase : base, file.replace(/^!/, ''))),
|
|
53
|
+
${JSON.stringify(sdkGlob)}],
|
|
54
|
+
} };
|
|
55
|
+
`);
|
|
56
|
+
wrappers.set(config, wrapper);
|
|
57
|
+
}
|
|
58
|
+
const directive = `@config ${JSON.stringify('./' + relative(dirname(id.split('?')[0]), wrapper).replaceAll('\\', '/'))};`;
|
|
59
|
+
// Imports must remain before @config. Vite resolves them before PostCSS.
|
|
60
|
+
const code = explicit ? source.replace(explicit[0], directive) : `${source}\n${directive}\n`;
|
|
61
|
+
return { code, map: null };
|
|
62
|
+
},
|
|
63
|
+
closeBundle: cleanup,
|
|
64
|
+
closeWatcher: cleanup,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
export interface RenderChartDataProps {
|
|
7
|
+
title: string;
|
|
8
|
+
columns: readonly string[];
|
|
9
|
+
rows: readonly (readonly (string | number | boolean | null)[])[];
|
|
10
|
+
/** The visible chart. `Chart` and `ChartTable` already render their own data table. */
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A chart's data, available to view Markdown without adding a second visible table (toolkit spec 2.11; moved from
|
|
16
|
+
* `src/components/RenderChartData.tsx`). Keep labels and values identical to the chart. The renderer recognizes this
|
|
17
|
+
* semantic table inside Shadow DOM; snapshots never infer numbers from SVG geometry. `Chart` renders its numbers
|
|
18
|
+
* through it.
|
|
19
|
+
*/
|
|
20
|
+
export function RenderChartData({ title, columns, rows, children }: RenderChartDataProps) {
|
|
21
|
+
return <>{children}<table hidden data-notis-render="chart-data" aria-label={title}>
|
|
22
|
+
<thead><tr>{columns.map((column, index) => <th key={index}>{column}</th>)}</tr></thead>
|
|
23
|
+
<tbody>{rows.map((row, index) => <tr key={index}>{columns.map((_, column) => <td key={column}>{row[column] == null ? '' : String(row[column])}</td>)}</tr>)}</tbody>
|
|
24
|
+
</table></>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one keyboard listener (toolkit spec 2.1). Document capture phase; React handlers no longer own keys.
|
|
3
|
+
* No React here: the React pieces (`KeyboardEngine`, hooks) drive one shared core.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DEFAULT_KEYMAP, isModifierOnlyKey, matchChord, type Chord, type Locale, type Platform } from '../../words';
|
|
7
|
+
import {
|
|
8
|
+
blocksShortcuts,
|
|
9
|
+
eventTarget,
|
|
10
|
+
fieldOwnsEscape,
|
|
11
|
+
foreignFloatingLayers,
|
|
12
|
+
hasOpenModal,
|
|
13
|
+
isActivatable,
|
|
14
|
+
keysOff,
|
|
15
|
+
ownsArrows,
|
|
16
|
+
} from './dom';
|
|
17
|
+
import { ROOT_OWNER, ShortcutRegistry, safeWhen, type RegisteredShortcut } from './registry';
|
|
18
|
+
import type { EngineSettings, ShortcutDef } from './types';
|
|
19
|
+
|
|
20
|
+
/** Escape chain order, highest first: menus and dialogs, help sheet, selection clear, detail panel, route back. */
|
|
21
|
+
export const ESCAPE_PRIORITY = Object.freeze({
|
|
22
|
+
route: 0,
|
|
23
|
+
detail: 100,
|
|
24
|
+
selection: 200,
|
|
25
|
+
help: 300,
|
|
26
|
+
overlay: 400,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const SEQUENCE_TIMEOUT_MS = 1500;
|
|
30
|
+
|
|
31
|
+
interface EscapeLayerRecord { id: string; priority: number; onEscape: (event?: KeyboardEvent) => boolean; seq: number }
|
|
32
|
+
interface EscapeBlockerRecord { priority: number; seq: number; isActive?: () => boolean }
|
|
33
|
+
interface PendingSequence { candidates: Array<{ entry: RegisteredShortcut; chords: Chord[] }>; step: number; at: number }
|
|
34
|
+
|
|
35
|
+
export function detectPlatform(): Platform {
|
|
36
|
+
if (typeof navigator === 'undefined') return 'other';
|
|
37
|
+
const nav = navigator as Navigator & { userAgentData?: { platform?: string } };
|
|
38
|
+
const name = nav.userAgentData?.platform || nav.platform || nav.userAgent || '';
|
|
39
|
+
return /mac|iphone|ipad|ipod/i.test(name) ? 'mac' : 'other';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function detectLocale(): Locale {
|
|
43
|
+
if (typeof document === 'undefined') return 'en';
|
|
44
|
+
// The page language set by the host on <html>.
|
|
45
|
+
const lang = document.querySelector('html')?.getAttribute('lang') ?? '';
|
|
46
|
+
return /^fr\b/i.test(lang) ? 'fr' : 'en';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function defaultStrict(): boolean {
|
|
50
|
+
const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;
|
|
51
|
+
return env?.NODE_ENV !== 'production';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function consume(event: KeyboardEvent, immediate = false): void {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
if (immediate) event.stopImmediatePropagation();
|
|
57
|
+
else event.stopPropagation();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isUnmodifiedEscape(event: KeyboardEvent): boolean {
|
|
61
|
+
return event.key === 'Escape' && !event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class KeyboardEngineCore {
|
|
65
|
+
readonly registry: ShortcutRegistry;
|
|
66
|
+
settings: EngineSettings;
|
|
67
|
+
/** Mounted `<KeyboardEngine>` count. With none, settings hooks fall back to the document (lang). */
|
|
68
|
+
mounted = 0;
|
|
69
|
+
/** Clock, replaceable in tests. */
|
|
70
|
+
now: () => number = () => Date.now();
|
|
71
|
+
|
|
72
|
+
private layers: EscapeLayerRecord[] = [];
|
|
73
|
+
private blockers: EscapeBlockerRecord[] = [];
|
|
74
|
+
private seq = 0;
|
|
75
|
+
private held = new Set<string>();
|
|
76
|
+
private pending: PendingSequence | null = null;
|
|
77
|
+
private attached = new Map<Document, { refs: number; detach: () => void }>();
|
|
78
|
+
private helpOpen = false;
|
|
79
|
+
private helpUnregister: (() => void) | null = null;
|
|
80
|
+
private forcedRepeat = new WeakSet<KeyboardEvent>();
|
|
81
|
+
|
|
82
|
+
constructor(settings: Partial<EngineSettings> = {}) {
|
|
83
|
+
this.settings = {
|
|
84
|
+
host: settings.host ?? 'portal',
|
|
85
|
+
locale: settings.locale ?? detectLocale(),
|
|
86
|
+
platform: settings.platform ?? detectPlatform(),
|
|
87
|
+
strict: settings.strict ?? defaultStrict(),
|
|
88
|
+
onClash: settings.onClash,
|
|
89
|
+
};
|
|
90
|
+
this.registry = new ShortcutRegistry({ strict: this.settings.strict, onClash: this.settings.onClash });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Updates settings. Returns true when one changed; listeners hear it unless `silent` (during a React render). */
|
|
94
|
+
configure(settings: Partial<EngineSettings>, options: { silent?: boolean } = {}): boolean {
|
|
95
|
+
const next = { ...this.settings };
|
|
96
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
97
|
+
if (value !== undefined) (next as Record<string, unknown>)[key] = value;
|
|
98
|
+
}
|
|
99
|
+
const changed = (Object.keys(next) as Array<keyof EngineSettings>).some((key) => next[key] !== this.settings[key]);
|
|
100
|
+
this.settings = next;
|
|
101
|
+
this.registry.options = { strict: next.strict, onClash: next.onClash };
|
|
102
|
+
if (changed && !options.silent) this.registry.notify();
|
|
103
|
+
return changed;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
subscribe(listener: () => void): () => void {
|
|
107
|
+
return this.registry.subscribe(listener);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getVersion(): number {
|
|
111
|
+
return this.registry.getVersion();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- listeners ----
|
|
115
|
+
|
|
116
|
+
/** Starts listening on a document. Ref counted; the last detach removes the listeners. */
|
|
117
|
+
attach(doc: Document): () => void {
|
|
118
|
+
const existing = this.attached.get(doc);
|
|
119
|
+
if (existing) {
|
|
120
|
+
existing.refs += 1;
|
|
121
|
+
} else {
|
|
122
|
+
const onKeyDown = (event: Event) => this.handleKeyDown(event as KeyboardEvent);
|
|
123
|
+
const onKeyUp = (event: Event) => this.handleKeyUp(event as KeyboardEvent);
|
|
124
|
+
const onReset = () => this.resetHeld();
|
|
125
|
+
const onVisibility = () => { if (doc.visibilityState === 'hidden') this.resetHeld(); };
|
|
126
|
+
doc.addEventListener('keydown', onKeyDown, true);
|
|
127
|
+
doc.addEventListener('keyup', onKeyUp, true);
|
|
128
|
+
doc.addEventListener('visibilitychange', onVisibility);
|
|
129
|
+
doc.defaultView?.addEventListener('blur', onReset);
|
|
130
|
+
this.attached.set(doc, {
|
|
131
|
+
refs: 1,
|
|
132
|
+
detach: () => {
|
|
133
|
+
doc.removeEventListener('keydown', onKeyDown, true);
|
|
134
|
+
doc.removeEventListener('keyup', onKeyUp, true);
|
|
135
|
+
doc.removeEventListener('visibilitychange', onVisibility);
|
|
136
|
+
doc.defaultView?.removeEventListener('blur', onReset);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
this.ensureHelpShortcut();
|
|
141
|
+
let detached = false;
|
|
142
|
+
return () => {
|
|
143
|
+
if (detached) return;
|
|
144
|
+
detached = true;
|
|
145
|
+
const record = this.attached.get(doc);
|
|
146
|
+
if (!record) return;
|
|
147
|
+
record.refs -= 1;
|
|
148
|
+
if (record.refs <= 0) {
|
|
149
|
+
record.detach();
|
|
150
|
+
this.attached.delete(doc);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
isAttached(doc: Document): boolean {
|
|
156
|
+
return this.attached.has(doc);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The first document the engine listens on (the host page), or null before KeyboardEngine mounts. */
|
|
160
|
+
attachedDocument(): Document | null {
|
|
161
|
+
for (const doc of this.attached.keys()) return doc;
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private resetHeld(): void {
|
|
166
|
+
this.held.clear();
|
|
167
|
+
this.pending = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---- registration ----
|
|
171
|
+
|
|
172
|
+
register(def: ShortcutDef, owner?: string): (() => void) | null {
|
|
173
|
+
return this.registry.register(def, owner ?? def.owner ?? ROOT_OWNER);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
defineOwner: ShortcutRegistry['defineOwner'] = (id, options) => this.registry.defineOwner(id, options);
|
|
177
|
+
|
|
178
|
+
setOwnerAvailable(id: string, available: boolean): void {
|
|
179
|
+
this.registry.setOwnerAvailable(id, available);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
isOwnerAvailable(id: string): boolean {
|
|
183
|
+
return this.registry.isOwnerAvailable(id);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---- escape chain ----
|
|
187
|
+
|
|
188
|
+
addEscapeLayer(id: string, onEscape: (event?: KeyboardEvent) => boolean,
|
|
189
|
+
priority: number = ESCAPE_PRIORITY.overlay): () => void {
|
|
190
|
+
const layer: EscapeLayerRecord = { id, onEscape, priority, seq: ++this.seq };
|
|
191
|
+
this.layers.push(layer);
|
|
192
|
+
return () => {
|
|
193
|
+
const index = this.layers.indexOf(layer);
|
|
194
|
+
if (index !== -1) this.layers.splice(index, 1);
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
addEscapeBlocker(priority: number = ESCAPE_PRIORITY.detail + 50, isActive?: () => boolean): () => void {
|
|
199
|
+
const blocker: EscapeBlockerRecord = { priority, seq: ++this.seq, isActive };
|
|
200
|
+
this.blockers.push(blocker);
|
|
201
|
+
return () => {
|
|
202
|
+
const index = this.blockers.indexOf(blocker);
|
|
203
|
+
if (index !== -1) this.blockers.splice(index, 1);
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
escapeLayerIds(): string[] {
|
|
208
|
+
return this.sortedLayers().map((layer) => layer.id);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private sortedLayers(): EscapeLayerRecord[] {
|
|
212
|
+
return [...this.layers].sort((a, b) => b.priority - a.priority || b.seq - a.seq);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Runs the chain. True when a layer handled it. Called without an event by a Close button that means "Escape";
|
|
217
|
+
* from a key press each layer gets the keydown.
|
|
218
|
+
*/
|
|
219
|
+
runEscapeChain(event?: KeyboardEvent): boolean {
|
|
220
|
+
const blockers = this.blockers.filter(blocker => !blocker.isActive || blocker.isActive());
|
|
221
|
+
const top = blockers.reduce((max, blocker) => Math.max(max, blocker.priority), -Infinity);
|
|
222
|
+
for (const layer of this.sortedLayers()) {
|
|
223
|
+
if (layer.priority < top) return true;
|
|
224
|
+
let handled = false;
|
|
225
|
+
try { handled = layer.onEscape(event) === true; } catch (error) { reportError(error); handled = true; }
|
|
226
|
+
if (handled) return true;
|
|
227
|
+
}
|
|
228
|
+
return blockers.length > 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---- help sheet ----
|
|
232
|
+
|
|
233
|
+
isHelpOpen(): boolean {
|
|
234
|
+
return this.helpOpen;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
openHelp(): void {
|
|
238
|
+
if (this.helpOpen) return;
|
|
239
|
+
this.helpOpen = true;
|
|
240
|
+
this.registry.notify();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
closeHelp(): void {
|
|
244
|
+
if (!this.helpOpen) return;
|
|
245
|
+
this.helpOpen = false;
|
|
246
|
+
this.registry.notify();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
toggleHelp(): void {
|
|
250
|
+
if (this.helpOpen) this.closeHelp(); else this.openHelp();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private ensureHelpShortcut(): void {
|
|
254
|
+
if (this.helpUnregister) return;
|
|
255
|
+
const entry = DEFAULT_KEYMAP['host.help'];
|
|
256
|
+
this.helpUnregister = this.registry.register({
|
|
257
|
+
id: entry.id,
|
|
258
|
+
keys: [...entry.keys],
|
|
259
|
+
scope: 'app',
|
|
260
|
+
label: entry.label,
|
|
261
|
+
group: 'notis',
|
|
262
|
+
run: () => { this.toggleHelp(); },
|
|
263
|
+
}, ROOT_OWNER);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---- dispatch ----
|
|
267
|
+
|
|
268
|
+
/** True for a key repeat, including a press of a key that is still held (some hosts drop `event.repeat`). */
|
|
269
|
+
isRepeat(event: KeyboardEvent): boolean {
|
|
270
|
+
return event.repeat || this.forcedRepeat.has(event);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
handleKeyUp(event: KeyboardEvent): void {
|
|
274
|
+
if (event.key === 'Meta') {
|
|
275
|
+
// A Mac sends no keyup for keys released while Cmd was down.
|
|
276
|
+
this.held.clear();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this.held.delete(event.code || event.key);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
handleKeyDown(event: KeyboardEvent): void {
|
|
283
|
+
if (event.isComposing || event.keyCode === 229) return;
|
|
284
|
+
const key = event.key;
|
|
285
|
+
if (!key || key === 'Dead' || key === 'Process' || isModifierOnlyKey(key)) return;
|
|
286
|
+
const physical = event.code || key;
|
|
287
|
+
const repeat = event.repeat || this.held.has(physical);
|
|
288
|
+
if (!event.metaKey) this.held.add(physical);
|
|
289
|
+
if (repeat && !event.repeat) {
|
|
290
|
+
this.forcedRepeat.add(event);
|
|
291
|
+
try { Object.defineProperty(event, 'repeat', { value: true, configurable: true }); } catch { /* read-only host */ }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const target = eventTarget(event);
|
|
295
|
+
const doc = target?.ownerDocument ?? (typeof document !== 'undefined' ? document : null);
|
|
296
|
+
if (!doc || keysOff(target)) return;
|
|
297
|
+
|
|
298
|
+
if (isUnmodifiedEscape(event) && this.handleEscape(event, target, doc, repeat)) return;
|
|
299
|
+
if (event.defaultPrevented) return;
|
|
300
|
+
|
|
301
|
+
const lower = key.toLowerCase();
|
|
302
|
+
// A button takes its own plain Enter and Space; Cmd+Enter (save) still reaches the shortcuts.
|
|
303
|
+
const plain = !event.metaKey && !event.ctrlKey && !event.altKey;
|
|
304
|
+
if (plain && (lower === 'enter' || lower === ' ') && isActivatable(target)) { this.pending = null; return; }
|
|
305
|
+
if (lower.startsWith('arrow') && ownsArrows(target)) return;
|
|
306
|
+
|
|
307
|
+
let modal: boolean | null = null;
|
|
308
|
+
const isModal = () => (modal ??= hasOpenModal(doc, event));
|
|
309
|
+
const { platform } = this.settings;
|
|
310
|
+
|
|
311
|
+
const eligible = (entry: RegisteredShortcut): boolean => {
|
|
312
|
+
const { def } = entry;
|
|
313
|
+
if (repeat && !def.repeat) return false;
|
|
314
|
+
if (isModal() && def.scope !== 'modal') return false;
|
|
315
|
+
if (blocksShortcuts(target, { inFields: !!def.allowInFields, modal: def.scope === 'modal' })) return false;
|
|
316
|
+
if (def.when && !safeWhen(def.when)) return false;
|
|
317
|
+
return true;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// A sequence in progress ('g' then 'i').
|
|
321
|
+
const pending = this.pending;
|
|
322
|
+
this.pending = null;
|
|
323
|
+
if (pending && !repeat && this.now() - pending.at <= SEQUENCE_TIMEOUT_MS) {
|
|
324
|
+
const next = pending.candidates.filter(({ entry, chords }) =>
|
|
325
|
+
matchChord(event, chords[pending.step]!, platform) && this.registry.all().includes(entry) && eligible(entry));
|
|
326
|
+
if (next.length) {
|
|
327
|
+
const done = next.filter(({ chords }) => chords.length === pending.step + 1);
|
|
328
|
+
for (const { entry } of done) {
|
|
329
|
+
if (this.run(entry, event)) { consume(event); return; }
|
|
330
|
+
}
|
|
331
|
+
const longer = next.filter(({ chords }) => chords.length > pending.step + 1);
|
|
332
|
+
if (longer.length) {
|
|
333
|
+
this.pending = { candidates: longer, step: pending.step + 1, at: this.now() };
|
|
334
|
+
consume(event);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const active = this.registry.active();
|
|
341
|
+
for (const entry of active) {
|
|
342
|
+
if (!entry.chords.some((chords) => chords.length === 1 && matchChord(event, chords[0]!, platform))) continue;
|
|
343
|
+
if (!eligible(entry)) continue;
|
|
344
|
+
if (this.run(entry, event)) { consume(event); return; }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (repeat) return;
|
|
348
|
+
const starts: PendingSequence['candidates'] = [];
|
|
349
|
+
for (const entry of active) {
|
|
350
|
+
for (const chords of entry.chords) {
|
|
351
|
+
if (chords.length > 1 && matchChord(event, chords[0]!, platform) && eligible(entry)) starts.push({ entry, chords });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (starts.length) {
|
|
355
|
+
this.pending = { candidates: starts, step: 1, at: this.now() };
|
|
356
|
+
consume(event);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private run(entry: RegisteredShortcut, event: KeyboardEvent): boolean {
|
|
361
|
+
try {
|
|
362
|
+
return entry.def.run(event) !== false;
|
|
363
|
+
} catch (error) {
|
|
364
|
+
reportError(error);
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** True when Escape is fully handled here (nothing else should see it as a shortcut). */
|
|
370
|
+
private handleEscape(event: KeyboardEvent, target: Element | null, doc: Document, repeat: boolean): boolean {
|
|
371
|
+
this.pending = null;
|
|
372
|
+
// A field owns Escape, handled or not; inside a toolkit Popover or Menu, Escape in a field closes the layer.
|
|
373
|
+
if (fieldOwnsEscape(target)) return true;
|
|
374
|
+
const foreign = foreignFloatingLayers(doc, event);
|
|
375
|
+
if (repeat) {
|
|
376
|
+
// A held Escape must not peel off one layer after another.
|
|
377
|
+
if (foreign.length || this.layers.length) consume(event, true);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
if (event.defaultPrevented) return true;
|
|
381
|
+
// Radix and native dialogs close themselves.
|
|
382
|
+
if (foreign.length) return true;
|
|
383
|
+
if (!this.layers.length && !this.blockers.length) return false;
|
|
384
|
+
const handled = this.runEscapeChain(event);
|
|
385
|
+
if (handled) consume(event);
|
|
386
|
+
return handled;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function reportError(error: unknown): void {
|
|
391
|
+
if (typeof console !== 'undefined') console.error('[notis keys] A shortcut failed.', error);
|
|
392
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM rules of the keyboard engine: typing targets, modal isolation and floating layers, across open shadow roots
|
|
3
|
+
* (Spaces render in a Shadow DOM inside the Portal).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Inputs that take no text: keys pass through them. */
|
|
7
|
+
const NON_TEXT_INPUTS = new Set(['button', 'checkbox', 'color', 'file', 'hidden', 'image', 'radio', 'reset', 'submit']);
|
|
8
|
+
|
|
9
|
+
function isElement(value: unknown): value is Element {
|
|
10
|
+
return !!value && typeof (value as Element).matches === 'function';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The real target, inside shadow roots. */
|
|
14
|
+
export function eventTarget(event: Event): Element | null {
|
|
15
|
+
const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
|
|
16
|
+
const first = path.find(isElement);
|
|
17
|
+
if (first) return first;
|
|
18
|
+
return isElement(event.target) ? event.target : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** `closest` that crosses shadow boundaries. */
|
|
22
|
+
export function closestComposed(element: Element | null, selector: string): Element | null {
|
|
23
|
+
let current: Element | null = element;
|
|
24
|
+
while (current) {
|
|
25
|
+
const found = current.closest(selector);
|
|
26
|
+
if (found) return found;
|
|
27
|
+
const root = current.getRootNode();
|
|
28
|
+
current = 'host' in root ? (root as ShadowRoot).host : null;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** `contains` across shadow boundaries: `element` is `container` or sits inside it, maybe in a nested shadow root. */
|
|
34
|
+
export function containsComposed(container: Element | null, element: Element | null): boolean {
|
|
35
|
+
let current: Element | null = element;
|
|
36
|
+
while (current && container) {
|
|
37
|
+
if (container.contains(current)) return true;
|
|
38
|
+
const root = current.getRootNode();
|
|
39
|
+
current = 'host' in root ? (root as ShadowRoot).host : null;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A text field owns its keys (and its Escape). */
|
|
45
|
+
export function isTextEntry(element: Element | null): boolean {
|
|
46
|
+
if (!element) return false;
|
|
47
|
+
const tag = element.tagName;
|
|
48
|
+
if (tag === 'TEXTAREA' || tag === 'SELECT') return true;
|
|
49
|
+
if (tag === 'INPUT') return !NON_TEXT_INPUTS.has(((element as HTMLInputElement).type || 'text').toLowerCase());
|
|
50
|
+
if ((element as HTMLElement).isContentEditable) return true;
|
|
51
|
+
return closestComposed(element,
|
|
52
|
+
'[contenteditable=""], [contenteditable="true"], [contenteditable="plaintext-only"], [role="textbox"], [role="combobox"], [role="searchbox"]') !== null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A text field owns its Escape, except inside a toolkit floating layer (Popover, Menu mark `data-notis-floating`):
|
|
57
|
+
* there Escape closes the layer, unless the field has its own open list (`aria-expanded="true"`).
|
|
58
|
+
*/
|
|
59
|
+
export function fieldOwnsEscape(element: Element | null): boolean {
|
|
60
|
+
if (!element || !isTextEntry(element)) return false;
|
|
61
|
+
if (!closestComposed(element, '[data-notis-floating]')) return true;
|
|
62
|
+
return element.getAttribute('aria-expanded') === 'true';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Inside an open menu or listbox: its own keys move through it. */
|
|
66
|
+
export function isInsideOpenMenu(element: Element | null): boolean {
|
|
67
|
+
if (!element) return false;
|
|
68
|
+
const menu = closestComposed(element, '[role="menu"], [role="listbox"], [role="menubar"]');
|
|
69
|
+
// A listbox the engine itself drives (ListReader, useListKeys) keeps J/K/E alive while a row has focus.
|
|
70
|
+
if (!menu || menu.hasAttribute('data-notis-engine-list')) return false;
|
|
71
|
+
const state = menu.getAttribute('data-state');
|
|
72
|
+
return state !== 'closed';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Typing rule (toolkit spec 2.1). Buttons and links do not block. */
|
|
76
|
+
export function blocksShortcuts(element: Element | null, options: { inFields: boolean; modal: boolean }): boolean {
|
|
77
|
+
if (options.inFields) return false;
|
|
78
|
+
if (isTextEntry(element)) return true;
|
|
79
|
+
if (!options.modal && isInsideOpenMenu(element)) return true;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Enter and Space on a control press the control, not a shortcut. */
|
|
84
|
+
export function isActivatable(element: Element | null): boolean {
|
|
85
|
+
return closestComposed(element,
|
|
86
|
+
'button, a[href], [role="button"], [role="switch"], [role="checkbox"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], summary, input[type="checkbox"], input[type="radio"]') !== null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Arrows inside these move the control itself. */
|
|
90
|
+
export function ownsArrows(element: Element | null): boolean {
|
|
91
|
+
return closestComposed(element,
|
|
92
|
+
'input[type="range"], [role="slider"], [role="tablist"], [role="radiogroup"], [role="spinbutton"], [role="separator"][tabindex], [role="tree"], [role="grid"]') !== null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Elements opt out of every engine key with `data-notis-keys="off"`. */
|
|
96
|
+
export function keysOff(element: Element | null): boolean {
|
|
97
|
+
return closestComposed(element, '[data-notis-keys="off"]') !== null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Visible without layout: no hidden, inert, aria-hidden, display none or visibility hidden up the composed tree. */
|
|
101
|
+
export function isVisibleComposed(element: Element | null): boolean {
|
|
102
|
+
if (!element?.isConnected) return false;
|
|
103
|
+
let current: Element | null = element;
|
|
104
|
+
while (current) {
|
|
105
|
+
if (current.hasAttribute('hidden') || current.hasAttribute('inert') || current.getAttribute('aria-hidden') === 'true') return false;
|
|
106
|
+
const style = current.ownerDocument.defaultView?.getComputedStyle(current);
|
|
107
|
+
if (style?.display === 'none' || style?.visibility === 'hidden') return false;
|
|
108
|
+
const root: Node = current.getRootNode();
|
|
109
|
+
current = current.parentElement ?? ('host' in root ? (root as ShadowRoot).host : null);
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The document and every open shadow root inside it, recursively. */
|
|
115
|
+
export function collectRoots(doc: Document): Array<Document | ShadowRoot> {
|
|
116
|
+
const roots: Array<Document | ShadowRoot> = [doc];
|
|
117
|
+
const visit = (root: Document | ShadowRoot) => {
|
|
118
|
+
const walker = doc.createTreeWalker(root, 1 /* NodeFilter.SHOW_ELEMENT */);
|
|
119
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
120
|
+
const shadow = (node as Element).shadowRoot;
|
|
121
|
+
if (shadow) { roots.push(shadow); visit(shadow); }
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
visit(doc);
|
|
125
|
+
return roots;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function queryAllComposed(doc: Document, selector: string, event?: Event): Element[] {
|
|
129
|
+
const found = new Set<Element>();
|
|
130
|
+
for (const root of collectRoots(doc)) for (const el of root.querySelectorAll(selector)) found.add(el);
|
|
131
|
+
if (event && typeof event.composedPath === 'function') {
|
|
132
|
+
for (const target of event.composedPath()) if (isElement(target) && target.matches(selector)) found.add(target);
|
|
133
|
+
}
|
|
134
|
+
return [...found];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Open dialogs and menus that isolate keys to the modal scope. `data-notis-nonmodal` opts a popover out. */
|
|
138
|
+
const MODAL_LAYER = [
|
|
139
|
+
'[role="dialog"][aria-modal="true"]',
|
|
140
|
+
'[role="dialog"][data-state="open"]',
|
|
141
|
+
'[role="alertdialog"]:not([data-state="closed"])',
|
|
142
|
+
'dialog.notis-dialog[open]',
|
|
143
|
+
'dialog[open][aria-modal="true"]',
|
|
144
|
+
'[role="menu"][data-state="open"]',
|
|
145
|
+
'[role="listbox"][data-state="open"]',
|
|
146
|
+
].join(', ');
|
|
147
|
+
|
|
148
|
+
export function openModalLayers(doc: Document, event?: Event): Element[] {
|
|
149
|
+
return queryAllComposed(doc, MODAL_LAYER, event)
|
|
150
|
+
.filter((el) => !el.closest('[data-notis-nonmodal]') && isVisibleComposed(el));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function hasOpenModal(doc: Document, event?: Event): boolean {
|
|
154
|
+
return openModalLayers(doc, event).length > 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The open modal layer `element` sits in (a dialog, menu or listbox), across shadow roots; null on the page. */
|
|
158
|
+
export function modalLayerOf(element: Element | null): Element | null {
|
|
159
|
+
let current: Element | null = element;
|
|
160
|
+
while (current) {
|
|
161
|
+
const layer = closestComposed(current, MODAL_LAYER);
|
|
162
|
+
if (!layer) return null;
|
|
163
|
+
if (!layer.closest('[data-notis-nonmodal]')) return layer;
|
|
164
|
+
// A non-modal popover leaves keys alone: look above it.
|
|
165
|
+
const root = layer.getRootNode();
|
|
166
|
+
current = layer.parentElement ?? ('host' in root ? (root as ShadowRoot).host : null);
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Floating layers that handle Escape themselves (Radix, native dialogs). Toolkit layers mark `data-notis-escape-owned`.
|
|
173
|
+
* An open Radix Select list counts too: it hides the dialog under it from assistive tech, so without it the dialog
|
|
174
|
+
* would look closed and Escape would also close the reader.
|
|
175
|
+
*/
|
|
176
|
+
const FLOATING_LAYER = 'dialog[open], [role="dialog"]:not([data-state="closed"]), '
|
|
177
|
+
+ '[role="alertdialog"]:not([data-state="closed"]), [role="menu"]:not([data-state="closed"]), '
|
|
178
|
+
+ '[role="listbox"][data-state="open"]';
|
|
179
|
+
|
|
180
|
+
export function foreignFloatingLayers(doc: Document, event?: Event): Element[] {
|
|
181
|
+
return queryAllComposed(doc, FLOATING_LAYER, event)
|
|
182
|
+
.filter((el) => !el.closest('[data-notis-escape-owned]') && isVisibleComposed(el));
|
|
183
|
+
}
|