@notis_ai/cli 0.2.14 → 0.2.16
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 +83 -21
- package/dist/agent-hooks/notis-agent-hook.mjs +20168 -0
- package/{skills → dist/base-skills}/notis-apps/SKILL.md +167 -72
- package/{skills → dist/base-skills}/notis-cli/SKILL.md +86 -28
- package/dist/base-skills/notis-query/SKILL.md +705 -0
- package/dist/skill-sync/index.js +1528 -0
- package/dist/skill-sync/index.js.map +7 -0
- package/package.json +5 -2
- package/skills/notis-apps/cli.md +39 -18
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
- package/skills/notis-onboarding/BRIEF.md +16 -0
- package/src/agent-hook-entry.js +5 -0
- package/src/cli.js +23 -14
- package/src/command-specs/agents.js +392 -0
- package/src/command-specs/apps.js +1021 -424
- package/src/command-specs/auth.js +63 -10
- package/src/command-specs/index.js +6 -0
- package/src/command-specs/onboarding.js +69 -5
- package/src/command-specs/skills.js +56 -0
- package/src/runtime/agent-memory-state.js +126 -0
- package/src/runtime/agent-setup.js +383 -0
- package/src/runtime/app-dev-consumers.js +154 -0
- package/src/runtime/app-dev-host-lock.js +80 -0
- package/src/runtime/app-dev-process-identity.js +111 -0
- package/src/runtime/app-dev-roots.js +284 -0
- package/src/runtime/app-dev-server.js +447 -29
- package/src/runtime/app-dev-sessions.js +105 -145
- package/src/runtime/app-platform.js +1314 -166
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/base-skills.d.ts +20 -0
- package/src/runtime/base-skills.js +167 -0
- package/src/runtime/login-listener.js +15 -0
- package/src/runtime/oauth.js +1506 -141
- package/src/runtime/output.js +7 -0
- package/src/runtime/ports.js +16 -0
- package/src/runtime/profiles.js +19 -16
- package/src/runtime/skill-sync/cloud-client.ts +96 -0
- package/src/runtime/skill-sync/index.ts +644 -0
- package/src/runtime/skill-sync/local-scanner.ts +1046 -0
- package/src/runtime/skill-sync/symlink-manager.ts +383 -0
- package/src/runtime/skill-sync/sync-plan.ts +22 -0
- package/src/runtime/skill-sync/types.ts +103 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
- package/src/runtime/store-screenshot.js +6 -1
- package/src/runtime/sync-skills.d.ts +37 -0
- package/src/runtime/sync-skills.js +231 -0
- package/template/app/layout.tsx +5 -1
- package/template/notis.config.ts +1 -0
- package/template/packages/sdk/package.json +2 -1
- package/template/packages/sdk/src/components/DocumentEditor.tsx +13 -3
- package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +43 -119
- package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +5 -1
- package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +1 -1
- package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +55 -0
- package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
- package/template/packages/sdk/src/config.ts +25 -0
- package/template/packages/sdk/src/documents.ts +7 -1
- package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
- package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +726 -0
- package/template/packages/sdk/src/hooks/useHandover.ts +3 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +44 -482
- package/template/packages/sdk/src/hooks/useNotis.ts +3 -0
- package/template/packages/sdk/src/hooks/useNotisNavigation.ts +7 -4
- package/template/packages/sdk/src/hooks/useTool.ts +5 -4
- package/template/packages/sdk/src/index.ts +47 -2
- package/template/packages/sdk/src/interactions/actions.ts +46 -0
- package/template/packages/sdk/src/interactions/shortcuts.tsx +634 -0
- package/template/packages/sdk/src/interactions.ts +41 -0
- package/template/packages/sdk/src/provider.tsx +2 -1
- package/template/packages/sdk/src/runtime.ts +92 -4
- package/dist/scaffolds/notis-database/CHANGELOG.md +0 -5
- package/dist/scaffolds/notis-database/app/globals.css +0 -44
- package/dist/scaffolds/notis-database/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-database/app/page.tsx +0 -1088
- package/dist/scaffolds/notis-database/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-database/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-database/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-database/components/ui/table.tsx +0 -120
- package/dist/scaffolds/notis-database/components.json +0 -20
- package/dist/scaffolds/notis-database/index.html +0 -12
- package/dist/scaffolds/notis-database/lib/types.ts +0 -132
- package/dist/scaffolds/notis-database/lib/utils.ts +0 -6
- package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +0 -1839
- package/dist/scaffolds/notis-database/notis.config.ts +0 -73
- package/dist/scaffolds/notis-database/package-lock.json +0 -3935
- package/dist/scaffolds/notis-database/package.json +0 -32
- package/dist/scaffolds/notis-database/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-database/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-database/src/dev-main.tsx +0 -23
- package/dist/scaffolds/notis-database/src/mock-runtime.ts +0 -561
- package/dist/scaffolds/notis-database/tailwind.config.ts +0 -59
- package/dist/scaffolds/notis-database/tsconfig.json +0 -23
- package/dist/scaffolds/notis-database/vite.config.ts +0 -22
- package/dist/scaffolds/notis-journal/CHANGELOG.md +0 -29
- package/dist/scaffolds/notis-journal/app/globals.css +0 -37
- package/dist/scaffolds/notis-journal/app/insights/page.tsx +0 -513
- package/dist/scaffolds/notis-journal/app/journal-core.tsx +0 -362
- package/dist/scaffolds/notis-journal/app/journal-ui.tsx +0 -337
- package/dist/scaffolds/notis-journal/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-journal/app/page.tsx +0 -486
- package/dist/scaffolds/notis-journal/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-journal/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-journal/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-journal/components.json +0 -20
- package/dist/scaffolds/notis-journal/index.html +0 -12
- package/dist/scaffolds/notis-journal/lib/utils.ts +0 -6
- package/dist/scaffolds/notis-journal/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-6.png +0 -0
- package/dist/scaffolds/notis-journal/metadata/screenshot-fixtures.json +0 -132
- package/dist/scaffolds/notis-journal/notis.config.ts +0 -93
- package/dist/scaffolds/notis-journal/package-lock.json +0 -4615
- package/dist/scaffolds/notis-journal/package.json +0 -34
- package/dist/scaffolds/notis-journal/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-journal/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-journal/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-journal/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-journal/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-journal/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-journal/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-journal/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-journal/skills/journal-onboarding/SKILL.md +0 -120
- package/dist/scaffolds/notis-journal/src/dev-main.tsx +0 -58
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +0 -199
- package/dist/scaffolds/notis-journal/tailwind.config.ts +0 -58
- package/dist/scaffolds/notis-journal/tsconfig.json +0 -23
- package/dist/scaffolds/notis-journal/vite.config.ts +0 -10
- package/dist/scaffolds/notis-notes/CHANGELOG.md +0 -5
- package/dist/scaffolds/notis-notes/app/globals.css +0 -3
- package/dist/scaffolds/notis-notes/app/layout.tsx +0 -6
- package/dist/scaffolds/notis-notes/app/page.tsx +0 -1943
- package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +0 -596
- package/dist/scaffolds/notis-notes/components/ui/badge.tsx +0 -28
- package/dist/scaffolds/notis-notes/components/ui/button.tsx +0 -53
- package/dist/scaffolds/notis-notes/components/ui/card.tsx +0 -56
- package/dist/scaffolds/notis-notes/components.json +0 -20
- package/dist/scaffolds/notis-notes/lib/utils.ts +0 -6
- package/dist/scaffolds/notis-notes/lib/visible-properties.ts +0 -144
- package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +0 -752
- package/dist/scaffolds/notis-notes/notis.config.ts +0 -80
- package/dist/scaffolds/notis-notes/package-lock.json +0 -4636
- package/dist/scaffolds/notis-notes/package.json +0 -35
- package/dist/scaffolds/notis-notes/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-notes/postcss.config.mjs +0 -8
- package/dist/scaffolds/notis-notes/tailwind.config.ts +0 -58
- package/dist/scaffolds/notis-notes/tsconfig.json +0 -23
- package/dist/scaffolds/notis-notes/vite.config.ts +0 -10
- package/dist/scaffolds/notis-random/CHANGELOG.md +0 -15
- package/dist/scaffolds/notis-random/README.md +0 -33
- package/dist/scaffolds/notis-random/app/globals.css +0 -11
- package/dist/scaffolds/notis-random/app/history/page.tsx +0 -67
- package/dist/scaffolds/notis-random/app/layout.tsx +0 -7
- package/dist/scaffolds/notis-random/app/page.tsx +0 -289
- package/dist/scaffolds/notis-random/components/ui/button.tsx +0 -50
- package/dist/scaffolds/notis-random/components/ui/card.tsx +0 -16
- package/dist/scaffolds/notis-random/components/ui/input.tsx +0 -23
- package/dist/scaffolds/notis-random/components.json +0 -20
- package/dist/scaffolds/notis-random/index.html +0 -12
- package/dist/scaffolds/notis-random/lib/notis-tools.ts +0 -128
- package/dist/scaffolds/notis-random/lib/rng.ts +0 -202
- package/dist/scaffolds/notis-random/lib/roll-record.ts +0 -189
- package/dist/scaffolds/notis-random/lib/utils.ts +0 -25
- package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
- package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +0 -753
- package/dist/scaffolds/notis-random/notis.config.ts +0 -86
- package/dist/scaffolds/notis-random/package-lock.json +0 -4513
- package/dist/scaffolds/notis-random/package.json +0 -36
- package/dist/scaffolds/notis-random/packages/sdk/package.json +0 -36
- package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +0 -93
- package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +0 -60
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +0 -278
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +0 -91
- package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +0 -39
- package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +0 -234
- package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +0 -250
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +0 -41
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useCloudComputer.ts +0 -97
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +0 -85
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSubscription.ts +0 -76
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +0 -78
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +0 -121
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useHandover.ts +0 -75
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +0 -539
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +0 -34
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +0 -64
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +0 -56
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +0 -73
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +0 -95
- package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +0 -100
- package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +0 -43
- package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +0 -351
- package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +0 -186
- package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +0 -15
- package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +0 -56
- package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +0 -15
- package/dist/scaffolds/notis-random/postcss.config.mjs +0 -6
- package/dist/scaffolds/notis-random/src/dev-main.tsx +0 -70
- package/dist/scaffolds/notis-random/src/mock-runtime.ts +0 -129
- package/dist/scaffolds/notis-random/tailwind.config.ts +0 -50
- package/dist/scaffolds/notis-random/tsconfig.json +0 -23
- package/dist/scaffolds/notis-random/vite.config.ts +0 -11
- package/dist/scaffolds.json +0 -49
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { reconcileBaseSkills } from './base-skills.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
|
|
9
|
+
const DEFAULT_LOCK_STALE_MS = 10 * 60_000;
|
|
10
|
+
const DEFAULT_LOCK_POLL_MS = 50;
|
|
11
|
+
|
|
12
|
+
async function lockSnapshot(lockDirectory) {
|
|
13
|
+
try {
|
|
14
|
+
const [raw, metadata] = await Promise.all([
|
|
15
|
+
readFile(join(lockDirectory, 'owner'), 'utf8').catch(() => ''),
|
|
16
|
+
stat(lockDirectory),
|
|
17
|
+
]);
|
|
18
|
+
let owner = {};
|
|
19
|
+
try {
|
|
20
|
+
owner = raw ? JSON.parse(raw) : {};
|
|
21
|
+
} catch {
|
|
22
|
+
// A process can die between mkdir and the atomic owner write. Preserve
|
|
23
|
+
// the directory mtime as a reclaimable ownerless lease.
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
id: typeof owner.id === 'string' ? owner.id : null,
|
|
27
|
+
pid: Number.isInteger(Number(owner.pid)) ? Number(owner.pid) : null,
|
|
28
|
+
at: Number.isFinite(Number(owner.at)) ? Number(owner.at) : metadata.mtimeMs,
|
|
29
|
+
mtimeMs: metadata.mtimeMs,
|
|
30
|
+
};
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sameLockSnapshot(left, right) {
|
|
37
|
+
return Boolean(
|
|
38
|
+
left
|
|
39
|
+
&& right
|
|
40
|
+
&& left.id === right.id
|
|
41
|
+
&& left.pid === right.pid
|
|
42
|
+
&& left.at === right.at
|
|
43
|
+
&& left.mtimeMs === right.mtimeMs,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function processIsAlive(pid) {
|
|
48
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
49
|
+
try {
|
|
50
|
+
process.kill(pid, 0);
|
|
51
|
+
return true;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return error?.code === 'EPERM';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function delay(milliseconds) {
|
|
58
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function quarantineStaleLock(lockDirectory, snapshot) {
|
|
62
|
+
const quarantineRoot = join(dirname(lockDirectory), '.stale-operation-locks');
|
|
63
|
+
const snapshotToken = createHash('sha256')
|
|
64
|
+
.update(JSON.stringify(snapshot))
|
|
65
|
+
.digest('hex');
|
|
66
|
+
await mkdir(quarantineRoot, { recursive: true, mode: 0o700 });
|
|
67
|
+
try {
|
|
68
|
+
// The deterministic destination is the compare-and-swap guard. If two
|
|
69
|
+
// waiters observed the same stale owner, only one can move it here. The
|
|
70
|
+
// retained non-empty tombstone prevents the loser from ever moving a new
|
|
71
|
+
// live lock that appeared at the shared pathname in the meantime.
|
|
72
|
+
await rename(lockDirectory, join(quarantineRoot, snapshotToken));
|
|
73
|
+
return true;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (['EEXIST', 'ENOTEMPTY', 'ENOENT'].includes(error?.code)) return false;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function writeLockOwnerAtomically(lockDirectory, owner) {
|
|
81
|
+
const temporaryOwnerPath = join(lockDirectory, `.owner.${owner.id}.tmp`);
|
|
82
|
+
const ownerPath = join(lockDirectory, 'owner');
|
|
83
|
+
await writeFile(temporaryOwnerPath, JSON.stringify(owner), { mode: 0o600 });
|
|
84
|
+
await rename(temporaryOwnerPath, ownerPath);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function releaseOwnedLock(lockDirectory, ownerId) {
|
|
88
|
+
const owner = await lockSnapshot(lockDirectory);
|
|
89
|
+
if (owner?.id !== ownerId) return;
|
|
90
|
+
|
|
91
|
+
const quarantineRoot = join(dirname(lockDirectory), '.stale-operation-locks');
|
|
92
|
+
const releasedDirectory = join(quarantineRoot, `released.${ownerId}`);
|
|
93
|
+
await mkdir(quarantineRoot, { recursive: true, mode: 0o700 });
|
|
94
|
+
try {
|
|
95
|
+
// Moving the owned directory releases the shared pathname atomically.
|
|
96
|
+
// Delete only the private destination so a waiter can safely acquire a new
|
|
97
|
+
// lock without racing this owner's recursive cleanup.
|
|
98
|
+
await rename(lockDirectory, releasedDirectory);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error?.code === 'ENOENT') return;
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
await rm(releasedDirectory, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Serialize Desktop and terminal skill sync across processes on one Mac. */
|
|
107
|
+
export async function withSkillSyncLock(callback, {
|
|
108
|
+
home = homedir(),
|
|
109
|
+
timeoutMs = DEFAULT_LOCK_TIMEOUT_MS,
|
|
110
|
+
staleMs = DEFAULT_LOCK_STALE_MS,
|
|
111
|
+
pollMs = DEFAULT_LOCK_POLL_MS,
|
|
112
|
+
now = () => Date.now(),
|
|
113
|
+
} = {}) {
|
|
114
|
+
const lockDirectory = join(home, '.notis', 'skills', '.operation-lock');
|
|
115
|
+
const ownerId = `${process.pid}.${randomUUID()}`;
|
|
116
|
+
const deadline = now() + timeoutMs;
|
|
117
|
+
await mkdir(dirname(lockDirectory), { recursive: true, mode: 0o700 });
|
|
118
|
+
|
|
119
|
+
for (;;) {
|
|
120
|
+
try {
|
|
121
|
+
await mkdir(lockDirectory, { mode: 0o700 });
|
|
122
|
+
try {
|
|
123
|
+
await writeLockOwnerAtomically(lockDirectory, {
|
|
124
|
+
id: ownerId,
|
|
125
|
+
pid: process.pid,
|
|
126
|
+
at: now(),
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// This process exclusively created the directory and has not yet
|
|
130
|
+
// published an owner, so it is safe to undo a failed acquisition.
|
|
131
|
+
await rm(lockDirectory, { recursive: true, force: true });
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const observed = await lockSnapshot(lockDirectory);
|
|
140
|
+
if (
|
|
141
|
+
observed
|
|
142
|
+
&& now() - observed.at > staleMs
|
|
143
|
+
&& !processIsAlive(observed.pid)
|
|
144
|
+
) {
|
|
145
|
+
// Require an unchanged observation interval before stealing. This avoids
|
|
146
|
+
// racing a holder that is publishing or refreshing its owner lease.
|
|
147
|
+
await delay(Math.max(pollMs, 10));
|
|
148
|
+
const current = await lockSnapshot(lockDirectory);
|
|
149
|
+
if (sameLockSnapshot(observed, current) && !processIsAlive(current?.pid)) {
|
|
150
|
+
if (await quarantineStaleLock(lockDirectory, current)) continue;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (now() >= deadline) {
|
|
154
|
+
throw new Error('Timed out waiting for another Notis skill sync to finish.');
|
|
155
|
+
}
|
|
156
|
+
await delay(pollMs);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const heartbeatMs = Math.max(10, Math.min(30_000, Math.floor(staleMs / 3)));
|
|
160
|
+
let heartbeatStopped = false;
|
|
161
|
+
let heartbeatInFlight = Promise.resolve();
|
|
162
|
+
const refreshHeartbeat = async () => {
|
|
163
|
+
if (heartbeatStopped) return;
|
|
164
|
+
const temporaryOwnerPath = join(lockDirectory, `.owner.${ownerId}.tmp`);
|
|
165
|
+
await writeFile(
|
|
166
|
+
temporaryOwnerPath,
|
|
167
|
+
JSON.stringify({ id: ownerId, pid: process.pid, at: now() }),
|
|
168
|
+
{ mode: 0o600 },
|
|
169
|
+
);
|
|
170
|
+
const owner = await lockSnapshot(lockDirectory);
|
|
171
|
+
if (owner?.id !== ownerId) {
|
|
172
|
+
await rm(temporaryOwnerPath, { force: true });
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// If a stale-lock reclaimer moved this directory after the ownership
|
|
176
|
+
// check, the source path disappears and rename fails instead of
|
|
177
|
+
// overwriting the owner of a newly acquired lock at the shared path.
|
|
178
|
+
await rename(temporaryOwnerPath, join(lockDirectory, 'owner'));
|
|
179
|
+
};
|
|
180
|
+
const heartbeat = setInterval(() => {
|
|
181
|
+
// Serialize refreshes and retain the active promise so cleanup cannot race
|
|
182
|
+
// a temporary owner write that was already in flight when the interval was
|
|
183
|
+
// cleared.
|
|
184
|
+
heartbeatInFlight = heartbeatInFlight
|
|
185
|
+
.then(refreshHeartbeat)
|
|
186
|
+
.catch(() => undefined);
|
|
187
|
+
}, heartbeatMs);
|
|
188
|
+
heartbeat.unref?.();
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
return await callback();
|
|
192
|
+
} finally {
|
|
193
|
+
heartbeatStopped = true;
|
|
194
|
+
clearInterval(heartbeat);
|
|
195
|
+
await heartbeatInFlight;
|
|
196
|
+
await releaseOwnedLock(lockDirectory, ownerId);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Shared CLI-owned orchestration for base-skill installation plus account sync.
|
|
202
|
+
* Desktop injects the source engine that webpack bundles; the CLI command
|
|
203
|
+
* injects the generated dist engine used by the published package.
|
|
204
|
+
*/
|
|
205
|
+
export async function reconcileAllSkills({
|
|
206
|
+
serverUrl,
|
|
207
|
+
jwt,
|
|
208
|
+
honorSyncEnabled,
|
|
209
|
+
userId = null,
|
|
210
|
+
home,
|
|
211
|
+
runAccountSync,
|
|
212
|
+
reconcileBase = reconcileBaseSkills,
|
|
213
|
+
lockOptions = {},
|
|
214
|
+
}) {
|
|
215
|
+
return withSkillSyncLock(async () => {
|
|
216
|
+
const base = reconcileBase({ userId, ...(home ? { home } : {}) });
|
|
217
|
+
const account = await runAccountSync(
|
|
218
|
+
serverUrl,
|
|
219
|
+
jwt,
|
|
220
|
+
{},
|
|
221
|
+
{ honorSyncEnabled },
|
|
222
|
+
);
|
|
223
|
+
return {
|
|
224
|
+
...account,
|
|
225
|
+
baseSkills: base.skills,
|
|
226
|
+
baseInstalled: base.installed,
|
|
227
|
+
baseLinked: base.linked,
|
|
228
|
+
baseBackups: base.backups,
|
|
229
|
+
};
|
|
230
|
+
}, { ...lockOptions, ...(home ? { home } : {}) });
|
|
231
|
+
}
|
package/template/app/layout.tsx
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import '@notis/sdk/styles.css';
|
|
2
|
+
import { ShortcutProvider } from '@notis/sdk/interactions';
|
|
2
3
|
import './globals.css';
|
|
3
4
|
|
|
4
5
|
export default function AppShell({ children }: { children: React.ReactNode }) {
|
|
5
|
-
|
|
6
|
+
// NotisProvider supplies this in the product host. Keeping the shell-level
|
|
7
|
+
// provider makes the same scope arbitration available in the local harness;
|
|
8
|
+
// ShortcutProvider is nesting-safe, so the hosted app still has one registry.
|
|
9
|
+
return <ShortcutProvider>{children}</ShortcutProvider>;
|
|
6
10
|
}
|
package/template/notis.config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { defineNotisApp } from '@notis/sdk/config';
|
|
|
2
2
|
|
|
3
3
|
export default defineNotisApp({
|
|
4
4
|
name: 'my-notis-app',
|
|
5
|
+
devSlug: 'my-notis-app',
|
|
5
6
|
title: 'My Notis App',
|
|
6
7
|
description: 'A focused Notis app with one route and one database-backed workflow.',
|
|
7
8
|
icon: 'phosphor:squares-four',
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@notis/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.ts",
|
|
8
|
+
"./interactions": "./src/interactions.ts",
|
|
8
9
|
"./config": "./src/config.ts",
|
|
9
10
|
"./vite": "./src/vite.ts",
|
|
10
11
|
"./ui": "./src/ui.ts",
|
|
@@ -51,7 +51,7 @@ function DocumentEditorFallback({ documentId, variant = 'full', className }: Not
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
const isMarkdown = document.contentType
|
|
54
|
+
const isMarkdown = document.contentType === 'markdown' || !document.contentType;
|
|
55
55
|
return (
|
|
56
56
|
<div className={className}>
|
|
57
57
|
{variant === 'full' ? <h1 style={fallbackTitleStyle}>{document.title}</h1> : null}
|
|
@@ -62,6 +62,15 @@ function DocumentEditorFallback({ documentId, variant = 'full', className }: Not
|
|
|
62
62
|
<p style={fallbackNoticeStyle}>Read-only preview — editing is available inside the Notis portal.</p>
|
|
63
63
|
)}
|
|
64
64
|
</>
|
|
65
|
+
) : document.contentType === 'view' ? (
|
|
66
|
+
<div style={fallbackFrameStyle}>
|
|
67
|
+
{document.viewType === 'report'
|
|
68
|
+
? 'Interactive report'
|
|
69
|
+
: document.viewType === 'html'
|
|
70
|
+
? 'Interactive HTML document'
|
|
71
|
+
: 'View document'} — open it in Notis Documents for
|
|
72
|
+
the full native experience.
|
|
73
|
+
</div>
|
|
65
74
|
) : (
|
|
66
75
|
<div style={fallbackFrameStyle}>
|
|
67
76
|
{document.fileType ? `${document.fileType.toUpperCase()} document` : 'File document'} — open it in
|
|
@@ -76,8 +85,9 @@ function DocumentEditorFallback({ documentId, variant = 'full', className }: Not
|
|
|
76
85
|
* Embeds the document editor for a document anywhere in an app view. The host
|
|
77
86
|
* (portal) provides the implementation through `runtime.ui.DocumentEditor` and
|
|
78
87
|
* dispatches on the document's content type (markdown -> rich text editor,
|
|
79
|
-
* files -> matching viewer
|
|
80
|
-
* preview
|
|
88
|
+
* files -> matching viewer, views -> their trusted native Documents surface).
|
|
89
|
+
* Outside the portal this falls back to a read-only preview or an explicit
|
|
90
|
+
* open-in-Documents notice for native views.
|
|
81
91
|
*
|
|
82
92
|
* ```tsx
|
|
83
93
|
* <DocumentEditor documentId={entry.id} variant="body" className="min-h-[320px]" />
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ChangeEvent, KeyboardEvent, ReactElement } from 'react';
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useNotisRuntime } from '../provider';
|
|
4
|
+
import type { NotisMarkdownEditorProps } from '../runtime';
|
|
5
|
+
|
|
6
|
+
function MarkdownEditorFallback({
|
|
7
|
+
value,
|
|
8
|
+
revision,
|
|
9
|
+
readOnly,
|
|
10
|
+
autosaveMs = 1500,
|
|
11
|
+
placeholder = 'Write in Markdown…',
|
|
12
|
+
className,
|
|
13
|
+
onChange,
|
|
14
|
+
onSave,
|
|
15
|
+
onDirtyChange,
|
|
16
|
+
onSavingChange,
|
|
17
|
+
}: NotisMarkdownEditorProps) {
|
|
18
|
+
const [draft, setDraft] = useState(value);
|
|
19
|
+
const [saveAttempt, setSaveAttempt] = useState(0);
|
|
20
|
+
const draftRef = useRef(value);
|
|
21
|
+
const dirtyRef = useRef(false);
|
|
22
|
+
const savingRef = useRef(false);
|
|
23
|
+
const revisionRef = useRef(revision);
|
|
24
|
+
const onSaveRef = useRef(onSave);
|
|
25
|
+
const readOnlyRef = useRef(readOnly);
|
|
26
|
+
const unmountedRef = useRef(false);
|
|
27
|
+
const saveRef = useRef<() => Promise<void>>(async () => undefined);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
onSaveRef.current = onSave;
|
|
31
|
+
readOnlyRef.current = readOnly;
|
|
32
|
+
}, [onSave, readOnly]);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (dirtyRef.current) return;
|
|
36
|
+
draftRef.current = value;
|
|
37
|
+
revisionRef.current = revision;
|
|
38
|
+
setDraft(value);
|
|
39
|
+
}, [revision, value]);
|
|
40
|
+
|
|
41
|
+
const save = useCallback(async () => {
|
|
42
|
+
if (!onSave || !dirtyRef.current || readOnly || savingRef.current) return;
|
|
43
|
+
const markdown = draftRef.current;
|
|
44
|
+
savingRef.current = true;
|
|
45
|
+
if (!unmountedRef.current) onSavingChange?.(true);
|
|
46
|
+
try {
|
|
47
|
+
const result = await onSave({ markdown, expectedRevision: revisionRef.current });
|
|
48
|
+
if (result?.revision) revisionRef.current = result.revision;
|
|
49
|
+
if (draftRef.current === markdown) {
|
|
50
|
+
dirtyRef.current = false;
|
|
51
|
+
if (!unmountedRef.current) onDirtyChange?.(false);
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
savingRef.current = false;
|
|
55
|
+
if (!unmountedRef.current) onSavingChange?.(false);
|
|
56
|
+
if (dirtyRef.current && draftRef.current !== markdown) {
|
|
57
|
+
if (unmountedRef.current) {
|
|
58
|
+
void saveRef.current().catch((error) => console.error('Could not save Markdown on unmount', error));
|
|
59
|
+
} else {
|
|
60
|
+
setSaveAttempt((attempt) => attempt + 1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}, [onDirtyChange, onSave, onSavingChange, readOnly]);
|
|
65
|
+
saveRef.current = save;
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
unmountedRef.current = false;
|
|
69
|
+
return () => {
|
|
70
|
+
unmountedRef.current = true;
|
|
71
|
+
if (!savingRef.current) {
|
|
72
|
+
void saveRef.current().catch((error) => console.error('Could not save Markdown on unmount', error));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!dirtyRef.current || !onSave || readOnly) return;
|
|
79
|
+
const timer = window.setTimeout(() => void save(), Math.max(250, autosaveMs));
|
|
80
|
+
return () => window.clearTimeout(timer);
|
|
81
|
+
}, [autosaveMs, draft, onSave, readOnly, save, saveAttempt]);
|
|
82
|
+
|
|
83
|
+
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
84
|
+
const next = event.currentTarget.value;
|
|
85
|
+
draftRef.current = next;
|
|
86
|
+
setDraft(next);
|
|
87
|
+
if (!dirtyRef.current) {
|
|
88
|
+
dirtyRef.current = true;
|
|
89
|
+
onDirtyChange?.(true);
|
|
90
|
+
}
|
|
91
|
+
onChange?.(next);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
95
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 's') {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
void save();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<textarea
|
|
103
|
+
value={draft}
|
|
104
|
+
readOnly={readOnly}
|
|
105
|
+
placeholder={placeholder}
|
|
106
|
+
className={className}
|
|
107
|
+
onChange={handleChange}
|
|
108
|
+
onKeyDown={handleKeyDown}
|
|
109
|
+
style={{ minHeight: 360, width: '100%', resize: 'vertical' }}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Use Notis' editor while leaving persistence entirely under app control. */
|
|
115
|
+
export function MarkdownEditor(props: NotisMarkdownEditorProps): ReactElement {
|
|
116
|
+
const runtime = useNotisRuntime();
|
|
117
|
+
const HostEditor = runtime?.ui?.MarkdownEditor;
|
|
118
|
+
return HostEditor
|
|
119
|
+
? <HostEditor key={props.resourceKey} {...props} />
|
|
120
|
+
: <MarkdownEditorFallback key={props.resourceKey} {...props} />;
|
|
121
|
+
}
|
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
useEffect,
|
|
3
|
+
import React, {
|
|
5
4
|
useMemo,
|
|
6
|
-
useRef,
|
|
7
5
|
useState,
|
|
8
6
|
type CSSProperties,
|
|
9
7
|
type ReactElement,
|
|
10
|
-
type ReactNode,
|
|
11
8
|
} from 'react';
|
|
9
|
+
import type { ResolvedCollectionAction } from '../interactions/actions';
|
|
10
|
+
import { shortcutDisplay, useShortcuts, type ShortcutDefinition } from '../interactions/shortcuts';
|
|
11
|
+
import type { ShortcutScope } from '../interactions/shortcuts';
|
|
12
12
|
|
|
13
|
-
export
|
|
14
|
-
id: string;
|
|
15
|
-
label: string;
|
|
16
|
-
icon: ReactNode;
|
|
17
|
-
/** Single-key (e.g. "E", "#") or "Mod+K" shortcut. Bound while the bar is mounted with count > 0. */
|
|
18
|
-
shortcut?: string;
|
|
19
|
-
onRun: () => void | Promise<void>;
|
|
20
|
-
/** Hint for future destructive styling. Currently unused visually. */
|
|
21
|
-
destructive?: boolean;
|
|
22
|
-
}
|
|
13
|
+
export type MultiSelectAction = ResolvedCollectionAction;
|
|
23
14
|
|
|
24
15
|
export interface MultiSelectActionBarProps {
|
|
25
16
|
selectedCount: number;
|
|
@@ -30,73 +21,10 @@ export interface MultiSelectActionBarProps {
|
|
|
30
21
|
className?: string;
|
|
31
22
|
/** Optional override for the bar's positioning style. Defaults to bottom-center floating. */
|
|
32
23
|
style?: CSSProperties;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
needsMod: boolean;
|
|
38
|
-
needsShift: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function parseShortcut(raw: string): ParsedShortcut | null {
|
|
42
|
-
const parts = raw.split('+').map((p) => p.trim()).filter(Boolean);
|
|
43
|
-
if (parts.length === 0) return null;
|
|
44
|
-
let needsMod = false;
|
|
45
|
-
let needsShift = false;
|
|
46
|
-
let key = '';
|
|
47
|
-
for (const part of parts) {
|
|
48
|
-
const lower = part.toLowerCase();
|
|
49
|
-
if (lower === 'mod' || lower === 'cmd' || lower === 'ctrl' || lower === 'meta') {
|
|
50
|
-
needsMod = true;
|
|
51
|
-
} else if (lower === 'shift') {
|
|
52
|
-
needsShift = true;
|
|
53
|
-
} else {
|
|
54
|
-
key = lower;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (!key) return null;
|
|
58
|
-
return { key, needsMod, needsShift };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function shortcutMatches(shortcut: ParsedShortcut, event: KeyboardEvent): boolean {
|
|
62
|
-
const eventKey = event.key.toLowerCase();
|
|
63
|
-
// Special-case "#" so Shift+3 matches without the consumer specifying Shift.
|
|
64
|
-
if (shortcut.key === '#') {
|
|
65
|
-
if (eventKey === '#') return true;
|
|
66
|
-
if (event.shiftKey && eventKey === '3') return true;
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
if (eventKey !== shortcut.key) return false;
|
|
70
|
-
if (shortcut.needsMod !== Boolean(event.metaKey || event.ctrlKey)) return false;
|
|
71
|
-
if (shortcut.needsShift !== event.shiftKey) return false;
|
|
72
|
-
if (event.altKey) return false;
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function isEditableEvent(event: Event): boolean {
|
|
77
|
-
// Notis apps mount inside a Shadow DOM, so a document-level listener sees
|
|
78
|
-
// `event.target` retargeted to the shadow host. `composedPath()[0]` pierces
|
|
79
|
-
// the shadow boundary to the real focused element, so action-bar shortcuts
|
|
80
|
-
// don't fire while the user is typing in an app input.
|
|
81
|
-
const target = event.composedPath?.()[0] ?? event.target;
|
|
82
|
-
if (!(target instanceof HTMLElement)) return false;
|
|
83
|
-
if (target.isContentEditable) return true;
|
|
84
|
-
const tag = target.tagName;
|
|
85
|
-
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function shortcutDisplay(raw: string): string {
|
|
89
|
-
const parts = raw.split('+').map((p) => p.trim()).filter(Boolean);
|
|
90
|
-
return parts
|
|
91
|
-
.map((part) => {
|
|
92
|
-
const lower = part.toLowerCase();
|
|
93
|
-
if (lower === 'mod' || lower === 'cmd' || lower === 'meta') return '⌘';
|
|
94
|
-
if (lower === 'ctrl') return '⌃';
|
|
95
|
-
if (lower === 'shift') return '⇧';
|
|
96
|
-
if (lower === 'alt' || lower === 'option') return '⌥';
|
|
97
|
-
return part.length === 1 ? part.toUpperCase() : part;
|
|
98
|
-
})
|
|
99
|
-
.join('');
|
|
24
|
+
/** Disable action key bindings while leaving the visible toolbar mounted. */
|
|
25
|
+
shortcutsEnabled?: boolean;
|
|
26
|
+
/** Override the default collection shortcut scope. */
|
|
27
|
+
shortcutScope?: ShortcutScope;
|
|
100
28
|
}
|
|
101
29
|
|
|
102
30
|
const containerBaseStyle: CSSProperties = {
|
|
@@ -121,10 +49,12 @@ const containerBaseStyle: CSSProperties = {
|
|
|
121
49
|
};
|
|
122
50
|
|
|
123
51
|
const countStyle: CSSProperties = {
|
|
52
|
+
flexShrink: 0,
|
|
124
53
|
padding: '0 0.5rem',
|
|
125
54
|
fontSize: '12px',
|
|
126
55
|
fontWeight: 500,
|
|
127
56
|
fontVariantNumeric: 'tabular-nums',
|
|
57
|
+
whiteSpace: 'nowrap',
|
|
128
58
|
color: 'color-mix(in srgb, hsl(var(--background)) 70%, transparent)',
|
|
129
59
|
};
|
|
130
60
|
|
|
@@ -136,6 +66,7 @@ const dividerStyle: CSSProperties = {
|
|
|
136
66
|
};
|
|
137
67
|
|
|
138
68
|
const baseButtonStyle: CSSProperties = {
|
|
69
|
+
flexShrink: 0,
|
|
139
70
|
display: 'inline-flex',
|
|
140
71
|
alignItems: 'center',
|
|
141
72
|
gap: '0.375rem',
|
|
@@ -147,6 +78,7 @@ const baseButtonStyle: CSSProperties = {
|
|
|
147
78
|
cursor: 'pointer',
|
|
148
79
|
fontSize: '13px',
|
|
149
80
|
fontFamily: 'inherit',
|
|
81
|
+
whiteSpace: 'nowrap',
|
|
150
82
|
transition: 'background-color 120ms ease, color 120ms ease',
|
|
151
83
|
};
|
|
152
84
|
|
|
@@ -180,43 +112,25 @@ export function MultiSelectActionBar({
|
|
|
180
112
|
itemLabel,
|
|
181
113
|
className,
|
|
182
114
|
style,
|
|
115
|
+
shortcutsEnabled = true,
|
|
116
|
+
shortcutScope = 'collection',
|
|
183
117
|
}: MultiSelectActionBarProps): ReactElement | null {
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
})
|
|
194
|
-
.filter((entry): entry is { action: MultiSelectAction; parsed: ParsedShortcut } => entry !== null);
|
|
118
|
+
const actionShortcuts = useMemo<ShortcutDefinition[]>(() => {
|
|
119
|
+
return actions.flatMap((action): ShortcutDefinition[] => {
|
|
120
|
+
if (!action.shortcut || action.disabled || action.pending) return [];
|
|
121
|
+
return [{
|
|
122
|
+
id: `collection.action.${action.id}`,
|
|
123
|
+
keys: action.shortcut,
|
|
124
|
+
label: action.label,
|
|
125
|
+
onTrigger: () => action.onRun(),
|
|
126
|
+
}];
|
|
127
|
+
});
|
|
195
128
|
}, [actions]);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (isEditableEvent(event)) return;
|
|
202
|
-
for (const { action, parsed } of parsedShortcuts) {
|
|
203
|
-
if (shortcutMatches(parsed, event)) {
|
|
204
|
-
event.preventDefault();
|
|
205
|
-
event.stopPropagation();
|
|
206
|
-
if (typeof event.stopImmediatePropagation === 'function') {
|
|
207
|
-
event.stopImmediatePropagation();
|
|
208
|
-
}
|
|
209
|
-
void action.onRun();
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
document.addEventListener('keydown', handleKeyDown, true);
|
|
216
|
-
return () => {
|
|
217
|
-
document.removeEventListener('keydown', handleKeyDown, true);
|
|
218
|
-
};
|
|
219
|
-
}, [parsedShortcuts, selectedCount]);
|
|
129
|
+
useShortcuts(actionShortcuts, {
|
|
130
|
+
enabled: shortcutsEnabled && selectedCount > 0,
|
|
131
|
+
scope: shortcutScope,
|
|
132
|
+
priority: 25,
|
|
133
|
+
});
|
|
220
134
|
|
|
221
135
|
if (selectedCount === 0) return null;
|
|
222
136
|
|
|
@@ -258,21 +172,31 @@ function ActionButton({ action }: { action: MultiSelectAction }) {
|
|
|
258
172
|
color: hover
|
|
259
173
|
? 'hsl(var(--background))'
|
|
260
174
|
: baseButtonStyle.color,
|
|
175
|
+
opacity: action.disabled ? 0.45 : 1,
|
|
176
|
+
cursor: action.disabled || action.pending ? 'not-allowed' : 'pointer',
|
|
261
177
|
};
|
|
262
178
|
|
|
263
179
|
return (
|
|
264
180
|
<button
|
|
265
181
|
type="button"
|
|
266
|
-
onClick={() =>
|
|
182
|
+
onClick={() => {
|
|
183
|
+
if (!action.disabled && !action.pending) void action.onRun();
|
|
184
|
+
}}
|
|
185
|
+
disabled={action.disabled || action.pending}
|
|
186
|
+
aria-busy={action.pending || undefined}
|
|
187
|
+
aria-keyshortcuts={action.shortcut || undefined}
|
|
188
|
+
data-destructive={action.destructive ? 'true' : 'false'}
|
|
267
189
|
onMouseEnter={() => setHover(true)}
|
|
268
190
|
onMouseLeave={() => setHover(false)}
|
|
269
191
|
onFocus={() => setHover(true)}
|
|
270
192
|
onBlur={() => setHover(false)}
|
|
271
193
|
style={buttonStyle}
|
|
272
194
|
>
|
|
273
|
-
|
|
195
|
+
{!action.shortcut && action.icon ? (
|
|
196
|
+
<span aria-hidden style={iconSlotStyle}>{action.icon}</span>
|
|
197
|
+
) : null}
|
|
274
198
|
{display ? <kbd aria-hidden style={keycapStyle}>{display}</kbd> : null}
|
|
275
|
-
<span>{action.label}</span>
|
|
199
|
+
<span>{action.pending ? `${action.label}…` : action.label}</span>
|
|
276
200
|
</button>
|
|
277
201
|
);
|
|
278
202
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { type CSSProperties, type MouseEvent as ReactMouseEvent, type ReactElement } from 'react';
|
|
3
|
+
import React, { type CSSProperties, type MouseEvent as ReactMouseEvent, type ReactElement } from 'react';
|
|
4
4
|
|
|
5
5
|
export interface MultiSelectCheckboxProps {
|
|
6
6
|
isSelected: boolean;
|
|
7
7
|
onClick: (event: ReactMouseEvent) => void;
|
|
8
|
+
disabled?: boolean;
|
|
8
9
|
/** When true, the checkbox is always visible. Default false (hover/focus reveal via the parent's :hover state). */
|
|
9
10
|
alwaysVisible?: boolean;
|
|
10
11
|
/** Optional aria-label override. Defaults to "Select item" / "Deselect item". */
|
|
@@ -40,6 +41,7 @@ const checkPath = 'M3.5 7.5l2.5 2.5 6.5-6.5';
|
|
|
40
41
|
export function MultiSelectCheckbox({
|
|
41
42
|
isSelected,
|
|
42
43
|
onClick,
|
|
44
|
+
disabled = false,
|
|
43
45
|
alwaysVisible = false,
|
|
44
46
|
ariaLabel,
|
|
45
47
|
className,
|
|
@@ -49,6 +51,7 @@ export function MultiSelectCheckbox({
|
|
|
49
51
|
...baseStyle,
|
|
50
52
|
...(isSelected ? selectedStyle : null),
|
|
51
53
|
...(alwaysVisible || isSelected ? { opacity: 1 } : null),
|
|
54
|
+
...(disabled ? { opacity: 0.45, cursor: 'not-allowed' } : null),
|
|
52
55
|
...(style || null),
|
|
53
56
|
};
|
|
54
57
|
|
|
@@ -62,6 +65,7 @@ export function MultiSelectCheckbox({
|
|
|
62
65
|
type="button"
|
|
63
66
|
role="checkbox"
|
|
64
67
|
aria-checked={isSelected}
|
|
68
|
+
disabled={disabled}
|
|
65
69
|
aria-label={ariaLabel ?? (isSelected ? 'Deselect item' : 'Select item')}
|
|
66
70
|
onClick={onClick}
|
|
67
71
|
onMouseDown={(e) => e.stopPropagation()}
|