@djangocfg/ui-tools 2.1.239 → 2.1.240
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 +49 -3
- package/dist/index.cjs +731 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +233 -1
- package/dist/index.d.ts +233 -1
- package/dist/index.mjs +726 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -7
- package/src/tools/CodeEditor/CodeEditor.story.tsx +202 -0
- package/src/tools/CodeEditor/README.md +189 -0
- package/src/tools/CodeEditor/components/DiffEditor.tsx +123 -0
- package/src/tools/CodeEditor/components/Editor.tsx +222 -0
- package/src/tools/CodeEditor/components/index.ts +2 -0
- package/src/tools/CodeEditor/context/EditorProvider.tsx +194 -0
- package/src/tools/CodeEditor/context/index.ts +1 -0
- package/src/tools/CodeEditor/hooks/index.ts +4 -0
- package/src/tools/CodeEditor/hooks/useEditor.ts +36 -0
- package/src/tools/CodeEditor/hooks/useEditorTheme.ts +158 -0
- package/src/tools/CodeEditor/hooks/useLanguage.ts +29 -0
- package/src/tools/CodeEditor/hooks/useMonaco.ts +64 -0
- package/src/tools/CodeEditor/index.ts +16 -0
- package/src/tools/CodeEditor/lib/index.ts +2 -0
- package/src/tools/CodeEditor/lib/languages.ts +227 -0
- package/src/tools/CodeEditor/lib/themes.ts +78 -0
- package/src/tools/CodeEditor/types/index.ts +130 -0
- package/src/tools/CodeEditor/workers/index.ts +1 -0
- package/src/tools/CodeEditor/workers/setup.ts +58 -0
- package/src/tools/index.ts +25 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.240",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"gallery",
|
|
15
15
|
"lightbox",
|
|
16
16
|
"map",
|
|
17
|
-
"maplibre"
|
|
17
|
+
"maplibre",
|
|
18
|
+
"monaco-editor",
|
|
19
|
+
"code-editor"
|
|
18
20
|
],
|
|
19
21
|
"author": {
|
|
20
22
|
"name": "DjangoCFG",
|
|
@@ -62,6 +64,11 @@
|
|
|
62
64
|
"import": "./src/tools/Tour/index.ts",
|
|
63
65
|
"require": "./src/tools/Tour/index.ts"
|
|
64
66
|
},
|
|
67
|
+
"./code-editor": {
|
|
68
|
+
"types": "./src/tools/CodeEditor/index.ts",
|
|
69
|
+
"import": "./src/tools/CodeEditor/index.ts",
|
|
70
|
+
"require": "./src/tools/CodeEditor/index.ts"
|
|
71
|
+
},
|
|
65
72
|
"./styles": "./src/styles/index.css"
|
|
66
73
|
},
|
|
67
74
|
"files": [
|
|
@@ -78,8 +85,8 @@
|
|
|
78
85
|
"check": "tsc --noEmit"
|
|
79
86
|
},
|
|
80
87
|
"peerDependencies": {
|
|
81
|
-
"@djangocfg/i18n": "^2.1.
|
|
82
|
-
"@djangocfg/ui-core": "^2.1.
|
|
88
|
+
"@djangocfg/i18n": "^2.1.240",
|
|
89
|
+
"@djangocfg/ui-core": "^2.1.240",
|
|
83
90
|
"lucide-react": "^0.545.0",
|
|
84
91
|
"react": "^19.1.0",
|
|
85
92
|
"react-dom": "^19.1.0",
|
|
@@ -94,6 +101,7 @@
|
|
|
94
101
|
"@rjsf/validator-ajv8": "^6.1.2",
|
|
95
102
|
"@vidstack/react": "next",
|
|
96
103
|
"@wavesurfer/react": "^1.0.12",
|
|
104
|
+
"monaco-editor": "^0.55.1",
|
|
97
105
|
"maplibre-gl": "^4.7.1",
|
|
98
106
|
"media-icons": "next",
|
|
99
107
|
"mermaid": "^11.12.0",
|
|
@@ -112,10 +120,10 @@
|
|
|
112
120
|
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
113
121
|
},
|
|
114
122
|
"devDependencies": {
|
|
115
|
-
"@djangocfg/i18n": "^2.1.
|
|
123
|
+
"@djangocfg/i18n": "^2.1.240",
|
|
116
124
|
"@djangocfg/playground": "workspace:*",
|
|
117
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
118
|
-
"@djangocfg/ui-core": "^2.1.
|
|
125
|
+
"@djangocfg/typescript-config": "^2.1.240",
|
|
126
|
+
"@djangocfg/ui-core": "^2.1.240",
|
|
119
127
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
120
128
|
"@types/node": "^24.7.2",
|
|
121
129
|
"@types/react": "^19.1.0",
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { defineStory, useSelect, useBoolean } from '@djangocfg/playground';
|
|
2
|
+
import { Editor, DiffEditor } from './components';
|
|
3
|
+
|
|
4
|
+
export default defineStory({
|
|
5
|
+
title: 'Tools/Code Editor',
|
|
6
|
+
component: Editor,
|
|
7
|
+
description: 'Monaco-based code editor with syntax highlighting, diff view, and full IDE features.',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const SAMPLE_CODE = {
|
|
11
|
+
typescript: `import { useState, useCallback, useMemo } from 'react';
|
|
12
|
+
|
|
13
|
+
interface User {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
email: string;
|
|
17
|
+
role: 'admin' | 'user' | 'viewer';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useUsers() {
|
|
21
|
+
const [users, setUsers] = useState<User[]>([]);
|
|
22
|
+
|
|
23
|
+
const addUser = useCallback((user: Omit<User, 'id'>) => {
|
|
24
|
+
setUsers(prev => [...prev, { ...user, id: crypto.randomUUID() }]);
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const admins = useMemo(
|
|
28
|
+
() => users.filter(u => u.role === 'admin'),
|
|
29
|
+
[users],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return { users, admins, addUser };
|
|
33
|
+
}`,
|
|
34
|
+
|
|
35
|
+
python: `from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
from datetime import datetime
|
|
39
|
+
from typing import AsyncIterator
|
|
40
|
+
|
|
41
|
+
import httpx
|
|
42
|
+
from pydantic import BaseModel, Field
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class User(BaseModel):
|
|
46
|
+
id: str
|
|
47
|
+
name: str
|
|
48
|
+
email: str
|
|
49
|
+
role: str = "user"
|
|
50
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class UserService:
|
|
55
|
+
base_url: str
|
|
56
|
+
client: httpx.AsyncClient | None = None
|
|
57
|
+
|
|
58
|
+
async def list_users(self, page: int = 1) -> list[User]:
|
|
59
|
+
async with httpx.AsyncClient(base_url=self.base_url) as client:
|
|
60
|
+
response = await client.get("/users", params={"page": page})
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
return [User(**u) for u in response.json()["data"]]`,
|
|
63
|
+
|
|
64
|
+
json: `{
|
|
65
|
+
"name": "@djangocfg/ui-tools",
|
|
66
|
+
"version": "2.1.0",
|
|
67
|
+
"description": "Heavy React tools with lazy loading",
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"monaco-editor": "^0.55.1",
|
|
70
|
+
"react": "^19.1.0"
|
|
71
|
+
},
|
|
72
|
+
"exports": {
|
|
73
|
+
".": "./src/index.ts",
|
|
74
|
+
"./code-editor": "./src/tools/CodeEditor/index.ts"
|
|
75
|
+
}
|
|
76
|
+
}`,
|
|
77
|
+
|
|
78
|
+
css: `/* Design tokens */
|
|
79
|
+
:root {
|
|
80
|
+
--radius: 0.5rem;
|
|
81
|
+
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.editor-container {
|
|
85
|
+
position: relative;
|
|
86
|
+
border-radius: var(--radius);
|
|
87
|
+
border: 1px solid hsl(var(--border));
|
|
88
|
+
overflow: hidden;
|
|
89
|
+
|
|
90
|
+
&:focus-within {
|
|
91
|
+
outline: 2px solid hsl(var(--ring));
|
|
92
|
+
outline-offset: 2px;
|
|
93
|
+
}
|
|
94
|
+
}`,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const DIFF_ORIGINAL = `function greet(name: string) {
|
|
98
|
+
console.log("Hello, " + name);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
greet("World");`;
|
|
102
|
+
|
|
103
|
+
const DIFF_MODIFIED = `function greet(name: string, greeting = "Hello") {
|
|
104
|
+
console.log(\`\${greeting}, \${name}!\`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function farewell(name: string) {
|
|
108
|
+
console.log(\`Goodbye, \${name}!\`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
greet("World");
|
|
112
|
+
farewell("World");`;
|
|
113
|
+
|
|
114
|
+
export const Interactive = () => {
|
|
115
|
+
const [language] = useSelect('language', {
|
|
116
|
+
options: ['typescript', 'python', 'json', 'css'] as const,
|
|
117
|
+
defaultValue: 'typescript',
|
|
118
|
+
label: 'Language',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const [readOnly] = useBoolean('readOnly', {
|
|
122
|
+
defaultValue: false,
|
|
123
|
+
label: 'Read Only',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const [minimap] = useBoolean('minimap', {
|
|
127
|
+
defaultValue: false,
|
|
128
|
+
label: 'Minimap',
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const [autoHeight] = useBoolean('autoHeight', {
|
|
132
|
+
defaultValue: false,
|
|
133
|
+
label: 'Auto Height',
|
|
134
|
+
description: 'Resize editor to fit content',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return autoHeight ? (
|
|
138
|
+
<Editor
|
|
139
|
+
value={SAMPLE_CODE[language]}
|
|
140
|
+
language={language}
|
|
141
|
+
autoHeight
|
|
142
|
+
minHeight={100}
|
|
143
|
+
maxHeight={500}
|
|
144
|
+
options={{ readOnly, minimap, fontSize: 14 }}
|
|
145
|
+
/>
|
|
146
|
+
) : (
|
|
147
|
+
<div style={{ height: 400 }}>
|
|
148
|
+
<Editor
|
|
149
|
+
value={SAMPLE_CODE[language]}
|
|
150
|
+
language={language}
|
|
151
|
+
options={{ readOnly, minimap, fontSize: 14 }}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const TypeScript = () => (
|
|
158
|
+
<div style={{ height: 350 }}>
|
|
159
|
+
<Editor value={SAMPLE_CODE.typescript} language="typescript" />
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
export const Python = () => (
|
|
164
|
+
<div style={{ height: 350 }}>
|
|
165
|
+
<Editor value={SAMPLE_CODE.python} language="python" />
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
export const ReadOnly = () => (
|
|
170
|
+
<div style={{ height: 300 }}>
|
|
171
|
+
<Editor
|
|
172
|
+
value={SAMPLE_CODE.json}
|
|
173
|
+
language="json"
|
|
174
|
+
options={{ readOnly: true, lineNumbers: 'on' }}
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
export const AutoHeight = () => (
|
|
180
|
+
<div className="flex flex-col gap-4 max-w-3xl">
|
|
181
|
+
<p className="text-sm text-muted-foreground">
|
|
182
|
+
Editor grows with content (min 100px, max 400px). Try typing new lines.
|
|
183
|
+
</p>
|
|
184
|
+
<Editor
|
|
185
|
+
value={`const x = 1;\nconst y = 2;\nconst z = x + y;`}
|
|
186
|
+
language="typescript"
|
|
187
|
+
autoHeight
|
|
188
|
+
minHeight={100}
|
|
189
|
+
maxHeight={400}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
export const DiffView = () => (
|
|
195
|
+
<div style={{ height: 400 }}>
|
|
196
|
+
<DiffEditor
|
|
197
|
+
original={DIFF_ORIGINAL}
|
|
198
|
+
modified={DIFF_MODIFIED}
|
|
199
|
+
language="typescript"
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# CodeEditor
|
|
2
|
+
|
|
3
|
+
Monaco-based code editor for React. Full IDE features: syntax highlighting, IntelliSense, diff view, multi-file editing.
|
|
4
|
+
|
|
5
|
+
## Components
|
|
6
|
+
|
|
7
|
+
### Editor
|
|
8
|
+
|
|
9
|
+
Full-featured code editor with ref forwarding.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { Editor } from '@djangocfg/ui-tools/code-editor';
|
|
13
|
+
|
|
14
|
+
<Editor
|
|
15
|
+
value="const x = 42;"
|
|
16
|
+
language="typescript"
|
|
17
|
+
onChange={(value) => console.log(value)}
|
|
18
|
+
options={{
|
|
19
|
+
theme: 'vs-dark',
|
|
20
|
+
fontSize: 14,
|
|
21
|
+
minimap: false,
|
|
22
|
+
readOnly: false,
|
|
23
|
+
wordWrap: 'on',
|
|
24
|
+
lineNumbers: 'on',
|
|
25
|
+
tabSize: 2,
|
|
26
|
+
}}
|
|
27
|
+
/>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Sizing:**
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
// Fixed height (default)
|
|
34
|
+
<Editor height={400} />
|
|
35
|
+
|
|
36
|
+
// Fill container (default: height="100%")
|
|
37
|
+
<div style={{ height: 500 }}>
|
|
38
|
+
<Editor />
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
// Auto-resize to fit content
|
|
42
|
+
<Editor autoHeight minHeight={100} maxHeight={600} />
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| Prop | Type | Default | Description |
|
|
46
|
+
|------|------|---------|-------------|
|
|
47
|
+
| `height` | `string \| number` | `'100%'` | Fixed height. Ignored when `autoHeight` |
|
|
48
|
+
| `width` | `string \| number` | `'100%'` | Width |
|
|
49
|
+
| `autoHeight` | `boolean` | `false` | Grow/shrink with content |
|
|
50
|
+
| `minHeight` | `number` | `100` | Min height in px (autoHeight only) |
|
|
51
|
+
| `maxHeight` | `number` | `600` | Max height in px (autoHeight only) |
|
|
52
|
+
|
|
53
|
+
**Ref API:**
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
const ref = useRef<EditorRef>(null);
|
|
57
|
+
<Editor ref={ref} value="..." language="typescript" />
|
|
58
|
+
|
|
59
|
+
ref.current?.getValue(); // get current content
|
|
60
|
+
ref.current?.setValue('...'); // set content
|
|
61
|
+
ref.current?.focus(); // focus editor
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### DiffEditor
|
|
65
|
+
|
|
66
|
+
Side-by-side code comparison.
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { DiffEditor } from '@djangocfg/ui-tools/code-editor';
|
|
70
|
+
|
|
71
|
+
<DiffEditor
|
|
72
|
+
original="const x = 1;"
|
|
73
|
+
modified="const x = 42;"
|
|
74
|
+
language="typescript"
|
|
75
|
+
/>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Context
|
|
79
|
+
|
|
80
|
+
Multi-file editor state management.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { EditorProvider, useEditorContext } from '@djangocfg/ui-tools/code-editor';
|
|
84
|
+
|
|
85
|
+
<EditorProvider onSave={async (path) => { /* save */ }}>
|
|
86
|
+
<MyEditor />
|
|
87
|
+
</EditorProvider>
|
|
88
|
+
|
|
89
|
+
// In child components:
|
|
90
|
+
const {
|
|
91
|
+
openFiles, // currently open files
|
|
92
|
+
activeFile, // active file
|
|
93
|
+
openFile, // (path, content, language?) => void
|
|
94
|
+
closeFile, // (path) => void
|
|
95
|
+
setActiveFile, // (path) => void
|
|
96
|
+
updateContent, // (path, content) => void
|
|
97
|
+
saveFile, // (path) => Promise<void>
|
|
98
|
+
isDirty, // (path) => boolean
|
|
99
|
+
getContent, // (path) => string | null
|
|
100
|
+
} = useEditorContext();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Theme
|
|
104
|
+
|
|
105
|
+
Editor automatically follows the app theme (dark/light) via `useResolvedTheme()` from `@djangocfg/ui-core`. No configuration needed — just works with `next-themes` or any setup that toggles `.dark` class on `<html>`.
|
|
106
|
+
|
|
107
|
+
Custom Monaco themes (`app-dark`, `app-light`) are registered automatically with colors derived from CSS variables (`--background`, `--foreground`, `--card`, `--border`).
|
|
108
|
+
|
|
109
|
+
Override with explicit theme:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<Editor options={{ theme: 'vs-dark' }} /> {/* force dark */}
|
|
113
|
+
<Editor options={{ theme: 'hc-black' }} /> {/* high contrast */}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Hooks
|
|
117
|
+
|
|
118
|
+
| Hook | Returns | Description |
|
|
119
|
+
|------|---------|-------------|
|
|
120
|
+
| `useMonaco()` | `{ monaco, isLoading, error }` | Load Monaco namespace |
|
|
121
|
+
| `useEditor()` | `{ editor, isReady, setEditor }` | Editor instance management |
|
|
122
|
+
| `useLanguage(filename)` | `string` | Detect language from file extension |
|
|
123
|
+
| `useEditorTheme(monaco, override?)` | `string` | Resolved Monaco theme name (auto dark/light) |
|
|
124
|
+
|
|
125
|
+
## Types
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import type {
|
|
129
|
+
EditorFile,
|
|
130
|
+
EditorOptions,
|
|
131
|
+
EditorProps,
|
|
132
|
+
DiffEditorProps,
|
|
133
|
+
EditorContextValue,
|
|
134
|
+
UseEditorReturn,
|
|
135
|
+
UseMonacoReturn,
|
|
136
|
+
} from '@djangocfg/ui-tools/code-editor';
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Workers
|
|
140
|
+
|
|
141
|
+
Monaco runs language services (TypeScript, JSON, CSS) in Web Workers for performance. Workers require bundler-specific setup. Without workers, Monaco falls back to main-thread execution (fully functional, but slower for large files).
|
|
142
|
+
|
|
143
|
+
### Next.js (Webpack)
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
pnpm add monaco-editor-webpack-plugin
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
// next.config.js
|
|
151
|
+
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
webpack: (config) => {
|
|
155
|
+
config.plugins.push(new MonacoWebpackPlugin({
|
|
156
|
+
languages: ['typescript', 'javascript', 'json', 'css', 'html', 'python'],
|
|
157
|
+
}));
|
|
158
|
+
return config;
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Vite
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
// Call once in app entry (e.g. main.tsx)
|
|
167
|
+
import { setupMonacoWorkers } from '@djangocfg/ui-tools/code-editor';
|
|
168
|
+
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
|
169
|
+
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
|
|
170
|
+
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
|
171
|
+
|
|
172
|
+
setupMonacoWorkers((label) => {
|
|
173
|
+
if (label === 'typescript' || label === 'javascript') return new tsWorker();
|
|
174
|
+
if (label === 'json') return new jsonWorker();
|
|
175
|
+
return new editorWorker();
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### No config
|
|
180
|
+
|
|
181
|
+
Just use the Editor — Monaco works without workers. You'll see a console warning which can be ignored.
|
|
182
|
+
|
|
183
|
+
## Bundle
|
|
184
|
+
|
|
185
|
+
~550KB (Monaco Editor). Loaded only when component is rendered — use tree-shakeable import:
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
import { Editor } from '@djangocfg/ui-tools/code-editor';
|
|
189
|
+
```
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useRef, useEffect } from 'react';
|
|
4
|
+
import type * as monaco from 'monaco-editor';
|
|
5
|
+
|
|
6
|
+
import { useMonaco } from '../hooks/useMonaco';
|
|
7
|
+
import { useEditorTheme } from '../hooks/useEditorTheme';
|
|
8
|
+
import type { DiffEditorProps } from '../types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Monaco Diff Editor Component
|
|
12
|
+
*
|
|
13
|
+
* Side-by-side or inline diff view for comparing two versions of content.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <DiffEditor
|
|
18
|
+
* original={originalCode}
|
|
19
|
+
* modified={modifiedCode}
|
|
20
|
+
* language="typescript"
|
|
21
|
+
* />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function DiffEditor({
|
|
25
|
+
original,
|
|
26
|
+
modified,
|
|
27
|
+
language = 'plaintext',
|
|
28
|
+
options = {},
|
|
29
|
+
className = '',
|
|
30
|
+
height = '100%',
|
|
31
|
+
}: DiffEditorProps) {
|
|
32
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
33
|
+
const editorRef = useRef<monaco.editor.IStandaloneDiffEditor | null>(null);
|
|
34
|
+
const { monaco, isLoading } = useMonaco();
|
|
35
|
+
const resolvedTheme = useEditorTheme(monaco, options.theme);
|
|
36
|
+
|
|
37
|
+
// Create diff editor
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!monaco || !containerRef.current || editorRef.current) return;
|
|
40
|
+
|
|
41
|
+
const editor = monaco.editor.createDiffEditor(containerRef.current, {
|
|
42
|
+
theme: resolvedTheme,
|
|
43
|
+
fontSize: options.fontSize || 14,
|
|
44
|
+
fontFamily: options.fontFamily || "'Fira Code', 'Consolas', monospace",
|
|
45
|
+
readOnly: true,
|
|
46
|
+
automaticLayout: true,
|
|
47
|
+
renderSideBySide: true,
|
|
48
|
+
scrollBeyondLastLine: false,
|
|
49
|
+
minimap: { enabled: false },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const originalModel = monaco.editor.createModel(original, language);
|
|
53
|
+
const modifiedModel = monaco.editor.createModel(modified, language);
|
|
54
|
+
|
|
55
|
+
editor.setModel({
|
|
56
|
+
original: originalModel,
|
|
57
|
+
modified: modifiedModel,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
editorRef.current = editor;
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
originalModel.dispose();
|
|
64
|
+
modifiedModel.dispose();
|
|
65
|
+
editor.dispose();
|
|
66
|
+
editorRef.current = null;
|
|
67
|
+
};
|
|
68
|
+
}, [monaco]);
|
|
69
|
+
|
|
70
|
+
// Update models when content changes
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const editor = editorRef.current;
|
|
73
|
+
if (!editor || !monaco) return;
|
|
74
|
+
|
|
75
|
+
const model = editor.getModel();
|
|
76
|
+
if (model) {
|
|
77
|
+
model.original.setValue(original);
|
|
78
|
+
model.modified.setValue(modified);
|
|
79
|
+
}
|
|
80
|
+
}, [original, modified, monaco]);
|
|
81
|
+
|
|
82
|
+
// Update language
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const editor = editorRef.current;
|
|
85
|
+
if (!editor || !monaco) return;
|
|
86
|
+
|
|
87
|
+
const model = editor.getModel();
|
|
88
|
+
if (model) {
|
|
89
|
+
monaco.editor.setModelLanguage(model.original, language);
|
|
90
|
+
monaco.editor.setModelLanguage(model.modified, language);
|
|
91
|
+
}
|
|
92
|
+
}, [language, monaco]);
|
|
93
|
+
|
|
94
|
+
if (isLoading) {
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
className={className}
|
|
98
|
+
style={{
|
|
99
|
+
width: '100%',
|
|
100
|
+
height,
|
|
101
|
+
display: 'flex',
|
|
102
|
+
alignItems: 'center',
|
|
103
|
+
justifyContent: 'center',
|
|
104
|
+
backgroundColor: '#1e1e1e',
|
|
105
|
+
color: '#666',
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
Loading diff editor...
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
ref={containerRef}
|
|
116
|
+
className={className}
|
|
117
|
+
style={{
|
|
118
|
+
width: '100%',
|
|
119
|
+
height,
|
|
120
|
+
}}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|