tangled
alpha
login
or
join now
danabra.mov
/
rscexplorer
37
fork
atom
A tool for people curious about the React Server Components protocol
rscexplorer.dev/
rsc
react
37
fork
atom
overview
issues
pulls
pipelines
link to source
danabra.mov
3 months ago
277b96b9
e247f9cd
+163
-59
2 changed files
expand all
collapse all
unified
split
index.html
src
client
ui
App.jsx
+27
-7
index.html
···
37
37
padding: 0 16px;
38
38
display: flex;
39
39
align-items: center;
40
40
-
gap: 12px;
40
40
+
gap: 16px;
41
41
border-bottom: 1px solid var(--border);
42
42
background: var(--surface);
43
43
flex-shrink: 0;
···
54
54
display: flex;
55
55
align-items: center;
56
56
gap: 10px;
57
57
-
margin-left: 16px;
58
57
padding-left: 16px;
59
58
border-left: 1px solid var(--border);
60
59
}
···
231
230
display: flex;
232
231
align-items: center;
233
232
gap: 8px;
234
234
-
padding-left: 12px;
235
235
-
border-left: 1px solid var(--border);
236
233
}
237
234
.build-switcher label {
238
235
font-size: 11px;
···
243
240
.build-switcher .mode-select {
244
241
min-width: 70px;
245
242
}
243
243
+
.header-links {
244
244
+
display: flex;
245
245
+
align-items: center;
246
246
+
gap: 8px;
247
247
+
padding-right: 16px;
248
248
+
border-right: 1px solid var(--border);
249
249
+
}
250
250
+
.github-link,
251
251
+
.tangled-link {
252
252
+
display: flex;
253
253
+
align-items: center;
254
254
+
justify-content: center;
255
255
+
color: var(--text-dim);
256
256
+
padding: 4px;
257
257
+
border-radius: 4px;
258
258
+
transition: color 0.15s;
259
259
+
}
260
260
+
.github-link:hover,
261
261
+
.tangled-link:hover {
262
262
+
color: var(--text-bright);
263
263
+
}
264
264
+
@media (max-width: 900px) {
265
265
+
.header-links {
266
266
+
display: none;
267
267
+
}
268
268
+
}
246
269
@media (max-width: 768px) {
247
270
header {
248
271
padding: 0 8px;
···
270
293
padding: 4px 20px 4px 6px;
271
294
font-size: 16px; /* Prevents iOS zoom */
272
295
background-position: right 4px center;
273
273
-
}
274
274
-
header .save-btn {
275
275
-
display: none;
276
296
}
277
297
.build-switcher {
278
298
padding-left: 6px;
+136
-52
src/client/ui/App.jsx
···
1
1
-
import React, { useState, useRef, useEffect, useMemo, version } from 'react';
2
2
-
import { SAMPLES } from '../samples.js';
3
3
-
import REACT_VERSIONS from '../../../scripts/versions.json';
1
1
+
import React, { useState, useRef, useEffect, useMemo, version } from "react";
2
2
+
import { SAMPLES } from "../samples.js";
3
3
+
import REACT_VERSIONS from "../../../scripts/versions.json";
4
4
5
5
-
const isDev = process.env.NODE_ENV === 'development';
5
5
+
const isDev = process.env.NODE_ENV === "development";
6
6
7
7
function BuildSwitcher() {
8
8
-
if (!import.meta.env.PROD) return null;
8
8
+
const isDisabled = !import.meta.env.PROD;
9
9
10
10
const handleVersionChange = (e) => {
11
11
const newVersion = e.target.value;
12
12
if (newVersion !== version) {
13
13
-
const modePath = isDev ? '/dev' : '';
14
14
-
window.location.href = `/${newVersion}${modePath}/` + window.location.search;
13
13
+
const modePath = isDev ? "/dev" : "";
14
14
+
window.location.href =
15
15
+
`/${newVersion}${modePath}/` + window.location.search;
15
16
}
16
17
};
17
18
18
19
const handleModeChange = (e) => {
19
19
-
const newIsDev = e.target.value === 'dev';
20
20
+
const newIsDev = e.target.value === "dev";
20
21
if (newIsDev !== isDev) {
21
21
-
const modePath = newIsDev ? '/dev' : '';
22
22
+
const modePath = newIsDev ? "/dev" : "";
22
23
window.location.href = `/${version}${modePath}/` + window.location.search;
23
24
}
24
25
};
···
26
27
return (
27
28
<div className="build-switcher">
28
29
<label>React</label>
29
29
-
<select value={version} onChange={handleVersionChange}>
30
30
+
<select
31
31
+
value={version}
32
32
+
onChange={handleVersionChange}
33
33
+
disabled={isDisabled}
34
34
+
>
30
35
{REACT_VERSIONS.map((v) => (
31
31
-
<option key={v} value={v}>{v}</option>
36
36
+
<option key={v} value={v}>
37
37
+
{v}
38
38
+
</option>
32
39
))}
33
40
</select>
34
34
-
<select value={isDev ? 'dev' : 'prod'} onChange={handleModeChange} className="mode-select">
41
41
+
<select
42
42
+
value={isDev ? "dev" : "prod"}
43
43
+
onChange={handleModeChange}
44
44
+
className="mode-select"
45
45
+
disabled={isDisabled}
46
46
+
>
35
47
<option value="prod">prod</option>
36
48
<option value="dev">dev</option>
37
49
</select>
···
41
53
42
54
function getInitialCode() {
43
55
const params = new URLSearchParams(window.location.search);
44
44
-
const sampleKey = params.get('s');
45
45
-
const encodedCode = params.get('c');
56
56
+
const sampleKey = params.get("s");
57
57
+
const encodedCode = params.get("c");
46
58
47
59
if (encodedCode) {
48
60
try {
49
61
const decoded = JSON.parse(decodeURIComponent(escape(atob(encodedCode))));
50
50
-
return { server: decoded.server, client: decoded.client, sampleKey: null };
62
62
+
return {
63
63
+
server: decoded.server,
64
64
+
client: decoded.client,
65
65
+
sampleKey: null,
66
66
+
};
51
67
} catch (e) {
52
52
-
console.error('Failed to decode URL code:', e);
68
68
+
console.error("Failed to decode URL code:", e);
53
69
}
54
70
}
55
71
56
72
if (sampleKey && SAMPLES[sampleKey]) {
57
57
-
return { server: SAMPLES[sampleKey].server, client: SAMPLES[sampleKey].client, sampleKey };
73
73
+
return {
74
74
+
server: SAMPLES[sampleKey].server,
75
75
+
client: SAMPLES[sampleKey].client,
76
76
+
sampleKey,
77
77
+
};
58
78
}
59
79
60
60
-
return { server: SAMPLES.pagination.server, client: SAMPLES.pagination.client, sampleKey: 'pagination' };
80
80
+
return {
81
81
+
server: SAMPLES.pagination.server,
82
82
+
client: SAMPLES.pagination.client,
83
83
+
sampleKey: "pagination",
84
84
+
};
61
85
}
62
86
63
87
function saveToUrl(serverCode, clientCode) {
···
66
90
// Don't wrap in encodeURIComponent - searchParams.set() handles that
67
91
const encoded = btoa(unescape(encodeURIComponent(json)));
68
92
const url = new URL(window.location.href);
69
69
-
url.searchParams.delete('s');
70
70
-
url.searchParams.set('c', encoded);
71
71
-
window.history.pushState({}, '', url);
93
93
+
url.searchParams.delete("s");
94
94
+
url.searchParams.set("c", encoded);
95
95
+
window.history.pushState({}, "", url);
72
96
}
73
97
74
98
function EmbedModal({ code, onClose }) {
···
76
100
const [copied, setCopied] = useState(false);
77
101
78
102
const embedCode = useMemo(() => {
79
79
-
const base = window.location.origin + window.location.pathname.replace(/\/$/, '');
103
103
+
const base =
104
104
+
window.location.origin + window.location.pathname.replace(/\/$/, "");
80
105
const id = Math.random().toString(36).slice(2, 6);
81
106
return `<div id="rsc-${id}" style="height: 500px;"></div>
82
107
<script type="module">
···
101
126
102
127
return (
103
128
<div className="modal-overlay" onClick={onClose}>
104
104
-
<div className="modal" onClick={e => e.stopPropagation()}>
129
129
+
<div className="modal" onClick={(e) => e.stopPropagation()}>
105
130
<div className="modal-header">
106
131
<h2>Embed this example</h2>
107
107
-
<button className="modal-close" onClick={onClose}>×</button>
132
132
+
<button className="modal-close" onClick={onClose}>
133
133
+
×
134
134
+
</button>
108
135
</div>
109
136
<div className="modal-body">
110
137
<p>Copy and paste this code into your HTML:</p>
···
112
139
ref={textareaRef}
113
140
readOnly
114
141
value={embedCode}
115
115
-
onClick={e => e.target.select()}
142
142
+
onClick={(e) => e.target.select()}
116
143
/>
117
144
</div>
118
145
<div className="modal-footer">
119
146
<button className="copy-btn" onClick={handleCopy}>
120
120
-
{copied ? 'Copied!' : 'Copy to clipboard'}
147
147
+
{copied ? "Copied!" : "Copy to clipboard"}
121
148
</button>
122
149
</div>
123
150
</div>
···
138
165
139
166
useEffect(() => {
140
167
const handleMessage = (event) => {
141
141
-
if (event.data?.type === 'rsc-embed:ready') {
142
142
-
iframeRef.current?.contentWindow?.postMessage({
143
143
-
type: 'rsc-embed:init',
144
144
-
code: workspaceCode,
145
145
-
showFullscreen: false
146
146
-
}, '*');
168
168
+
if (event.data?.type === "rsc-embed:ready") {
169
169
+
iframeRef.current?.contentWindow?.postMessage(
170
170
+
{
171
171
+
type: "rsc-embed:init",
172
172
+
code: workspaceCode,
173
173
+
showFullscreen: false,
174
174
+
},
175
175
+
"*",
176
176
+
);
147
177
}
148
148
-
if (event.data?.type === 'rsc-embed:code-changed') {
178
178
+
if (event.data?.type === "rsc-embed:code-changed") {
149
179
setLiveCode(event.data.code);
150
180
}
151
181
};
152
182
153
153
-
window.addEventListener('message', handleMessage);
154
154
-
return () => window.removeEventListener('message', handleMessage);
183
183
+
window.addEventListener("message", handleMessage);
184
184
+
return () => window.removeEventListener("message", handleMessage);
155
185
}, [workspaceCode]);
156
186
157
187
useEffect(() => {
158
188
setLiveCode(workspaceCode);
159
189
if (iframeRef.current?.contentWindow) {
160
160
-
iframeRef.current.contentWindow.postMessage({
161
161
-
type: 'rsc-embed:init',
162
162
-
code: workspaceCode,
163
163
-
showFullscreen: false
164
164
-
}, '*');
190
190
+
iframeRef.current.contentWindow.postMessage(
191
191
+
{
192
192
+
type: "rsc-embed:init",
193
193
+
code: workspaceCode,
194
194
+
showFullscreen: false,
195
195
+
},
196
196
+
"*",
197
197
+
);
165
198
}
166
199
}, [workspaceCode]);
167
200
···
171
204
};
172
205
173
206
const isDirty = currentSample
174
174
-
? liveCode.server !== SAMPLES[currentSample].server || liveCode.client !== SAMPLES[currentSample].client
175
175
-
: liveCode.server !== initialCode.server || liveCode.client !== initialCode.client;
207
207
+
? liveCode.server !== SAMPLES[currentSample].server ||
208
208
+
liveCode.client !== SAMPLES[currentSample].client
209
209
+
: liveCode.server !== initialCode.server ||
210
210
+
liveCode.client !== initialCode.client;
176
211
177
212
const handleSampleChange = (e) => {
178
213
const key = e.target.value;
···
184
219
setWorkspaceCode(newCode);
185
220
setCurrentSample(key);
186
221
const url = new URL(window.location.href);
187
187
-
url.searchParams.delete('c');
188
188
-
url.searchParams.set('s', key);
189
189
-
window.history.pushState({}, '', url);
222
222
+
url.searchParams.delete("c");
223
223
+
url.searchParams.set("s", key);
224
224
+
window.history.pushState({}, "", url);
190
225
}
191
226
};
192
227
···
196
231
<h1>RSC Explorer</h1>
197
232
<div className="example-select-wrapper">
198
233
<label>Example</label>
199
199
-
<select value={currentSample || ''} onChange={handleSampleChange}>
234
234
+
<select value={currentSample || ""} onChange={handleSampleChange}>
200
235
{!currentSample && <option value="">Custom</option>}
201
236
{Object.entries(SAMPLES).map(([key, sample]) => (
202
202
-
<option key={key} value={key}>{sample.name}</option>
237
237
+
<option key={key} value={key}>
238
238
+
{sample.name}
239
239
+
</option>
203
240
))}
204
241
</select>
205
205
-
<button className="save-btn" onClick={handleSave} disabled={!isDirty} title="Save to URL">
206
206
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
242
242
+
<button
243
243
+
className="save-btn"
244
244
+
onClick={handleSave}
245
245
+
disabled={!isDirty}
246
246
+
title="Save to URL"
247
247
+
>
248
248
+
<svg
249
249
+
width="16"
250
250
+
height="16"
251
251
+
viewBox="0 0 24 24"
252
252
+
fill="none"
253
253
+
stroke="currentColor"
254
254
+
strokeWidth="2"
255
255
+
>
207
256
<path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z" />
208
257
<polyline points="17 21 17 13 7 13 7 21" />
209
258
<polyline points="7 3 7 8 15 8" />
210
259
</svg>
211
260
</button>
212
212
-
<button className="embed-btn" onClick={() => setShowEmbedModal(true)} title="Embed">
213
213
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
261
261
+
<button
262
262
+
className="embed-btn"
263
263
+
onClick={() => setShowEmbedModal(true)}
264
264
+
title="Embed"
265
265
+
>
266
266
+
<svg
267
267
+
width="16"
268
268
+
height="16"
269
269
+
viewBox="0 0 24 24"
270
270
+
fill="none"
271
271
+
stroke="currentColor"
272
272
+
strokeWidth="2"
273
273
+
>
214
274
<polyline points="16 18 22 12 16 6" />
215
275
<polyline points="8 6 2 12 8 18" />
216
276
</svg>
217
277
</button>
218
278
</div>
219
279
<div className="header-spacer" />
280
280
+
<div className="header-links">
281
281
+
<a
282
282
+
href="https://github.com/gaearon/rscexplorer"
283
283
+
target="_blank"
284
284
+
rel="noopener noreferrer"
285
285
+
className="github-link"
286
286
+
title="View on GitHub"
287
287
+
>
288
288
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
289
289
+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
290
290
+
</svg>
291
291
+
</a>
292
292
+
<a
293
293
+
href="https://tangled.sh/danabra.mov/rscexplorer"
294
294
+
target="_blank"
295
295
+
rel="noopener noreferrer"
296
296
+
className="tangled-link"
297
297
+
title="View on Tangled"
298
298
+
>
299
299
+
<svg width="20" height="20" viewBox="0 0 26 26" fill="currentColor">
300
300
+
<path d="m 16.775491,24.987061 c -0.78517,-0.0064 -1.384202,-0.234614 -2.033994,-0.631295 -0.931792,-0.490188 -1.643475,-1.31368 -2.152014,-2.221647 C 11.781409,23.136647 10.701392,23.744942 9.4922931,24.0886 8.9774725,24.238111 8.0757679,24.389777 6.5811304,23.84827 4.4270703,23.124679 2.8580086,20.883331 3.0363279,18.599583 3.0037061,17.652919 3.3488675,16.723769 3.8381157,15.925061 2.5329485,15.224503 1.4686756,14.048584 1.0611184,12.606459 0.81344502,11.816973 0.82385989,10.966486 0.91519098,10.154906 1.2422711,8.2387903 2.6795811,6.5725716 4.5299585,5.9732484 5.2685364,4.290122 6.8802592,3.0349975 8.706276,2.7794663 c 1.2124148,-0.1688264 2.46744,0.084987 3.52811,0.7011837 1.545426,-1.7139736 4.237779,-2.2205077 6.293579,-1.1676231 1.568222,0.7488935 2.689625,2.3113526 2.961888,4.0151464 1.492195,0.5977882 2.749007,1.8168898 3.242225,3.3644951 0.329805,0.9581836 0.340709,2.0135956 0.127128,2.9974286 -0.381606,1.535184 -1.465322,2.842146 -2.868035,3.556463 0.0034,0.273204 0.901506,2.243045 0.751284,3.729647 -0.03281,1.858525 -1.211631,3.619894 -2.846433,4.475452 -0.953967,0.556812 -2.084452,0.546309 -3.120531,0.535398 z m -4.470079,-5.349839 c 1.322246,-0.147248 2.189053,-1.300106 2.862307,-2.338363 0.318287,-0.472954 0.561404,-1.002348 0.803,-1.505815 0.313265,0.287151 0.578698,0.828085 1.074141,0.956909 0.521892,0.162542 1.133743,0.03052 1.45325,-0.443554 0.611414,-1.140449 0.31004,-2.516537 -0.04602,-3.698347 C 18.232844,11.92927 17.945151,11.232927 17.397785,10.751793 17.514522,9.9283111 17.026575,9.0919791 16.332883,8.6609491 15.741721,9.1323278 14.842258,9.1294949 14.271975,8.6252369 13.178927,9.7400102 12.177239,9.7029996 11.209704,8.8195135 10.992255,8.6209543 10.577326,10.031484 9.1211947,9.2324497 8.2846288,9.9333947 7.6359672,10.607693 7.0611981,11.578553 6.5026891,12.62523 5.9177873,13.554793 5.867393,14.69141 c -0.024234,0.66432 0.4948601,1.360337 1.1982269,1.306329 0.702996,0.06277 1.1815208,-0.629091 1.7138087,-0.916491 0.079382,0.927141 0.1688108,1.923227 0.4821259,2.828358 0.3596254,1.171275 1.6262605,1.915695 2.8251855,1.745211 0.08481,-0.0066 0.218672,-0.01769 0.218672,-0.0176 z m 0.686342,-3.497495 c -0.643126,-0.394168 -0.33365,-1.249599 -0.359402,-1.870938 0.064,-0.749774 0.115321,-1.538054 0.452402,-2.221125 0.356724,-0.487008 1.226721,-0.299139 1.265134,0.325689 -0.02558,0.628509 -0.314101,1.25416 -0.279646,1.9057 -0.07482,0.544043 0.05418,1.155133 -0.186476,1.652391 -0.197455,0.275121 -0.599638,0.355105 -0.892012,0.208283 z m -2.808766,-0.358124 c -0.605767,-0.328664 -0.4133176,-1.155655 -0.5083256,-1.73063 0.078762,-0.66567 0.013203,-1.510085 0.5705316,-1.976886 0.545037,-0.380109 1.286917,0.270803 1.029164,0.868384 -0.274913,0.755214 -0.09475,1.580345 -0.08893,2.34609 -0.104009,0.451702 -0.587146,0.691508 -1.002445,0.493042 z" />
301
301
+
</svg>
302
302
+
</a>
303
303
+
</div>
220
304
<BuildSwitcher />
221
305
</header>
222
306
<iframe
223
307
ref={iframeRef}
224
308
src="embed.html"
225
225
-
style={{ flex: 1, border: 'none', width: '100%' }}
309
309
+
style={{ flex: 1, border: "none", width: "100%" }}
226
310
/>
227
311
{showEmbedModal && (
228
312
<EmbedModal code={liveCode} onClose={() => setShowEmbedModal(false)} />