@notis_ai/cli 0.2.14 → 0.2.15
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 +82 -20
- package/dist/agent-hooks/notis-agent-hook.mjs +19750 -0
- package/{skills → dist/base-skills}/notis-apps/SKILL.md +84 -53
- package/{skills → dist/base-skills}/notis-cli/SKILL.md +9 -9
- 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 +38 -17
- 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 +871 -387
- 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-roots.js +284 -0
- package/src/runtime/app-dev-server.js +212 -22
- package/src/runtime/app-dev-sessions.js +105 -145
- package/src/runtime/app-platform.js +1291 -165
- 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 +0 -5
- 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 +215 -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 +37 -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 +19 -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 +709 -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/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 +628 -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 +89 -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,215 @@
|
|
|
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
|
+
/** Serialize Desktop and terminal skill sync across processes on one Mac. */
|
|
88
|
+
export async function withSkillSyncLock(callback, {
|
|
89
|
+
home = homedir(),
|
|
90
|
+
timeoutMs = DEFAULT_LOCK_TIMEOUT_MS,
|
|
91
|
+
staleMs = DEFAULT_LOCK_STALE_MS,
|
|
92
|
+
pollMs = DEFAULT_LOCK_POLL_MS,
|
|
93
|
+
now = () => Date.now(),
|
|
94
|
+
} = {}) {
|
|
95
|
+
const lockDirectory = join(home, '.notis', 'skills', '.operation-lock');
|
|
96
|
+
const ownerId = `${process.pid}.${randomUUID()}`;
|
|
97
|
+
const deadline = now() + timeoutMs;
|
|
98
|
+
await mkdir(dirname(lockDirectory), { recursive: true, mode: 0o700 });
|
|
99
|
+
|
|
100
|
+
for (;;) {
|
|
101
|
+
try {
|
|
102
|
+
await mkdir(lockDirectory, { mode: 0o700 });
|
|
103
|
+
try {
|
|
104
|
+
await writeLockOwnerAtomically(lockDirectory, {
|
|
105
|
+
id: ownerId,
|
|
106
|
+
pid: process.pid,
|
|
107
|
+
at: now(),
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
// This process exclusively created the directory and has not yet
|
|
111
|
+
// published an owner, so it is safe to undo a failed acquisition.
|
|
112
|
+
await rm(lockDirectory, { recursive: true, force: true });
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const observed = await lockSnapshot(lockDirectory);
|
|
121
|
+
if (
|
|
122
|
+
observed
|
|
123
|
+
&& now() - observed.at > staleMs
|
|
124
|
+
&& !processIsAlive(observed.pid)
|
|
125
|
+
) {
|
|
126
|
+
// Require an unchanged observation interval before stealing. This avoids
|
|
127
|
+
// racing a holder that is publishing or refreshing its owner lease.
|
|
128
|
+
await delay(Math.max(pollMs, 10));
|
|
129
|
+
const current = await lockSnapshot(lockDirectory);
|
|
130
|
+
if (sameLockSnapshot(observed, current) && !processIsAlive(current?.pid)) {
|
|
131
|
+
if (await quarantineStaleLock(lockDirectory, current)) continue;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (now() >= deadline) {
|
|
135
|
+
throw new Error('Timed out waiting for another Notis skill sync to finish.');
|
|
136
|
+
}
|
|
137
|
+
await delay(pollMs);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const heartbeatMs = Math.max(10, Math.min(30_000, Math.floor(staleMs / 3)));
|
|
141
|
+
let heartbeatStopped = false;
|
|
142
|
+
let heartbeatInFlight = Promise.resolve();
|
|
143
|
+
const refreshHeartbeat = async () => {
|
|
144
|
+
if (heartbeatStopped) return;
|
|
145
|
+
const temporaryOwnerPath = join(lockDirectory, `.owner.${ownerId}.tmp`);
|
|
146
|
+
await writeFile(
|
|
147
|
+
temporaryOwnerPath,
|
|
148
|
+
JSON.stringify({ id: ownerId, pid: process.pid, at: now() }),
|
|
149
|
+
{ mode: 0o600 },
|
|
150
|
+
);
|
|
151
|
+
const owner = await lockSnapshot(lockDirectory);
|
|
152
|
+
if (owner?.id !== ownerId) {
|
|
153
|
+
await rm(temporaryOwnerPath, { force: true });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// If a stale-lock reclaimer moved this directory after the ownership
|
|
157
|
+
// check, the source path disappears and rename fails instead of
|
|
158
|
+
// overwriting the owner of a newly acquired lock at the shared path.
|
|
159
|
+
await rename(temporaryOwnerPath, join(lockDirectory, 'owner'));
|
|
160
|
+
};
|
|
161
|
+
const heartbeat = setInterval(() => {
|
|
162
|
+
// Serialize refreshes and retain the active promise so cleanup cannot race
|
|
163
|
+
// a temporary owner write that was already in flight when the interval was
|
|
164
|
+
// cleared.
|
|
165
|
+
heartbeatInFlight = heartbeatInFlight
|
|
166
|
+
.then(refreshHeartbeat)
|
|
167
|
+
.catch(() => undefined);
|
|
168
|
+
}, heartbeatMs);
|
|
169
|
+
heartbeat.unref?.();
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
return await callback();
|
|
173
|
+
} finally {
|
|
174
|
+
heartbeatStopped = true;
|
|
175
|
+
clearInterval(heartbeat);
|
|
176
|
+
await heartbeatInFlight;
|
|
177
|
+
const owner = await lockSnapshot(lockDirectory);
|
|
178
|
+
if (owner?.id === ownerId) {
|
|
179
|
+
await rm(lockDirectory, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Shared CLI-owned orchestration for base-skill installation plus account sync.
|
|
186
|
+
* Desktop injects the source engine that webpack bundles; the CLI command
|
|
187
|
+
* injects the generated dist engine used by the published package.
|
|
188
|
+
*/
|
|
189
|
+
export async function reconcileAllSkills({
|
|
190
|
+
serverUrl,
|
|
191
|
+
jwt,
|
|
192
|
+
honorSyncEnabled,
|
|
193
|
+
userId = null,
|
|
194
|
+
home,
|
|
195
|
+
runAccountSync,
|
|
196
|
+
reconcileBase = reconcileBaseSkills,
|
|
197
|
+
lockOptions = {},
|
|
198
|
+
}) {
|
|
199
|
+
return withSkillSyncLock(async () => {
|
|
200
|
+
const base = reconcileBase({ userId, ...(home ? { home } : {}) });
|
|
201
|
+
const account = await runAccountSync(
|
|
202
|
+
serverUrl,
|
|
203
|
+
jwt,
|
|
204
|
+
{},
|
|
205
|
+
{ honorSyncEnabled },
|
|
206
|
+
);
|
|
207
|
+
return {
|
|
208
|
+
...account,
|
|
209
|
+
baseSkills: base.skills,
|
|
210
|
+
baseInstalled: base.installed,
|
|
211
|
+
baseLinked: base.linked,
|
|
212
|
+
baseBackups: base.backups,
|
|
213
|
+
};
|
|
214
|
+
}, { ...lockOptions, ...(home ? { home } : {}) });
|
|
215
|
+
}
|
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 = {
|
|
@@ -180,43 +108,25 @@ export function MultiSelectActionBar({
|
|
|
180
108
|
itemLabel,
|
|
181
109
|
className,
|
|
182
110
|
style,
|
|
111
|
+
shortcutsEnabled = true,
|
|
112
|
+
shortcutScope = 'collection',
|
|
183
113
|
}: 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);
|
|
114
|
+
const actionShortcuts = useMemo<ShortcutDefinition[]>(() => {
|
|
115
|
+
return actions.flatMap((action): ShortcutDefinition[] => {
|
|
116
|
+
if (!action.shortcut || action.disabled || action.pending) return [];
|
|
117
|
+
return [{
|
|
118
|
+
id: `collection.action.${action.id}`,
|
|
119
|
+
keys: action.shortcut,
|
|
120
|
+
label: action.label,
|
|
121
|
+
onTrigger: () => action.onRun(),
|
|
122
|
+
}];
|
|
123
|
+
});
|
|
195
124
|
}, [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]);
|
|
125
|
+
useShortcuts(actionShortcuts, {
|
|
126
|
+
enabled: shortcutsEnabled && selectedCount > 0,
|
|
127
|
+
scope: shortcutScope,
|
|
128
|
+
priority: 25,
|
|
129
|
+
});
|
|
220
130
|
|
|
221
131
|
if (selectedCount === 0) return null;
|
|
222
132
|
|
|
@@ -258,21 +168,29 @@ function ActionButton({ action }: { action: MultiSelectAction }) {
|
|
|
258
168
|
color: hover
|
|
259
169
|
? 'hsl(var(--background))'
|
|
260
170
|
: baseButtonStyle.color,
|
|
171
|
+
opacity: action.disabled ? 0.45 : 1,
|
|
172
|
+
cursor: action.disabled || action.pending ? 'not-allowed' : 'pointer',
|
|
261
173
|
};
|
|
262
174
|
|
|
263
175
|
return (
|
|
264
176
|
<button
|
|
265
177
|
type="button"
|
|
266
|
-
onClick={() =>
|
|
178
|
+
onClick={() => {
|
|
179
|
+
if (!action.disabled && !action.pending) void action.onRun();
|
|
180
|
+
}}
|
|
181
|
+
disabled={action.disabled || action.pending}
|
|
182
|
+
aria-busy={action.pending || undefined}
|
|
183
|
+
aria-keyshortcuts={action.shortcut || undefined}
|
|
184
|
+
data-destructive={action.destructive ? 'true' : 'false'}
|
|
267
185
|
onMouseEnter={() => setHover(true)}
|
|
268
186
|
onMouseLeave={() => setHover(false)}
|
|
269
187
|
onFocus={() => setHover(true)}
|
|
270
188
|
onBlur={() => setHover(false)}
|
|
271
189
|
style={buttonStyle}
|
|
272
190
|
>
|
|
273
|
-
<span aria-hidden style={iconSlotStyle}>{action.icon}</span>
|
|
191
|
+
{action.icon ? <span aria-hidden style={iconSlotStyle}>{action.icon}</span> : null}
|
|
274
192
|
{display ? <kbd aria-hidden style={keycapStyle}>{display}</kbd> : null}
|
|
275
|
-
<span>{action.label}</span>
|
|
193
|
+
<span>{action.pending ? `${action.label}…` : action.label}</span>
|
|
276
194
|
</button>
|
|
277
195
|
);
|
|
278
196
|
}
|
|
@@ -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()}
|