@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-tools",
3
- "version": "2.1.239",
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.239",
82
- "@djangocfg/ui-core": "^2.1.239",
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.239",
123
+ "@djangocfg/i18n": "^2.1.240",
116
124
  "@djangocfg/playground": "workspace:*",
117
- "@djangocfg/typescript-config": "^2.1.239",
118
- "@djangocfg/ui-core": "^2.1.239",
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
+ }