@notis_ai/cli 0.2.18 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +42325 -18273
- 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 -1570
- 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 -3011
- 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,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hooks of the keyboard engine (toolkit spec 2.1). Every hook works without a mounted `<KeyboardEngine>`
|
|
3
|
+
* (unit tests, a bare preview): registrations land in the shared core, which listens only once an engine mounts or
|
|
4
|
+
* `core.attach(document)` is called, and settings fall back to the document language and the browser platform.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useCallback, useContext, useEffect, useLayoutEffect, useRef, useSyncExternalStore } from 'react';
|
|
8
|
+
import { keyList, type Locale, type Platform } from '../../words';
|
|
9
|
+
import { ESCAPE_PRIORITY, detectLocale, detectPlatform, type KeyboardEngineCore } from './core';
|
|
10
|
+
import { getSharedInstance } from './instance';
|
|
11
|
+
import type { EngineSettings, OwnerKind, ScopeHandle, ShortcutDef } from './types';
|
|
12
|
+
|
|
13
|
+
const useIsoLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
14
|
+
|
|
15
|
+
/** The engine core in effect here: an injected one, else the shared one. */
|
|
16
|
+
export function useEngineCore(): KeyboardEngineCore {
|
|
17
|
+
const shared = getSharedInstance();
|
|
18
|
+
return useContext(shared.EngineContext) ?? shared.core;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The owner shortcuts register under: the nearest `<ShortcutScope>`, else 'notis'. */
|
|
22
|
+
export function useShortcutOwner(): string {
|
|
23
|
+
return useContext(getSharedInstance().OwnerContext);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function useSetting<V>(read: (core: KeyboardEngineCore) => V): V {
|
|
27
|
+
const core = useEngineCore();
|
|
28
|
+
const getSnapshot = () => read(core);
|
|
29
|
+
return useSyncExternalStore(useCallback((listener) => core.subscribe(listener), [core]), getSnapshot, getSnapshot);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Toolkit language. Without a mounted engine: the document's `lang` ('fr...' gives 'fr', else 'en'). */
|
|
33
|
+
export function useToolkitLocale(): Locale {
|
|
34
|
+
return useSetting((core) => (core.mounted > 0 ? core.settings.locale : detectLocale()));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 'mac' or 'other'. Without a mounted engine: detected from the browser. */
|
|
38
|
+
export function useToolkitPlatform(): Platform {
|
|
39
|
+
return useSetting((core) => (core.mounted > 0 ? core.settings.platform : detectPlatform()));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Host, locale, platform and strict mode. */
|
|
43
|
+
export function useToolkitSettings(): Readonly<EngineSettings> {
|
|
44
|
+
const locale = useToolkitLocale();
|
|
45
|
+
const platform = useToolkitPlatform();
|
|
46
|
+
const core = useEngineCore();
|
|
47
|
+
const host = useSetting((c) => c.settings.host);
|
|
48
|
+
const strict = useSetting((c) => c.settings.strict);
|
|
49
|
+
return { host, locale, platform, strict, onClash: core.settings.onClash };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---- shortcuts ----
|
|
53
|
+
|
|
54
|
+
function defKey(def: ShortcutDef, owner: string): string {
|
|
55
|
+
return JSON.stringify([
|
|
56
|
+
def.id, keyList(def.keys), def.scope, def.owner ?? owner, def.group ?? '', !!def.hidden, !!def.repeat,
|
|
57
|
+
!!def.allowInFields, def.boost ?? 0, def.label.en, def.label.fr,
|
|
58
|
+
]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A def whose `run` and `when` always read the latest render's def, so handlers never go stale. */
|
|
62
|
+
function liveDef(ref: { readonly current: ShortcutDef }): ShortcutDef {
|
|
63
|
+
const def = ref.current;
|
|
64
|
+
return {
|
|
65
|
+
...def,
|
|
66
|
+
when: () => { const when = ref.current.when; return when ? when() : true; },
|
|
67
|
+
run: (event) => ref.current.run(event),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Registers one shortcut while mounted. Re-registers only when its id, keys, scope, owner, words or flags change
|
|
73
|
+
* (or `deps` change); `run` and `when` are read live.
|
|
74
|
+
*/
|
|
75
|
+
export function useShortcut(def: ShortcutDef, deps: readonly unknown[] = []): void {
|
|
76
|
+
const core = useEngineCore();
|
|
77
|
+
const owner = useShortcutOwner();
|
|
78
|
+
const ref = useRef(def);
|
|
79
|
+
ref.current = def;
|
|
80
|
+
const key = defKey(def, owner);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const unregister = core.register(liveDef(ref), ref.current.owner ?? owner);
|
|
83
|
+
return () => { unregister?.(); };
|
|
84
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
85
|
+
}, [core, owner, key, ...deps]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Registers several shortcuts while mounted (same rules as `useShortcut`). */
|
|
89
|
+
export function useShortcuts(defs: readonly ShortcutDef[], deps: readonly unknown[] = []): void {
|
|
90
|
+
const core = useEngineCore();
|
|
91
|
+
const owner = useShortcutOwner();
|
|
92
|
+
const ref = useRef(defs);
|
|
93
|
+
ref.current = defs;
|
|
94
|
+
const key = defs.map((def) => defKey(def, owner)).join('\n');
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const unregisters = ref.current.map((def, index) => {
|
|
97
|
+
const live = { get current() { return ref.current[index] ?? def; } };
|
|
98
|
+
return core.register(liveDef(live), def.owner ?? owner);
|
|
99
|
+
});
|
|
100
|
+
return () => { for (const unregister of unregisters) unregister?.(); };
|
|
101
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
102
|
+
}, [core, owner, key, ...deps]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- owners ----
|
|
106
|
+
|
|
107
|
+
export interface ShortcutScopeOptions {
|
|
108
|
+
/** False for a hidden retained view or a Space in the background: all its shortcuts go silent. */
|
|
109
|
+
available: boolean;
|
|
110
|
+
/** 'space' (default under the root) for Space code, 'host' for Notis pages. Inherited from the parent owner. */
|
|
111
|
+
kind?: OwnerKind;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Declares a Space or a retained view as an owner, nested under the current owner. Its shortcuts are those
|
|
116
|
+
* registered under a `<ShortcutScope owner>` with the same id (or with `def.owner`).
|
|
117
|
+
*/
|
|
118
|
+
export function useShortcutScope(owner: string, options: ShortcutScopeOptions): ScopeHandle {
|
|
119
|
+
const core = useEngineCore();
|
|
120
|
+
const parent = useShortcutOwner();
|
|
121
|
+
const { available, kind } = options;
|
|
122
|
+
const availableRef = useRef(available);
|
|
123
|
+
availableRef.current = available;
|
|
124
|
+
// Layout effect: owners exist before their children's shortcuts register (passive effects).
|
|
125
|
+
useIsoLayoutEffect(
|
|
126
|
+
() => core.defineOwner(owner, { parent, kind, available: availableRef.current }),
|
|
127
|
+
[core, owner, parent, kind],
|
|
128
|
+
);
|
|
129
|
+
useIsoLayoutEffect(() => { core.setOwnerAvailable(owner, available); }, [core, owner, available]);
|
|
130
|
+
return { owner, available, isAvailable: () => core.isOwnerAvailable(owner) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---- escape ----
|
|
134
|
+
|
|
135
|
+
export interface EscapeLayerOptions {
|
|
136
|
+
/** Chain position (ESCAPE_PRIORITY). Default overlay. */
|
|
137
|
+
priority?: number;
|
|
138
|
+
/** False keeps the hook mounted without a layer. Default true. */
|
|
139
|
+
enabled?: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Adds a step to the Escape chain while mounted. `onEscape` returns true when it handled the key (the chain
|
|
144
|
+
* stops), false to let the next step run. Higher priority runs first; the latest wins within a priority. It gets the
|
|
145
|
+
* keydown when a key press runs the chain (none from `runEscapeChain()` without an event).
|
|
146
|
+
*/
|
|
147
|
+
export function useEscapeLayer(id: string, onEscape: (event?: KeyboardEvent) => boolean,
|
|
148
|
+
options: EscapeLayerOptions = {}): void {
|
|
149
|
+
const core = useEngineCore();
|
|
150
|
+
const owner = useShortcutOwner();
|
|
151
|
+
const ref = useRef(onEscape);
|
|
152
|
+
ref.current = onEscape;
|
|
153
|
+
const priority = options.priority ?? ESCAPE_PRIORITY.overlay;
|
|
154
|
+
const enabled = options.enabled ?? true;
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (!enabled) return undefined;
|
|
157
|
+
return core.addEscapeLayer(id, (event) => core.isOwnerAvailable(owner) && ref.current(event), priority);
|
|
158
|
+
}, [core, owner, id, priority, enabled]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Stops the Escape chain below `priority` while `active` (the composer holds text). Default priority sits between
|
|
163
|
+
* the detail panel and selection clear, so Escape still clears a selection but never leaves the page.
|
|
164
|
+
*/
|
|
165
|
+
export function useEscapeBlocker(active: boolean, options: { priority?: number } = {}): void {
|
|
166
|
+
const core = useEngineCore();
|
|
167
|
+
const owner = useShortcutOwner();
|
|
168
|
+
const priority = options.priority ?? ESCAPE_PRIORITY.detail + 50;
|
|
169
|
+
useEffect(() => (active ? core.addEscapeBlocker(priority, () => core.isOwnerAvailable(owner)) : undefined), [core, owner, active, priority]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---- help sheet ----
|
|
173
|
+
|
|
174
|
+
export interface HelpSheetControls {
|
|
175
|
+
open(): void;
|
|
176
|
+
close(): void;
|
|
177
|
+
toggle(): void;
|
|
178
|
+
isOpen: boolean;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function useHelpSheet(): HelpSheetControls {
|
|
182
|
+
const core = useEngineCore();
|
|
183
|
+
const isOpen = useSetting((c) => c.isHelpOpen());
|
|
184
|
+
return {
|
|
185
|
+
open: useCallback(() => core.openHelp(), [core]),
|
|
186
|
+
close: useCallback(() => core.closeHelp(), [core]),
|
|
187
|
+
toggle: useCallback(() => core.toggleHelp(), [core]),
|
|
188
|
+
isOpen,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Re-renders on every registry change (the help sheet, dev overlays). */
|
|
193
|
+
export function useRegistryVersion(): number {
|
|
194
|
+
return useSetting((core) => core.getVersion());
|
|
195
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One engine per page, even when two copies of the toolkit load (a Space bundle that inlined `@notis/sdk/ui` by
|
|
3
|
+
* mistake). The first copy stores its core and React contexts on `globalThis` under a registered symbol; a second
|
|
4
|
+
* copy logs once and reuses them, so its shortcuts land in the same registry and its hooks read the same contexts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createContext, type Context } from 'react';
|
|
8
|
+
import { KeyboardEngineCore } from './core';
|
|
9
|
+
import { ROOT_OWNER } from './registry';
|
|
10
|
+
import type { KeycapOn } from './keycapContext';
|
|
11
|
+
|
|
12
|
+
export interface SharedInstance {
|
|
13
|
+
readonly version: 1;
|
|
14
|
+
readonly core: KeyboardEngineCore;
|
|
15
|
+
/** An injected core (tests, preview) overrides the shared one below it. */
|
|
16
|
+
readonly EngineContext: Context<KeyboardEngineCore | null>;
|
|
17
|
+
/** The nearest `<ShortcutScope>` owner. */
|
|
18
|
+
readonly OwnerContext: Context<string>;
|
|
19
|
+
/** Keycap `on` default for a subtree (SelectionBar: pill, Menu and HelpSheet: discreet). */
|
|
20
|
+
readonly KeycapContext: Context<KeycapOn | null>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const KEY = Symbol.for('notis.sdk.ui.instance');
|
|
24
|
+
|
|
25
|
+
type Slot = { [KEY]?: SharedInstance };
|
|
26
|
+
|
|
27
|
+
function create(): SharedInstance {
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
version: 1 as const,
|
|
30
|
+
core: new KeyboardEngineCore(),
|
|
31
|
+
EngineContext: createContext<KeyboardEngineCore | null>(null),
|
|
32
|
+
OwnerContext: createContext<string>(ROOT_OWNER),
|
|
33
|
+
KeycapContext: createContext<KeycapOn | null>(null),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let local: SharedInstance | null = null;
|
|
38
|
+
|
|
39
|
+
export function getSharedInstance(): SharedInstance {
|
|
40
|
+
if (local) return local;
|
|
41
|
+
const slot = globalThis as Slot;
|
|
42
|
+
const existing = slot[KEY];
|
|
43
|
+
if (existing && existing.version === 1) {
|
|
44
|
+
if (typeof console !== 'undefined') {
|
|
45
|
+
console.warn('[notis keys] A second copy of the Notis toolkit loaded. It shares the first keyboard engine.');
|
|
46
|
+
}
|
|
47
|
+
local = existing;
|
|
48
|
+
return local;
|
|
49
|
+
}
|
|
50
|
+
local = create();
|
|
51
|
+
try {
|
|
52
|
+
Object.defineProperty(slot, KEY, { value: local, configurable: true, enumerable: false, writable: false });
|
|
53
|
+
} catch {
|
|
54
|
+
// A frozen global: this copy keeps its own instance.
|
|
55
|
+
}
|
|
56
|
+
return local;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Tests only: forget this module's cached instance (the global slot stays). */
|
|
60
|
+
export function __resetSharedInstanceForTests(): void {
|
|
61
|
+
local = null;
|
|
62
|
+
try { delete (globalThis as Slot)[KEY]; } catch { /* non-configurable */ }
|
|
63
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shortcut registry: owners, availability, clash checks and dispatch order. Pure (no DOM, no React), so it runs
|
|
3
|
+
* under node in tests and in the design check.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
SCOPE_ORDER,
|
|
8
|
+
keyList,
|
|
9
|
+
pairClashes,
|
|
10
|
+
parseKeySpec,
|
|
11
|
+
selfClashes,
|
|
12
|
+
type ChordSequence,
|
|
13
|
+
type Clash,
|
|
14
|
+
type ClashCandidate,
|
|
15
|
+
type KeySpec,
|
|
16
|
+
type Locale,
|
|
17
|
+
} from '../../words';
|
|
18
|
+
import type { HelpEntry, OwnerKind, ShortcutDef, ShortcutGroup } from './types';
|
|
19
|
+
|
|
20
|
+
export const ROOT_OWNER = 'notis';
|
|
21
|
+
|
|
22
|
+
/** Thrown in dev and by the design check when two defs clash or a Space takes a host key. */
|
|
23
|
+
export class NotisKeyClash extends Error {
|
|
24
|
+
readonly clash: Clash;
|
|
25
|
+
constructor(clash: Clash) {
|
|
26
|
+
super(clash.message.startsWith('NotisKeyClash') ? clash.message : `NotisKeyClash: ${clash.message}`);
|
|
27
|
+
this.name = 'NotisKeyClash';
|
|
28
|
+
this.clash = clash;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RegisteredShortcut {
|
|
33
|
+
def: ShortcutDef;
|
|
34
|
+
owner: string;
|
|
35
|
+
ownerKind: OwnerKind;
|
|
36
|
+
/** Registration order; later wins within a scope. */
|
|
37
|
+
seq: number;
|
|
38
|
+
/** Scope order plus boost. */
|
|
39
|
+
rank: number;
|
|
40
|
+
/** Parsed alternatives. */
|
|
41
|
+
chords: ChordSequence[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface OwnerRecord {
|
|
45
|
+
id: string;
|
|
46
|
+
kind: OwnerKind;
|
|
47
|
+
parent: string;
|
|
48
|
+
available: boolean;
|
|
49
|
+
refs: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RegistryOptions {
|
|
53
|
+
strict: boolean;
|
|
54
|
+
onClash?: (message: string) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DefineOwnerOptions {
|
|
58
|
+
parent?: string;
|
|
59
|
+
kind?: OwnerKind;
|
|
60
|
+
available?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const MAX_BOOST = 99;
|
|
64
|
+
|
|
65
|
+
export class ShortcutRegistry {
|
|
66
|
+
private entries: RegisteredShortcut[] = [];
|
|
67
|
+
private owners = new Map<string, OwnerRecord>();
|
|
68
|
+
private seq = 0;
|
|
69
|
+
private version = 0;
|
|
70
|
+
private listeners = new Set<() => void>();
|
|
71
|
+
private warned = new Set<string>();
|
|
72
|
+
options: RegistryOptions;
|
|
73
|
+
|
|
74
|
+
constructor(options: Partial<RegistryOptions> = {}) {
|
|
75
|
+
this.options = { strict: options.strict ?? true, onClash: options.onClash };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---- change notification (useSyncExternalStore) ----
|
|
79
|
+
|
|
80
|
+
subscribe(listener: () => void): () => void {
|
|
81
|
+
this.listeners.add(listener);
|
|
82
|
+
return () => { this.listeners.delete(listener); };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getVersion(): number {
|
|
86
|
+
return this.version;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
notify(): void {
|
|
90
|
+
this.version += 1;
|
|
91
|
+
for (const listener of [...this.listeners]) listener();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---- owners ----
|
|
95
|
+
|
|
96
|
+
/** Declares a Space or a retained view. Ref counted: the last release removes it. */
|
|
97
|
+
defineOwner(id: string, options: DefineOwnerOptions = {}): () => void {
|
|
98
|
+
if (id === ROOT_OWNER) return () => {};
|
|
99
|
+
const parent = options.parent ?? ROOT_OWNER;
|
|
100
|
+
const existing = this.owners.get(id);
|
|
101
|
+
if (existing) {
|
|
102
|
+
existing.refs += 1;
|
|
103
|
+
existing.parent = parent;
|
|
104
|
+
if (options.kind) existing.kind = options.kind;
|
|
105
|
+
if (options.available !== undefined && existing.available !== options.available) {
|
|
106
|
+
existing.available = options.available;
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
this.owners.set(id, {
|
|
110
|
+
id,
|
|
111
|
+
parent,
|
|
112
|
+
kind: options.kind ?? this.inheritedKind(parent),
|
|
113
|
+
available: options.available ?? true,
|
|
114
|
+
refs: 1,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
this.notify();
|
|
118
|
+
let released = false;
|
|
119
|
+
return () => {
|
|
120
|
+
if (released) return;
|
|
121
|
+
released = true;
|
|
122
|
+
const record = this.owners.get(id);
|
|
123
|
+
if (!record) return;
|
|
124
|
+
record.refs -= 1;
|
|
125
|
+
if (record.refs <= 0) this.owners.delete(id);
|
|
126
|
+
this.notify();
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setOwnerAvailable(id: string, available: boolean): void {
|
|
131
|
+
const record = this.owners.get(id);
|
|
132
|
+
if (!record || record.available === available) return;
|
|
133
|
+
record.available = available;
|
|
134
|
+
this.notify();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private inheritedKind(parent: string): OwnerKind {
|
|
138
|
+
if (parent === ROOT_OWNER) return 'space';
|
|
139
|
+
return this.owners.get(parent)?.kind ?? 'space';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ownerKind(id: string): OwnerKind {
|
|
143
|
+
if (id === ROOT_OWNER) return 'host';
|
|
144
|
+
// An owner not declared yet (its scope mounts after the def) counts as a Space.
|
|
145
|
+
return this.owners.get(id)?.kind ?? 'space';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Available when it and every owner above it are available. Unknown owners count as available. */
|
|
149
|
+
isOwnerAvailable(id: string): boolean {
|
|
150
|
+
let current: string | undefined = id;
|
|
151
|
+
const seen = new Set<string>();
|
|
152
|
+
while (current && current !== ROOT_OWNER && !seen.has(current)) {
|
|
153
|
+
seen.add(current);
|
|
154
|
+
const record = this.owners.get(current);
|
|
155
|
+
if (!record) return true;
|
|
156
|
+
if (!record.available) return false;
|
|
157
|
+
current = record.parent;
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---- shortcuts ----
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Registers a def. Returns an unregister function, or null when the def was dropped (production clash).
|
|
166
|
+
* Strict mode throws NotisKeyClash instead of dropping.
|
|
167
|
+
*/
|
|
168
|
+
register(def: ShortcutDef, owner: string = def.owner ?? ROOT_OWNER): (() => void) | null {
|
|
169
|
+
const ownerKind = this.ownerKind(owner);
|
|
170
|
+
const candidate = this.candidate(def, owner, ownerKind);
|
|
171
|
+
const errors = selfClashes(candidate).filter((clash) => clash.severity === 'error');
|
|
172
|
+
if (errors.length) {
|
|
173
|
+
this.reject(errors[0]!);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
if (this.isOwnerAvailable(owner)) {
|
|
177
|
+
for (const entry of this.entries) {
|
|
178
|
+
if (entry.owner !== owner || entry.def.scope !== def.scope) continue;
|
|
179
|
+
if (!this.isOwnerAvailable(entry.owner)) continue;
|
|
180
|
+
const duplicate = pairClashes(this.candidate(entry.def, entry.owner, entry.ownerKind), candidate)
|
|
181
|
+
.find((clash) => clash.kind === 'duplicate');
|
|
182
|
+
if (duplicate) {
|
|
183
|
+
this.reject(duplicate);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const boost = Math.max(0, Math.min(MAX_BOOST, def.boost ?? 0));
|
|
189
|
+
const entry: RegisteredShortcut = {
|
|
190
|
+
def,
|
|
191
|
+
owner,
|
|
192
|
+
ownerKind,
|
|
193
|
+
seq: ++this.seq,
|
|
194
|
+
rank: SCOPE_ORDER[def.scope] + boost,
|
|
195
|
+
chords: keyList(def.keys).map((spec) => parseKeySpec(spec)),
|
|
196
|
+
};
|
|
197
|
+
this.entries.push(entry);
|
|
198
|
+
this.notify();
|
|
199
|
+
let removed = false;
|
|
200
|
+
return () => {
|
|
201
|
+
if (removed) return;
|
|
202
|
+
removed = true;
|
|
203
|
+
const index = this.entries.indexOf(entry);
|
|
204
|
+
if (index !== -1) this.entries.splice(index, 1);
|
|
205
|
+
this.notify();
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private candidate(def: ShortcutDef, owner: string, ownerKind: OwnerKind): ClashCandidate {
|
|
210
|
+
return { id: def.id, keys: def.keys, scope: def.scope, owner, ownerKind, available: true };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private reject(clash: Clash): void {
|
|
214
|
+
if (this.options.strict) throw new NotisKeyClash(clash);
|
|
215
|
+
if (this.warned.has(clash.message)) return;
|
|
216
|
+
this.warned.add(clash.message);
|
|
217
|
+
const log = this.options.onClash ?? ((message: string) => {
|
|
218
|
+
if (typeof console !== 'undefined') console.warn(`[notis keys] ${message} The later shortcut was ignored.`);
|
|
219
|
+
});
|
|
220
|
+
log(clash.message);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** All registered entries, registration order. */
|
|
224
|
+
all(): readonly RegisteredShortcut[] {
|
|
225
|
+
return this.entries;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Entries that may answer a key now: owner available, `when` passing, highest rank first, latest first within a
|
|
230
|
+
* rank. `modalOnly` keeps only the modal scope (an open dialog or menu).
|
|
231
|
+
*/
|
|
232
|
+
active(options: { modalOnly?: boolean } = {}): RegisteredShortcut[] {
|
|
233
|
+
return this.entries
|
|
234
|
+
.filter((entry) => (!options.modalOnly || entry.def.scope === 'modal') && this.isOwnerAvailable(entry.owner))
|
|
235
|
+
.sort((a, b) => b.rank - a.rank || b.seq - a.seq);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Help sheet rows: available, not hidden, `when` passing, latest def per id. Space group first, then Notis. */
|
|
239
|
+
helpEntries(): HelpEntry[] {
|
|
240
|
+
const byId = new Map<string, HelpEntry>();
|
|
241
|
+
const ordered = [...this.entries].sort((a, b) => a.seq - b.seq);
|
|
242
|
+
for (const entry of ordered) {
|
|
243
|
+
const { def } = entry;
|
|
244
|
+
if (def.hidden || def.scope === 'modal' || !this.isOwnerAvailable(entry.owner)) continue;
|
|
245
|
+
if (def.when && !safeWhen(def.when)) continue;
|
|
246
|
+
const group: ShortcutGroup = def.group ?? (entry.ownerKind === 'space' ? 'space' : 'notis');
|
|
247
|
+
byId.delete(def.id);
|
|
248
|
+
byId.set(def.id, { id: def.id, keys: keyList(def.keys), label: def.label, group, scope: def.scope, owner: entry.owner });
|
|
249
|
+
}
|
|
250
|
+
const rows = [...byId.values()];
|
|
251
|
+
return [...rows.filter((row) => row.group === 'space'), ...rows.filter((row) => row.group === 'notis')];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Labels for the help sheet in one language, for tests and text hosts. */
|
|
255
|
+
helpText(locale: Locale): Array<{ group: ShortcutGroup; label: string; keys: KeySpec[] }> {
|
|
256
|
+
return this.helpEntries().map((row) => ({ group: row.group, label: row.label[locale], keys: row.keys }));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function safeWhen(when: () => boolean): boolean {
|
|
261
|
+
try { return when() !== false; } catch { return false; }
|
|
262
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { KeySpec, Locale, Platform, Scope, Words } from '../../words';
|
|
2
|
+
|
|
3
|
+
export type { KeySpec, Locale, Platform, Scope, Words };
|
|
4
|
+
|
|
5
|
+
export type ShortcutGroup = 'space' | 'notis';
|
|
6
|
+
|
|
7
|
+
export interface ShortcutDef {
|
|
8
|
+
/** Stable id: 'list.next', 'space:tickets.triage'. */
|
|
9
|
+
id: string;
|
|
10
|
+
/** One spec or alternatives. */
|
|
11
|
+
keys: KeySpec | KeySpec[];
|
|
12
|
+
scope: Scope;
|
|
13
|
+
label: Words;
|
|
14
|
+
/** Help sheet group. Default 'space' under a Space owner, 'notis' otherwise. */
|
|
15
|
+
group?: ShortcutGroup;
|
|
16
|
+
/** Extra gate, evaluated at dispatch (and when the help sheet opens). */
|
|
17
|
+
when?: () => boolean;
|
|
18
|
+
/** Fire on key repeat (a held key). Default false. */
|
|
19
|
+
repeat?: boolean;
|
|
20
|
+
/** Fire while focus is in a text field. Default false. */
|
|
21
|
+
allowInFields?: boolean;
|
|
22
|
+
/** Not listed in the help sheet. */
|
|
23
|
+
hidden?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Added to the scope order (0 to 99). The selection bar registers its actions at `collection` with
|
|
26
|
+
* `boost: 25` while something is selected, so they win over row keys.
|
|
27
|
+
*/
|
|
28
|
+
boost?: number;
|
|
29
|
+
/** Owner override. Default: the nearest `<ShortcutScope>` (or `useShortcutScope`) above, else 'notis'. */
|
|
30
|
+
owner?: string;
|
|
31
|
+
/** Return false when not handled: the engine keeps looking. */
|
|
32
|
+
run: (event: KeyboardEvent) => void | boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type OwnerKind = 'space' | 'host';
|
|
36
|
+
|
|
37
|
+
export interface ScopeHandle {
|
|
38
|
+
owner: string;
|
|
39
|
+
available: boolean;
|
|
40
|
+
isAvailable(): boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type EngineHost = 'portal' | 'preview';
|
|
44
|
+
|
|
45
|
+
export interface EngineSettings {
|
|
46
|
+
host: EngineHost;
|
|
47
|
+
locale: Locale;
|
|
48
|
+
platform: Platform;
|
|
49
|
+
/** Dev and design check: clashes throw. Production: the later def is dropped and logged once. */
|
|
50
|
+
strict: boolean;
|
|
51
|
+
/** Production clash log (telemetry). Default console.warn, once per message. */
|
|
52
|
+
onClash?: (message: string) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface HelpEntry {
|
|
56
|
+
id: string;
|
|
57
|
+
keys: KeySpec[];
|
|
58
|
+
label: Words;
|
|
59
|
+
group: ShortcutGroup;
|
|
60
|
+
scope: Scope;
|
|
61
|
+
owner: string;
|
|
62
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The toolkit's built-in icons: a small inline SVG set under the Phosphor names the action catalogue uses, so the
|
|
3
|
+
* toolkit has no icon dependency. Every `icon` prop also takes any ReactNode (a Phosphor component, for example).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { type ReactNode } from 'react';
|
|
7
|
+
|
|
8
|
+
type Shape = ReactNode;
|
|
9
|
+
|
|
10
|
+
const S = { fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' } as const;
|
|
11
|
+
const F = { fill: 'currentColor', stroke: 'none' } as const;
|
|
12
|
+
|
|
13
|
+
const SHAPES: Record<string, Shape> = {
|
|
14
|
+
'arrow-left': <path {...S} d="M13 8H3m4-4L3 8l4 4" />,
|
|
15
|
+
x: <path {...S} d="M4 4l8 8M12 4l-8 8" />,
|
|
16
|
+
'caret-up': <path {...S} d="M4 10l4-4 4 4" />,
|
|
17
|
+
'caret-down': <path {...S} d="M4 6l4 4 4-4" />,
|
|
18
|
+
'caret-right': <path {...S} d="M6 4l4 4-4 4" />,
|
|
19
|
+
'caret-left': <path {...S} d="M10 4L6 8l4 4" />,
|
|
20
|
+
'arrows-out': <path {...S} d="M9.5 2.5h4v4M13.5 2.5L9 7M6.5 13.5h-4v-4M2.5 13.5L7 9" />,
|
|
21
|
+
'dots-three': <g {...F}><circle cx="3.5" cy="8" r="1.25" /><circle cx="8" cy="8" r="1.25" /><circle cx="12.5" cy="8" r="1.25" /></g>,
|
|
22
|
+
'sidebar-simple': <g {...S}><rect x="2" y="3" width="12" height="10" rx="1.5" /><path d="M9.5 3v10" /></g>,
|
|
23
|
+
'square-center': <g {...S}><rect x="2" y="3" width="12" height="10" rx="1.5" /><rect x="5" y="5.5" width="6" height="5" rx=".75" /></g>,
|
|
24
|
+
'corners-out': <path {...S} d="M2.5 6V2.5H6M10 2.5h3.5V6M13.5 10v3.5H10M6 13.5H2.5V10" />,
|
|
25
|
+
check: <path {...S} d="M3 8.5l3 3 7-7" />,
|
|
26
|
+
archive: <g {...S}><rect x="2" y="3" width="12" height="3" rx=".75" /><path d="M3 6v6.5a.5.5 0 00.5.5h9a.5.5 0 00.5-.5V6M6.5 8.5h3" /></g>,
|
|
27
|
+
'tray-arrow-up': <path {...S} d="M2.5 9.5v3a.5.5 0 00.5.5h10a.5.5 0 00.5-.5v-3h-3l-1 1.5h-3l-1-1.5h-3M8 8V2.5M5.5 5L8 2.5 10.5 5" />,
|
|
28
|
+
star: <path {...S} d="M8 2l1.8 3.8 4.2.5-3.1 2.9.8 4.1L8 11.3l-3.7 2 .8-4.1L2 6.3l4.2-.5z" />,
|
|
29
|
+
'star-fill': <path fill="currentColor" stroke="currentColor" strokeWidth={0.8} strokeLinejoin="round" d="M8 1.6l1.95 3.95 4.36.64-3.16 3.07.75 4.34L8 11.55 4.1 13.6l.75-4.34L1.69 6.19l4.36-.64z" />,
|
|
30
|
+
clock: <g {...S}><circle cx="8" cy="8" r="6" /><path d="M8 4.5V8h3" /></g>,
|
|
31
|
+
'arrow-bend-up-right': <path {...S} d="M10 3l3 3-3 3M13 6H7a4 4 0 00-4 4v3" />,
|
|
32
|
+
'user-plus': <g {...S}><circle cx="6.5" cy="5.5" r="2.75" /><path d="M1.5 13.5c.8-2.2 2.7-3.5 5-3.5s4.2 1.3 5 3.5M12.5 4v4M10.5 6h4" /></g>,
|
|
33
|
+
'envelope-open': <path {...S} d="M2 6.5v6a.5.5 0 00.5.5h11a.5.5 0 00.5-.5v-6L8 2.5zM2 6.5l6 4 6-4" />,
|
|
34
|
+
'envelope-simple': <g {...S}><rect x="2" y="3.5" width="12" height="9" rx="1" /><path d="M2 4.5l6 4.5 6-4.5" /></g>,
|
|
35
|
+
'pencil-simple': <path {...S} d="M10.8 2.7l2.5 2.5L5.6 12.9 2.5 13.5l.6-3.1zM9.3 4.2l2.5 2.5" />,
|
|
36
|
+
'file-text': <path {...S} d="M9.5 2H4a.5.5 0 00-.5.5v11a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V5zM9.5 2v3h3M6 8.5h4M6 11h4" />,
|
|
37
|
+
flask: <path {...S} d="M6 2h4M6.8 2v4.3l-3.6 6.2a1 1 0 00.9 1.5h7.8a1 1 0 00.9-1.5L9.2 6.3V2M4.6 10.2h6.8" />,
|
|
38
|
+
copy: <path {...S} d="M6.5 5.5h5a1 1 0 011 1v5a1 1 0 01-1 1h-5a1 1 0 01-1-1v-5a1 1 0 011-1zM10.5 5.5v-1a1 1 0 00-1-1h-5a1 1 0 00-1 1v5a1 1 0 001 1h1" />,
|
|
39
|
+
'chat-circle-text': <path {...S} d="M4.7 13.1A5.8 5.8 0 102.9 11.3L2.3 13.7zM6 7h4M6 9.3h3" />,
|
|
40
|
+
trash: <path {...S} d="M2.5 4h11M6.5 2.5h3M4 4l.6 9a.5.5 0 00.5.5h5.8a.5.5 0 00.5-.5L12 4M6.5 6.5v4.5M9.5 6.5v4.5" />,
|
|
41
|
+
'circle-half': <g><circle {...S} cx="8" cy="8" r="6" /><path {...F} d="M8 2a6 6 0 010 12z" /></g>,
|
|
42
|
+
'circle-three-quarter': <g><circle {...S} cx="8" cy="8" r="6" /><path {...F} d="M8 8V2a6 6 0 11-6 6z" /></g>,
|
|
43
|
+
pause: <path {...S} d="M5.5 3v10M10.5 3v10" />,
|
|
44
|
+
play: <path {...S} d="M4.5 2.5v11L13 8z" />,
|
|
45
|
+
'toggle-right': <g {...S}><rect x="1.5" y="4" width="13" height="8" rx="4" /><circle cx="10.5" cy="8" r="2" /></g>,
|
|
46
|
+
'toggle-left': <g {...S}><rect x="1.5" y="4" width="13" height="8" rx="4" /><circle cx="5.5" cy="8" r="2" /></g>,
|
|
47
|
+
hourglass: <path {...S} d="M4 2.5h8M4 13.5h8M5 2.5c0 3 6 3.5 6 5.5s-6 2.5-6 5.5M11 2.5c0 3-6 3.5-6 5.5s6 2.5 6 5.5" />,
|
|
48
|
+
'arrow-counter-clockwise': <path {...S} d="M4.5 6H2V3.5M2.3 6A6 6 0 112 9" />,
|
|
49
|
+
'arrow-clockwise': <path {...S} d="M11.5 6H14V3.5M13.7 6A6 6 0 1 0 14 9" />,
|
|
50
|
+
'arrows-clockwise': <path {...S} d="M3 6a5.4 5.4 0 0110 0M13 3v3h-3M13 10a5.4 5.4 0 01-10 0M3 13v-3h3" />,
|
|
51
|
+
plus: <path {...S} d="M8 3v10M3 8h10" />,
|
|
52
|
+
'circle-dashed': <circle {...S} cx="8" cy="8" r="6" strokeDasharray="2.2 2.2" />,
|
|
53
|
+
circle: <circle {...S} cx="8" cy="8" r="6" />,
|
|
54
|
+
'clock-countdown': <g {...S}><path d="M8 2a6 6 0 11-6 6" strokeDasharray="0" /><path d="M8 5v3l2 1.5M2.6 5.3l.1-.1M4.6 3.1l.1-.1" /></g>,
|
|
55
|
+
'check-circle-fill': <g><circle {...F} cx="8" cy="8" r="6.5" /><path d="M5 8.2l2 2 4-4" fill="none" stroke="hsl(var(--background))" strokeWidth={1.5} strokeLinecap="round" strokeLinejoin="round" /></g>,
|
|
56
|
+
'minus-circle': <g {...S}><circle cx="8" cy="8" r="6" /><path d="M5.5 8h5" /></g>,
|
|
57
|
+
'paper-plane-right': <path {...S} d="M2.5 2.8l11 5.2-11 5.2 1.8-5.2zM4.3 8h4.7" />,
|
|
58
|
+
paperclip: <path {...S} d="M10.5 5l-4.6 4.6a1.1 1.1 0 001.6 1.6l5-5a2.3 2.3 0 00-3.2-3.2l-5 5a3.4 3.4 0 004.8 4.8L13 8.9" />,
|
|
59
|
+
'clipboard-text': <g {...S}><path d="M10.5 3h2a.5.5 0 01.5.5v10a.5.5 0 01-.5.5h-9a.5.5 0 01-.5-.5v-10a.5.5 0 01.5-.5h2" /><rect x="5.5" y="2" width="5" height="2.5" rx=".75" /><path d="M6 8h4M6 10.5h4" /></g>,
|
|
60
|
+
'x-circle-fill': <g><circle {...F} cx="8" cy="8" r="6.5" /><path d="M5.8 5.8l4.4 4.4M10.2 5.8l-4.4 4.4" fill="none" stroke="hsl(var(--background))" strokeWidth={1.5} strokeLinecap="round" /></g>,
|
|
61
|
+
'magnifying-glass-plus': <path {...S} d="M7 12A5 5 0 107 2a5 5 0 000 10zM10.6 10.6L14 14M5 7h4M7 5v4" />,
|
|
62
|
+
'magnifying-glass-minus': <path {...S} d="M7 12A5 5 0 107 2a5 5 0 000 10zM10.6 10.6L14 14M5 7h4" />,
|
|
63
|
+
'download-simple': <path {...S} d="M8 2.5v7.5M4.5 6.5L8 10l3.5-3.5M2.5 10v3a.5.5 0 00.5.5h10a.5.5 0 00.5-.5v-3" />,
|
|
64
|
+
link: <path {...S} d="M6.8 9.2l2.4-2.4M7.6 4.6l1.2-1.2a2.8 2.8 0 014 4l-1.2 1.2M8.4 11.4l-1.2 1.2a2.8 2.8 0 01-4-4l1.2-1.2" />,
|
|
65
|
+
file: <path {...S} d="M9.5 2H4a.5.5 0 00-.5.5v11a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V5zM9.5 2v3h3" />,
|
|
66
|
+
'file-image': <g {...S}><path d="M9.5 2H4a.5.5 0 00-.5.5v11a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V5zM9.5 2v3h3" /><path d="M3.5 12l3-3 2 2 1.5-1.5 2.5 2.5" /><circle cx="6.5" cy="6.75" r=".75" /></g>,
|
|
67
|
+
'file-video': <g {...S}><path d="M9.5 2H4a.5.5 0 00-.5.5v11a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V5zM9.5 2v3h3" /><path d="M6.5 8v3.5L9.5 9.75z" /></g>,
|
|
68
|
+
'file-audio': <g {...S}><path d="M9.5 2H4a.5.5 0 00-.5.5v11a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V5zM9.5 2v3h3" /><path d="M6 9.5v1.5h1.25L9 12.25v-4.5L7.25 9H6M10.5 8.5a1.8 1.8 0 010 2.5" /></g>,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function hasIcon(name: string): boolean {
|
|
72
|
+
return Object.prototype.hasOwnProperty.call(SHAPES, name);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A built-in icon by name; an unknown name renders nothing. Decorative: the control carries the label. */
|
|
76
|
+
export function Icon({ name, className }: { name: string; className?: string }) {
|
|
77
|
+
const shape = SHAPES[name];
|
|
78
|
+
if (!shape) return null;
|
|
79
|
+
return (
|
|
80
|
+
<svg className={className ? `nui-icon ${className}` : 'nui-icon'} viewBox="0 0 16 16" aria-hidden="true" focusable="false">
|
|
81
|
+
{shape}
|
|
82
|
+
</svg>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Renders an `icon` prop: a name from the set, or any node the caller passes. */
|
|
87
|
+
export function renderIcon(icon: ReactNode | string | undefined, className?: string): ReactNode {
|
|
88
|
+
if (icon === undefined || icon === null || icon === false) return null;
|
|
89
|
+
if (typeof icon === 'string') return <Icon name={icon} className={className} />;
|
|
90
|
+
return icon;
|
|
91
|
+
}
|