@nimbalyst/extension-sdk 0.1.0
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/CHANGELOG.md +39 -0
- package/README.md +151 -0
- package/dist/MaterialSymbol.d.ts +2 -0
- package/dist/MaterialSymbol.d.ts.map +1 -0
- package/dist/MaterialSymbol.js +1 -0
- package/dist/clipboard.d.ts +14 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +27 -0
- package/dist/createReadOnlyHost.d.ts +45 -0
- package/dist/createReadOnlyHost.d.ts.map +1 -0
- package/dist/createReadOnlyHost.js +81 -0
- package/dist/documentPath.d.ts +6 -0
- package/dist/documentPath.d.ts.map +1 -0
- package/dist/documentPath.js +4 -0
- package/dist/externals.d.ts +23 -0
- package/dist/externals.d.ts.map +1 -0
- package/dist/externals.js +42 -0
- package/dist/externalsPlugin.d.ts +39 -0
- package/dist/externalsPlugin.d.ts.map +1 -0
- package/dist/externalsPlugin.js +251 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/tailwind.d.ts +112 -0
- package/dist/tailwind.d.ts.map +1 -0
- package/dist/tailwind.js +129 -0
- package/dist/testing.d.ts +122 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +199 -0
- package/dist/types/editor.d.ts +385 -0
- package/dist/types/editor.d.ts.map +1 -0
- package/dist/types/editor.js +15 -0
- package/dist/types/editors.d.ts +56 -0
- package/dist/types/editors.d.ts.map +1 -0
- package/dist/types/editors.js +19 -0
- package/dist/types/extension.d.ts +744 -0
- package/dist/types/extension.d.ts.map +1 -0
- package/dist/types/extension.js +4 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/panel.d.ts +575 -0
- package/dist/types/panel.d.ts.map +1 -0
- package/dist/types/panel.js +14 -0
- package/dist/types/theme.d.ts +148 -0
- package/dist/types/theme.d.ts.map +1 -0
- package/dist/types/theme.js +7 -0
- package/dist/useEditorLifecycle.d.ts +166 -0
- package/dist/useEditorLifecycle.d.ts.map +1 -0
- package/dist/useEditorLifecycle.js +327 -0
- package/dist/validate.d.ts +31 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +128 -0
- package/dist/vite.d.ts +92 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +179 -0
- package/package.json +91 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for Nimbalyst theme system.
|
|
3
|
+
*
|
|
4
|
+
* Themes are separate from extensions - they provide styling only, no code.
|
|
5
|
+
* This module defines the schema for theme.json manifests and theme metadata.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Available theme color keys that can be overridden.
|
|
9
|
+
* These map to CSS variables (--nim-*) used throughout the UI.
|
|
10
|
+
*/
|
|
11
|
+
export type ThemeColorKey = 'bg' | 'bg-secondary' | 'bg-tertiary' | 'bg-hover' | 'bg-selected' | 'bg-active' | 'text' | 'text-muted' | 'text-faint' | 'text-disabled' | 'border' | 'border-focus' | 'primary' | 'primary-hover' | 'link' | 'link-hover' | 'success' | 'warning' | 'error' | 'info';
|
|
12
|
+
/**
|
|
13
|
+
* Theme color values.
|
|
14
|
+
* All colors are optional - missing colors will fall back to base theme.
|
|
15
|
+
*/
|
|
16
|
+
export type ThemeColors = Partial<Record<ThemeColorKey, string>>;
|
|
17
|
+
/**
|
|
18
|
+
* Theme manifest schema (theme.json).
|
|
19
|
+
* This is the schema for standalone theme packages.
|
|
20
|
+
*/
|
|
21
|
+
export interface ThemeManifest {
|
|
22
|
+
/** Unique theme identifier (e.g., 'solarized-dark') */
|
|
23
|
+
id: string;
|
|
24
|
+
/** Display name shown in UI */
|
|
25
|
+
name: string;
|
|
26
|
+
/** Semantic version */
|
|
27
|
+
version: string;
|
|
28
|
+
/** Theme author name or organization */
|
|
29
|
+
author?: string;
|
|
30
|
+
/** Brief description */
|
|
31
|
+
description?: string;
|
|
32
|
+
/** Whether this is a dark theme (determines base theme for fallbacks) */
|
|
33
|
+
isDark: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Theme color overrides.
|
|
36
|
+
* Only include colors you want to override from the base theme.
|
|
37
|
+
* All color values must be valid hex codes or CSS color names.
|
|
38
|
+
*/
|
|
39
|
+
colors: ThemeColors;
|
|
40
|
+
/** Path to preview image (relative to theme.json) */
|
|
41
|
+
preview?: string;
|
|
42
|
+
/** Tags for discovery and filtering */
|
|
43
|
+
tags?: string[];
|
|
44
|
+
/** License identifier (SPDX) */
|
|
45
|
+
license?: string;
|
|
46
|
+
/** Homepage or repository URL */
|
|
47
|
+
homepage?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Full theme object with metadata and resolved colors.
|
|
51
|
+
* This is what's used at runtime after loading a theme.
|
|
52
|
+
*/
|
|
53
|
+
export interface Theme {
|
|
54
|
+
/** Theme ID (unique across all themes) */
|
|
55
|
+
id: string;
|
|
56
|
+
/** Display name */
|
|
57
|
+
name: string;
|
|
58
|
+
/** Semantic version */
|
|
59
|
+
version: string;
|
|
60
|
+
/** Author */
|
|
61
|
+
author?: string;
|
|
62
|
+
/** Description */
|
|
63
|
+
description?: string;
|
|
64
|
+
/** Is dark theme */
|
|
65
|
+
isDark: boolean;
|
|
66
|
+
/** Resolved color values (merged with base theme) */
|
|
67
|
+
colors: ThemeColors;
|
|
68
|
+
/** Tags */
|
|
69
|
+
tags?: string[];
|
|
70
|
+
/** Preview image path (absolute) */
|
|
71
|
+
previewPath?: string;
|
|
72
|
+
/** Source of the theme */
|
|
73
|
+
source: ThemeSource;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Where the theme came from.
|
|
77
|
+
*/
|
|
78
|
+
export type ThemeSource = {
|
|
79
|
+
type: 'builtin';
|
|
80
|
+
} | {
|
|
81
|
+
type: 'user';
|
|
82
|
+
installPath: string;
|
|
83
|
+
} | {
|
|
84
|
+
type: 'extension';
|
|
85
|
+
extensionId: string;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Marketplace theme metadata.
|
|
89
|
+
* Used for browsing and discovering themes before installation.
|
|
90
|
+
*/
|
|
91
|
+
export interface MarketplaceTheme {
|
|
92
|
+
/** Theme ID */
|
|
93
|
+
id: string;
|
|
94
|
+
/** Display name */
|
|
95
|
+
name: string;
|
|
96
|
+
/** Author */
|
|
97
|
+
author: string;
|
|
98
|
+
/** Version */
|
|
99
|
+
version: string;
|
|
100
|
+
/** Description */
|
|
101
|
+
description: string;
|
|
102
|
+
/** Download count */
|
|
103
|
+
downloads: number;
|
|
104
|
+
/** Average rating (0-5) */
|
|
105
|
+
rating: number;
|
|
106
|
+
/** Tags */
|
|
107
|
+
tags: string[];
|
|
108
|
+
/** URL to preview image */
|
|
109
|
+
preview: string;
|
|
110
|
+
/** URL to download .nimtheme file */
|
|
111
|
+
downloadUrl: string;
|
|
112
|
+
/** Last update timestamp */
|
|
113
|
+
lastUpdated: string;
|
|
114
|
+
/** License */
|
|
115
|
+
license?: string;
|
|
116
|
+
/** Homepage URL */
|
|
117
|
+
homepage?: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Theme validation result.
|
|
121
|
+
*/
|
|
122
|
+
export interface ThemeValidationResult {
|
|
123
|
+
/** Whether the theme is valid */
|
|
124
|
+
valid: boolean;
|
|
125
|
+
/** Validation errors (if any) */
|
|
126
|
+
errors: string[];
|
|
127
|
+
/** Validation warnings (non-fatal) */
|
|
128
|
+
warnings: string[];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Theme installation options.
|
|
132
|
+
*/
|
|
133
|
+
export interface ThemeInstallOptions {
|
|
134
|
+
/** Path to .nimtheme file or directory */
|
|
135
|
+
source: string;
|
|
136
|
+
/** Whether to overwrite existing theme with same ID */
|
|
137
|
+
overwrite?: boolean;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Theme uninstall options.
|
|
141
|
+
*/
|
|
142
|
+
export interface ThemeUninstallOptions {
|
|
143
|
+
/** Theme ID to uninstall */
|
|
144
|
+
themeId: string;
|
|
145
|
+
/** Whether to keep theme files (just disable) */
|
|
146
|
+
keepFiles?: boolean;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=theme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/types/theme.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useEditorLifecycle Hook
|
|
3
|
+
*
|
|
4
|
+
* The single hook for custom editors to integrate with the EditorHost API.
|
|
5
|
+
* Handles all lifecycle concerns: loading, saving, echo detection, dirty state,
|
|
6
|
+
* file change handling, diff mode, theme changes.
|
|
7
|
+
*
|
|
8
|
+
* Content state NEVER lives in this hook. The hook interacts with the editor's
|
|
9
|
+
* state through pull/push callbacks:
|
|
10
|
+
* - applyContent: push new content INTO the editor (on load, external file change)
|
|
11
|
+
* - getCurrentContent: pull current content FROM the editor (on save)
|
|
12
|
+
*
|
|
13
|
+
* This design works for all editor types:
|
|
14
|
+
* - Library-managed (Excalidraw, Three.js): callbacks talk to the library's imperative API
|
|
15
|
+
* - Store-managed (Mindmap, DatamodelLM): callbacks talk to a Zustand store
|
|
16
|
+
* - Read-only (PDF, SQLite): only applyContent, no getCurrentContent
|
|
17
|
+
*
|
|
18
|
+
* @example Library-managed editor (Excalidraw)
|
|
19
|
+
* ```tsx
|
|
20
|
+
* function ExcalidrawEditor({ host }: EditorHostProps) {
|
|
21
|
+
* const apiRef = useRef<ExcalidrawImperativeAPI>(null);
|
|
22
|
+
*
|
|
23
|
+
* const { markDirty, isLoading, theme } = useEditorLifecycle(host, {
|
|
24
|
+
* applyContent: (elements) => apiRef.current?.updateScene({ elements }),
|
|
25
|
+
* getCurrentContent: () => apiRef.current?.getSceneElements() ?? [],
|
|
26
|
+
* parse: (raw) => JSON.parse(raw).elements,
|
|
27
|
+
* serialize: (elements) => JSON.stringify({ elements }),
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* return <Excalidraw ref={apiRef} onChange={(el) => { if (changed(el)) markDirty(); }} />;
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example Store-managed editor (Mindmap)
|
|
35
|
+
* ```tsx
|
|
36
|
+
* function MindmapEditor({ host }: EditorHostProps) {
|
|
37
|
+
* const storeRef = useRef(createMindmapStore());
|
|
38
|
+
* const store = storeRef.current;
|
|
39
|
+
*
|
|
40
|
+
* const { markDirty } = useEditorLifecycle(host, {
|
|
41
|
+
* applyContent: (doc) => store.getState().loadDocument(doc),
|
|
42
|
+
* getCurrentContent: () => store.getState().document,
|
|
43
|
+
* parse: parseDocument,
|
|
44
|
+
* serialize: serializeDocument,
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* return <MindmapCanvas store={store} markDirty={markDirty} />;
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @example Read-only viewer (PDF)
|
|
52
|
+
* ```tsx
|
|
53
|
+
* function PDFViewer({ host }: EditorHostProps) {
|
|
54
|
+
* const dataRef = useRef<ArrayBuffer | null>(null);
|
|
55
|
+
* const [, forceRender] = useReducer((x) => x + 1, 0);
|
|
56
|
+
*
|
|
57
|
+
* const { isLoading } = useEditorLifecycle(host, {
|
|
58
|
+
* applyContent: (data) => { dataRef.current = data; forceRender(); },
|
|
59
|
+
* binary: true,
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* return isLoading ? <Loading /> : <PDFRenderer data={dataRef.current} />;
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
import type { EditorHost, DiffConfig } from './types/editor.js';
|
|
67
|
+
export interface UseEditorLifecycleOptions<T> {
|
|
68
|
+
/**
|
|
69
|
+
* Push new content into the editor.
|
|
70
|
+
* Called on initial load and on external file changes.
|
|
71
|
+
* This should update the editor's internal state WITHOUT triggering
|
|
72
|
+
* a React re-render of the entire editor tree.
|
|
73
|
+
*/
|
|
74
|
+
applyContent: (content: T) => void;
|
|
75
|
+
/**
|
|
76
|
+
* Pull current content from the editor for saving.
|
|
77
|
+
* Omit for read-only editors (PDF viewer, SQLite browser, etc.)
|
|
78
|
+
*/
|
|
79
|
+
getCurrentContent?: () => T;
|
|
80
|
+
/**
|
|
81
|
+
* Parse raw file content (string) into the editor's internal format.
|
|
82
|
+
* If omitted, raw string is passed through as-is.
|
|
83
|
+
*/
|
|
84
|
+
parse?: (raw: string) => T;
|
|
85
|
+
/**
|
|
86
|
+
* Serialize editor's internal format back to string for saving.
|
|
87
|
+
* If omitted, String(content) is used.
|
|
88
|
+
*/
|
|
89
|
+
serialize?: (content: T) => string;
|
|
90
|
+
/**
|
|
91
|
+
* Use loadBinaryContent() instead of loadContent().
|
|
92
|
+
* For binary file formats (PDF, SQLite, images, etc.)
|
|
93
|
+
* When true, parse/serialize are ignored -- applyContent receives the raw
|
|
94
|
+
* ArrayBuffer and getCurrentContent should return an ArrayBuffer.
|
|
95
|
+
*/
|
|
96
|
+
binary?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Called after initial content is loaded and applied.
|
|
99
|
+
* Use for post-load setup (e.g., fitting viewport to content).
|
|
100
|
+
*/
|
|
101
|
+
onLoaded?: () => void;
|
|
102
|
+
/**
|
|
103
|
+
* Called when an external file change is detected and applied.
|
|
104
|
+
* NOT called for echoes from our own saves.
|
|
105
|
+
* Use for editor-specific reactions to external changes (e.g., re-layout).
|
|
106
|
+
*/
|
|
107
|
+
onExternalChange?: (content: T) => void;
|
|
108
|
+
/**
|
|
109
|
+
* Custom save handler. When provided, replaces the default
|
|
110
|
+
* getCurrentContent -> serialize -> saveContent flow.
|
|
111
|
+
* Use for editors that require async content extraction (e.g., RevoGrid).
|
|
112
|
+
* The hook still handles dirty state clearing after onSave resolves.
|
|
113
|
+
*/
|
|
114
|
+
onSave?: () => Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Custom diff request handler. When provided, replaces the default
|
|
117
|
+
* diff parsing and state management. The editor manages its own diff state.
|
|
118
|
+
* Use for editors with specialized diff rendering (e.g., cell-level CSV diff).
|
|
119
|
+
*/
|
|
120
|
+
onDiffRequested?: (config: DiffConfig) => void;
|
|
121
|
+
/**
|
|
122
|
+
* Custom diff cleared handler. When provided, replaces the default
|
|
123
|
+
* diff cleared behavior (reload from disk). Paired with onDiffRequested.
|
|
124
|
+
*/
|
|
125
|
+
onDiffCleared?: () => Promise<void>;
|
|
126
|
+
}
|
|
127
|
+
export interface DiffState<T> {
|
|
128
|
+
/** Content before the AI edit */
|
|
129
|
+
original: T;
|
|
130
|
+
/** Content after the AI edit (what's on disk now) */
|
|
131
|
+
modified: T;
|
|
132
|
+
/** History tag ID for this diff */
|
|
133
|
+
tagId: string;
|
|
134
|
+
/** AI session ID that made the edit */
|
|
135
|
+
sessionId: string;
|
|
136
|
+
/** Accept the AI's changes */
|
|
137
|
+
accept: () => void;
|
|
138
|
+
/** Reject the AI's changes (revert to original) */
|
|
139
|
+
reject: () => void;
|
|
140
|
+
}
|
|
141
|
+
export interface UseEditorLifecycleResult<T> {
|
|
142
|
+
/**
|
|
143
|
+
* Call when the user makes an edit. Marks the document dirty.
|
|
144
|
+
* No-op for read-only editors (when getCurrentContent is not provided).
|
|
145
|
+
*/
|
|
146
|
+
markDirty: () => void;
|
|
147
|
+
/** Whether initial content is still loading. */
|
|
148
|
+
isLoading: boolean;
|
|
149
|
+
/** Error from initial load, or null. */
|
|
150
|
+
error: Error | null;
|
|
151
|
+
/** Current theme (reactive -- updates when user changes theme). */
|
|
152
|
+
theme: string;
|
|
153
|
+
/** Whether the editor has unsaved changes. */
|
|
154
|
+
isDirty: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Diff state when AI proposes changes. null when not in diff mode.
|
|
157
|
+
* Editors should render a diff view when this is non-null.
|
|
158
|
+
*/
|
|
159
|
+
diffState: DiffState<T> | null;
|
|
160
|
+
/** Toggle source mode (switches to Monaco). Only present if host supports it. */
|
|
161
|
+
toggleSourceMode: (() => void) | undefined;
|
|
162
|
+
/** Whether source mode is currently active. */
|
|
163
|
+
isSourceMode: boolean;
|
|
164
|
+
}
|
|
165
|
+
export declare function useEditorLifecycle<T = string>(host: EditorHost, options: UseEditorLifecycleOptions<T>): UseEditorLifecycleResult<T>;
|
|
166
|
+
//# sourceMappingURL=useEditorLifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEditorLifecycle.d.ts","sourceRoot":"","sources":["../src/useEditorLifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAMhE,MAAM,WAAW,yBAAyB,CAAC,CAAC;IAC1C;;;;;OAKG;IACH,YAAY,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;IAEnC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC,CAAC;IAE5B;;;OAGG;IACH,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,CAAC,CAAC;IAE3B;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,MAAM,CAAC;IAEnC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,IAAI,CAAC;IAExC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE7B;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAE/C;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,iCAAiC;IACjC,QAAQ,EAAE,CAAC,CAAC;IAEZ,qDAAqD;IACrD,QAAQ,EAAE,CAAC,CAAC;IAEZ,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IAEd,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAElB,8BAA8B;IAC9B,MAAM,EAAE,MAAM,IAAI,CAAC;IAEnB,mDAAmD;IACnD,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB,CAAC,CAAC;IACzC;;;OAGG;IACH,SAAS,EAAE,MAAM,IAAI,CAAC;IAEtB,gDAAgD;IAChD,SAAS,EAAE,OAAO,CAAC;IAEnB,wCAAwC;IACxC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAEpB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IAEd,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAE/B,iFAAiF;IACjF,gBAAgB,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IAE3C,+CAA+C;IAC/C,YAAY,EAAE,OAAO,CAAC;CACvB;AAMD,wBAAgB,kBAAkB,CAAC,CAAC,GAAG,MAAM,EAC3C,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,yBAAyB,CAAC,CAAC,CAAC,GACpC,wBAAwB,CAAC,CAAC,CAAC,CA+R7B"}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useEditorLifecycle Hook
|
|
3
|
+
*
|
|
4
|
+
* The single hook for custom editors to integrate with the EditorHost API.
|
|
5
|
+
* Handles all lifecycle concerns: loading, saving, echo detection, dirty state,
|
|
6
|
+
* file change handling, diff mode, theme changes.
|
|
7
|
+
*
|
|
8
|
+
* Content state NEVER lives in this hook. The hook interacts with the editor's
|
|
9
|
+
* state through pull/push callbacks:
|
|
10
|
+
* - applyContent: push new content INTO the editor (on load, external file change)
|
|
11
|
+
* - getCurrentContent: pull current content FROM the editor (on save)
|
|
12
|
+
*
|
|
13
|
+
* This design works for all editor types:
|
|
14
|
+
* - Library-managed (Excalidraw, Three.js): callbacks talk to the library's imperative API
|
|
15
|
+
* - Store-managed (Mindmap, DatamodelLM): callbacks talk to a Zustand store
|
|
16
|
+
* - Read-only (PDF, SQLite): only applyContent, no getCurrentContent
|
|
17
|
+
*
|
|
18
|
+
* @example Library-managed editor (Excalidraw)
|
|
19
|
+
* ```tsx
|
|
20
|
+
* function ExcalidrawEditor({ host }: EditorHostProps) {
|
|
21
|
+
* const apiRef = useRef<ExcalidrawImperativeAPI>(null);
|
|
22
|
+
*
|
|
23
|
+
* const { markDirty, isLoading, theme } = useEditorLifecycle(host, {
|
|
24
|
+
* applyContent: (elements) => apiRef.current?.updateScene({ elements }),
|
|
25
|
+
* getCurrentContent: () => apiRef.current?.getSceneElements() ?? [],
|
|
26
|
+
* parse: (raw) => JSON.parse(raw).elements,
|
|
27
|
+
* serialize: (elements) => JSON.stringify({ elements }),
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* return <Excalidraw ref={apiRef} onChange={(el) => { if (changed(el)) markDirty(); }} />;
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example Store-managed editor (Mindmap)
|
|
35
|
+
* ```tsx
|
|
36
|
+
* function MindmapEditor({ host }: EditorHostProps) {
|
|
37
|
+
* const storeRef = useRef(createMindmapStore());
|
|
38
|
+
* const store = storeRef.current;
|
|
39
|
+
*
|
|
40
|
+
* const { markDirty } = useEditorLifecycle(host, {
|
|
41
|
+
* applyContent: (doc) => store.getState().loadDocument(doc),
|
|
42
|
+
* getCurrentContent: () => store.getState().document,
|
|
43
|
+
* parse: parseDocument,
|
|
44
|
+
* serialize: serializeDocument,
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* return <MindmapCanvas store={store} markDirty={markDirty} />;
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @example Read-only viewer (PDF)
|
|
52
|
+
* ```tsx
|
|
53
|
+
* function PDFViewer({ host }: EditorHostProps) {
|
|
54
|
+
* const dataRef = useRef<ArrayBuffer | null>(null);
|
|
55
|
+
* const [, forceRender] = useReducer((x) => x + 1, 0);
|
|
56
|
+
*
|
|
57
|
+
* const { isLoading } = useEditorLifecycle(host, {
|
|
58
|
+
* applyContent: (data) => { dataRef.current = data; forceRender(); },
|
|
59
|
+
* binary: true,
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* return isLoading ? <Loading /> : <PDFRenderer data={dataRef.current} />;
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Hook Implementation
|
|
69
|
+
// ============================================================================
|
|
70
|
+
export function useEditorLifecycle(host, options) {
|
|
71
|
+
// -- React state: only for things that SHOULD cause re-renders --
|
|
72
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
73
|
+
const [error, setError] = useState(null);
|
|
74
|
+
const [isDirty, setIsDirty] = useState(false);
|
|
75
|
+
const [theme, setTheme] = useState(host.theme);
|
|
76
|
+
const [diffState, setDiffState] = useState(null);
|
|
77
|
+
const [isSourceMode, setIsSourceMode] = useState(host.isSourceModeActive?.() ?? false);
|
|
78
|
+
// -- Refs: for everything that should NOT cause re-renders --
|
|
79
|
+
// Options ref -- updated every render so callbacks are never stale
|
|
80
|
+
const optionsRef = useRef(options);
|
|
81
|
+
optionsRef.current = options;
|
|
82
|
+
// Echo detection: stores the last serialized string we saved
|
|
83
|
+
const lastSavedContentRef = useRef('');
|
|
84
|
+
// Prevent double-loading
|
|
85
|
+
const loadedRef = useRef(false);
|
|
86
|
+
// Track dirty state in a ref too (for use in callbacks without stale closure)
|
|
87
|
+
const isDirtyRef = useRef(false);
|
|
88
|
+
// ---- Parse / Serialize helpers ----
|
|
89
|
+
const parseContent = useCallback((raw) => {
|
|
90
|
+
const { parse } = optionsRef.current;
|
|
91
|
+
if (parse)
|
|
92
|
+
return parse(raw);
|
|
93
|
+
return raw;
|
|
94
|
+
}, [] // stable -- reads from ref
|
|
95
|
+
);
|
|
96
|
+
const serializeContent = useCallback((content) => {
|
|
97
|
+
const { serialize } = optionsRef.current;
|
|
98
|
+
if (serialize)
|
|
99
|
+
return serialize(content);
|
|
100
|
+
return String(content);
|
|
101
|
+
}, [] // stable -- reads from ref
|
|
102
|
+
);
|
|
103
|
+
// ---- markDirty ----
|
|
104
|
+
const markDirty = useCallback(() => {
|
|
105
|
+
if (!optionsRef.current.getCurrentContent)
|
|
106
|
+
return; // read-only
|
|
107
|
+
if (!isDirtyRef.current) {
|
|
108
|
+
isDirtyRef.current = true;
|
|
109
|
+
setIsDirty(true);
|
|
110
|
+
host.setDirty(true);
|
|
111
|
+
}
|
|
112
|
+
}, [host]);
|
|
113
|
+
const clearDirty = useCallback(() => {
|
|
114
|
+
if (isDirtyRef.current) {
|
|
115
|
+
isDirtyRef.current = false;
|
|
116
|
+
setIsDirty(false);
|
|
117
|
+
host.setDirty(false);
|
|
118
|
+
}
|
|
119
|
+
}, [host]);
|
|
120
|
+
// ---- Initial load ----
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (loadedRef.current)
|
|
123
|
+
return;
|
|
124
|
+
loadedRef.current = true;
|
|
125
|
+
let mounted = true;
|
|
126
|
+
const doLoad = async () => {
|
|
127
|
+
try {
|
|
128
|
+
const opts = optionsRef.current;
|
|
129
|
+
if (opts.binary) {
|
|
130
|
+
const data = await host.loadBinaryContent();
|
|
131
|
+
if (!mounted)
|
|
132
|
+
return;
|
|
133
|
+
opts.applyContent(data);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const raw = await host.loadContent();
|
|
137
|
+
if (!mounted)
|
|
138
|
+
return;
|
|
139
|
+
const parsed = parseContent(raw);
|
|
140
|
+
lastSavedContentRef.current = raw;
|
|
141
|
+
opts.applyContent(parsed);
|
|
142
|
+
}
|
|
143
|
+
if (!mounted)
|
|
144
|
+
return;
|
|
145
|
+
setIsLoading(false);
|
|
146
|
+
optionsRef.current.onLoaded?.();
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (!mounted)
|
|
150
|
+
return;
|
|
151
|
+
const loadError = err instanceof Error ? err : new Error(String(err));
|
|
152
|
+
setError(loadError);
|
|
153
|
+
setIsLoading(false);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
doLoad();
|
|
157
|
+
return () => {
|
|
158
|
+
mounted = false;
|
|
159
|
+
};
|
|
160
|
+
}, [host, parseContent]);
|
|
161
|
+
// ---- Save handling ----
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
return host.onSaveRequested(async () => {
|
|
164
|
+
const opts = optionsRef.current;
|
|
165
|
+
try {
|
|
166
|
+
if (opts.onSave) {
|
|
167
|
+
// Custom save handler (e.g., async content extraction from RevoGrid)
|
|
168
|
+
await opts.onSave();
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
if (!opts.getCurrentContent)
|
|
172
|
+
return; // read-only
|
|
173
|
+
const content = opts.getCurrentContent();
|
|
174
|
+
if (opts.binary) {
|
|
175
|
+
await host.saveContent(content);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const serialized = serializeContent(content);
|
|
179
|
+
// Update echo detection BEFORE saving so the subsequent onFileChanged is ignored
|
|
180
|
+
lastSavedContentRef.current = serialized;
|
|
181
|
+
await host.saveContent(serialized);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
clearDirty();
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
// Save failed -- dirty state remains
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}, [host, serializeContent, clearDirty]);
|
|
191
|
+
// ---- External file changes ----
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
return host.onFileChanged((newRawContent) => {
|
|
194
|
+
// Echo detection: ignore if this is our own save echoing back
|
|
195
|
+
if (newRawContent === lastSavedContentRef.current) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const opts = optionsRef.current;
|
|
199
|
+
try {
|
|
200
|
+
const parsed = parseContent(newRawContent);
|
|
201
|
+
lastSavedContentRef.current = newRawContent;
|
|
202
|
+
opts.applyContent(parsed);
|
|
203
|
+
clearDirty();
|
|
204
|
+
opts.onExternalChange?.(parsed);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
// Failed to parse external change
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}, [host, parseContent, clearDirty]);
|
|
211
|
+
// ---- Theme changes ----
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
return host.onThemeChanged((newTheme) => {
|
|
214
|
+
setTheme(newTheme);
|
|
215
|
+
});
|
|
216
|
+
}, [host]);
|
|
217
|
+
// ---- Diff mode ----
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (!host.onDiffRequested)
|
|
220
|
+
return;
|
|
221
|
+
const unsubDiff = host.onDiffRequested((config) => {
|
|
222
|
+
const opts = optionsRef.current;
|
|
223
|
+
// Custom diff handler -- editor manages its own diff state
|
|
224
|
+
if (opts.onDiffRequested) {
|
|
225
|
+
opts.onDiffRequested(config);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
let original;
|
|
229
|
+
let modified;
|
|
230
|
+
try {
|
|
231
|
+
original = opts.binary
|
|
232
|
+
? config.originalContent
|
|
233
|
+
: parseContent(config.originalContent);
|
|
234
|
+
modified = opts.binary
|
|
235
|
+
? config.modifiedContent
|
|
236
|
+
: parseContent(config.modifiedContent);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
setDiffState({
|
|
242
|
+
original,
|
|
243
|
+
modified,
|
|
244
|
+
tagId: config.tagId,
|
|
245
|
+
sessionId: config.sessionId,
|
|
246
|
+
accept: () => {
|
|
247
|
+
// Accept: apply modified content, report result
|
|
248
|
+
optionsRef.current.applyContent(modified);
|
|
249
|
+
if (!opts.binary) {
|
|
250
|
+
lastSavedContentRef.current =
|
|
251
|
+
serializeContent(modified);
|
|
252
|
+
}
|
|
253
|
+
host.reportDiffResult?.({
|
|
254
|
+
content: opts.binary
|
|
255
|
+
? ''
|
|
256
|
+
: serializeContent(modified),
|
|
257
|
+
action: 'accept',
|
|
258
|
+
});
|
|
259
|
+
clearDirty();
|
|
260
|
+
setDiffState(null);
|
|
261
|
+
},
|
|
262
|
+
reject: () => {
|
|
263
|
+
// Reject: apply original content, report result
|
|
264
|
+
optionsRef.current.applyContent(original);
|
|
265
|
+
if (!opts.binary) {
|
|
266
|
+
lastSavedContentRef.current =
|
|
267
|
+
serializeContent(original);
|
|
268
|
+
}
|
|
269
|
+
host.reportDiffResult?.({
|
|
270
|
+
content: opts.binary
|
|
271
|
+
? ''
|
|
272
|
+
: serializeContent(original),
|
|
273
|
+
action: 'reject',
|
|
274
|
+
});
|
|
275
|
+
clearDirty();
|
|
276
|
+
setDiffState(null);
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
return unsubDiff;
|
|
281
|
+
}, [host, parseContent, serializeContent, clearDirty]);
|
|
282
|
+
// Handle diff cleared externally (via unified diff header)
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
if (!host.onDiffCleared)
|
|
285
|
+
return;
|
|
286
|
+
return host.onDiffCleared(async () => {
|
|
287
|
+
const opts = optionsRef.current;
|
|
288
|
+
// Custom diff cleared handler
|
|
289
|
+
if (opts.onDiffCleared) {
|
|
290
|
+
await opts.onDiffCleared();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
setDiffState(null);
|
|
294
|
+
// Reload content since the file may have changed
|
|
295
|
+
if (!opts.binary) {
|
|
296
|
+
host.loadContent().then((raw) => {
|
|
297
|
+
const parsed = parseContent(raw);
|
|
298
|
+
lastSavedContentRef.current = raw;
|
|
299
|
+
optionsRef.current.applyContent(parsed);
|
|
300
|
+
clearDirty();
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}, [host, parseContent, clearDirty]);
|
|
305
|
+
// ---- Source mode ----
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
if (!host.onSourceModeChanged)
|
|
308
|
+
return;
|
|
309
|
+
return host.onSourceModeChanged((isActive) => {
|
|
310
|
+
setIsSourceMode(isActive);
|
|
311
|
+
});
|
|
312
|
+
}, [host]);
|
|
313
|
+
const toggleSourceMode = host.toggleSourceMode
|
|
314
|
+
? useCallback(() => host.toggleSourceMode?.(), [host])
|
|
315
|
+
: undefined;
|
|
316
|
+
// ---- Return ----
|
|
317
|
+
return {
|
|
318
|
+
markDirty,
|
|
319
|
+
isLoading,
|
|
320
|
+
error,
|
|
321
|
+
theme,
|
|
322
|
+
isDirty,
|
|
323
|
+
diffState,
|
|
324
|
+
toggleSourceMode,
|
|
325
|
+
isSourceMode,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface ValidationResult {
|
|
2
|
+
/** Whether the bundle passed validation */
|
|
3
|
+
valid: boolean;
|
|
4
|
+
/** Critical errors that will cause runtime failures */
|
|
5
|
+
errors: string[];
|
|
6
|
+
/** Warnings that may cause issues */
|
|
7
|
+
warnings: string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Validates an extension bundle for common issues.
|
|
11
|
+
*
|
|
12
|
+
* Run this after building to catch configuration mistakes before runtime.
|
|
13
|
+
*
|
|
14
|
+
* @param distPath - Path to the dist directory containing the bundle
|
|
15
|
+
* @param bundleName - Name of the bundle file (default: 'index.js')
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const result = await validateExtensionBundle('./dist');
|
|
20
|
+
* if (!result.valid) {
|
|
21
|
+
* console.error('Build failed:', result.errors);
|
|
22
|
+
* process.exit(1);
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function validateExtensionBundle(distPath: string, bundleName?: string): Promise<ValidationResult>;
|
|
27
|
+
/**
|
|
28
|
+
* Prints validation results to console with formatting.
|
|
29
|
+
*/
|
|
30
|
+
export declare function printValidationResult(result: ValidationResult): void;
|
|
31
|
+
//# sourceMappingURL=validate.d.ts.map
|