@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Reforms.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @djangocfg/widget-code
|
|
2
|
+
|
|
3
|
+
Code surfaces: an editor, a diff, a highlighted listing, a JSON editor, and the
|
|
4
|
+
markdown renderer that embeds them.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm add @djangocfg/widget-code @djangocfg/ui-core @djangocfg/i18n
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
| Subpath | What | Weight |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| `./editor` | Monaco, single file or diff | ~550 KB, lazy |
|
|
15
|
+
| `./json-editor` | an editable JSON tree | `json-edit-react` |
|
|
16
|
+
| `./pretty-code` | a read-only listing with a panel header | Prism |
|
|
17
|
+
| `./diff-viewer` | unified or split diff, virtualised | Prism + virtuoso |
|
|
18
|
+
| `./diff-viewer/full` | the same, eagerly | |
|
|
19
|
+
| `./markdown-message` | markdown with embedded code, diagrams and chips | |
|
|
20
|
+
| `./role-tokens` | the `ANCHOR` / `TOGGLE` class contracts | |
|
|
21
|
+
|
|
22
|
+
## No root barrel worth importing
|
|
23
|
+
|
|
24
|
+
Monaco alone is ~550 KB, and Prism and `json-edit-react` sit behind their own
|
|
25
|
+
subpaths. The root exports the role tokens and nothing else, on purpose.
|
|
26
|
+
|
|
27
|
+
## Read-only code elsewhere
|
|
28
|
+
|
|
29
|
+
`@djangocfg/widget-media/code` renders a listing with **shiki**, and a console
|
|
30
|
+
should pick one highlighter rather than match two themes by hand. Prefer it for
|
|
31
|
+
a new surface; this package's Prism stack stays because `DiffViewer` and
|
|
32
|
+
`MarkdownMessage` are built on it.
|
|
33
|
+
|
|
34
|
+
## Verify
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm --filter @djangocfg/widget-code check-types test
|
|
38
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@djangocfg/widget-code",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Code surfaces: a Monaco editor, a diff viewer, a Prism listing, a JSON editor and the markdown renderer that embeds them",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"code-editor",
|
|
7
|
+
"monaco",
|
|
8
|
+
"diff",
|
|
9
|
+
"syntax-highlighting",
|
|
10
|
+
"markdown",
|
|
11
|
+
"react",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "DjangoCFG",
|
|
16
|
+
"url": "https://djangocfg.com"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://djangocfg.com",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/markolofsen/django-cfg.git",
|
|
22
|
+
"directory": "widgets/code"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./src/index.ts",
|
|
27
|
+
"types": "./src/index.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./src/index.ts",
|
|
31
|
+
"default": "./src/index.ts"
|
|
32
|
+
},
|
|
33
|
+
"./editor": {
|
|
34
|
+
"types": "./src/editor/lazy.tsx",
|
|
35
|
+
"default": "./src/editor/lazy.tsx"
|
|
36
|
+
},
|
|
37
|
+
"./json-editor": {
|
|
38
|
+
"types": "./src/json-editor/lazy.tsx",
|
|
39
|
+
"default": "./src/json-editor/lazy.tsx"
|
|
40
|
+
},
|
|
41
|
+
"./pretty-code": {
|
|
42
|
+
"types": "./src/pretty-code/lazy.tsx",
|
|
43
|
+
"default": "./src/pretty-code/lazy.tsx"
|
|
44
|
+
},
|
|
45
|
+
"./diff-viewer": {
|
|
46
|
+
"types": "./src/diff-viewer/lazy.tsx",
|
|
47
|
+
"default": "./src/diff-viewer/lazy.tsx"
|
|
48
|
+
},
|
|
49
|
+
"./diff-viewer/full": {
|
|
50
|
+
"types": "./src/diff-viewer/index.ts",
|
|
51
|
+
"default": "./src/diff-viewer/index.ts"
|
|
52
|
+
},
|
|
53
|
+
"./markdown-message": {
|
|
54
|
+
"types": "./src/markdown-message/index.ts",
|
|
55
|
+
"default": "./src/markdown-message/index.ts"
|
|
56
|
+
},
|
|
57
|
+
"./role-tokens": {
|
|
58
|
+
"types": "./src/role-tokens.ts",
|
|
59
|
+
"default": "./src/role-tokens.ts"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"files": [
|
|
63
|
+
"src",
|
|
64
|
+
"!src/**/*.test.ts",
|
|
65
|
+
"!src/**/*.test.tsx",
|
|
66
|
+
"README.md",
|
|
67
|
+
"LICENSE"
|
|
68
|
+
],
|
|
69
|
+
"scripts": {
|
|
70
|
+
"check": "tsc --noEmit",
|
|
71
|
+
"check-types": "tsc --noEmit",
|
|
72
|
+
"test": "vitest run",
|
|
73
|
+
"test:watch": "vitest"
|
|
74
|
+
},
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"@djangocfg/widget-data": "^0.1.1",
|
|
77
|
+
"@djangocfg/widget-diagram": "^0.1.1",
|
|
78
|
+
"@djangocfg/widget-editor": "^0.1.1",
|
|
79
|
+
"@djangocfg/widget-kit": "^0.1.1",
|
|
80
|
+
"json-edit-react": "^1.30.2",
|
|
81
|
+
"monaco-editor": "^0.55.1",
|
|
82
|
+
"prism-react-renderer": "^2.4.1",
|
|
83
|
+
"prismjs": "^1.30.0",
|
|
84
|
+
"react-markdown": "10.1.0",
|
|
85
|
+
"react-virtuoso": "^4.18.10",
|
|
86
|
+
"rehype-external-links": "^3.0.0",
|
|
87
|
+
"rehype-raw": "^7.0.0",
|
|
88
|
+
"rehype-sanitize": "^6.0.0",
|
|
89
|
+
"remark-breaks": "^4.0.0",
|
|
90
|
+
"remark-emoji": "^5.0.2",
|
|
91
|
+
"remark-gfm": "4.0.1",
|
|
92
|
+
"remark-smartypants": "^3.0.2"
|
|
93
|
+
},
|
|
94
|
+
"peerDependencies": {
|
|
95
|
+
"@djangocfg/i18n": "^2.1.542",
|
|
96
|
+
"@djangocfg/ui-core": "^2.1.542",
|
|
97
|
+
"lucide-react": "^0.545.0",
|
|
98
|
+
"react": "^19.0.0",
|
|
99
|
+
"react-dom": "^19.0.0"
|
|
100
|
+
},
|
|
101
|
+
"devDependencies": {
|
|
102
|
+
"@djangocfg/i18n": "^2.1.542",
|
|
103
|
+
"@djangocfg/typescript-config": "^2.1.542",
|
|
104
|
+
"@djangocfg/ui-core": "^2.1.542",
|
|
105
|
+
"@storybook/react-vite": "^10.5.0",
|
|
106
|
+
"@types/node": "^25.9.5",
|
|
107
|
+
"@types/react": "19.2.15",
|
|
108
|
+
"@types/react-dom": "19.2.3",
|
|
109
|
+
"lucide-react": "0.545.0",
|
|
110
|
+
"react": "19.2.7",
|
|
111
|
+
"react-dom": "19.2.7",
|
|
112
|
+
"storybook": "^10.5.0",
|
|
113
|
+
"typescript": "^5.9.3",
|
|
114
|
+
"vitest": "^4.1.10"
|
|
115
|
+
},
|
|
116
|
+
"publishConfig": {
|
|
117
|
+
"access": "public"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
|
+
//
|
|
3
|
+
// Line-by-line code diff viewer. Supports unified (single column) and
|
|
4
|
+
// split (side-by-side) layouts. Syntax highlighting reuses the same
|
|
5
|
+
// `prism-react-renderer` bundle that the `PrettyCode` tool already
|
|
6
|
+
// pulls in — no second highlighter is loaded.
|
|
7
|
+
//
|
|
8
|
+
// Requires <UiProviders> at the host root (no nested providers here).
|
|
9
|
+
// See CONTRACT.md §5.
|
|
10
|
+
//
|
|
11
|
+
// Unlike `PrettyCode` (intentionally always-dark), DiffViewer adapts to
|
|
12
|
+
// the host theme. Diffs are commonly embedded in documentation pages
|
|
13
|
+
// where forcing a dark surface on a light page reads as a bug. The
|
|
14
|
+
// Prism theme is picked per host theme (`themes.vsDark` for dark,
|
|
15
|
+
// `themes.github` for light) and surface chrome uses semantic tokens.
|
|
16
|
+
|
|
17
|
+
'use client';
|
|
18
|
+
|
|
19
|
+
import * as React from 'react';
|
|
20
|
+
import { themes, type PrismTheme } from 'prism-react-renderer';
|
|
21
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
22
|
+
import { useResolvedTheme } from '@djangocfg/ui-core/hooks';
|
|
23
|
+
import { CopyButton } from './components/CopyButton';
|
|
24
|
+
import { SplitView } from './components/SplitView';
|
|
25
|
+
import { UnifiedView } from './components/UnifiedView';
|
|
26
|
+
import { useDiff } from './hooks/useDiff';
|
|
27
|
+
import { useDiffLanguage } from './hooks/useHighlighter';
|
|
28
|
+
import {
|
|
29
|
+
DIFF_TONE_CLASSES,
|
|
30
|
+
type DiffInput,
|
|
31
|
+
type DiffViewerProps,
|
|
32
|
+
} from './types';
|
|
33
|
+
|
|
34
|
+
export function DiffViewer({
|
|
35
|
+
layout = 'unified',
|
|
36
|
+
language,
|
|
37
|
+
oldTitle,
|
|
38
|
+
newTitle,
|
|
39
|
+
copyable = true,
|
|
40
|
+
context = 3,
|
|
41
|
+
hostScrolls = false,
|
|
42
|
+
className,
|
|
43
|
+
...allProps
|
|
44
|
+
}: DiffViewerProps) {
|
|
45
|
+
// Split the diff-input keys off the DOM rest props so we don't spread
|
|
46
|
+
// `oldCode` / `newCode` / `patch` onto the wrapping <div>.
|
|
47
|
+
const { oldCode, newCode, patch, ...divProps } =
|
|
48
|
+
allProps as DiffViewerProps & Record<string, unknown>;
|
|
49
|
+
const input: DiffInput =
|
|
50
|
+
patch !== undefined
|
|
51
|
+
? ({ patch: patch as string } as DiffInput)
|
|
52
|
+
: ({
|
|
53
|
+
oldCode: (oldCode as string) ?? '',
|
|
54
|
+
newCode: (newCode as string) ?? '',
|
|
55
|
+
} as DiffInput);
|
|
56
|
+
|
|
57
|
+
const { lines, numWidth, stats } = useDiff({ input, context });
|
|
58
|
+
|
|
59
|
+
// Longest content line in ch — the shared minimum width of the content
|
|
60
|
+
// column in BOTH layouts. A single global value (like `numWidth`) keeps
|
|
61
|
+
// every row the same width, so the whole block scrolls as one unit inside
|
|
62
|
+
// the outer `overflow-x-auto` wrapper below, and split-view columns stay
|
|
63
|
+
// aligned across rows. Tabs render 8ch wide under `whitespace-pre`, so
|
|
64
|
+
// they count as 8.
|
|
65
|
+
const maxContentCh = React.useMemo(() => {
|
|
66
|
+
let max = 0;
|
|
67
|
+
for (const l of lines) {
|
|
68
|
+
const tabs = (l.content.match(/\t/g) ?? []).length;
|
|
69
|
+
// CJK / fullwidth glyphs are one UTF-16 unit but render two columns
|
|
70
|
+
// wide in monospace — count them twice. (Emoji are already two units.)
|
|
71
|
+
const wide = (l.content.match(/[\u2E80-\u9FFF\uF900-\uFAFF\uFF01-\uFF60]/g) ?? []).length;
|
|
72
|
+
const len = l.content.length + tabs * 7 + wide;
|
|
73
|
+
if (len > max) max = len;
|
|
74
|
+
}
|
|
75
|
+
return max;
|
|
76
|
+
}, [lines]);
|
|
77
|
+
|
|
78
|
+
// Pass `null` when no language → cheap plain-text render path.
|
|
79
|
+
const prismLang = useDiffLanguage(language);
|
|
80
|
+
const resolvedLanguage = language ? prismLang : null;
|
|
81
|
+
|
|
82
|
+
// Pick a Prism palette to match the host theme. Resolved once at the
|
|
83
|
+
// viewer root, then forwarded to every line — keeps the hook out of
|
|
84
|
+
// per-line render paths (could be thousands of rows on big diffs).
|
|
85
|
+
const resolved = useResolvedTheme();
|
|
86
|
+
const prismTheme: PrismTheme =
|
|
87
|
+
resolved === 'dark' ? themes.vsDark : themes.github;
|
|
88
|
+
|
|
89
|
+
// Full text used for the copy button — concatenates current lines as
|
|
90
|
+
// displayed, including removed ones. Mirrors jalcoui's behavior.
|
|
91
|
+
const fullCode = React.useMemo(
|
|
92
|
+
() => lines.map((l) => l.content).join('\n'),
|
|
93
|
+
[lines],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const showHeader = Boolean(oldTitle || newTitle);
|
|
97
|
+
const addedTone = DIFF_TONE_CLASSES.added.stat;
|
|
98
|
+
const removedTone = DIFF_TONE_CLASSES.removed.stat;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
data-slot="diff-viewer"
|
|
103
|
+
data-layout={layout}
|
|
104
|
+
className={cn(
|
|
105
|
+
'relative overflow-hidden rounded-xl border border-border/60 bg-card text-foreground shadow-sm',
|
|
106
|
+
className,
|
|
107
|
+
)}
|
|
108
|
+
{...(divProps as React.ComponentProps<'div'>)}
|
|
109
|
+
>
|
|
110
|
+
{showHeader && (
|
|
111
|
+
<div className="flex items-center justify-between gap-3 border-b border-border/60 bg-muted/40 px-4 py-2.5">
|
|
112
|
+
<div className="flex items-center gap-3 text-sm">
|
|
113
|
+
{oldTitle && (
|
|
114
|
+
<span className="text-muted-foreground">{oldTitle}</span>
|
|
115
|
+
)}
|
|
116
|
+
{oldTitle && newTitle && (
|
|
117
|
+
<span className="text-muted-foreground/60" aria-hidden="true">
|
|
118
|
+
→
|
|
119
|
+
</span>
|
|
120
|
+
)}
|
|
121
|
+
{newTitle && (
|
|
122
|
+
<span className="font-medium text-foreground">{newTitle}</span>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
<div className="flex items-center gap-3">
|
|
126
|
+
<div className="flex items-center gap-2 text-xs tabular-nums">
|
|
127
|
+
{stats.added > 0 && (
|
|
128
|
+
<span className={addedTone}>+{stats.added}</span>
|
|
129
|
+
)}
|
|
130
|
+
{stats.removed > 0 && (
|
|
131
|
+
<span className={removedTone}>-{stats.removed}</span>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
{copyable && <CopyButton value={fullCode} />}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{!showHeader && copyable && (
|
|
140
|
+
<div className="absolute right-2 top-2 z-10">
|
|
141
|
+
<CopyButton value={fullCode} />
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
|
|
145
|
+
<div className="overflow-x-auto">
|
|
146
|
+
{layout === 'split' ? (
|
|
147
|
+
<SplitView
|
|
148
|
+
lines={lines}
|
|
149
|
+
numWidth={numWidth}
|
|
150
|
+
maxContentCh={maxContentCh}
|
|
151
|
+
language={resolvedLanguage}
|
|
152
|
+
prismTheme={prismTheme}
|
|
153
|
+
hostScrolls={hostScrolls}
|
|
154
|
+
/>
|
|
155
|
+
) : (
|
|
156
|
+
<UnifiedView
|
|
157
|
+
lines={lines}
|
|
158
|
+
numWidth={numWidth}
|
|
159
|
+
maxContentCh={maxContentCh}
|
|
160
|
+
language={resolvedLanguage}
|
|
161
|
+
prismTheme={prismTheme}
|
|
162
|
+
hostScrolls={hostScrolls}
|
|
163
|
+
/>
|
|
164
|
+
)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# DiffViewer
|
|
2
|
+
|
|
3
|
+
Line-by-line diff viewer with unified or split layout. Reuses the `prism-react-renderer` instance from `PrettyCode` for optional syntax highlighting — no second highlighter is loaded.
|
|
4
|
+
|
|
5
|
+
Pass either `oldCode` + `newCode` (computed diff) or a pre-built `patch` string (unified format).
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { DiffViewer } from '@djangocfg/widget-code/diff-viewer';
|
|
9
|
+
|
|
10
|
+
<DiffViewer
|
|
11
|
+
oldCode="const x = 1;"
|
|
12
|
+
newCode="const x = 2;\nconst y = 3;"
|
|
13
|
+
language="ts"
|
|
14
|
+
layout="unified"
|
|
15
|
+
/>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Props
|
|
19
|
+
|
|
20
|
+
| Prop | Type | Default | Description |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `oldCode` / `newCode` | `string` | — | Source pair to diff. Mutually exclusive with `patch`. |
|
|
23
|
+
| `patch` | `string` | — | Pre-computed unified diff. Mutually exclusive with `oldCode`/`newCode`. |
|
|
24
|
+
| `layout` | `'unified' \| 'split'` | `'unified'` | Single column vs side-by-side. |
|
|
25
|
+
| `language` | `Language \| string` | — | Prism language key for highlighting. Omit for plain monospace. |
|
|
26
|
+
| `oldTitle` | `string` | — | LHS file header label. |
|
|
27
|
+
| `newTitle` | `string` | — | RHS file header label. |
|
|
28
|
+
|
|
29
|
+
Storybook: `apps/storybook/stories/ui-tools/dev/DiffViewer.stories.tsx`
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
Adapted from jalcoui (MIT).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { Check, Copy } from 'lucide-react';
|
|
7
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
|
+
|
|
9
|
+
interface CopyButtonProps extends Omit<React.ComponentProps<'button'>, 'value'> {
|
|
10
|
+
value: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Tiny copy-to-clipboard button used in the DiffViewer header. Local
|
|
15
|
+
* component (not a hoist of `CopyAction` from `FloatingToolbar`) so the
|
|
16
|
+
* viewer can render without any provider context.
|
|
17
|
+
*
|
|
18
|
+
* Uses semantic tokens — the diff viewer adapts to the host theme,
|
|
19
|
+
* unlike `PrettyCode` which forces dark.
|
|
20
|
+
*/
|
|
21
|
+
export function CopyButton({ value, className, ...props }: CopyButtonProps) {
|
|
22
|
+
const [copied, setCopied] = React.useState(false);
|
|
23
|
+
|
|
24
|
+
const handleCopy = React.useCallback(() => {
|
|
25
|
+
void navigator.clipboard.writeText(value).then(() => {
|
|
26
|
+
setCopied(true);
|
|
27
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
28
|
+
});
|
|
29
|
+
}, [value]);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
onClick={handleCopy}
|
|
35
|
+
data-slot="diff-viewer-copy-button"
|
|
36
|
+
className={cn(
|
|
37
|
+
'inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs transition-colors',
|
|
38
|
+
'text-muted-foreground hover:text-foreground hover:bg-muted',
|
|
39
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50',
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
aria-label={copied ? 'Copied' : 'Copy code'}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
|
|
46
|
+
{copied ? 'Copied' : 'Copy'}
|
|
47
|
+
</button>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { Highlight, type Language, type PrismTheme } from 'prism-react-renderer';
|
|
7
|
+
|
|
8
|
+
interface DiffLineContentProps {
|
|
9
|
+
content: string;
|
|
10
|
+
language: Language | null;
|
|
11
|
+
/**
|
|
12
|
+
* Prism palette resolved by the parent `DiffViewer` against the host
|
|
13
|
+
* theme. Forwarded here so we don't run a theme-detection hook on
|
|
14
|
+
* every line (a big diff can be thousands of rows).
|
|
15
|
+
*/
|
|
16
|
+
prismTheme: PrismTheme;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Single-line content cell. When `language` is non-null the line is
|
|
21
|
+
* tokenized by `prism-react-renderer` (the same instance PrettyCode
|
|
22
|
+
* uses). When null we render plain text — cheap path for `language`-less
|
|
23
|
+
* diffs and very large lists.
|
|
24
|
+
*
|
|
25
|
+
* Each line tokenizes independently. For diffs > a few thousand lines
|
|
26
|
+
* this can get warm; in practice the LCS already drops to context lines
|
|
27
|
+
* before we get there.
|
|
28
|
+
*/
|
|
29
|
+
export const DiffLineContent = React.memo(function DiffLineContent({
|
|
30
|
+
content,
|
|
31
|
+
language,
|
|
32
|
+
prismTheme,
|
|
33
|
+
}: DiffLineContentProps) {
|
|
34
|
+
if (!language) {
|
|
35
|
+
return <>{content || ' '}</>;
|
|
36
|
+
}
|
|
37
|
+
return (
|
|
38
|
+
<Highlight theme={prismTheme} code={content || ' '} language={language}>
|
|
39
|
+
{({ tokens, getTokenProps }) => (
|
|
40
|
+
<>
|
|
41
|
+
{tokens[0]?.map((token, key) => (
|
|
42
|
+
<span key={key} {...getTokenProps({ token })} />
|
|
43
|
+
))}
|
|
44
|
+
</>
|
|
45
|
+
)}
|
|
46
|
+
</Highlight>
|
|
47
|
+
);
|
|
48
|
+
});
|