@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.
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/package.json +119 -0
- package/src/diff-viewer/DiffViewer.tsx +168 -0
- package/src/diff-viewer/README.md +33 -0
- package/src/diff-viewer/components/CopyButton.tsx +49 -0
- package/src/diff-viewer/components/DiffLineContent.tsx +48 -0
- package/src/diff-viewer/components/SplitView.tsx +270 -0
- package/src/diff-viewer/components/UnifiedView.tsx +189 -0
- package/src/diff-viewer/diff-viewer.stories.tsx +168 -0
- package/src/diff-viewer/hooks/useDiff.ts +47 -0
- package/src/diff-viewer/hooks/useHighlighter.ts +54 -0
- package/src/diff-viewer/index.ts +22 -0
- package/src/diff-viewer/lazy.tsx +25 -0
- package/src/diff-viewer/types.ts +125 -0
- package/src/diff-viewer/utils/computeDiff.ts +171 -0
- package/src/editor/README.md +187 -0
- package/src/editor/components/DiffEditor.tsx +158 -0
- package/src/editor/components/Editor.tsx +293 -0
- package/src/editor/components/index.ts +2 -0
- package/src/editor/context/EditorProvider.tsx +212 -0
- package/src/editor/context/index.ts +1 -0
- package/src/editor/editor.stories.tsx +397 -0
- package/src/editor/hooks/index.ts +4 -0
- package/src/editor/hooks/useEditor.ts +36 -0
- package/src/editor/hooks/useEditorTheme.ts +91 -0
- package/src/editor/hooks/useLanguage.ts +29 -0
- package/src/editor/hooks/useMonaco.ts +79 -0
- package/src/editor/index.ts +16 -0
- package/src/editor/lazy.tsx +76 -0
- package/src/editor/lib/index.ts +2 -0
- package/src/editor/lib/languages.ts +227 -0
- package/src/editor/lib/themes.ts +42 -0
- package/src/editor/types/index.ts +137 -0
- package/src/editor/workers/index.ts +1 -0
- package/src/editor/workers/setup.ts +58 -0
- package/src/index.ts +10 -0
- package/src/json-editor/JsonEditor.tsx +115 -0
- package/src/json-editor/index.ts +1 -0
- package/src/json-editor/lazy.tsx +24 -0
- package/src/markdown-message/ActionRow.tsx +53 -0
- package/src/markdown-message/ChatMessageRow.tsx +103 -0
- package/src/markdown-message/CodeBlock.tsx +137 -0
- package/src/markdown-message/CollapseToggle.tsx +69 -0
- package/src/markdown-message/MarkdownMessage.tsx +376 -0
- package/src/markdown-message/README.md +221 -0
- package/src/markdown-message/builtinRules.tsx +92 -0
- package/src/markdown-message/components.tsx +224 -0
- package/src/markdown-message/index.ts +22 -0
- package/src/markdown-message/linkRules.ts +83 -0
- package/src/markdown-message/plainText.ts +83 -0
- package/src/markdown-message/sanitize.ts +88 -0
- package/src/markdown-message/streamingBlocks.ts +168 -0
- package/src/markdown-message/types.ts +168 -0
- package/src/panel-header/CodePanelHeader.tsx +121 -0
- package/src/panel-header/index.ts +1 -0
- package/src/pretty-code/PrettyCode.client.tsx +402 -0
- package/src/pretty-code/README.md +81 -0
- package/src/pretty-code/index.tsx +72 -0
- package/src/pretty-code/lazy.tsx +69 -0
- package/src/pretty-code/pretty-code.stories.tsx +353 -0
- package/src/pretty-code/registerPrismLanguages.ts +116 -0
- package/src/role-tokens.ts +39 -0
- package/src/use-collapsible-content.ts +236 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
|
+
//
|
|
3
|
+
// Shared tokenizer for DiffViewer. Reuses the same `prism-react-renderer`
|
|
4
|
+
// bundle that `PrettyCode` ships — no second highlighter is loaded. We
|
|
5
|
+
// also re-import `PrettyCode`'s grammar-registration hook so `bash`,
|
|
6
|
+
// `ruby`, `java`, `php` work in diffs without duplicating the dynamic
|
|
7
|
+
// imports.
|
|
8
|
+
|
|
9
|
+
import { useMemo } from 'react';
|
|
10
|
+
import type { Language } from 'prism-react-renderer';
|
|
11
|
+
import { useEnsurePrismLanguages } from '../../pretty-code/registerPrismLanguages';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Normalize a free-form language string into a Prism language key. Falls
|
|
15
|
+
* back to `text` when the key isn't one Prism knows about.
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeLanguage(language: string | undefined): Language {
|
|
18
|
+
if (!language) return 'text' as Language;
|
|
19
|
+
const lang = language.toLowerCase();
|
|
20
|
+
switch (lang) {
|
|
21
|
+
case 'js':
|
|
22
|
+
return 'javascript' as Language;
|
|
23
|
+
case 'ts':
|
|
24
|
+
return 'typescript' as Language;
|
|
25
|
+
case 'py':
|
|
26
|
+
return 'python' as Language;
|
|
27
|
+
case 'rb':
|
|
28
|
+
return 'ruby' as Language;
|
|
29
|
+
case 'sh':
|
|
30
|
+
case 'shell':
|
|
31
|
+
return 'bash' as Language;
|
|
32
|
+
case 'html':
|
|
33
|
+
case 'xml':
|
|
34
|
+
return 'markup' as Language;
|
|
35
|
+
case 'md':
|
|
36
|
+
return 'markdown' as Language;
|
|
37
|
+
case 'yml':
|
|
38
|
+
return 'yaml' as Language;
|
|
39
|
+
case 'golang':
|
|
40
|
+
return 'go' as Language;
|
|
41
|
+
default:
|
|
42
|
+
return lang as Language;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Subscribe to PrettyCode's extra-grammar loader and return the resolved
|
|
48
|
+
* Prism language key. The component re-renders once `bash`/`ruby`/… are
|
|
49
|
+
* registered so the first paint isn't stuck on plain text.
|
|
50
|
+
*/
|
|
51
|
+
export function useDiffLanguage(language: string | undefined): Language {
|
|
52
|
+
useEnsurePrismLanguages();
|
|
53
|
+
return useMemo(() => normalizeLanguage(language), [language]);
|
|
54
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
|
+
|
|
3
|
+
export { DiffViewer } from './DiffViewer';
|
|
4
|
+
export { DIFF_TONE_CLASSES } from './types';
|
|
5
|
+
export {
|
|
6
|
+
diffLines,
|
|
7
|
+
parseUnifiedPatch,
|
|
8
|
+
computeStats as computeDiffStats,
|
|
9
|
+
lineNumberWidth as diffLineNumberWidth,
|
|
10
|
+
} from './utils/computeDiff';
|
|
11
|
+
export { useDiff } from './hooks/useDiff';
|
|
12
|
+
export { normalizeLanguage as normalizeDiffLanguage } from './hooks/useHighlighter';
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
DiffLayout,
|
|
16
|
+
DiffLine,
|
|
17
|
+
DiffLineType,
|
|
18
|
+
DiffStats,
|
|
19
|
+
DiffInput,
|
|
20
|
+
DiffToneClasses,
|
|
21
|
+
DiffViewerProps,
|
|
22
|
+
} from './types';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import { createLazyComponent } from '@djangocfg/widget-kit/lazy';
|
|
6
|
+
import type { DiffViewerProps } from './types';
|
|
7
|
+
|
|
8
|
+
export const LazyDiffViewer = createLazyComponent<DiffViewerProps>(
|
|
9
|
+
() =>
|
|
10
|
+
import('./DiffViewer').then((mod) => ({ default: mod.DiffViewer })),
|
|
11
|
+
{
|
|
12
|
+
displayName: 'LazyDiffViewer',
|
|
13
|
+
fallback: (
|
|
14
|
+
<div
|
|
15
|
+
data-slot="diff-viewer-skeleton"
|
|
16
|
+
className="h-48 w-full animate-pulse rounded-xl border border-border/60 bg-muted"
|
|
17
|
+
/>
|
|
18
|
+
),
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// `DiffLayout` rides the public subpath too: a consumer that sets the
|
|
23
|
+
// viewer's `layout` needs to name the type, and reaching into
|
|
24
|
+
// `DiffViewer/types` directly would make a private path public by habit.
|
|
25
|
+
export type { DiffViewerProps, DiffLayout } from './types';
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
|
+
|
|
3
|
+
import type * as React from 'react';
|
|
4
|
+
import type { Language } from 'prism-react-renderer';
|
|
5
|
+
|
|
6
|
+
/** Layout of the diff: single-column unified vs. side-by-side split. */
|
|
7
|
+
export type DiffLayout = 'unified' | 'split';
|
|
8
|
+
|
|
9
|
+
/** Per-line kind in a computed diff. */
|
|
10
|
+
export type DiffLineType = 'added' | 'removed' | 'context';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One rendered line in the diff. Old / new line numbers are independent:
|
|
14
|
+
* an added line has no `oldNumber`, a removed line has no `newNumber`.
|
|
15
|
+
*/
|
|
16
|
+
export interface DiffLine {
|
|
17
|
+
type: DiffLineType;
|
|
18
|
+
content: string;
|
|
19
|
+
oldNumber: number | null;
|
|
20
|
+
newNumber: number | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Diff stats — rendered in the header pill. */
|
|
24
|
+
export interface DiffStats {
|
|
25
|
+
added: number;
|
|
26
|
+
removed: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface WithStrings {
|
|
30
|
+
/** Original code (LHS). */
|
|
31
|
+
oldCode: string;
|
|
32
|
+
/** Updated code (RHS). */
|
|
33
|
+
newCode: string;
|
|
34
|
+
patch?: never;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface WithPatch {
|
|
38
|
+
/** Pre-computed unified diff string (alternative to oldCode / newCode). */
|
|
39
|
+
patch: string;
|
|
40
|
+
oldCode?: never;
|
|
41
|
+
newCode?: never;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type DiffInput = WithStrings | WithPatch;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Tone classes for a single diff line. Tailwind class strings backed by
|
|
48
|
+
* semantic tokens — the diff viewer adapts to the host theme (light or
|
|
49
|
+
* dark) unlike `PrettyCode`, which is intentionally always-dark. Diffs
|
|
50
|
+
* are commonly embedded in documentation pages, where forcing a dark
|
|
51
|
+
* surface on a light page looks like a bug.
|
|
52
|
+
*/
|
|
53
|
+
export interface DiffToneClasses {
|
|
54
|
+
/** Row background tint. */
|
|
55
|
+
row: string;
|
|
56
|
+
/** Gutter / line-number column foreground. */
|
|
57
|
+
num: string;
|
|
58
|
+
/** Body text color. */
|
|
59
|
+
text: string;
|
|
60
|
+
/** Header stat pill color (e.g. `+12`). */
|
|
61
|
+
stat: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Mapping from line type to tone class bundle. Semantic tokens —
|
|
66
|
+
* resolved by the host theme (light surfaces in light mode, dark in
|
|
67
|
+
* dark mode).
|
|
68
|
+
*/
|
|
69
|
+
export const DIFF_TONE_CLASSES: Record<DiffLineType, DiffToneClasses> = {
|
|
70
|
+
added: {
|
|
71
|
+
row: 'bg-success/10',
|
|
72
|
+
num: 'text-success/80',
|
|
73
|
+
text: 'text-success',
|
|
74
|
+
stat: 'text-success',
|
|
75
|
+
},
|
|
76
|
+
removed: {
|
|
77
|
+
row: 'bg-destructive/15',
|
|
78
|
+
num: 'text-destructive/80',
|
|
79
|
+
text: 'text-destructive',
|
|
80
|
+
stat: 'text-destructive',
|
|
81
|
+
},
|
|
82
|
+
context: {
|
|
83
|
+
row: '',
|
|
84
|
+
num: 'text-muted-foreground/60',
|
|
85
|
+
text: 'text-foreground',
|
|
86
|
+
stat: 'text-muted-foreground',
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type DiffViewerProps = DiffInput &
|
|
91
|
+
Omit<React.ComponentProps<'div'>, 'children'> & {
|
|
92
|
+
/** Layout. @default `'unified'` */
|
|
93
|
+
layout?: DiffLayout;
|
|
94
|
+
/**
|
|
95
|
+
* Prism language key for optional syntax highlighting. When omitted,
|
|
96
|
+
* lines render as plain monospace. Highlighting reuses the same
|
|
97
|
+
* `prism-react-renderer` instance as the `PrettyCode` tool — no
|
|
98
|
+
* second highlighter is loaded.
|
|
99
|
+
*/
|
|
100
|
+
language?: Language | string;
|
|
101
|
+
/** Label for the LHS file header. */
|
|
102
|
+
oldTitle?: string;
|
|
103
|
+
/** Label for the RHS file header. */
|
|
104
|
+
newTitle?: string;
|
|
105
|
+
/** Show the copy-to-clipboard button. @default `true` */
|
|
106
|
+
copyable?: boolean;
|
|
107
|
+
/** Number of context lines around each hunk when diffing strings. @default `3` */
|
|
108
|
+
context?: number;
|
|
109
|
+
/**
|
|
110
|
+
* The HOST caps this viewer's height and owns the scroller (e.g. a chat
|
|
111
|
+
* bubble with `maxHeight` + `overflowY:auto`). Set it and the >100-line
|
|
112
|
+
* virtualized path is skipped, because Virtuoso would otherwise open its
|
|
113
|
+
* own fixed-height scroller INSIDE the host's — two scrollers of the same
|
|
114
|
+
* size, neither owning the scroll position Virtuoso measures against,
|
|
115
|
+
* leaving blank bands between consecutively-numbered rows.
|
|
116
|
+
*
|
|
117
|
+
* Not a styling hint and not a performance knob: it answers "who owns the
|
|
118
|
+
* height", which has exactly one right answer per embedding. Leave it false
|
|
119
|
+
* (the default) wherever the viewer is free to grow — documentation embeds
|
|
120
|
+
* expect that, and they are the case virtualization exists for.
|
|
121
|
+
*
|
|
122
|
+
* @default `false`
|
|
123
|
+
*/
|
|
124
|
+
hostScrolls?: boolean;
|
|
125
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
|
+
//
|
|
3
|
+
// Line-level diff via Longest Common Subsequence. The jalcoui original
|
|
4
|
+
// uses the `diff` npm package (Myers diff + unified patches). We don't
|
|
5
|
+
// pull that dependency: a 60-line LCS in TS handles up to ~5k lines
|
|
6
|
+
// fast enough for the viewer's use case (small/medium code reviews) and
|
|
7
|
+
// keeps the bundle lean. For pre-computed patches the caller can pass
|
|
8
|
+
// `patch` and we parse the unified-diff text directly — no algorithm
|
|
9
|
+
// runs at all.
|
|
10
|
+
|
|
11
|
+
import type { DiffLine } from '../types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compute line-level diff between two strings. Returns a flat sequence
|
|
15
|
+
* of `DiffLine`s in display order. `context` controls how many unchanged
|
|
16
|
+
* lines are emitted around each change-hunk; pass `Infinity` to keep the
|
|
17
|
+
* full file.
|
|
18
|
+
*/
|
|
19
|
+
export function diffLines(
|
|
20
|
+
oldCode: string,
|
|
21
|
+
newCode: string,
|
|
22
|
+
context = 3,
|
|
23
|
+
): DiffLine[] {
|
|
24
|
+
const oldLines = oldCode.split('\n');
|
|
25
|
+
const newLines = newCode.split('\n');
|
|
26
|
+
|
|
27
|
+
// Build LCS table (rows = oldLines + 1, cols = newLines + 1).
|
|
28
|
+
// For each cell, store the length of the longest common subsequence
|
|
29
|
+
// of `oldLines[0..i]` and `newLines[0..j]`.
|
|
30
|
+
const m = oldLines.length;
|
|
31
|
+
const n = newLines.length;
|
|
32
|
+
// Use a single flat Int32Array for cache friendliness.
|
|
33
|
+
const lcs = new Int32Array((m + 1) * (n + 1));
|
|
34
|
+
const cols = n + 1;
|
|
35
|
+
// Every `lcs` index below is derived from the loop bounds and is therefore
|
|
36
|
+
// in range; `at()` would cost a bounds re-check per cell in the hot loop, so
|
|
37
|
+
// read through a helper that keeps the `number` type without changing the
|
|
38
|
+
// arithmetic.
|
|
39
|
+
const cell = (index: number): number => lcs[index] ?? 0;
|
|
40
|
+
for (let i = 1; i <= m; i++) {
|
|
41
|
+
for (let j = 1; j <= n; j++) {
|
|
42
|
+
const idx = i * cols + j;
|
|
43
|
+
if (oldLines[i - 1] === newLines[j - 1]) {
|
|
44
|
+
lcs[idx] = cell((i - 1) * cols + (j - 1)) + 1;
|
|
45
|
+
} else {
|
|
46
|
+
const up = cell((i - 1) * cols + j);
|
|
47
|
+
const left = cell(i * cols + (j - 1));
|
|
48
|
+
lcs[idx] = up >= left ? up : left;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Walk back from (m, n) to (0, 0) emitting tagged lines.
|
|
54
|
+
type Tagged = { type: DiffLine['type']; oldNum: number | null; newNum: number | null; content: string };
|
|
55
|
+
const tagged: Tagged[] = [];
|
|
56
|
+
let i = m;
|
|
57
|
+
let j = n;
|
|
58
|
+
while (i > 0 || j > 0) {
|
|
59
|
+
// `i`/`j` are > 0 in each branch that indexes, so the reads are in range;
|
|
60
|
+
// the `?? ''` only satisfies noUncheckedIndexedAccess.
|
|
61
|
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
62
|
+
tagged.push({ type: 'context', oldNum: i, newNum: j, content: oldLines[i - 1] ?? '' });
|
|
63
|
+
i--;
|
|
64
|
+
j--;
|
|
65
|
+
} else if (j > 0 && (i === 0 || cell(i * cols + (j - 1)) >= cell((i - 1) * cols + j))) {
|
|
66
|
+
tagged.push({ type: 'added', oldNum: null, newNum: j, content: newLines[j - 1] ?? '' });
|
|
67
|
+
j--;
|
|
68
|
+
} else if (i > 0) {
|
|
69
|
+
tagged.push({ type: 'removed', oldNum: i, newNum: null, content: oldLines[i - 1] ?? '' });
|
|
70
|
+
i--;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
tagged.reverse();
|
|
74
|
+
|
|
75
|
+
// Apply context window — drop runs of `context`-only lines that are
|
|
76
|
+
// farther than `context` from any change. `Infinity` keeps every line.
|
|
77
|
+
if (!Number.isFinite(context)) {
|
|
78
|
+
return tagged.map((t) => ({
|
|
79
|
+
type: t.type,
|
|
80
|
+
content: t.content,
|
|
81
|
+
oldNumber: t.oldNum,
|
|
82
|
+
newNumber: t.newNum,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Mark indices that are within `context` of a change.
|
|
87
|
+
const keep = new Array<boolean>(tagged.length).fill(false);
|
|
88
|
+
for (let k = 0; k < tagged.length; k++) {
|
|
89
|
+
const t = tagged[k];
|
|
90
|
+
if (t === undefined) continue;
|
|
91
|
+
if (t.type !== 'context') {
|
|
92
|
+
const from = Math.max(0, k - context);
|
|
93
|
+
const to = Math.min(tagged.length - 1, k + context);
|
|
94
|
+
for (let s = from; s <= to; s++) keep[s] = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Drop unkept context lines. We don't render hunk separators (`@@`) —
|
|
99
|
+
// the viewer is meant for compact diffs, not full-file patches.
|
|
100
|
+
const out: DiffLine[] = [];
|
|
101
|
+
for (let k = 0; k < tagged.length; k++) {
|
|
102
|
+
if (!keep[k]) continue;
|
|
103
|
+
const t = tagged[k];
|
|
104
|
+
if (t === undefined) continue;
|
|
105
|
+
out.push({
|
|
106
|
+
type: t.type,
|
|
107
|
+
content: t.content,
|
|
108
|
+
oldNumber: t.oldNum,
|
|
109
|
+
newNumber: t.newNum,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Parse a unified diff patch (`@@ -a,b +c,d @@` hunks) into the same
|
|
117
|
+
* `DiffLine[]` shape `diffLines()` produces. Only the line tags and
|
|
118
|
+
* numbers from the hunk headers are honored — file headers
|
|
119
|
+
* (`--- a/file`, `+++ b/file`) are skipped.
|
|
120
|
+
*/
|
|
121
|
+
export function parseUnifiedPatch(patch: string): DiffLine[] {
|
|
122
|
+
const lines: DiffLine[] = [];
|
|
123
|
+
const HUNK = /^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/;
|
|
124
|
+
let oldNum = 0;
|
|
125
|
+
let newNum = 0;
|
|
126
|
+
let inHunk = false;
|
|
127
|
+
|
|
128
|
+
for (const raw of patch.split('\n')) {
|
|
129
|
+
const hunk = HUNK.exec(raw);
|
|
130
|
+
if (hunk) {
|
|
131
|
+
// Both groups are non-optional in HUNK, so a match always fills them.
|
|
132
|
+
oldNum = parseInt(hunk[1] ?? '0', 10);
|
|
133
|
+
newNum = parseInt(hunk[2] ?? '0', 10);
|
|
134
|
+
inHunk = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!inHunk) continue;
|
|
138
|
+
if (raw.startsWith('--- ') || raw.startsWith('+++ ') || raw.startsWith('\\')) continue;
|
|
139
|
+
|
|
140
|
+
if (raw.startsWith('+')) {
|
|
141
|
+
lines.push({ type: 'added', content: raw.slice(1), oldNumber: null, newNumber: newNum++ });
|
|
142
|
+
} else if (raw.startsWith('-')) {
|
|
143
|
+
lines.push({ type: 'removed', content: raw.slice(1), oldNumber: oldNum++, newNumber: null });
|
|
144
|
+
} else {
|
|
145
|
+
const content = raw.startsWith(' ') ? raw.slice(1) : raw;
|
|
146
|
+
lines.push({ type: 'context', content, oldNumber: oldNum++, newNumber: newNum++ });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return lines;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Widest gutter required for the line numbers, in characters. */
|
|
153
|
+
export function lineNumberWidth(lines: DiffLine[]): number {
|
|
154
|
+
let max = 0;
|
|
155
|
+
for (const line of lines) {
|
|
156
|
+
if (line.oldNumber && line.oldNumber > max) max = line.oldNumber;
|
|
157
|
+
if (line.newNumber && line.newNumber > max) max = line.newNumber;
|
|
158
|
+
}
|
|
159
|
+
return Math.max(String(max).length, 2);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Count `+` and `-` lines for the header stat pill. */
|
|
163
|
+
export function computeStats(lines: DiffLine[]): { added: number; removed: number } {
|
|
164
|
+
let added = 0;
|
|
165
|
+
let removed = 0;
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
if (line.type === 'added') added++;
|
|
168
|
+
else if (line.type === 'removed') removed++;
|
|
169
|
+
}
|
|
170
|
+
return { added, removed };
|
|
171
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
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/widget-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/widget-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/widget-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
|
+
The editor is always dark (devtool-style), regardless of the app theme. The custom `app-dark` Monaco theme is registered automatically with colors derived from CSS variables (`--background`, `--foreground`, `--card`, `--border`).
|
|
106
|
+
|
|
107
|
+
Override with an explicit theme:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
<Editor options={{ theme: 'vs-dark' }} /> {/* built-in dark */}
|
|
111
|
+
<Editor options={{ theme: 'hc-black' }} /> {/* high contrast */}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Hooks
|
|
115
|
+
|
|
116
|
+
| Hook | Returns | Description |
|
|
117
|
+
|------|---------|-------------|
|
|
118
|
+
| `useMonaco()` | `{ monaco, isLoading, error }` | Load Monaco namespace |
|
|
119
|
+
| `useEditor()` | `{ editor, isReady, setEditor }` | Editor instance management |
|
|
120
|
+
| `useLanguage(filename)` | `string` | Detect language from file extension |
|
|
121
|
+
| `useEditorTheme(monaco, override?)` | `string` | Resolved Monaco theme name (always dark) |
|
|
122
|
+
|
|
123
|
+
## Types
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import type {
|
|
127
|
+
EditorFile,
|
|
128
|
+
EditorOptions,
|
|
129
|
+
EditorProps,
|
|
130
|
+
DiffEditorProps,
|
|
131
|
+
EditorContextValue,
|
|
132
|
+
UseEditorReturn,
|
|
133
|
+
UseMonacoReturn,
|
|
134
|
+
} from '@djangocfg/widget-code/editor';
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Workers
|
|
138
|
+
|
|
139
|
+
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).
|
|
140
|
+
|
|
141
|
+
### Next.js (Webpack)
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
pnpm add monaco-editor-webpack-plugin
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
// next.config.js
|
|
149
|
+
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
webpack: (config) => {
|
|
153
|
+
config.plugins.push(new MonacoWebpackPlugin({
|
|
154
|
+
languages: ['typescript', 'javascript', 'json', 'css', 'html', 'python'],
|
|
155
|
+
}));
|
|
156
|
+
return config;
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Vite
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// Call once in app entry (e.g. main.tsx)
|
|
165
|
+
import { setupMonacoWorkers } from '@djangocfg/widget-code/editor';
|
|
166
|
+
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
|
167
|
+
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';
|
|
168
|
+
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
|
169
|
+
|
|
170
|
+
setupMonacoWorkers((label) => {
|
|
171
|
+
if (label === 'typescript' || label === 'javascript') return new tsWorker();
|
|
172
|
+
if (label === 'json') return new jsonWorker();
|
|
173
|
+
return new editorWorker();
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### No config
|
|
178
|
+
|
|
179
|
+
Just use the Editor — Monaco works without workers. You'll see a console warning which can be ignored.
|
|
180
|
+
|
|
181
|
+
## Bundle
|
|
182
|
+
|
|
183
|
+
~550KB (Monaco Editor). Loaded only when component is rendered — use tree-shakeable import:
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import { Editor } from '@djangocfg/widget-code/editor';
|
|
187
|
+
```
|