@djangocfg/widget-code 0.1.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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -0
  3. package/package.json +119 -0
  4. package/src/diff-viewer/DiffViewer.tsx +168 -0
  5. package/src/diff-viewer/README.md +33 -0
  6. package/src/diff-viewer/components/CopyButton.tsx +49 -0
  7. package/src/diff-viewer/components/DiffLineContent.tsx +48 -0
  8. package/src/diff-viewer/components/SplitView.tsx +270 -0
  9. package/src/diff-viewer/components/UnifiedView.tsx +189 -0
  10. package/src/diff-viewer/diff-viewer.stories.tsx +168 -0
  11. package/src/diff-viewer/hooks/useDiff.ts +47 -0
  12. package/src/diff-viewer/hooks/useHighlighter.ts +54 -0
  13. package/src/diff-viewer/index.ts +22 -0
  14. package/src/diff-viewer/lazy.tsx +25 -0
  15. package/src/diff-viewer/types.ts +125 -0
  16. package/src/diff-viewer/utils/computeDiff.ts +171 -0
  17. package/src/editor/README.md +187 -0
  18. package/src/editor/components/DiffEditor.tsx +158 -0
  19. package/src/editor/components/Editor.tsx +293 -0
  20. package/src/editor/components/index.ts +2 -0
  21. package/src/editor/context/EditorProvider.tsx +212 -0
  22. package/src/editor/context/index.ts +1 -0
  23. package/src/editor/editor.stories.tsx +397 -0
  24. package/src/editor/hooks/index.ts +4 -0
  25. package/src/editor/hooks/useEditor.ts +36 -0
  26. package/src/editor/hooks/useEditorTheme.ts +91 -0
  27. package/src/editor/hooks/useLanguage.ts +29 -0
  28. package/src/editor/hooks/useMonaco.ts +79 -0
  29. package/src/editor/index.ts +16 -0
  30. package/src/editor/lazy.tsx +76 -0
  31. package/src/editor/lib/index.ts +2 -0
  32. package/src/editor/lib/languages.ts +227 -0
  33. package/src/editor/lib/themes.ts +42 -0
  34. package/src/editor/types/index.ts +137 -0
  35. package/src/editor/workers/index.ts +1 -0
  36. package/src/editor/workers/setup.ts +58 -0
  37. package/src/index.ts +10 -0
  38. package/src/json-editor/JsonEditor.tsx +115 -0
  39. package/src/json-editor/index.ts +1 -0
  40. package/src/json-editor/lazy.tsx +24 -0
  41. package/src/markdown-message/ActionRow.tsx +53 -0
  42. package/src/markdown-message/ChatMessageRow.tsx +103 -0
  43. package/src/markdown-message/CodeBlock.tsx +137 -0
  44. package/src/markdown-message/CollapseToggle.tsx +69 -0
  45. package/src/markdown-message/MarkdownMessage.tsx +376 -0
  46. package/src/markdown-message/README.md +221 -0
  47. package/src/markdown-message/builtinRules.tsx +92 -0
  48. package/src/markdown-message/components.tsx +224 -0
  49. package/src/markdown-message/index.ts +22 -0
  50. package/src/markdown-message/linkRules.ts +83 -0
  51. package/src/markdown-message/plainText.ts +83 -0
  52. package/src/markdown-message/sanitize.ts +88 -0
  53. package/src/markdown-message/streamingBlocks.ts +168 -0
  54. package/src/markdown-message/types.ts +168 -0
  55. package/src/panel-header/CodePanelHeader.tsx +121 -0
  56. package/src/panel-header/index.ts +1 -0
  57. package/src/pretty-code/PrettyCode.client.tsx +402 -0
  58. package/src/pretty-code/README.md +81 -0
  59. package/src/pretty-code/index.tsx +72 -0
  60. package/src/pretty-code/lazy.tsx +69 -0
  61. package/src/pretty-code/pretty-code.stories.tsx +353 -0
  62. package/src/pretty-code/registerPrismLanguages.ts +116 -0
  63. package/src/role-tokens.ts +39 -0
  64. package/src/use-collapsible-content.ts +236 -0
@@ -0,0 +1,158 @@
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, EDITOR_BACKGROUND } 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, error } = useMonaco();
35
+ const resolvedTheme = useEditorTheme(monaco, options.theme);
36
+
37
+ // Create diff editor — runs once Monaco is available.
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
+ // Dispose models before the editor so Monaco releases all references.
64
+ originalModel.dispose();
65
+ modifiedModel.dispose();
66
+ editor.dispose();
67
+ editorRef.current = null;
68
+ };
69
+ // eslint-disable-next-line react-hooks/exhaustive-deps
70
+ }, [monaco]);
71
+
72
+ // Update models when content changes — skip no-op writes to avoid
73
+ // resetting scroll/decorations on unrelated re-renders.
74
+ useEffect(() => {
75
+ const editor = editorRef.current;
76
+ if (!editor) return;
77
+
78
+ const model = editor.getModel();
79
+ if (!model) return;
80
+ if (model.original.getValue() !== original) model.original.setValue(original);
81
+ if (model.modified.getValue() !== modified) model.modified.setValue(modified);
82
+ }, [original, modified]);
83
+
84
+ // Update language
85
+ useEffect(() => {
86
+ const editor = editorRef.current;
87
+ if (!editor || !monaco) return;
88
+
89
+ const model = editor.getModel();
90
+ if (model) {
91
+ monaco.editor.setModelLanguage(model.original, language);
92
+ monaco.editor.setModelLanguage(model.modified, language);
93
+ }
94
+ }, [language, monaco]);
95
+
96
+ // Apply theme — must go through setTheme(), updateOptions ignores `theme`.
97
+ useEffect(() => {
98
+ if (!monaco || !editorRef.current) return;
99
+ monaco.editor.setTheme(resolvedTheme);
100
+ }, [monaco, resolvedTheme]);
101
+
102
+ if (error) {
103
+ return (
104
+ <div
105
+ className={className}
106
+ role="alert"
107
+ style={{
108
+ width: '100%',
109
+ height,
110
+ display: 'flex',
111
+ alignItems: 'center',
112
+ justifyContent: 'center',
113
+ padding: 16,
114
+ textAlign: 'center',
115
+ backgroundColor: EDITOR_BACKGROUND,
116
+ color: 'hsl(var(--destructive))',
117
+ fontSize: 13,
118
+ }}
119
+ >
120
+ Failed to load diff editor: {error.message}
121
+ </div>
122
+ );
123
+ }
124
+
125
+ if (isLoading) {
126
+ return (
127
+ <div
128
+ className={className}
129
+ role="status"
130
+ aria-busy="true"
131
+ style={{
132
+ width: '100%',
133
+ height,
134
+ display: 'flex',
135
+ alignItems: 'center',
136
+ justifyContent: 'center',
137
+ backgroundColor: EDITOR_BACKGROUND,
138
+ color: 'hsl(var(--muted-foreground))',
139
+ fontSize: 13,
140
+ }}
141
+ >
142
+ Loading diff editor…
143
+ </div>
144
+ );
145
+ }
146
+
147
+ return (
148
+ <div
149
+ ref={containerRef}
150
+ className={className}
151
+ style={{
152
+ width: '100%',
153
+ height,
154
+ backgroundColor: EDITOR_BACKGROUND,
155
+ }}
156
+ />
157
+ );
158
+ }
@@ -0,0 +1,293 @@
1
+ 'use client';
2
+
3
+ import { useRef, useEffect, useState, useCallback, forwardRef, useImperativeHandle } from 'react';
4
+ import type * as monaco from 'monaco-editor';
5
+
6
+ import { useMonaco } from '../hooks/useMonaco';
7
+ import { useEditorTheme, EDITOR_BACKGROUND } from '../hooks/useEditorTheme';
8
+ import type { EditorProps } from '../types';
9
+
10
+ export interface EditorRef {
11
+ /** Get editor instance */
12
+ getEditor: () => monaco.editor.IStandaloneCodeEditor | null;
13
+ /** Get current value */
14
+ getValue: () => string;
15
+ /** Set value */
16
+ setValue: (value: string) => void;
17
+ /** Focus editor */
18
+ focus: () => void;
19
+ }
20
+
21
+ /**
22
+ * Monaco Editor Component
23
+ *
24
+ * A React wrapper around Monaco Editor with full TypeScript support.
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <Editor
29
+ * value={code}
30
+ * language="typescript"
31
+ * onChange={(value) => setCode(value)}
32
+ * options={{ fontSize: 14, minimap: false }}
33
+ * />
34
+ * ```
35
+ */
36
+ export const Editor = forwardRef<EditorRef, EditorProps>(function Editor(
37
+ {
38
+ value = '',
39
+ language = 'plaintext',
40
+ onChange,
41
+ onMount,
42
+ options = {},
43
+ className = '',
44
+ height = '100%',
45
+ width = '100%',
46
+ autoHeight = false,
47
+ minHeight = 100,
48
+ maxHeight = 600,
49
+ autoFocus = false,
50
+ onSave,
51
+ },
52
+ ref
53
+ ) {
54
+ const containerRef = useRef<HTMLDivElement>(null);
55
+ const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
56
+ const { monaco, isLoading, error } = useMonaco();
57
+ const resolvedTheme = useEditorTheme(monaco, options.theme);
58
+
59
+ // Auto-height state
60
+ const [contentHeight, setContentHeight] = useState<number | null>(null);
61
+
62
+ const updateContentHeight = useCallback((editor: monaco.editor.IStandaloneCodeEditor) => {
63
+ if (!autoHeight) return;
64
+ const h = editor.getContentHeight();
65
+ setContentHeight(Math.min(Math.max(h, minHeight), maxHeight));
66
+ }, [autoHeight, minHeight, maxHeight]);
67
+
68
+ // Keep latest callbacks in refs so the create-effect can stay [monaco]-only
69
+ // without going stale when the parent passes new function identities.
70
+ const onChangeRef = useRef(onChange);
71
+ const onMountRef = useRef(onMount);
72
+ const onSaveRef = useRef(onSave);
73
+ useEffect(() => {
74
+ onChangeRef.current = onChange;
75
+ onMountRef.current = onMount;
76
+ onSaveRef.current = onSave;
77
+ });
78
+
79
+ // Expose editor methods via ref
80
+ useImperativeHandle(ref, () => ({
81
+ getEditor: () => editorRef.current,
82
+ getValue: () => editorRef.current?.getValue() ?? '',
83
+ setValue: (val: string) => editorRef.current?.setValue(val),
84
+ focus: () => editorRef.current?.focus(),
85
+ }), []);
86
+
87
+ // Create editor — runs once Monaco is available.
88
+ useEffect(() => {
89
+ if (!monaco || !containerRef.current || editorRef.current) return;
90
+
91
+ const editor = monaco.editor.create(containerRef.current, {
92
+ value,
93
+ language,
94
+ theme: resolvedTheme,
95
+ fontSize: options.fontSize || 14,
96
+ fontFamily: options.fontFamily || "'Fira Code', 'Consolas', monospace",
97
+ tabSize: options.tabSize || 2,
98
+ insertSpaces: options.insertSpaces !== false,
99
+ wordWrap: options.wordWrap || 'on',
100
+ minimap: { enabled: options.minimap !== false },
101
+ lineNumbers: options.lineNumbers || 'on',
102
+ readOnly: options.readOnly || false,
103
+ automaticLayout: true,
104
+ scrollBeyondLastLine: !autoHeight,
105
+ scrollbar: autoHeight ? { vertical: 'hidden', horizontal: 'auto' } : undefined,
106
+ overviewRulerLanes: autoHeight ? 0 : undefined,
107
+ padding: { top: 16, bottom: 16 },
108
+ renderLineHighlight: 'all',
109
+ cursorBlinking: 'smooth',
110
+ cursorSmoothCaretAnimation: 'on',
111
+ smoothScrolling: true,
112
+ bracketPairColorization: { enabled: true },
113
+ guides: {
114
+ bracketPairs: true,
115
+ indentation: true,
116
+ },
117
+ });
118
+
119
+ editorRef.current = editor;
120
+
121
+ // Change listener — always attached; reads latest onChange via ref.
122
+ const changeSub = editor.onDidChangeModelContent(() => {
123
+ onChangeRef.current?.(editor.getValue());
124
+ });
125
+
126
+ // Auto-height: resize container to fit content
127
+ let sizeSub: monaco.IDisposable | undefined;
128
+ if (autoHeight) {
129
+ sizeSub = editor.onDidContentSizeChange(() => updateContentHeight(editor));
130
+ updateContentHeight(editor);
131
+ }
132
+
133
+ // Cmd/Ctrl+S → save. Registered as a Monaco command so it wins over
134
+ // the browser's "save page" default whenever the editor has focus.
135
+ // Read through the ref so swapping handlers does not need to rebuild.
136
+ editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
137
+ onSaveRef.current?.(editor.getValue());
138
+ });
139
+
140
+ // autoFocus on first mount — Monaco refuses focus during layout
141
+ // measurement otherwise.
142
+ if (autoFocus) {
143
+ // queueMicrotask: defer past Monaco's own post-create layout so the
144
+ // focus call lands on a fully laid-out editor.
145
+ queueMicrotask(() => editorRef.current?.focus());
146
+ }
147
+
148
+ // Call onMount callback
149
+ onMountRef.current?.(editor);
150
+
151
+ return () => {
152
+ changeSub.dispose();
153
+ sizeSub?.dispose();
154
+ // Dispose the model explicitly — editor.dispose() does not always
155
+ // release models in older Monaco builds, and never the ones we own.
156
+ editor.getModel()?.dispose();
157
+ editor.dispose();
158
+ editorRef.current = null;
159
+ };
160
+ // eslint-disable-next-line react-hooks/exhaustive-deps
161
+ }, [monaco]);
162
+
163
+ // Sync value when the prop changes from outside (skip echoes of user typing).
164
+ useEffect(() => {
165
+ const editor = editorRef.current;
166
+ if (!editor) return;
167
+
168
+ const currentValue = editor.getValue();
169
+ // If the prop already matches the editor, nothing to do — this also
170
+ // covers the common controlled case where onChange echoes value back,
171
+ // so we never reset the cursor on the user's own keystrokes.
172
+ if (value === currentValue) return;
173
+
174
+ const position = editor.getPosition();
175
+ const selections = editor.getSelections();
176
+
177
+ // executeEdits preserves undo history (unlike setValue, which wipes it).
178
+ const model = editor.getModel();
179
+ if (model) {
180
+ editor.executeEdits('external-sync', [
181
+ { range: model.getFullModelRange(), text: value, forceMoveMarkers: true },
182
+ ]);
183
+ editor.pushUndoStop();
184
+ } else {
185
+ editor.setValue(value);
186
+ }
187
+
188
+ if (position) editor.setPosition(position);
189
+ if (selections && selections.length > 0) editor.setSelections(selections);
190
+ }, [value]);
191
+
192
+ // Update language when prop changes
193
+ useEffect(() => {
194
+ const editor = editorRef.current;
195
+ if (!editor || !monaco) return;
196
+
197
+ const model = editor.getModel();
198
+ if (model && model.getLanguageId() !== language) {
199
+ monaco.editor.setModelLanguage(model, language);
200
+ }
201
+ }, [language, monaco]);
202
+
203
+ // Apply theme — Monaco ignores `theme` inside updateOptions, it must go
204
+ // through setTheme(). This is what makes light/dark switching actually work.
205
+ useEffect(() => {
206
+ if (!monaco || !editorRef.current) return;
207
+ monaco.editor.setTheme(resolvedTheme);
208
+ }, [monaco, resolvedTheme]);
209
+
210
+ // Update options when props change
211
+ useEffect(() => {
212
+ const editor = editorRef.current;
213
+ if (!editor) return;
214
+
215
+ editor.updateOptions({
216
+ fontSize: options.fontSize,
217
+ readOnly: options.readOnly,
218
+ minimap: { enabled: options.minimap !== false },
219
+ wordWrap: options.wordWrap,
220
+ lineNumbers: options.lineNumbers,
221
+ tabSize: options.tabSize,
222
+ insertSpaces: options.insertSpaces,
223
+ });
224
+ }, [
225
+ options.fontSize,
226
+ options.readOnly,
227
+ options.minimap,
228
+ options.wordWrap,
229
+ options.lineNumbers,
230
+ options.tabSize,
231
+ options.insertSpaces,
232
+ ]);
233
+
234
+ if (error) {
235
+ return (
236
+ <div
237
+ className={className}
238
+ role="alert"
239
+ style={{
240
+ width,
241
+ height,
242
+ display: 'flex',
243
+ alignItems: 'center',
244
+ justifyContent: 'center',
245
+ padding: 16,
246
+ textAlign: 'center',
247
+ backgroundColor: EDITOR_BACKGROUND,
248
+ color: 'hsl(var(--destructive))',
249
+ fontSize: 13,
250
+ }}
251
+ >
252
+ Failed to load editor: {error.message}
253
+ </div>
254
+ );
255
+ }
256
+
257
+ if (isLoading) {
258
+ return (
259
+ <div
260
+ className={className}
261
+ role="status"
262
+ aria-busy="true"
263
+ style={{
264
+ width,
265
+ height,
266
+ display: 'flex',
267
+ alignItems: 'center',
268
+ justifyContent: 'center',
269
+ backgroundColor: EDITOR_BACKGROUND,
270
+ color: 'hsl(var(--muted-foreground))',
271
+ fontSize: 13,
272
+ }}
273
+ >
274
+ Loading editor…
275
+ </div>
276
+ );
277
+ }
278
+
279
+ const resolvedHeight = autoHeight && contentHeight != null ? contentHeight : height;
280
+
281
+ return (
282
+ <div
283
+ ref={containerRef}
284
+ className={className}
285
+ style={{
286
+ width,
287
+ height: resolvedHeight,
288
+ backgroundColor: EDITOR_BACKGROUND,
289
+ ...(autoHeight && { minHeight, maxHeight, overflow: 'hidden' }),
290
+ }}
291
+ />
292
+ );
293
+ });
@@ -0,0 +1,2 @@
1
+ export { Editor } from './Editor';
2
+ export { DiffEditor } from './DiffEditor';
@@ -0,0 +1,212 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, useState, useCallback, useMemo } from 'react';
4
+ import type * as monaco from 'monaco-editor';
5
+
6
+ import { useMonaco } from '../hooks/useMonaco';
7
+ import { getLanguageByFilename } from '../lib/languages';
8
+ import type { EditorFile, EditorContextValue } from '../types';
9
+
10
+ const EditorContext = createContext<EditorContextValue | null>(null);
11
+
12
+ /**
13
+ * Hook to access editor context
14
+ * Must be used within EditorProvider
15
+ */
16
+ export function useEditorContext(): EditorContextValue {
17
+ const context = useContext(EditorContext);
18
+ if (!context) {
19
+ throw new Error('useEditorContext must be used within EditorProvider');
20
+ }
21
+ return context;
22
+ }
23
+
24
+ interface EditorProviderProps {
25
+ children: React.ReactNode;
26
+ /** Callback when file save is requested */
27
+ onSave?: (path: string, content: string) => Promise<void>;
28
+ }
29
+
30
+ /**
31
+ * Editor Context Provider
32
+ *
33
+ * Manages multiple open files, active file state, and editor instance.
34
+ *
35
+ * @example
36
+ * ```tsx
37
+ * <EditorProvider onSave={handleSave}>
38
+ * <EditorTabs />
39
+ * <Editor />
40
+ * </EditorProvider>
41
+ * ```
42
+ */
43
+ export function EditorProvider({ children, onSave }: EditorProviderProps) {
44
+ const { monaco } = useMonaco();
45
+ const [editor, setEditor] = useState<monaco.editor.IStandaloneCodeEditor | null>(null);
46
+ const [openFiles, setOpenFiles] = useState<EditorFile[]>([]);
47
+ const [activeFilePath, setActiveFilePath] = useState<string | null>(null);
48
+
49
+ // Get active file
50
+ const activeFile = useMemo(
51
+ () => openFiles.find((f) => f.path === activeFilePath) || null,
52
+ [openFiles, activeFilePath]
53
+ );
54
+
55
+ // Open a file
56
+ const openFile = useCallback(
57
+ (path: string, content: string, language?: string) => {
58
+ setOpenFiles((files) => {
59
+ // Check if already open
60
+ const existing = files.find((f) => f.path === path);
61
+ if (existing) {
62
+ return files;
63
+ }
64
+
65
+ // Detect language from filename
66
+ const basename = path.split('/').pop() || path;
67
+ const detectedLanguage = language || getLanguageByFilename(basename);
68
+
69
+ // Create new file entry
70
+ const newFile: EditorFile = {
71
+ path,
72
+ content,
73
+ language: detectedLanguage,
74
+ isDirty: false,
75
+ };
76
+
77
+ return [...files, newFile];
78
+ });
79
+
80
+ // Set as active
81
+ setActiveFilePath(path);
82
+ },
83
+ []
84
+ );
85
+
86
+ // Close a file
87
+ const closeFile = useCallback(
88
+ (path: string) => {
89
+ setOpenFiles((files) => {
90
+ const index = files.findIndex((f) => f.path === path);
91
+ if (index === -1) return files;
92
+
93
+ const newFiles = files.filter((f) => f.path !== path);
94
+
95
+ // If closing active file, activate adjacent file
96
+ if (activeFilePath === path && newFiles.length > 0) {
97
+ const newIndex = Math.min(index, newFiles.length - 1);
98
+ const next = newFiles[newIndex];
99
+ if (next) setActiveFilePath(next.path);
100
+ } else if (newFiles.length === 0) {
101
+ setActiveFilePath(null);
102
+ }
103
+
104
+ return newFiles;
105
+ });
106
+ },
107
+ [activeFilePath]
108
+ );
109
+
110
+ // Set active file
111
+ const setActiveFile = useCallback((path: string) => {
112
+ setActiveFilePath(path);
113
+ }, []);
114
+
115
+ // Update file content
116
+ const updateContent = useCallback((path: string, content: string) => {
117
+ setOpenFiles((files) =>
118
+ files.map((f) =>
119
+ f.path === path
120
+ ? { ...f, content, isDirty: true }
121
+ : f
122
+ )
123
+ );
124
+ }, []);
125
+
126
+ // Save file
127
+ const saveFile = useCallback(
128
+ async (path: string) => {
129
+ const file = openFiles.find((f) => f.path === path);
130
+ if (!file) return;
131
+
132
+ if (onSave) {
133
+ await onSave(path, file.content);
134
+ }
135
+
136
+ // Mark as not dirty
137
+ setOpenFiles((files) =>
138
+ files.map((f) =>
139
+ f.path === path ? { ...f, isDirty: false } : f
140
+ )
141
+ );
142
+ },
143
+ [openFiles, onSave]
144
+ );
145
+
146
+ // Check if file is dirty
147
+ const isDirty = useCallback(
148
+ (path: string) => {
149
+ const file = openFiles.find((f) => f.path === path);
150
+ return file?.isDirty || false;
151
+ },
152
+ [openFiles]
153
+ );
154
+
155
+ // Get file content
156
+ const getContent = useCallback(
157
+ (path: string) => {
158
+ const file = openFiles.find((f) => f.path === path);
159
+ return file?.content || null;
160
+ },
161
+ [openFiles]
162
+ );
163
+
164
+ // Get file by path
165
+ const getFile = useCallback(
166
+ (path: string) => {
167
+ return openFiles.find((f) => f.path === path) || null;
168
+ },
169
+ [openFiles]
170
+ );
171
+
172
+ // Memoize so consumers don't re-render on unrelated parent renders.
173
+ const value = useMemo<EditorContextValue>(
174
+ () => ({
175
+ openFiles,
176
+ activeFile,
177
+ monaco,
178
+ editor,
179
+ isReady: monaco !== null && editor !== null,
180
+
181
+ openFile,
182
+ closeFile,
183
+ setActiveFile,
184
+ updateContent,
185
+ saveFile,
186
+
187
+ isDirty,
188
+ getContent,
189
+ getFile,
190
+ }),
191
+ [
192
+ openFiles,
193
+ activeFile,
194
+ monaco,
195
+ editor,
196
+ openFile,
197
+ closeFile,
198
+ setActiveFile,
199
+ updateContent,
200
+ saveFile,
201
+ isDirty,
202
+ getContent,
203
+ getFile,
204
+ ],
205
+ );
206
+
207
+ return (
208
+ <EditorContext.Provider value={value}>
209
+ {children}
210
+ </EditorContext.Provider>
211
+ );
212
+ }
@@ -0,0 +1 @@
1
+ export { EditorProvider, useEditorContext } from './EditorProvider';