Skip to content
This repository was archived by the owner on Mar 6, 2022. It is now read-only.

Commit a449ba5

Browse files
committed
init
0 parents  commit a449ba5

15 files changed

+1709
-0
lines changed

.gitignore

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Play React
2+
3+
A React playground powered by [React Runner](https://github.com/nihgwu/react-runner).
4+
5+
- No iframe
6+
- Dynamic imports
7+
- Style import

index.html

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/png" sizes="32x32" href="/src/favicon.png" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Play React</title>
8+
</head>
9+
<body>
10+
<div id="root">
11+
<header id="header">
12+
<img alt="logo" src="./src/favicon.png" />
13+
<h2><a href="/">Play React</a></h2>
14+
<a href="https://github.com/nihgwu/play-react">Github</a>
15+
<select id="examples" title="examples"></select>
16+
</header>
17+
<main id="main">
18+
<div id="editor"></div>
19+
<div id="preview"></div>
20+
</main>
21+
</div>
22+
<script type="module" src="/src/index.ts"></script>
23+
</body>
24+
</html>

package.json

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "play-react",
3+
"version": "0.0.0",
4+
"scripts": {
5+
"dev": "vite",
6+
"build": "tsc && vite build",
7+
"preview": "vite preview"
8+
},
9+
"dependencies": {
10+
"@codemirror/basic-setup": "^0.19.1",
11+
"@codemirror/lang-javascript": "^0.19.6",
12+
"@codemirror/theme-one-dark": "^0.19.1",
13+
"construct-style-sheets-polyfill": "^3.0.5",
14+
"lz-string": "^1.4.4",
15+
"react": "^17.0.2",
16+
"react-dom": "^17.0.2",
17+
"react-runner": "^1.0.0-rc.2"
18+
},
19+
"devDependencies": {
20+
"@types/lz-string": "^1.3.34",
21+
"@types/react": "^17.0.33",
22+
"@types/react-dom": "^17.0.10",
23+
"@vitejs/plugin-react": "^1.0.7",
24+
"typescript": "^4.5.4",
25+
"vite": "^2.7.2"
26+
}
27+
}

src/CodeMirror.ts

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { EditorState, basicSetup } from "@codemirror/basic-setup";
2+
import { EditorView, keymap, KeyBinding } from "@codemirror/view";
3+
import { defaultKeymap, indentMore, indentLess } from "@codemirror/commands";
4+
import { javascript } from "@codemirror/lang-javascript";
5+
import { oneDark } from "@codemirror/theme-one-dark";
6+
7+
import { getHashCode, updateHash, decodeHash, defaultHash } from "./urlHash";
8+
9+
const insertSoftTab = ({ state, dispatch }: EditorView) => {
10+
if (state.selection.ranges.some((r) => !r.empty)) {
11+
return indentMore({ state, dispatch });
12+
}
13+
14+
dispatch(
15+
state.update(state.replaceSelection(" "), {
16+
scrollIntoView: true,
17+
userEvent: "input",
18+
})
19+
);
20+
return true;
21+
};
22+
23+
const extendedKeymap: KeyBinding[] = [
24+
...defaultKeymap,
25+
{
26+
key: "Tab",
27+
run: insertSoftTab,
28+
shift: indentLess,
29+
},
30+
];
31+
32+
const view = new EditorView({
33+
state: EditorState.create({
34+
doc: getHashCode(),
35+
extensions: [
36+
basicSetup,
37+
oneDark,
38+
keymap.of(extendedKeymap),
39+
javascript({
40+
jsx: true,
41+
typescript: true,
42+
}),
43+
],
44+
}),
45+
parent: document.getElementById("editor")!,
46+
dispatch: (tr) => {
47+
view.update([tr]);
48+
49+
if (tr.docChanged) {
50+
const newCode = tr.newDoc.sliceString(0, tr.newDoc.length);
51+
updateHash(newCode);
52+
window.dispatchEvent(
53+
new CustomEvent("code", {
54+
detail: newCode,
55+
})
56+
);
57+
}
58+
},
59+
});
60+
61+
const examples = [
62+
{ name: "Select example...", hash: "" },
63+
{
64+
name: "Hacker News",
65+
hash: defaultHash,
66+
},
67+
{
68+
name: "React Runner",
69+
hash: "JYWwDg9gTgLgBAbzgYQgEwKYFE3BtAGjgFcBnDAGWADcMAlYgO0YyiIGMoMBDGejAI7FgXOAF84AMygQQcAORdu7GAFoANjQyqoTFlHkAoUJFiI47dBgBC6iOwDW4qTLnzLIEBEaqY3AOakRoaWjKTwwIx4wNzqqJhwALwWVrb2DgAGhnBwJtDwSPHYuPhsJORUtAzMrBxKfHSCwqIS0rIKSioaWjp6rME5GAAepvCYktzE6vAAFACUSQB8iNk5KWEFcBjqGCAYjDBErDJllphE3sgAFtyM-hjOyWSUWtX6Mwira3CklmAYAC4Vt9vpForEigQviDMGAYFcgbD4XAANRwACMUJBay4QhEgIs9X4eK4H2h2MUPC6mlovRqBiBhSsODwhHKLyqfVORMaJIeYix2PEc0F3wF5NyURgMTiVlFwsM0OAkjgMyRVzgywATAAGBZcGDEKCMOCMKbqRXfA1Gk0zCUAHlw1B+MAAnjtEggkGBuGhcHcgfIABxgIZwHXhuAhobycRiRYSnL2oos0pwaixYgYT1nDASS43O7ZhAF273CQAegTQsQx2gcAAZA24PawFxFgg61AxPaK22MIsxImW06O9tdvsYD2K6OHTOaNW1nNVkOsoZhqM4ONJtNVQtEstPjlQuFzOO9gcjlAThw5XBS0XHuzKvQuWTvr8IP9GRKwdKIXKErqkCOryjkuLNASnA8A0TT4u+QqUsoag0touj0vIjIoMyJRss8L5vLUhIwcSkHiGBwryuKoJSjKkIrsuqzWsaqrQvai7fMmOGslA6aZsWub5ow1xlsWD7lnAVYSp2171k2Lb9mOsndr2ilDtijoLp2OwXlOvazpxUk5MuQ5AA",
70+
},
71+
{
72+
name: "React Select",
73+
hash: "JYWwDg9gTgLgBAZQKYBskGN4DMoRHAciiQENMBaAZ1QxgICh70IA7S+CMGYVyuAXjgBtenDgBvOADcSKAK5IAXIXQALCMxQkYSAgBo4WgEaplBAMLrN23XAC+e0ROmyFZ9lBIB3E1CgBPfUMSExQzBBhPHyQ-QPtHMUkZeSVCGRZgFC0g41NCADUSDKySAnt6AF1GJAAPSFg4ABMkLBI5FHgACgBKAQA+OE6nAB5kNEw4Tm5efnEpnjY7OAB6Pvpu+iA",
74+
},
75+
{
76+
name: "Reach UI",
77+
hash: "JYWwDg9gTgLgBAFQhANjYYA0cDecCuAzgKZKrpaLJoYAKEY+YcAvnAGZQQhwBEAAlGIBDAMYALAPQxqFXgG4AUKEiw+gkROmyMkwjACeKYoQB0owoQWLFxAB6r4AE2Lth+NB3wA7UegjecACidsLgxgAUAJS4inBwQjD4UIERcfFwADxOwABuAHzpGVlkNMwowgBGxCgAvLwAchDo7MCiwv7eVoXFxZmV+DAygfpGxLU4eOwBMADKwABexABccABMAKysLD29fYRgwt75gLwbgCi7mXqHx0W9lwNDAbt9kqUUzxmZbxhwFdV1vFmxCGwG8AHNurc+g9hnBRsYJlMZvMlqtNtsPntMgcjnBhFBgMIALTiYBOFzHQCZZIB4P8uOJue3i90Gw0xWVeOjAhShWRyuThhgRkw4KAgHVWvAJYPEMF4GJ5TO+5SqNXqTRabQ6wACVjxBOJf1VvAAzHB1cBWu1OpDGZ8YQEBWNERxkYsVustiwdgq7vTThcrkc2Vi-ca6ddg59JPaGYzLkq2Zc+R8k3ldlElCwgA",
78+
},
79+
{
80+
name: "React Three Fiber",
81+
hash: "JYWwDg9gTgLgBAJQKYEMDGMA0cDecCuAzksgGbZFIDKMKMScAvnKVBCHAORSoacBQoSLFxwAwigB2ANxSEKxAGJQUIBs1bsuAAR7oYAWhgALHkgD0pYACMkUAf1L5JGYBElwAQhAAeACjA2MEIASlx+ODhzczgAFWNgQjgeUjskFwYAd2AAGxy4AHNgaQYiOAATYB4MOHQ0JEIkmAg4EwY1QmMIuDR3QngO4zgAXgJiMj8Q7ui4KiR4fDA4froGUmhW4wZjCBKectrJA-1ihhX6bt7JfrgAbR29pHLsYhgACV27AF0Rsepaeh+UgoHLEKaRK43W4nEoveYAQVcJR+o0oNFWQJBYOmMSo+GshDQUBsDBMiR67EgknS8GamwYPCOdgMOQgEDA2DYANJWzggzgSD2AE8WCo1N1KMpVEg-H5zkhsOUkDlaGFhgA+OB+QYAOjQ+CgjJgOq5dDckh1PjgAGpRgAGHV2gCMIXBURiyBgBo80mASEy2DaxFqPGSSAK+ByKCgmzMOoAVkllUg1JIYEmfIEGsQDsAPAApKgADW6PC9UA8fm6kQAPINq5FcDrm4F2YRGA3IilhjhBh3G43CSCkD2YQwAPxwJ06gCscAAXFP+wO4O4xDlgGgANY9vyCmlqzWvREwU5+ACEY5Cy4H7gAChA8-QoAB5Pa7-dpw-LeYfPZ+GAoHwJBr07VdJAfJ87BffAYA-EovxGI9f0+KBMVBEDGHVMCa2sXwAHEkHYeYoBFaMCkIHtbidbAaKnL5mHMbCVzgOsGmMdEjmjcoAFlVmJEEKVZKAeweNIDknTgdhgMA8y3TgFy4aApAKJBOEY5jGxrcxBk0qYO34RklTQ7oawkGQ5E02tVGsP00wAGWAApjHgJiGxrSAn0c5z4EgQhgFPdwqKdO1aNCqc7QYqIrNY7wrT8gLzSogxpwAJmwcLIo09y4rgBLAskYKdXSuBMqitza3MczZEIbCQiAA",
82+
},
83+
{
84+
name: "canvas-confetti",
85+
hash: "JYWwDg9gTgLgBAYwgOwGYFMY2HVUIhwDkCAhsgG6kDOAtEmptkQFAsMZbAAUAlEA",
86+
},
87+
{
88+
name: "swr",
89+
hash: "JYWwDg9gTgLgBAJQKYEMDG8BmUIjgIilQ3wG4AoUSWOAVwGckBlAdQTm1wPoHcozy5NBAB29LEhhoAFkihwAvHAAUtKABsAlIoB8HSTNUbNAOhiyRy5UXraFemyYBW9Ucs2aK5JAA9q8ABMkTBRadSxaEQxgUTgAQTAwdzgAb3I4OGExeBS4AJQYFAAaODkceQBfRTpGVgRldIyCaRgYMHoALgB6LpQwYBMAc2BzWgAjE2EQLqJIei6ANzk0JHUu3n4ixozMA1koRs9BDOBMFTLobSIYNRECOLuL+WkUejgINDQ1IgCTAROzsoAIT5QpXSS3AgAGQgKACwBEgxMyP+cGukIaTTgAB54QsdNsmtjpABGHQpUEoEwiFAgJAVbFdUkErEZbFgcmUkxBehoKDAMAwGIiBldDmEtniHCInSAXg3AII7qS59HGvP5Yzk9AA+sJIjBRVLRINyfgCBUJTjDTLABTkSoKVPEKCggxQAC9NTqIHqDTBpcaUqb8ObWZbfUadIAeDcALvt2womTDQADW2t1In1jKtxsJjLxLLgR3NQA",
90+
},
91+
];
92+
93+
const select = document.getElementById("examples") as HTMLSelectElement;
94+
select.onchange = () => {
95+
const hash = examples[select.selectedIndex].hash;
96+
view.dispatch({
97+
changes: {
98+
from: 0,
99+
to: view.state.doc.length,
100+
insert: decodeHash(hash),
101+
},
102+
});
103+
};
104+
105+
examples.forEach((example) => {
106+
const option = document.createElement("option");
107+
option.innerText = example.name;
108+
select.appendChild(option);
109+
});

src/Preview.tsx

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React, { useEffect, useState } from "react";
2+
import ReactDOM from "react-dom";
3+
4+
import { useAsyncRunner } from "./useAsyncRunner";
5+
import { getHashCode } from "./urlHash";
6+
7+
// @ts-expect-error
8+
window.process = { env: {} };
9+
10+
const Preview = () => {
11+
const [code, setCode] = useState(() => getHashCode());
12+
const { element, error, isLoading } = useAsyncRunner({ code });
13+
14+
useEffect(() => {
15+
const handler = (e: CustomEvent<string>) => {
16+
setCode(e.detail);
17+
};
18+
19+
window.addEventListener("code" as any, handler, true);
20+
return () => window.removeEventListener("code" as any, handler, true);
21+
}, []);
22+
23+
return (
24+
<>
25+
{isLoading && <div className="preview-loading"></div>}
26+
<div className="preview-element">{element}</div>
27+
{error && <pre className="preview-error">{error}</pre>}
28+
</>
29+
);
30+
};
31+
32+
ReactDOM.render(
33+
<React.StrictMode>
34+
<Preview />
35+
</React.StrictMode>,
36+
document.getElementById("preview")
37+
);

src/favicon.png

805 Bytes
Loading

src/index.css

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
body {
2+
margin: 0;
3+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4+
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5+
sans-serif;
6+
-webkit-font-smoothing: antialiased;
7+
-moz-osx-font-smoothing: grayscale;
8+
}
9+
10+
* {
11+
box-sizing: border-box;
12+
}
13+
14+
code {
15+
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
16+
monospace;
17+
}
18+
19+
#header {
20+
height: 48px;
21+
background-color: steelblue;
22+
color: white;
23+
padding: 0 16px;
24+
display: flex;
25+
align-items: center;
26+
}
27+
28+
#header h2 {
29+
margin: 0 auto 0 4px;
30+
}
31+
32+
#header a {
33+
color: white;
34+
text-decoration: none;
35+
}
36+
37+
#header select {
38+
margin-left: 8px;
39+
padding: 4px;
40+
}
41+
42+
#main {
43+
width: 100vw;
44+
height: calc(100vh - 48px);
45+
display: flex;
46+
position: relative;
47+
overflow: hidden;
48+
}
49+
50+
@media (max-width: 960px) {
51+
#main {
52+
flex-direction: column-reverse;
53+
}
54+
}
55+
56+
#editor {
57+
flex: 1 0 50%;
58+
overflow: auto;
59+
background-color: #282c34;
60+
padding: 8px;
61+
}
62+
63+
#preview {
64+
flex: 1 0 50%;
65+
position: relative;
66+
width: 100%;
67+
height: 100%;
68+
overflow: hidden;
69+
background-color: antiquewhite;
70+
}
71+
72+
.cm-editor {
73+
height: 100%;
74+
outline: none !important;
75+
}
76+
77+
.preview-element {
78+
width: 100%;
79+
height: 100%;
80+
overflow: auto;
81+
padding: 16px;
82+
background: inherit;
83+
}
84+
85+
.preview-error {
86+
position: absolute;
87+
top: 0;
88+
margin: 0;
89+
padding: 8px;
90+
width: 100%;
91+
background-color: red;
92+
color: white;
93+
white-space: pre-wrap;
94+
}
95+
96+
.preview-loading {
97+
position: absolute;
98+
top: 0;
99+
left: 0;
100+
right: 0;
101+
height: 8px;
102+
background-color: white;
103+
}
104+
105+
.preview-loading::after {
106+
content: "";
107+
display: block;
108+
height: 8px;
109+
background-color: lightsteelblue;
110+
111+
animation: progres 1s infinite ease-in-out;
112+
}
113+
@keyframes progres {
114+
0% {
115+
width: 0%;
116+
}
117+
100% {
118+
width: 100%;
119+
}
120+
}
121+
122+
.cm-scroller::-webkit-scrollbar {
123+
appearance: none;
124+
width: 8px;
125+
height: 8px;
126+
}
127+
128+
.cm-scroller::-webkit-scrollbar-track {
129+
background: transparent;
130+
}
131+
132+
.cm-scroller::-webkit-scrollbar-thumb {
133+
background-color: gray;
134+
border-radius: 9999px;
135+
}
136+
137+
.cm-scroller::-webkit-scrollbar-thumb:hover {
138+
background-color: darkgray;
139+
}
140+
141+
.cm-scroller::-webkit-scrollbar-corner {
142+
display: none;
143+
}

src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import "./index.css";
2+
3+
import("./CodeMirror");
4+
import("./Preview");

0 commit comments

Comments
 (0)