Skip to content

Commit 3b49995

Browse files
update site
1 parent 496d7e7 commit 3b49995

File tree

7 files changed

+473
-50
lines changed

7 files changed

+473
-50
lines changed

pnpm-lock.yaml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui/react-example/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@my-react/react-dom": "^0.3.22",
2222
"@tabler/icons-react": "^3.35.0",
2323
"lodash": "^4.17.21",
24+
"lz-string": "^1.5.0",
2425
"overlayscrollbars": "^2.12.0",
2526
"react": "^19.2.0",
2627
"react-dom": "^19.2.0",
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { SandpackCodeEditor, SandpackLayout, SandpackProvider, useActiveCode } from "@codesandbox/sandpack-react";
2+
import { useMantineColorScheme } from "@mantine/core";
3+
import { memo, useEffect, useRef, useState } from "react";
4+
5+
// Internal component to sync code changes back to parent
6+
const CodeSync = ({ onChange, initialCode }: { onChange?: (code: string) => void; initialCode: string }) => {
7+
const { code, updateCode } = useActiveCode();
8+
const prevCodeRef = useRef<string>(initialCode);
9+
const isExternalUpdate = useRef(false);
10+
11+
// Sync external code changes into sandpack
12+
useEffect(() => {
13+
if (initialCode !== prevCodeRef.current && initialCode !== code) {
14+
isExternalUpdate.current = true;
15+
updateCode(initialCode, false);
16+
prevCodeRef.current = initialCode;
17+
}
18+
}, [initialCode, updateCode, code]);
19+
20+
// Notify parent of internal code changes
21+
useEffect(() => {
22+
if (isExternalUpdate.current) {
23+
isExternalUpdate.current = false;
24+
return;
25+
}
26+
if (code !== prevCodeRef.current) {
27+
prevCodeRef.current = code;
28+
onChange?.(code);
29+
}
30+
}, [code, onChange]);
31+
32+
return null;
33+
};
34+
35+
export interface CodeEditorProps {
36+
code: string;
37+
onChange?: (code: string) => void;
38+
lang?: string;
39+
minHeight?: string;
40+
readOnly?: boolean;
41+
showLineNumbers?: boolean;
42+
}
43+
44+
// Map common language names to file extensions
45+
const getFileExtension = (lang: string) => {
46+
const langMap: Record<string, string> = {
47+
typescript: "ts",
48+
javascript: "js",
49+
ts: "ts",
50+
js: "js",
51+
tsx: "tsx",
52+
jsx: "jsx",
53+
json: "json",
54+
html: "html",
55+
css: "css",
56+
scss: "scss",
57+
less: "less",
58+
markdown: "md",
59+
md: "md",
60+
python: "py",
61+
py: "py",
62+
rust: "rs",
63+
rs: "rs",
64+
go: "go",
65+
java: "java",
66+
c: "c",
67+
cpp: "cpp",
68+
"c++": "cpp",
69+
diff: "diff",
70+
patch: "diff",
71+
vue: "vue",
72+
svelte: "svelte",
73+
shell: "sh",
74+
bash: "sh",
75+
sh: "sh",
76+
yaml: "yaml",
77+
yml: "yaml",
78+
xml: "xml",
79+
sql: "sql",
80+
graphql: "graphql",
81+
swift: "swift",
82+
kotlin: "kt",
83+
ruby: "rb",
84+
php: "php",
85+
txt: "txt",
86+
text: "txt",
87+
};
88+
return langMap[lang.toLowerCase()] || "txt";
89+
};
90+
91+
export const CodeEditor = memo(
92+
({ code, onChange, lang = "ts", minHeight = "200px", readOnly = false, showLineNumbers = true }: CodeEditorProps) => {
93+
const { colorScheme } = useMantineColorScheme();
94+
const [height, setHeight] = useState(minHeight);
95+
const containerRef = useRef<HTMLDivElement>(null);
96+
const isResizing = useRef(false);
97+
const startY = useRef(0);
98+
const startHeight = useRef(0);
99+
100+
const ext = getFileExtension(lang);
101+
const filePath = `/main.${ext}`;
102+
103+
const handleMouseDown = (e: React.MouseEvent) => {
104+
isResizing.current = true;
105+
startY.current = e.clientY;
106+
startHeight.current = containerRef.current?.offsetHeight || parseInt(minHeight);
107+
document.body.style.cursor = "ns-resize";
108+
document.body.style.userSelect = "none";
109+
110+
const handleMouseMove = (e: MouseEvent) => {
111+
if (!isResizing.current) return;
112+
const delta = e.clientY - startY.current;
113+
const newHeight = Math.max(parseInt(minHeight), startHeight.current + delta);
114+
setHeight(`${newHeight}px`);
115+
};
116+
117+
const handleMouseUp = () => {
118+
isResizing.current = false;
119+
document.body.style.cursor = "";
120+
document.body.style.userSelect = "";
121+
document.removeEventListener("mousemove", handleMouseMove);
122+
document.removeEventListener("mouseup", handleMouseUp);
123+
};
124+
125+
document.addEventListener("mousemove", handleMouseMove);
126+
document.addEventListener("mouseup", handleMouseUp);
127+
};
128+
129+
return (
130+
<div ref={containerRef} className="relative" style={{ height }}>
131+
<SandpackProvider
132+
files={{
133+
[filePath]: {
134+
code,
135+
active: true,
136+
},
137+
}}
138+
theme={colorScheme === "dark" ? "dark" : "light"}
139+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
140+
// @ts-ignore
141+
style={{ ["--sp-layout-height"]: "100%", height: "100%" }}
142+
>
143+
<SandpackLayout className="border-color h-full overflow-hidden rounded-[6px] border">
144+
<SandpackCodeEditor showLineNumbers={showLineNumbers} showReadOnly={false} readOnly={readOnly} />
145+
</SandpackLayout>
146+
{!readOnly && onChange && <CodeSync onChange={onChange} initialCode={code} />}
147+
</SandpackProvider>
148+
{/* Resize handle */}
149+
<div
150+
className="border-color absolute bottom-0 left-0 right-0 flex h-[8px] cursor-ns-resize items-center justify-center border-t bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700"
151+
onMouseDown={handleMouseDown}
152+
>
153+
<div className="h-[2px] w-[30px] rounded bg-gray-400" />
154+
</div>
155+
</div>
156+
);
157+
}
158+
);
159+
160+
CodeEditor.displayName = "CodeEditor";
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* Share URL utilities for encoding/decoding playground state
3+
* Uses lz-string compression + URL-safe encoding to keep URLs short
4+
*/
5+
6+
import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "lz-string";
7+
8+
// Git diff mode state
9+
export interface GitDiffShareState {
10+
lang: string;
11+
diffString: string;
12+
content: string;
13+
}
14+
15+
// File diff mode state
16+
export interface FileDiffShareState {
17+
lang1: string;
18+
lang2: string;
19+
file1: string;
20+
file2: string;
21+
}
22+
23+
/**
24+
* Encode Git diff state to URL parameter
25+
*/
26+
export function encodeGitDiffState(state: GitDiffShareState): string {
27+
const json = JSON.stringify(state);
28+
return compressToEncodedURIComponent(json);
29+
}
30+
31+
/**
32+
* Decode Git diff state from URL parameter
33+
*/
34+
export function decodeGitDiffState(encoded: string): GitDiffShareState | null {
35+
try {
36+
const json = decompressFromEncodedURIComponent(encoded);
37+
if (!json) return null;
38+
return JSON.parse(json) as GitDiffShareState;
39+
} catch {
40+
return null;
41+
}
42+
}
43+
44+
/**
45+
* Encode File diff state to URL parameter
46+
*/
47+
export function encodeFileDiffState(state: FileDiffShareState): string {
48+
const json = JSON.stringify(state);
49+
return compressToEncodedURIComponent(json);
50+
}
51+
52+
/**
53+
* Decode File diff state from URL parameter
54+
*/
55+
export function decodeFileDiffState(encoded: string): FileDiffShareState | null {
56+
try {
57+
const json = decompressFromEncodedURIComponent(encoded);
58+
if (!json) return null;
59+
return JSON.parse(json) as FileDiffShareState;
60+
} catch {
61+
return null;
62+
}
63+
}
64+
65+
/**
66+
* Generate share URL for Git diff mode
67+
*/
68+
export function generateGitDiffShareUrl(state: GitDiffShareState): string {
69+
const url = new URL(window.location.href);
70+
url.searchParams.set("type", "try");
71+
url.searchParams.set("tab", "git");
72+
73+
const encoded = encodeGitDiffState(state);
74+
url.searchParams.set("data", encoded);
75+
76+
return url.toString();
77+
}
78+
79+
/**
80+
* Generate share URL for File diff mode
81+
*/
82+
export function generateFileDiffShareUrl(state: FileDiffShareState): string {
83+
const url = new URL(window.location.href);
84+
url.searchParams.set("type", "try");
85+
url.searchParams.set("tab", "file");
86+
87+
const encoded = encodeFileDiffState(state);
88+
url.searchParams.set("data", encoded);
89+
90+
return url.toString();
91+
}
92+
93+
/**
94+
* Get share data from current URL
95+
*/
96+
export function getShareDataFromUrl(): string | null {
97+
const url = new URL(window.location.href);
98+
return url.searchParams.get("data");
99+
}
100+
101+
/**
102+
* Update URL with Git diff state (without page reload)
103+
*/
104+
export function updateUrlWithGitDiffState(state: GitDiffShareState): void {
105+
const url = new URL(window.location.href);
106+
url.searchParams.set("type", "try");
107+
url.searchParams.set("tab", "git");
108+
109+
const encoded = encodeGitDiffState(state);
110+
url.searchParams.set("data", encoded);
111+
112+
window.history.replaceState({}, "", url.toString());
113+
}
114+
115+
/**
116+
* Update URL with File diff state (without page reload)
117+
*/
118+
export function updateUrlWithFileDiffState(state: FileDiffShareState): void {
119+
const url = new URL(window.location.href);
120+
url.searchParams.set("type", "try");
121+
url.searchParams.set("tab", "file");
122+
123+
const encoded = encodeFileDiffState(state);
124+
url.searchParams.set("data", encoded);
125+
126+
window.history.replaceState({}, "", url.toString());
127+
}
128+
129+
/**
130+
* Copy text to clipboard
131+
*/
132+
export async function copyToClipboard(text: string): Promise<boolean> {
133+
try {
134+
await navigator.clipboard.writeText(text);
135+
return true;
136+
} catch {
137+
// Fallback for older browsers
138+
try {
139+
const textarea = document.createElement("textarea");
140+
textarea.value = text;
141+
textarea.style.position = "fixed";
142+
textarea.style.opacity = "0";
143+
document.body.appendChild(textarea);
144+
textarea.select();
145+
document.execCommand("copy");
146+
document.body.removeChild(textarea);
147+
return true;
148+
} catch {
149+
return false;
150+
}
151+
}
152+
}

0 commit comments

Comments
 (0)