@notis_ai/cli 0.2.0-beta.16.1 → 0.2.0-beta.161.1
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 +435 -133
- package/bin/check-runtime.js +15 -0
- package/bin/notis.js +2 -0
- package/config/notis_app_boundary_rules.json +50 -0
- package/config/notis_app_design_rules.json +135 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +19008 -0
- package/dist/base-skills/notis-apps/SKILL.md +70 -0
- package/dist/base-skills/notis-apps/references/architecture.md +164 -0
- package/dist/base-skills/notis-apps/references/context.md +81 -0
- package/dist/base-skills/notis-apps/references/design.md +165 -0
- package/dist/base-skills/notis-apps/references/reading.md +89 -0
- package/dist/base-skills/notis-apps/references/release.md +99 -0
- package/dist/base-skills/notis-apps/references/sdk.md +62 -0
- package/dist/base-skills/notis-apps/references/troubleshooting.md +23 -0
- package/dist/base-skills/notis-cli/SKILL.md +140 -0
- package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +20 -0
- package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
- package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
- package/dist/base-skills/notis-query/SKILL.md +67 -0
- package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
- package/dist/base-skills/notis-query/references/documents.md +50 -0
- package/dist/base-skills/notis-query/references/query.md +543 -0
- package/dist/skill-sync/index.js +1626 -0
- package/dist/skill-sync/index.js.map +7 -0
- package/dist/skill-sync-worker.mjs +2990 -0
- package/package.json +18 -7
- package/skills/notis-apps/cli.md +313 -0
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
- package/skills/notis-onboarding/BRIEF.md +129 -0
- package/skills/notis-query/cli.md +39 -0
- package/src/agent-hook-entry.js +5 -0
- package/src/cli.js +294 -25
- package/src/command-specs/agents.js +392 -0
- package/src/command-specs/apps.js +1470 -202
- package/src/command-specs/auth.js +114 -137
- package/src/command-specs/diagnostics.js +729 -0
- package/src/command-specs/handover.js +374 -0
- package/src/command-specs/helpers.js +84 -82
- package/src/command-specs/index.js +25 -6
- package/src/command-specs/meta.js +150 -18
- package/src/command-specs/onboarding.js +290 -0
- package/src/command-specs/profile.js +358 -0
- package/src/command-specs/reports.js +86 -0
- package/src/command-specs/skills.js +75 -0
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/tools.js +455 -139
- package/src/runtime/agent-browser.js +632 -0
- package/src/runtime/agent-memory-state.js +126 -0
- package/src/runtime/agent-setup.js +383 -0
- package/src/runtime/app-boundary-validator.js +404 -0
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-platform.js +2633 -210
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/app-test-server.js +292 -0
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/auth-recovery.js +110 -0
- package/src/runtime/base-skills.d.ts +20 -0
- package/src/runtime/base-skills.js +167 -0
- package/src/runtime/channel.js +133 -0
- package/src/runtime/delegated-context.js +68 -0
- package/src/runtime/errors.js +1 -0
- package/src/runtime/git.js +233 -0
- package/src/runtime/login-listener.js +15 -0
- package/src/runtime/oauth.js +2622 -0
- package/src/runtime/output.js +37 -5
- package/src/runtime/ports.js +31 -0
- package/src/runtime/profiles.js +906 -55
- package/src/runtime/skill-sync/cloud-client.ts +99 -0
- package/src/runtime/skill-sync/index.ts +697 -0
- package/src/runtime/skill-sync/local-scanner.ts +1046 -0
- package/src/runtime/skill-sync/symlink-manager.ts +433 -0
- package/src/runtime/skill-sync/sync-plan.ts +22 -0
- package/src/runtime/skill-sync/types.ts +110 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
- package/src/runtime/skill-sync-service.js +109 -0
- package/src/runtime/store-screenshot.js +143 -0
- package/src/runtime/sync-skills.d.ts +37 -0
- package/src/runtime/sync-skills.js +231 -0
- package/src/runtime/telemetry.js +92 -0
- package/src/runtime/transport.js +324 -45
- package/src/skill-sync-worker-entry.js +2 -0
- package/src/skill-sync-worker.js +50 -0
- package/template/.harness/index.html.tmpl +430 -0
- package/template/CHANGELOG.md +5 -0
- package/template/app/globals.css +28 -3
- package/template/app/layout.tsx +6 -3
- package/template/app/page.tsx +49 -42
- package/template/components/page-heading.tsx +23 -0
- package/template/components/ui/badge.tsx +7 -4
- package/template/components/ui/button.tsx +1 -1
- package/template/components/ui/card.tsx +24 -11
- package/template/components/ui/native-select.tsx +24 -0
- package/template/notis.config.ts +24 -6
- package/template/package-lock.json +3642 -0
- package/template/package.json +19 -16
- package/template/packages/{notis-sdk → sdk}/package.json +14 -4
- package/template/packages/sdk/src/agentContext.ts +36 -0
- package/template/packages/sdk/src/components/DocumentEditor.tsx +103 -0
- package/template/packages/sdk/src/components/Markdown.tsx +60 -0
- package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +285 -0
- package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
- package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/template/packages/sdk/src/components/NotisCommentBoundary.tsx +172 -0
- package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
- package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
- package/template/packages/sdk/src/components/Skeleton.tsx +24 -0
- package/template/packages/sdk/src/config.ts +257 -0
- package/template/packages/sdk/src/documents.ts +256 -0
- package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
- package/template/packages/sdk/src/hooks/useAgentContext.ts +23 -0
- package/template/packages/sdk/src/hooks/useCloudComputer.ts +64 -0
- package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +836 -0
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +49 -0
- package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/template/packages/sdk/src/hooks/useDocument.ts +43 -0
- package/template/packages/sdk/src/hooks/useDocuments.ts +84 -0
- package/template/packages/sdk/src/hooks/useHandover.ts +78 -0
- package/template/packages/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +95 -0
- package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +10 -4
- package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +11 -8
- package/template/packages/sdk/src/hooks/useQuery.ts +71 -0
- package/template/packages/sdk/src/hooks/useTool.ts +65 -0
- package/template/packages/sdk/src/hooks/useToolQuery.ts +12 -0
- package/template/packages/sdk/src/hooks/useTopBarSearch.ts +81 -0
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/template/packages/sdk/src/index.ts +161 -0
- package/template/packages/sdk/src/interactions/actions.ts +59 -0
- package/template/packages/sdk/src/interactions/shortcuts.tsx +694 -0
- package/template/packages/sdk/src/interactions/visibility.ts +13 -0
- package/template/packages/sdk/src/interactions.ts +45 -0
- package/template/packages/sdk/src/provider.tsx +44 -0
- package/template/packages/sdk/src/queryCache.ts +170 -0
- package/template/packages/sdk/src/runtime.ts +451 -0
- package/template/packages/sdk/src/styles.css +247 -0
- package/template/packages/sdk/src/tailwind.ts +66 -0
- package/template/packages/sdk/src/vite.ts +73 -0
- package/template/packages/{notis-sdk → sdk}/tsconfig.json +1 -0
- package/template/postcss.config.mjs +1 -1
- package/template/tailwind.config.ts +1 -6
- package/template/tsconfig.json +1 -0
- package/src/command-specs/db.js +0 -163
- package/src/runtime/app-preview-server.js +0 -312
- package/template/packages/notis-sdk/src/config.ts +0 -48
- package/template/packages/notis-sdk/src/helpers.ts +0 -131
- package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
- package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
- package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
- package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
- package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
- package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
- package/template/packages/notis-sdk/src/index.ts +0 -47
- package/template/packages/notis-sdk/src/provider.tsx +0 -44
- package/template/packages/notis-sdk/src/runtime.ts +0 -159
- package/template/packages/notis-sdk/src/styles.css +0 -123
- package/template/packages/notis-sdk/src/vite.ts +0 -54
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
package/template/package.json
CHANGED
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-notis-app",
|
|
3
3
|
"version": "0.1.0",
|
|
4
|
+
"notisAppVersion": "0.1.0",
|
|
4
5
|
"private": true,
|
|
5
6
|
"type": "module",
|
|
6
7
|
"scripts": {
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"type-check": "tsc --noEmit",
|
|
9
|
+
"dev": "vite --configLoader runner",
|
|
10
|
+
"build": "vite build --configLoader runner"
|
|
10
11
|
},
|
|
11
12
|
"dependencies": {
|
|
12
|
-
"@notis/sdk": "file:./packages/
|
|
13
|
-
"@
|
|
13
|
+
"@notis/sdk": "file:./packages/sdk",
|
|
14
|
+
"@phosphor-icons/react": "^2.1.10",
|
|
15
|
+
"@radix-ui/react-slot": "^1.3.3",
|
|
14
16
|
"class-variance-authority": "^0.7.0",
|
|
15
17
|
"clsx": "^2.1.1",
|
|
16
|
-
"
|
|
17
|
-
"react": "^19.
|
|
18
|
-
"
|
|
19
|
-
"tailwind-merge": "^2.6.0",
|
|
18
|
+
"react": "^19.2.8",
|
|
19
|
+
"react-dom": "^19.2.8",
|
|
20
|
+
"tailwind-merge": "^3.6.0",
|
|
20
21
|
"tailwindcss-animate": "^1.0.7"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@types/node": "^22.0.0",
|
|
24
|
-
"@types/react": "^19.
|
|
25
|
-
"@types/react-dom": "^19.
|
|
26
|
-
"@vitejs/plugin-react": "^
|
|
27
|
-
"postcss": "^8.
|
|
28
|
-
"tailwindcss": "^3.
|
|
29
|
-
"typescript": "
|
|
30
|
-
"
|
|
25
|
+
"@types/react": "^19.2.18",
|
|
26
|
+
"@types/react-dom": "^19.2.7",
|
|
27
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
28
|
+
"postcss": "^8.5.28",
|
|
29
|
+
"tailwindcss": "^4.3.3",
|
|
30
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
31
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
32
|
+
"vite": "^8.2.2",
|
|
33
|
+
"@tailwindcss/postcss": "^4.3.3"
|
|
31
34
|
}
|
|
32
35
|
}
|
|
@@ -1,19 +1,26 @@
|
|
|
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",
|
|
11
12
|
"./styles.css": "./src/styles.css"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
|
-
"src"
|
|
15
|
-
"template"
|
|
15
|
+
"src"
|
|
16
16
|
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"type-check": "tsc --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"react-markdown": "^10.1.0",
|
|
22
|
+
"remark-gfm": "^4.0.1"
|
|
23
|
+
},
|
|
17
24
|
"peerDependencies": {
|
|
18
25
|
"react": ">=18.0.0",
|
|
19
26
|
"react-dom": ">=18.0.0",
|
|
@@ -21,6 +28,9 @@
|
|
|
21
28
|
"@vitejs/plugin-react": ">=4.0.0"
|
|
22
29
|
},
|
|
23
30
|
"devDependencies": {
|
|
24
|
-
"
|
|
31
|
+
"@types/node": "^20.0.0",
|
|
32
|
+
"@types/react": "^19.2.18",
|
|
33
|
+
"@types/react-dom": "^19.2.7",
|
|
34
|
+
"typescript": "^7.0.2"
|
|
25
35
|
}
|
|
26
36
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Serializable context shared with the agent. Apps own annotation storage. */
|
|
2
|
+
export interface ContextAttachment {
|
|
3
|
+
/** Durable HTTPS URL supplied by the app. Sent through the host media pipeline. */
|
|
4
|
+
url: string;
|
|
5
|
+
name: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AgentContextContent {
|
|
10
|
+
title?: string;
|
|
11
|
+
/** Emoji, Phosphor icon name, or HTTPS image URL. Never executable markup. */
|
|
12
|
+
icon?: string;
|
|
13
|
+
/** An app-defined kind, not a closed list of annotation types. */
|
|
14
|
+
kind?: string;
|
|
15
|
+
text?: string;
|
|
16
|
+
comment?: string;
|
|
17
|
+
preview?: { format: 'text' | 'markdown'; content: string };
|
|
18
|
+
/** Any JSON-serializable value: coordinates, chart state, loaded records, etc. */
|
|
19
|
+
data?: unknown;
|
|
20
|
+
attachments?: ContextAttachment[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AgentContextItem extends AgentContextContent {
|
|
24
|
+
/** Stable within this app. The host scopes IDs to the originating app. */
|
|
25
|
+
id: string;
|
|
26
|
+
resource?: import('./runtime').ContextResource | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Host-stamped origin. Apps describe resources; the host identifies the app/view. */
|
|
30
|
+
export interface AgentContextSource {
|
|
31
|
+
appId: string;
|
|
32
|
+
appName: string;
|
|
33
|
+
viewSlug: string;
|
|
34
|
+
viewName: string;
|
|
35
|
+
viewUrl?: string | null;
|
|
36
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties, ReactElement } from 'react';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import { useDocument } from '../hooks/useDocument';
|
|
6
|
+
import { Markdown } from './Markdown';
|
|
7
|
+
import type { NotisDocumentEditorProps } from '../runtime';
|
|
8
|
+
|
|
9
|
+
const fallbackNoticeStyle: CSSProperties = {
|
|
10
|
+
marginTop: '0.75rem',
|
|
11
|
+
fontSize: '12px',
|
|
12
|
+
color: 'hsl(var(--muted-foreground))',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const fallbackFrameStyle: CSSProperties = {
|
|
16
|
+
borderRadius: '0.5rem',
|
|
17
|
+
border: '1px dashed hsl(var(--border))',
|
|
18
|
+
padding: '1rem',
|
|
19
|
+
fontSize: '13px',
|
|
20
|
+
color: 'hsl(var(--muted-foreground))',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const fallbackTitleStyle: CSSProperties = {
|
|
24
|
+
margin: '0 0 0.75rem',
|
|
25
|
+
fontSize: '1.5rem',
|
|
26
|
+
fontWeight: 700,
|
|
27
|
+
letterSpacing: '-0.01em',
|
|
28
|
+
color: 'hsl(var(--foreground))',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Read-only stand-in used when no host editor is available (standalone dev
|
|
33
|
+
* harness, snapshots). Markdown content renders via `Markdown`; other content
|
|
34
|
+
* types get a typed placeholder.
|
|
35
|
+
*/
|
|
36
|
+
function DocumentEditorFallback({ documentId, variant = 'full', className }: NotisDocumentEditorProps) {
|
|
37
|
+
const runtime = useNotisRuntime();
|
|
38
|
+
const { document, loading, error } = useDocument(documentId);
|
|
39
|
+
// Listing screenshots should show the app as it looks in the portal, so the
|
|
40
|
+
// harness-only editing notice stays out of scenario captures.
|
|
41
|
+
const isScreenshot = Boolean(runtime?.context?.screenshotScenario);
|
|
42
|
+
|
|
43
|
+
if (loading) {
|
|
44
|
+
return <div className={className} style={fallbackFrameStyle}>Loading document…</div>;
|
|
45
|
+
}
|
|
46
|
+
if (error || !document) {
|
|
47
|
+
return (
|
|
48
|
+
<div className={className} style={fallbackFrameStyle}>
|
|
49
|
+
{error?.message ?? 'Document not found.'}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isMarkdown = document.contentType === 'markdown' || !document.contentType;
|
|
55
|
+
return (
|
|
56
|
+
<div className={className}>
|
|
57
|
+
{variant === 'full' ? <h1 style={fallbackTitleStyle}>{document.title}</h1> : null}
|
|
58
|
+
{isMarkdown ? (
|
|
59
|
+
<>
|
|
60
|
+
<Markdown value={document.contentMarkdown ?? ''} />
|
|
61
|
+
{isScreenshot ? null : (
|
|
62
|
+
<p style={fallbackNoticeStyle}>Read-only preview — editing is available inside the Notis portal.</p>
|
|
63
|
+
)}
|
|
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>
|
|
74
|
+
) : (
|
|
75
|
+
<div style={fallbackFrameStyle}>
|
|
76
|
+
{document.fileType ? `${document.fileType.toUpperCase()} document` : 'File document'} — open it in
|
|
77
|
+
the Notis portal to view.
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Embeds the document editor for a document anywhere in an app view. The host
|
|
86
|
+
* (portal) provides the implementation through `runtime.ui.DocumentEditor` and
|
|
87
|
+
* dispatches on the document's content type (markdown -> rich text editor,
|
|
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.
|
|
91
|
+
*
|
|
92
|
+
* ```tsx
|
|
93
|
+
* <DocumentEditor documentId={entry.id} variant="body" className="min-h-[320px]" />
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export function DocumentEditor(props: NotisDocumentEditorProps): ReactElement {
|
|
97
|
+
const runtime = useNotisRuntime();
|
|
98
|
+
const HostEditor = runtime?.ui?.DocumentEditor;
|
|
99
|
+
if (HostEditor) {
|
|
100
|
+
return <HostEditor {...props} />;
|
|
101
|
+
}
|
|
102
|
+
return <DocumentEditorFallback {...props} />;
|
|
103
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { AnchorHTMLAttributes, ReactElement } from 'react';
|
|
4
|
+
import ReactMarkdown, { type Components } from 'react-markdown';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
6
|
+
|
|
7
|
+
export interface MarkdownProps {
|
|
8
|
+
/** Markdown source to render. */
|
|
9
|
+
value: string;
|
|
10
|
+
/** Typography scale. 'sm' suits dense surfaces (cards, list rows). */
|
|
11
|
+
size?: 'sm' | 'base';
|
|
12
|
+
className?: string;
|
|
13
|
+
/** Per-element overrides, passed through to react-markdown. */
|
|
14
|
+
components?: Components;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function MarkdownLink({
|
|
18
|
+
href,
|
|
19
|
+
children,
|
|
20
|
+
node: _node,
|
|
21
|
+
...rest
|
|
22
|
+
}: AnchorHTMLAttributes<HTMLAnchorElement> & { node?: unknown }) {
|
|
23
|
+
const isExternal = typeof href === 'string' && /^https?:\/\//i.test(href);
|
|
24
|
+
return (
|
|
25
|
+
<a
|
|
26
|
+
href={href}
|
|
27
|
+
{...(isExternal ? { target: '_blank', rel: 'noreferrer noopener' } : {})}
|
|
28
|
+
{...rest}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</a>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const defaultComponents: Components = {
|
|
36
|
+
a: MarkdownLink,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Renders a markdown value with Notis typography. Markdown is a value type:
|
|
41
|
+
* use this anywhere markdown appears — document content, database fields,
|
|
42
|
+
* generated text. Styling comes from `.notis-markdown` rules in the SDK
|
|
43
|
+
* stylesheet and follows the portal theme variables.
|
|
44
|
+
*/
|
|
45
|
+
export function Markdown({ value, size = 'base', className, components }: MarkdownProps): ReactElement {
|
|
46
|
+
const classes = ['notis-markdown', size === 'sm' ? 'notis-markdown--sm' : null, className]
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join(' ');
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className={classes}>
|
|
52
|
+
<ReactMarkdown
|
|
53
|
+
remarkPlugins={[remarkGfm]}
|
|
54
|
+
components={components ? { ...defaultComponents, ...components } : defaultComponents}
|
|
55
|
+
>
|
|
56
|
+
{value}
|
|
57
|
+
</ReactMarkdown>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -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
|
+
}
|