@aprovan/patchwork-editor 0.1.1-dev.6bd527d → 0.1.2-dev.ba8f277
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/.turbo/turbo-build.log +3 -3
- package/dist/components/CodePreview.d.ts +10 -1
- package/dist/components/MarkdownPreview.d.ts +8 -0
- package/dist/components/SaveStatusButton.d.ts +9 -0
- package/dist/components/ServicesInspector.d.ts +3 -4
- package/dist/components/WidgetPreview.d.ts +8 -0
- package/dist/components/edit/EditModal.d.ts +2 -1
- package/dist/components/edit/FileTree.d.ts +22 -3
- package/dist/components/edit/fileTypes.d.ts +2 -0
- package/dist/components/edit/useEditSession.d.ts +1 -0
- package/dist/components/index.d.ts +7 -5
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1030 -197
- package/package.json +4 -3
- package/src/components/CodePreview.tsx +118 -160
- package/src/components/MarkdownPreview.tsx +147 -0
- package/src/components/SaveStatusButton.tsx +55 -0
- package/src/components/ServicesInspector.tsx +101 -37
- package/src/components/WidgetPreview.tsx +102 -0
- package/src/components/edit/CodeBlockView.tsx +135 -19
- package/src/components/edit/EditModal.tsx +86 -28
- package/src/components/edit/FileTree.tsx +524 -29
- package/src/components/edit/MediaPreview.tsx +23 -5
- package/src/components/edit/api.ts +6 -1
- package/src/components/edit/fileTypes.ts +8 -0
- package/src/components/edit/useEditSession.ts +13 -3
- package/src/components/index.ts +7 -5
- package/src/index.ts +10 -6
|
@@ -1,66 +1,123 @@
|
|
|
1
|
-
import { useState } from
|
|
2
|
-
import { ChevronDown, Server } from
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { ChevronDown, Server } from "lucide-react";
|
|
3
3
|
|
|
4
4
|
export interface ServiceInfo {
|
|
5
5
|
name: string;
|
|
6
6
|
namespace: string;
|
|
7
7
|
procedure: string;
|
|
8
8
|
description: string;
|
|
9
|
-
parameters
|
|
10
|
-
jsonSchema: Record<string, unknown>;
|
|
11
|
-
};
|
|
9
|
+
parameters?: Record<string, unknown>;
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
interface ServicesInspectorProps {
|
|
15
13
|
namespaces: string[];
|
|
16
14
|
services?: ServiceInfo[];
|
|
17
15
|
/** Custom badge component for rendering the service count */
|
|
18
|
-
BadgeComponent?: React.ComponentType<{
|
|
16
|
+
BadgeComponent?: React.ComponentType<{
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
variant?: string;
|
|
19
|
+
className?: string;
|
|
20
|
+
}>;
|
|
19
21
|
/** Custom collapsible components */
|
|
20
|
-
CollapsibleComponent?: React.ComponentType<{
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
CollapsibleComponent?: React.ComponentType<{
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
defaultOpen?: boolean;
|
|
25
|
+
className?: string;
|
|
26
|
+
}>;
|
|
27
|
+
CollapsibleTriggerComponent?: React.ComponentType<{
|
|
28
|
+
children: React.ReactNode;
|
|
29
|
+
className?: string;
|
|
30
|
+
}>;
|
|
31
|
+
CollapsibleContentComponent?: React.ComponentType<{
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
}>;
|
|
23
34
|
/** Custom dialog components */
|
|
24
|
-
DialogComponent?: React.ComponentType<{
|
|
35
|
+
DialogComponent?: React.ComponentType<{
|
|
36
|
+
children: React.ReactNode;
|
|
37
|
+
open?: boolean;
|
|
38
|
+
onOpenChange?: (open: boolean) => void;
|
|
39
|
+
}>;
|
|
25
40
|
DialogHeaderComponent?: React.ComponentType<{ children: React.ReactNode }>;
|
|
26
|
-
DialogContentComponent?: React.ComponentType<{
|
|
41
|
+
DialogContentComponent?: React.ComponentType<{
|
|
42
|
+
children: React.ReactNode;
|
|
43
|
+
className?: string;
|
|
44
|
+
}>;
|
|
27
45
|
DialogCloseComponent?: React.ComponentType<{ onClose?: () => void }>;
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
// Fallback components for when custom UI is not provided
|
|
31
|
-
function DefaultBadge({
|
|
49
|
+
function DefaultBadge({
|
|
50
|
+
children,
|
|
51
|
+
className = "",
|
|
52
|
+
}: {
|
|
53
|
+
children: React.ReactNode;
|
|
54
|
+
className?: string;
|
|
55
|
+
}) {
|
|
32
56
|
return (
|
|
33
|
-
<span
|
|
57
|
+
<span
|
|
58
|
+
className={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${className}`}
|
|
59
|
+
>
|
|
34
60
|
{children}
|
|
35
61
|
</span>
|
|
36
62
|
);
|
|
37
63
|
}
|
|
38
64
|
|
|
39
|
-
function DefaultDialog({
|
|
65
|
+
function DefaultDialog({
|
|
66
|
+
children,
|
|
67
|
+
open,
|
|
68
|
+
onOpenChange,
|
|
69
|
+
}: {
|
|
70
|
+
children: React.ReactNode;
|
|
71
|
+
open?: boolean;
|
|
72
|
+
onOpenChange?: (open: boolean) => void;
|
|
73
|
+
}) {
|
|
40
74
|
if (!open) return null;
|
|
41
75
|
return (
|
|
42
|
-
<div
|
|
43
|
-
|
|
76
|
+
<div
|
|
77
|
+
className="fixed inset-0 z-50 bg-black/50"
|
|
78
|
+
onClick={() => onOpenChange?.(false)}
|
|
79
|
+
>
|
|
80
|
+
<div
|
|
81
|
+
className="fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 bg-background p-6 shadow-lg rounded-lg"
|
|
82
|
+
onClick={(e) => e.stopPropagation()}
|
|
83
|
+
>
|
|
44
84
|
{children}
|
|
45
85
|
</div>
|
|
46
86
|
</div>
|
|
47
87
|
);
|
|
48
88
|
}
|
|
49
89
|
|
|
50
|
-
export function ServicesInspector({
|
|
51
|
-
namespaces,
|
|
90
|
+
export function ServicesInspector({
|
|
91
|
+
namespaces,
|
|
52
92
|
services = [],
|
|
53
93
|
BadgeComponent = DefaultBadge,
|
|
54
94
|
DialogComponent = DefaultDialog,
|
|
95
|
+
DialogHeaderComponent = ({ children }) => (
|
|
96
|
+
<div className="flex justify-between items-center mb-4">{children}</div>
|
|
97
|
+
),
|
|
98
|
+
DialogContentComponent = ({ children, className = "" }) => (
|
|
99
|
+
<div className={className}>{children}</div>
|
|
100
|
+
),
|
|
101
|
+
DialogCloseComponent = ({ onClose }) => (
|
|
102
|
+
<button
|
|
103
|
+
onClick={() => onClose?.()}
|
|
104
|
+
className="text-muted-foreground hover:text-foreground"
|
|
105
|
+
>
|
|
106
|
+
×
|
|
107
|
+
</button>
|
|
108
|
+
),
|
|
55
109
|
}: ServicesInspectorProps) {
|
|
56
110
|
const [open, setOpen] = useState(false);
|
|
57
111
|
|
|
58
112
|
if (namespaces.length === 0) return null;
|
|
59
113
|
|
|
60
|
-
const groupedServices = services.reduce<Record<string, ServiceInfo[]>>(
|
|
61
|
-
(acc
|
|
62
|
-
|
|
63
|
-
|
|
114
|
+
const groupedServices = services.reduce<Record<string, ServiceInfo[]>>(
|
|
115
|
+
(acc, svc) => {
|
|
116
|
+
(acc[svc.namespace] ??= []).push(svc);
|
|
117
|
+
return acc;
|
|
118
|
+
},
|
|
119
|
+
{},
|
|
120
|
+
);
|
|
64
121
|
|
|
65
122
|
return (
|
|
66
123
|
<>
|
|
@@ -70,16 +127,16 @@ export function ServicesInspector({
|
|
|
70
127
|
>
|
|
71
128
|
<Server className="h-4 w-4 text-muted-foreground" />
|
|
72
129
|
<BadgeComponent className="text-xs">
|
|
73
|
-
{namespaces.length} service{namespaces.length !== 1 ?
|
|
130
|
+
{namespaces.length} service{namespaces.length !== 1 ? "s" : ""}
|
|
74
131
|
</BadgeComponent>
|
|
75
132
|
</button>
|
|
76
133
|
|
|
77
134
|
<DialogComponent open={open} onOpenChange={setOpen}>
|
|
78
|
-
<
|
|
135
|
+
<DialogHeaderComponent>
|
|
79
136
|
<h2 className="text-lg font-semibold">Available Services</h2>
|
|
80
|
-
<
|
|
81
|
-
</
|
|
82
|
-
<
|
|
137
|
+
<DialogCloseComponent onClose={() => setOpen(false)} />
|
|
138
|
+
</DialogHeaderComponent>
|
|
139
|
+
<DialogContentComponent className="space-y-3 max-h-96 overflow-auto">
|
|
83
140
|
{namespaces.map((ns) => (
|
|
84
141
|
<details key={ns} open={namespaces.length === 1}>
|
|
85
142
|
<summary className="flex items-center gap-2 w-full p-2 rounded bg-muted/50 hover:bg-muted transition-colors cursor-pointer">
|
|
@@ -87,7 +144,8 @@ export function ServicesInspector({
|
|
|
87
144
|
<span className="font-medium text-sm">{ns}</span>
|
|
88
145
|
{groupedServices[ns] && (
|
|
89
146
|
<BadgeComponent className="ml-auto text-xs">
|
|
90
|
-
{groupedServices[ns].length} tool
|
|
147
|
+
{groupedServices[ns].length} tool
|
|
148
|
+
{groupedServices[ns].length !== 1 ? "s" : ""}
|
|
91
149
|
</BadgeComponent>
|
|
92
150
|
)}
|
|
93
151
|
</summary>
|
|
@@ -95,23 +153,29 @@ export function ServicesInspector({
|
|
|
95
153
|
{groupedServices[ns]?.map((svc) => (
|
|
96
154
|
<details key={svc.name}>
|
|
97
155
|
<summary className="flex items-center gap-2 w-full text-left text-sm hover:text-foreground text-muted-foreground transition-colors cursor-pointer">
|
|
98
|
-
<ChevronDown className="h-3 w-3" />
|
|
156
|
+
{svc.parameters && <ChevronDown className="h-3 w-3" />}
|
|
99
157
|
<code className="font-mono text-xs">{svc.procedure}</code>
|
|
100
|
-
<span className="truncate text-xs opacity-70">
|
|
158
|
+
<span className="truncate text-xs opacity-70">
|
|
159
|
+
{svc.description}
|
|
160
|
+
</span>
|
|
101
161
|
</summary>
|
|
102
|
-
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
162
|
+
{svc.parameters && (
|
|
163
|
+
<div className="ml-5 mt-1 p-2 rounded border bg-muted/30 overflow-auto max-h-48">
|
|
164
|
+
<pre className="text-xs font-mono whitespace-pre-wrap break-words m-0">
|
|
165
|
+
{JSON.stringify(svc.parameters, null, 2)}
|
|
166
|
+
</pre>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
107
169
|
</details>
|
|
108
170
|
)) ?? (
|
|
109
|
-
<p className="text-xs text-muted-foreground">
|
|
171
|
+
<p className="text-xs text-muted-foreground">
|
|
172
|
+
No tool details available
|
|
173
|
+
</p>
|
|
110
174
|
)}
|
|
111
175
|
</div>
|
|
112
176
|
</details>
|
|
113
177
|
))}
|
|
114
|
-
</
|
|
178
|
+
</DialogContentComponent>
|
|
115
179
|
</DialogComponent>
|
|
116
180
|
</>
|
|
117
181
|
);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { AlertCircle, Loader2 } from 'lucide-react';
|
|
3
|
+
import type { Compiler, Manifest, MountedWidget } from '@aprovan/patchwork-compiler';
|
|
4
|
+
|
|
5
|
+
export interface WidgetPreviewProps {
|
|
6
|
+
code: string;
|
|
7
|
+
compiler: Compiler | null;
|
|
8
|
+
services?: string[];
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createManifest(services?: string[]): Manifest {
|
|
13
|
+
return {
|
|
14
|
+
name: 'preview',
|
|
15
|
+
version: '1.0.0',
|
|
16
|
+
platform: 'browser',
|
|
17
|
+
image: '@aprovan/patchwork-image-shadcn',
|
|
18
|
+
services,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function WidgetPreview({
|
|
23
|
+
code,
|
|
24
|
+
compiler,
|
|
25
|
+
services,
|
|
26
|
+
enabled = true,
|
|
27
|
+
}: WidgetPreviewProps) {
|
|
28
|
+
const [loading, setLoading] = useState(false);
|
|
29
|
+
const [error, setError] = useState<string | null>(null);
|
|
30
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
31
|
+
const mountedRef = useRef<MountedWidget | null>(null);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!enabled || !compiler || !containerRef.current) return;
|
|
35
|
+
|
|
36
|
+
let cancelled = false;
|
|
37
|
+
|
|
38
|
+
const compileAndMount = async () => {
|
|
39
|
+
setLoading(true);
|
|
40
|
+
setError(null);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
if (mountedRef.current) {
|
|
44
|
+
compiler.unmount(mountedRef.current);
|
|
45
|
+
mountedRef.current = null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const widget = await compiler.compile(code, createManifest(services), {
|
|
49
|
+
typescript: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (cancelled || !containerRef.current) return;
|
|
53
|
+
|
|
54
|
+
const mounted = await compiler.mount(widget, {
|
|
55
|
+
target: containerRef.current,
|
|
56
|
+
mode: 'embedded',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
mountedRef.current = mounted;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (!cancelled) {
|
|
62
|
+
setError(err instanceof Error ? err.message : 'Failed to render preview');
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
if (!cancelled) {
|
|
66
|
+
setLoading(false);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
void compileAndMount();
|
|
72
|
+
|
|
73
|
+
return () => {
|
|
74
|
+
cancelled = true;
|
|
75
|
+
if (mountedRef.current && compiler) {
|
|
76
|
+
compiler.unmount(mountedRef.current);
|
|
77
|
+
mountedRef.current = null;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}, [code, compiler, enabled, services]);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
{error && (
|
|
85
|
+
<div className="text-sm text-destructive flex items-center gap-2 p-3">
|
|
86
|
+
<AlertCircle className="h-4 w-4 shrink-0" />
|
|
87
|
+
<span>{error}</span>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
{loading && (
|
|
91
|
+
<div className="p-3 flex items-center gap-2 text-muted-foreground">
|
|
92
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
93
|
+
<span className="text-sm">Rendering preview...</span>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
{!compiler && enabled && !loading && !error && (
|
|
97
|
+
<div className="p-3 text-sm text-muted-foreground">Compiler not initialized</div>
|
|
98
|
+
)}
|
|
99
|
+
<div ref={containerRef} className="w-full" />
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -1,4 +1,60 @@
|
|
|
1
|
-
import { useCallback, useRef, useEffect } from 'react';
|
|
1
|
+
import { useCallback, useRef, useEffect, useState, useMemo } from 'react';
|
|
2
|
+
import { createHighlighter, type Highlighter, type BundledLanguage } from 'shiki';
|
|
3
|
+
|
|
4
|
+
// Singleton highlighter instance
|
|
5
|
+
let highlighterPromise: Promise<Highlighter> | null = null;
|
|
6
|
+
|
|
7
|
+
const COMMON_LANGUAGES: BundledLanguage[] = [
|
|
8
|
+
'typescript',
|
|
9
|
+
'javascript',
|
|
10
|
+
'tsx',
|
|
11
|
+
'jsx',
|
|
12
|
+
'json',
|
|
13
|
+
'html',
|
|
14
|
+
'css',
|
|
15
|
+
'markdown',
|
|
16
|
+
'yaml',
|
|
17
|
+
'python',
|
|
18
|
+
'bash',
|
|
19
|
+
'sql',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function getHighlighter(): Promise<Highlighter> {
|
|
23
|
+
if (!highlighterPromise) {
|
|
24
|
+
highlighterPromise = createHighlighter({
|
|
25
|
+
themes: ['github-light'],
|
|
26
|
+
langs: COMMON_LANGUAGES,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return highlighterPromise;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Map common file extensions/language names to shiki language identifiers
|
|
33
|
+
function normalizeLanguage(lang: string | null): BundledLanguage {
|
|
34
|
+
if (!lang) return 'typescript';
|
|
35
|
+
const normalized = lang.toLowerCase();
|
|
36
|
+
const mapping: Record<string, BundledLanguage> = {
|
|
37
|
+
ts: 'typescript',
|
|
38
|
+
tsx: 'tsx',
|
|
39
|
+
js: 'javascript',
|
|
40
|
+
jsx: 'jsx',
|
|
41
|
+
json: 'json',
|
|
42
|
+
html: 'html',
|
|
43
|
+
css: 'css',
|
|
44
|
+
md: 'markdown',
|
|
45
|
+
markdown: 'markdown',
|
|
46
|
+
yml: 'yaml',
|
|
47
|
+
yaml: 'yaml',
|
|
48
|
+
py: 'python',
|
|
49
|
+
python: 'python',
|
|
50
|
+
sh: 'bash',
|
|
51
|
+
bash: 'bash',
|
|
52
|
+
sql: 'sql',
|
|
53
|
+
typescript: 'typescript',
|
|
54
|
+
javascript: 'javascript',
|
|
55
|
+
};
|
|
56
|
+
return mapping[normalized] || 'typescript';
|
|
57
|
+
}
|
|
2
58
|
|
|
3
59
|
export interface CodeBlockViewProps {
|
|
4
60
|
content: string;
|
|
@@ -9,7 +65,19 @@ export interface CodeBlockViewProps {
|
|
|
9
65
|
|
|
10
66
|
export function CodeBlockView({ content, language, editable = false, onChange }: CodeBlockViewProps) {
|
|
11
67
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
68
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
69
|
+
const [highlighter, setHighlighter] = useState<Highlighter | null>(null);
|
|
12
70
|
|
|
71
|
+
// Load the highlighter
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
let mounted = true;
|
|
74
|
+
getHighlighter().then((h) => {
|
|
75
|
+
if (mounted) setHighlighter(h);
|
|
76
|
+
});
|
|
77
|
+
return () => { mounted = false; };
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
// Auto-resize textarea
|
|
13
81
|
useEffect(() => {
|
|
14
82
|
if (textareaRef.current) {
|
|
15
83
|
textareaRef.current.style.height = 'auto';
|
|
@@ -43,30 +111,78 @@ export function CodeBlockView({ content, language, editable = false, onChange }:
|
|
|
43
111
|
);
|
|
44
112
|
|
|
45
113
|
const langLabel = language || 'text';
|
|
114
|
+
const shikiLang = useMemo(() => normalizeLanguage(language), [language]);
|
|
115
|
+
|
|
116
|
+
// Generate highlighted HTML
|
|
117
|
+
const highlightedHtml = useMemo(() => {
|
|
118
|
+
if (!highlighter) return null;
|
|
119
|
+
try {
|
|
120
|
+
return highlighter.codeToHtml(content, {
|
|
121
|
+
lang: shikiLang,
|
|
122
|
+
theme: 'github-light',
|
|
123
|
+
});
|
|
124
|
+
} catch {
|
|
125
|
+
// Fallback if language is not supported
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}, [highlighter, content, shikiLang]);
|
|
46
129
|
|
|
47
130
|
return (
|
|
48
|
-
<div className="h-full flex flex-col bg-
|
|
49
|
-
<div className="flex items-center justify-between px-4 py-2 bg-
|
|
50
|
-
<span className="font-mono text-
|
|
131
|
+
<div className="h-full flex flex-col bg-[#ffffff]">
|
|
132
|
+
<div className="flex items-center justify-between px-4 py-2 bg-[#f6f8fa] border-b border-[#d0d7de] text-xs">
|
|
133
|
+
<span className="font-mono text-[#57606a]">{langLabel}</span>
|
|
51
134
|
</div>
|
|
135
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
|
52
136
|
{editable ? (
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
137
|
+
<div className="relative min-h-full">
|
|
138
|
+
{/* Highlighted code layer (background) - scrolls with content */}
|
|
139
|
+
<div
|
|
140
|
+
ref={containerRef}
|
|
141
|
+
className="absolute top-0 left-0 right-0 pointer-events-none p-4"
|
|
142
|
+
aria-hidden="true"
|
|
143
|
+
>
|
|
144
|
+
{highlightedHtml ? (
|
|
145
|
+
<div
|
|
146
|
+
className="highlighted-code font-mono text-xs leading-relaxed whitespace-pre-wrap break-words [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_code]:!bg-transparent [&_code]:whitespace-pre-wrap [&_code]:break-words"
|
|
147
|
+
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
|
|
148
|
+
/>
|
|
149
|
+
) : (
|
|
150
|
+
<pre className="text-xs font-mono whitespace-pre-wrap break-words text-[#24292f] m-0 leading-relaxed">
|
|
151
|
+
<code>{content}</code>
|
|
152
|
+
</pre>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
{/* Editable textarea layer (foreground) */}
|
|
156
|
+
<textarea
|
|
157
|
+
ref={textareaRef}
|
|
158
|
+
value={content}
|
|
159
|
+
onChange={handleChange}
|
|
160
|
+
onKeyDown={handleKeyDown}
|
|
161
|
+
className="relative w-full min-h-full font-mono text-xs leading-relaxed bg-transparent border-none outline-none resize-none p-4 text-transparent whitespace-pre-wrap break-words"
|
|
162
|
+
spellCheck={false}
|
|
163
|
+
style={{
|
|
164
|
+
tabSize: 2,
|
|
165
|
+
caretColor: '#24292f',
|
|
166
|
+
wordBreak: 'break-word',
|
|
167
|
+
overflowWrap: 'break-word',
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
65
171
|
) : (
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
172
|
+
<div className="p-4">
|
|
173
|
+
{highlightedHtml ? (
|
|
174
|
+
<div
|
|
175
|
+
className="highlighted-code font-mono text-xs leading-relaxed whitespace-pre-wrap break-words [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_code]:!bg-transparent [&_code]:whitespace-pre-wrap [&_code]:break-words"
|
|
176
|
+
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
|
|
177
|
+
/>
|
|
178
|
+
) : (
|
|
179
|
+
<pre className="text-xs font-mono whitespace-pre-wrap break-words m-0 leading-relaxed text-[#24292f]">
|
|
180
|
+
<code>{content}</code>
|
|
181
|
+
</pre>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
69
184
|
)}
|
|
185
|
+
</div>
|
|
70
186
|
</div>
|
|
71
187
|
);
|
|
72
188
|
}
|