@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,397 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Editor, DiffEditor } from './lazy';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Samples
6
+ // ---------------------------------------------------------------------------
7
+
8
+ const TS_SAMPLE = `import { useState, useCallback, useMemo } from 'react';
9
+
10
+ interface User {
11
+ id: string;
12
+ name: string;
13
+ email: string;
14
+ role: 'admin' | 'user' | 'viewer';
15
+ }
16
+
17
+ export function useUsers() {
18
+ const [users, setUsers] = useState<User[]>([]);
19
+
20
+ const addUser = useCallback((user: Omit<User, 'id'>) => {
21
+ setUsers((prev) => [...prev, { ...user, id: crypto.randomUUID() }]);
22
+ }, []);
23
+
24
+ const admins = useMemo(
25
+ () => users.filter((u) => u.role === 'admin'),
26
+ [users],
27
+ );
28
+
29
+ return { users, admins, addUser };
30
+ }
31
+ `;
32
+
33
+ const JS_SAMPLE = `export function debounce(fn, ms = 200) {
34
+ let t;
35
+ return (...args) => {
36
+ clearTimeout(t);
37
+ t = setTimeout(() => fn(...args), ms);
38
+ };
39
+ }
40
+ `;
41
+
42
+ const PY_SAMPLE = `from dataclasses import dataclass, field
43
+ from datetime import datetime
44
+ from typing import Iterable
45
+
46
+
47
+ @dataclass
48
+ class User:
49
+ id: str
50
+ name: str
51
+ email: str
52
+ role: str = "user"
53
+ created_at: datetime = field(default_factory=datetime.utcnow)
54
+
55
+
56
+ def admins(users: Iterable[User]) -> list[User]:
57
+ return [u for u in users if u.role == "admin"]
58
+ `;
59
+
60
+ const JSON_SAMPLE = `{
61
+ "name": "@djangocfg/ui-tools",
62
+ "version": "2.1.0",
63
+ "dependencies": {
64
+ "monaco-editor": "^0.55.1",
65
+ "react": "^19.1.0"
66
+ },
67
+ "exports": {
68
+ ".": "./src/index.ts",
69
+ "./code-editor": "./src/tools/CodeEditor/index.ts"
70
+ }
71
+ }
72
+ `;
73
+
74
+ const CSS_SAMPLE = `:root {
75
+ --radius: 0.5rem;
76
+ --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
77
+ }
78
+
79
+ .editor-container {
80
+ position: relative;
81
+ border-radius: var(--radius);
82
+ border: 1px solid var(--border);
83
+ overflow: hidden;
84
+ }
85
+
86
+ .editor-container:focus-within {
87
+ outline: 2px solid var(--ring);
88
+ outline-offset: 2px;
89
+ }
90
+ `;
91
+
92
+ const HTML_SAMPLE = `<!DOCTYPE html>
93
+ <html lang="en">
94
+ <head>
95
+ <meta charset="utf-8" />
96
+ <title>Hello</title>
97
+ </head>
98
+ <body>
99
+ <main>
100
+ <h1>Hello world</h1>
101
+ </main>
102
+ </body>
103
+ </html>
104
+ `;
105
+
106
+ const MARKDOWN_SAMPLE = `# CodeEditor
107
+
108
+ Monaco-based editor wrapped for React.
109
+
110
+ - Syntax highlighting
111
+ - Diff view
112
+ - Auto-height
113
+
114
+ \`\`\`ts
115
+ const x: number = 1;
116
+ \`\`\`
117
+ `;
118
+
119
+ const SQL_SAMPLE = `SELECT u.id, u.name, count(o.id) AS orders
120
+ FROM users AS u
121
+ LEFT JOIN orders AS o ON o.user_id = u.id
122
+ WHERE u.created_at > now() - interval '30 days'
123
+ GROUP BY u.id, u.name
124
+ ORDER BY orders DESC
125
+ LIMIT 50;
126
+ `;
127
+
128
+ const YAML_SAMPLE = `version: "3.9"
129
+ services:
130
+ api:
131
+ image: ghcr.io/djangocfg/api:latest
132
+ ports:
133
+ - "8000:8000"
134
+ environment:
135
+ DATABASE_URL: postgres://app:app@db:5432/app
136
+ db:
137
+ image: postgres:16
138
+ volumes:
139
+ - pgdata:/var/lib/postgresql/data
140
+ volumes:
141
+ pgdata: {}
142
+ `;
143
+
144
+ const DOCKERFILE_SAMPLE = `FROM node:20-alpine AS deps
145
+ WORKDIR /app
146
+ COPY package.json pnpm-lock.yaml ./
147
+ RUN corepack enable && pnpm install --frozen-lockfile
148
+
149
+ FROM node:20-alpine AS runner
150
+ WORKDIR /app
151
+ COPY --from=deps /app/node_modules ./node_modules
152
+ COPY . .
153
+ CMD ["pnpm", "start"]
154
+ `;
155
+
156
+ const SAMPLES: Record<string, string> = {
157
+ typescript: TS_SAMPLE,
158
+ javascript: JS_SAMPLE,
159
+ python: PY_SAMPLE,
160
+ json: JSON_SAMPLE,
161
+ css: CSS_SAMPLE,
162
+ html: HTML_SAMPLE,
163
+ markdown: MARKDOWN_SAMPLE,
164
+ sql: SQL_SAMPLE,
165
+ yaml: YAML_SAMPLE,
166
+ dockerfile: DOCKERFILE_SAMPLE,
167
+ };
168
+
169
+ // 200+ line sample for LargeFile preset.
170
+ const LARGE_SAMPLE = Array.from({ length: 220 }, (_, i) => {
171
+ const n = i + 1;
172
+ if (n % 20 === 0) return `// ── section ${n / 20} ──────────────────────────────`;
173
+ if (n % 5 === 0) return `export const value${n} = computeValue(${n});`;
174
+ return `const line${n} = ${n} * Math.PI; // line ${n}`;
175
+ }).join('\n');
176
+
177
+ const DIFF_ORIGINAL = `function greet(name: string) {
178
+ console.log("Hello, " + name);
179
+ }
180
+
181
+ greet("World");
182
+ `;
183
+
184
+ const DIFF_MODIFIED = `function greet(name: string, greeting = "Hello") {
185
+ console.log(\`\${greeting}, \${name}!\`);
186
+ }
187
+
188
+ function farewell(name: string) {
189
+ console.log(\`Goodbye, \${name}!\`);
190
+ }
191
+
192
+ greet("World");
193
+ farewell("World");
194
+ `;
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Meta
198
+ // ---------------------------------------------------------------------------
199
+
200
+ /**
201
+ * The playground flattens Monaco's nested `options` into top-level controls —
202
+ * Storybook cannot drive a nested object with radios and number inputs. Those
203
+ * knobs are therefore the props of a real harness component rather than a
204
+ * story-only args type, so `satisfies Meta<typeof EditorPlayground>` checks them.
205
+ */
206
+ interface EditorPlaygroundProps {
207
+ language: keyof typeof SAMPLES;
208
+ theme: 'vs-dark' | 'vs-light';
209
+ readOnly: boolean;
210
+ minimap: boolean;
211
+ lineNumbers: 'on' | 'off' | 'relative' | 'interval';
212
+ wordWrap: 'off' | 'on' | 'wordWrapColumn' | 'bounded';
213
+ fontSize: number;
214
+ }
215
+
216
+ function EditorPlayground({
217
+ language,
218
+ theme,
219
+ readOnly,
220
+ minimap,
221
+ lineNumbers,
222
+ wordWrap,
223
+ fontSize,
224
+ }: EditorPlaygroundProps) {
225
+ return (
226
+ <div className="h-[460px] w-[760px] overflow-hidden rounded-md border">
227
+ <Editor
228
+ language={language}
229
+ value={SAMPLES[language]}
230
+ options={{ theme, readOnly, minimap, lineNumbers, wordWrap, fontSize }}
231
+ />
232
+ </div>
233
+ );
234
+ }
235
+
236
+ const meta = {
237
+ title: 'Widgets/Code/Editor',
238
+ component: EditorPlayground,
239
+ tags: ['autodocs'],
240
+ parameters: {
241
+ layout: 'centered',
242
+ docs: {
243
+ description: {
244
+ component:
245
+ 'Monaco-based code editor (~550KB). Heavy — load behind a route or lazy boundary in real apps. Wrap in a fixed-height container unless `autoHeight` is set.',
246
+ },
247
+ },
248
+ },
249
+ argTypes: {
250
+ language: {
251
+ control: 'select',
252
+ options: Object.keys(SAMPLES),
253
+ table: { category: 'Content' },
254
+ },
255
+ theme: {
256
+ control: 'inline-radio',
257
+ options: ['vs-dark', 'vs-light'],
258
+ table: { category: 'Appearance' },
259
+ },
260
+ fontSize: {
261
+ control: { type: 'number', min: 10, max: 24, step: 1 },
262
+ table: { category: 'Appearance' },
263
+ },
264
+ minimap: { control: 'boolean', table: { category: 'Appearance' } },
265
+ lineNumbers: {
266
+ control: 'inline-radio',
267
+ options: ['on', 'off', 'relative', 'interval'],
268
+ table: { category: 'Appearance' },
269
+ },
270
+ wordWrap: {
271
+ control: 'inline-radio',
272
+ options: ['off', 'on', 'wordWrapColumn', 'bounded'],
273
+ table: { category: 'Behaviour' },
274
+ },
275
+ readOnly: { control: 'boolean', table: { category: 'Behaviour' } },
276
+ },
277
+ args: {
278
+ language: 'typescript',
279
+ theme: 'vs-dark',
280
+ readOnly: false,
281
+ minimap: true,
282
+ lineNumbers: 'on',
283
+ wordWrap: 'on',
284
+ fontSize: 14,
285
+ },
286
+ } satisfies Meta<typeof EditorPlayground>;
287
+
288
+ export default meta;
289
+
290
+ type Story = StoryObj<typeof meta>;
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // Playground
294
+ // ---------------------------------------------------------------------------
295
+
296
+ export const Playground: Story = {};
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // Curated presets
300
+ // ---------------------------------------------------------------------------
301
+
302
+ export const TypeScript: Story = {
303
+ render: () => (
304
+ <div className="h-[420px] w-[760px] overflow-hidden rounded-md border">
305
+ <Editor language="typescript" value={TS_SAMPLE} />
306
+ </div>
307
+ ),
308
+ };
309
+
310
+ export const Json: Story = {
311
+ render: () => (
312
+ <div className="h-[360px] w-[760px] overflow-hidden rounded-md border">
313
+ <Editor language="json" value={JSON_SAMPLE} />
314
+ </div>
315
+ ),
316
+ };
317
+
318
+ export const Python: Story = {
319
+ render: () => (
320
+ <div className="h-[420px] w-[760px] overflow-hidden rounded-md border">
321
+ <Editor language="python" value={PY_SAMPLE} />
322
+ </div>
323
+ ),
324
+ };
325
+
326
+ export const ReadOnly: Story = {
327
+ render: () => (
328
+ <div className="h-[420px] w-[760px] overflow-hidden rounded-md border">
329
+ <Editor
330
+ language="typescript"
331
+ value={TS_SAMPLE}
332
+ options={{ readOnly: true, lineNumbers: 'on' }}
333
+ />
334
+ </div>
335
+ ),
336
+ };
337
+
338
+ export const LargeFile: Story = {
339
+ parameters: {
340
+ docs: {
341
+ description: {
342
+ story: '220-line file — minimap and scroll behaviour become relevant.',
343
+ },
344
+ },
345
+ },
346
+ render: () => (
347
+ <div className="h-[520px] w-[760px] overflow-hidden rounded-md border">
348
+ <Editor
349
+ language="typescript"
350
+ value={LARGE_SAMPLE}
351
+ options={{ minimap: true, lineNumbers: 'on' }}
352
+ />
353
+ </div>
354
+ ),
355
+ };
356
+
357
+ export const Diff: Story = {
358
+ parameters: {
359
+ docs: {
360
+ description: {
361
+ story: 'Side-by-side diff view via `DiffEditor`. Always read-only.',
362
+ },
363
+ },
364
+ },
365
+ render: () => (
366
+ <div className="h-[420px] w-[920px] overflow-hidden rounded-md border">
367
+ <DiffEditor
368
+ original={DIFF_ORIGINAL}
369
+ modified={DIFF_MODIFIED}
370
+ language="typescript"
371
+ />
372
+ </div>
373
+ ),
374
+ };
375
+
376
+ export const Minimal: Story = {
377
+ parameters: {
378
+ docs: {
379
+ description: {
380
+ story: 'No minimap, no line numbers, no word-wrap — minimal chrome for embeds.',
381
+ },
382
+ },
383
+ },
384
+ render: () => (
385
+ <div className="h-[300px] w-[640px] overflow-hidden rounded-md border">
386
+ <Editor
387
+ language="markdown"
388
+ value={MARKDOWN_SAMPLE}
389
+ options={{
390
+ minimap: false,
391
+ lineNumbers: 'off',
392
+ wordWrap: 'off',
393
+ }}
394
+ />
395
+ </div>
396
+ ),
397
+ };
@@ -0,0 +1,4 @@
1
+ export { useMonaco } from './useMonaco';
2
+ export { useEditor } from './useEditor';
3
+ export { useLanguage } from './useLanguage';
4
+ export { useEditorTheme } from './useEditorTheme';
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+ import type * as monaco from 'monaco-editor';
5
+
6
+ import type { UseEditorReturn } from '../types';
7
+
8
+ /**
9
+ * Hook to manage editor instance reference
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * const { editor, isReady, setEditor } = useEditor();
14
+ *
15
+ * // Pass setEditor to Editor component's onMount
16
+ * <Editor onMount={setEditor} />
17
+ *
18
+ * // Use editor when ready
19
+ * if (isReady) {
20
+ * editor.getModel()?.getValue();
21
+ * }
22
+ * ```
23
+ */
24
+ export function useEditor(): UseEditorReturn {
25
+ const [editor, setEditorState] = useState<monaco.editor.IStandaloneCodeEditor | null>(null);
26
+
27
+ const setEditor = useCallback((editorInstance: monaco.editor.IStandaloneCodeEditor | null) => {
28
+ setEditorState(editorInstance);
29
+ }, []);
30
+
31
+ return {
32
+ editor,
33
+ isReady: editor !== null,
34
+ setEditor,
35
+ };
36
+ }
@@ -0,0 +1,91 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ /** Background used for the editor and its loading/error placeholders. */
6
+ export const EDITOR_BACKGROUND = '#0a0a0a';
7
+
8
+ /**
9
+ * Hook that registers and returns the Monaco editor theme.
10
+ *
11
+ * The editor is always dark (Chrome devtools / VSCode convention) — its
12
+ * palette does not follow the host app theme. Pass `themeOverride` only
13
+ * if you need a specific Monaco built-in (e.g. `'hc-black'`).
14
+ *
15
+ * @param monaco - Monaco namespace (null during loading)
16
+ * @param themeOverride - Optional explicit theme name
17
+ * @returns Resolved Monaco theme name to pass to editor options
18
+ */
19
+ export function useEditorTheme(
20
+ monaco: typeof import('monaco-editor') | null,
21
+ themeOverride?: string,
22
+ ): string {
23
+ // State (not ref) so consumers re-render once the custom theme is registered.
24
+ const [registered, setRegistered] = useState(false);
25
+
26
+ useEffect(() => {
27
+ if (!monaco) return;
28
+
29
+ try {
30
+ const colors = readEditorColors();
31
+
32
+ monaco.editor.defineTheme('app-dark', {
33
+ base: 'vs-dark',
34
+ inherit: true,
35
+ rules: [
36
+ { token: 'comment', foreground: '6A9955', fontStyle: 'italic' },
37
+ { token: 'keyword', foreground: 'C586C0' },
38
+ { token: 'string', foreground: 'CE9178' },
39
+ { token: 'number', foreground: 'B5CEA8' },
40
+ { token: 'type', foreground: '4EC9B0' },
41
+ { token: 'function', foreground: 'DCDCAA' },
42
+ { token: 'variable', foreground: '9CDCFE' },
43
+ ],
44
+ colors: {
45
+ 'editor.background': colors.background,
46
+ 'editor.foreground': colors.foreground,
47
+ 'editor.lineHighlightBackground': colors.lineHighlight,
48
+ 'editor.selectionBackground': colors.selection,
49
+ 'editorCursor.foreground': colors.foreground,
50
+ 'editorLineNumber.foreground': colors.mutedForeground,
51
+ 'editorWidget.background': colors.card,
52
+ 'editorWidget.border': colors.border,
53
+ 'input.background': colors.card,
54
+ 'dropdown.background': colors.card,
55
+ },
56
+ });
57
+
58
+ setRegistered(true);
59
+ } catch {
60
+ // Fallback — use the built-in dark theme.
61
+ setRegistered(false);
62
+ }
63
+ }, [monaco]);
64
+
65
+ if (themeOverride) return themeOverride;
66
+ return registered ? 'app-dark' : 'vs-dark';
67
+ }
68
+
69
+ /**
70
+ * Editor colors — hard-coded dark palette.
71
+ *
72
+ * Previously we resolved against CSS variables (`--background`, `--card`,
73
+ * …) so the editor matched the host theme, but the editor MUST be dark
74
+ * regardless of host theme (Chrome devtools / VSCode / Insomnia
75
+ * convention). On a light-themed app `--background` resolves to white,
76
+ * which Monaco then paints under `base: 'vs-dark'` — giving the broken
77
+ * "white Monaco" look. Hard-coding keeps the dark contract honest and
78
+ * removes a layout-time DOM probe.
79
+ */
80
+ function readEditorColors() {
81
+ return {
82
+ background: EDITOR_BACKGROUND,
83
+ foreground: '#f5f5f5',
84
+ card: '#141414',
85
+ border: '#262626',
86
+ mutedForeground: '#858585',
87
+ lineHighlight: '#1a1a1a',
88
+ selection: '#264F78',
89
+ };
90
+ }
91
+
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import { getLanguageByFilename } from '../lib/languages';
5
+
6
+ /**
7
+ * Hook to detect language from filename
8
+ *
9
+ * @param filename - File name or path
10
+ * @returns Monaco language ID
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * const language = useLanguage('app.tsx');
15
+ * // Returns: 'typescript'
16
+ *
17
+ * const language2 = useLanguage('/path/to/Dockerfile');
18
+ * // Returns: 'dockerfile'
19
+ * ```
20
+ */
21
+ export function useLanguage(filename: string | undefined): string {
22
+ return useMemo(() => {
23
+ if (!filename) return 'plaintext';
24
+
25
+ // Extract basename from path
26
+ const basename = filename.split('/').pop() || filename;
27
+ return getLanguageByFilename(basename);
28
+ }, [filename]);
29
+ }
@@ -0,0 +1,79 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import type * as MonacoEditor from 'monaco-editor';
5
+
6
+ import { setupMonacoWorkers } from '../workers/setup';
7
+ import type { UseMonacoReturn } from '../types';
8
+
9
+ /**
10
+ * Shared, module-level Monaco loader.
11
+ *
12
+ * Monaco is ~550KB — we load it exactly once per page and hand the same
13
+ * module to every `useMonaco()` consumer. Without this each Editor /
14
+ * DiffEditor / EditorProvider would manage its own loading state and
15
+ * briefly flash a spinner even though the module is already in memory.
16
+ */
17
+ let monacoPromise: Promise<typeof MonacoEditor> | null = null;
18
+
19
+ function loadMonaco(): Promise<typeof MonacoEditor> {
20
+ if (!monacoPromise) {
21
+ monacoPromise = (async () => {
22
+ setupMonacoWorkers();
23
+ return import('monaco-editor');
24
+ })().catch((err) => {
25
+ // Reset so a later mount can retry instead of being stuck on a
26
+ // permanently-rejected promise.
27
+ monacoPromise = null;
28
+ throw err;
29
+ });
30
+ }
31
+ return monacoPromise;
32
+ }
33
+
34
+ /**
35
+ * Hook to load and access Monaco Editor instance
36
+ *
37
+ * Handles:
38
+ * - Dynamic import of Monaco (client-side only, loaded once and shared)
39
+ * - Web worker configuration
40
+ * - Loading state management
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * const { monaco, isLoading, error } = useMonaco();
45
+ *
46
+ * if (isLoading) return <Spinner />;
47
+ * if (error) return <Error message={error.message} />;
48
+ *
49
+ * // Use monaco API
50
+ * monaco.editor.create(...)
51
+ * ```
52
+ */
53
+ export function useMonaco(): UseMonacoReturn {
54
+ const [monaco, setMonaco] = useState<typeof MonacoEditor | null>(null);
55
+ const [isLoading, setIsLoading] = useState(true);
56
+ const [error, setError] = useState<Error | null>(null);
57
+
58
+ useEffect(() => {
59
+ let mounted = true;
60
+
61
+ loadMonaco()
62
+ .then((monacoModule) => {
63
+ if (!mounted) return;
64
+ setMonaco(monacoModule);
65
+ setIsLoading(false);
66
+ })
67
+ .catch((err) => {
68
+ if (!mounted) return;
69
+ setError(err instanceof Error ? err : new Error('Failed to load Monaco Editor'));
70
+ setIsLoading(false);
71
+ });
72
+
73
+ return () => {
74
+ mounted = false;
75
+ };
76
+ }, []);
77
+
78
+ return { monaco, isLoading, error };
79
+ }
@@ -0,0 +1,16 @@
1
+ // ============================================================================
2
+ // CodeEditor - Monaco Editor Integration (~550KB)
3
+ // Code editor components with Next.js support
4
+ // ============================================================================
5
+
6
+ // Components
7
+ export * from './components';
8
+
9
+ // Hooks
10
+ export * from './hooks';
11
+
12
+ // Context
13
+ export * from './context';
14
+
15
+ // Types
16
+ export * from './types';