A tool for people curious about the React Server Components protocol rscexplorer.dev/
rsc react

link to source

+163 -59
+27 -7
index.html
··· 37 37 padding: 0 16px; 38 38 display: flex; 39 39 align-items: center; 40 - gap: 12px; 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 - 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 - padding-left: 12px; 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 + .header-links { 244 + display: flex; 245 + align-items: center; 246 + gap: 8px; 247 + padding-right: 16px; 248 + border-right: 1px solid var(--border); 249 + } 250 + .github-link, 251 + .tangled-link { 252 + display: flex; 253 + align-items: center; 254 + justify-content: center; 255 + color: var(--text-dim); 256 + padding: 4px; 257 + border-radius: 4px; 258 + transition: color 0.15s; 259 + } 260 + .github-link:hover, 261 + .tangled-link:hover { 262 + color: var(--text-bright); 263 + } 264 + @media (max-width: 900px) { 265 + .header-links { 266 + display: none; 267 + } 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 - } 274 - header .save-btn { 275 - display: none; 276 296 } 277 297 .build-switcher { 278 298 padding-left: 6px;
+136 -52
src/client/ui/App.jsx
··· 1 - import React, { useState, useRef, useEffect, useMemo, version } from 'react'; 2 - import { SAMPLES } from '../samples.js'; 3 - import REACT_VERSIONS from '../../../scripts/versions.json'; 1 + import React, { useState, useRef, useEffect, useMemo, version } from "react"; 2 + import { SAMPLES } from "../samples.js"; 3 + import REACT_VERSIONS from "../../../scripts/versions.json"; 4 4 5 - const isDev = process.env.NODE_ENV === 'development'; 5 + const isDev = process.env.NODE_ENV === "development"; 6 6 7 7 function BuildSwitcher() { 8 - if (!import.meta.env.PROD) return null; 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 - const modePath = isDev ? '/dev' : ''; 14 - window.location.href = `/${newVersion}${modePath}/` + window.location.search; 13 + const modePath = isDev ? "/dev" : ""; 14 + window.location.href = 15 + `/${newVersion}${modePath}/` + window.location.search; 15 16 } 16 17 }; 17 18 18 19 const handleModeChange = (e) => { 19 - const newIsDev = e.target.value === 'dev'; 20 + const newIsDev = e.target.value === "dev"; 20 21 if (newIsDev !== isDev) { 21 - const modePath = newIsDev ? '/dev' : ''; 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 - <select value={version} onChange={handleVersionChange}> 30 + <select 31 + value={version} 32 + onChange={handleVersionChange} 33 + disabled={isDisabled} 34 + > 30 35 {REACT_VERSIONS.map((v) => ( 31 - <option key={v} value={v}>{v}</option> 36 + <option key={v} value={v}> 37 + {v} 38 + </option> 32 39 ))} 33 40 </select> 34 - <select value={isDev ? 'dev' : 'prod'} onChange={handleModeChange} className="mode-select"> 41 + <select 42 + value={isDev ? "dev" : "prod"} 43 + onChange={handleModeChange} 44 + className="mode-select" 45 + disabled={isDisabled} 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 - const sampleKey = params.get('s'); 45 - const encodedCode = params.get('c'); 56 + const sampleKey = params.get("s"); 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 - return { server: decoded.server, client: decoded.client, sampleKey: null }; 62 + return { 63 + server: decoded.server, 64 + client: decoded.client, 65 + sampleKey: null, 66 + }; 51 67 } catch (e) { 52 - console.error('Failed to decode URL code:', e); 68 + console.error("Failed to decode URL code:", e); 53 69 } 54 70 } 55 71 56 72 if (sampleKey && SAMPLES[sampleKey]) { 57 - return { server: SAMPLES[sampleKey].server, client: SAMPLES[sampleKey].client, sampleKey }; 73 + return { 74 + server: SAMPLES[sampleKey].server, 75 + client: SAMPLES[sampleKey].client, 76 + sampleKey, 77 + }; 58 78 } 59 79 60 - return { server: SAMPLES.pagination.server, client: SAMPLES.pagination.client, sampleKey: 'pagination' }; 80 + return { 81 + server: SAMPLES.pagination.server, 82 + client: SAMPLES.pagination.client, 83 + sampleKey: "pagination", 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 - url.searchParams.delete('s'); 70 - url.searchParams.set('c', encoded); 71 - window.history.pushState({}, '', url); 93 + url.searchParams.delete("s"); 94 + url.searchParams.set("c", encoded); 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 - const base = window.location.origin + window.location.pathname.replace(/\/$/, ''); 103 + const base = 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 - <div className="modal" onClick={e => e.stopPropagation()}> 129 + <div className="modal" onClick={(e) => e.stopPropagation()}> 105 130 <div className="modal-header"> 106 131 <h2>Embed this example</h2> 107 - <button className="modal-close" onClick={onClose}>&times;</button> 132 + <button className="modal-close" onClick={onClose}> 133 + &times; 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 - onClick={e => e.target.select()} 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 - {copied ? 'Copied!' : 'Copy to clipboard'} 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 - if (event.data?.type === 'rsc-embed:ready') { 142 - iframeRef.current?.contentWindow?.postMessage({ 143 - type: 'rsc-embed:init', 144 - code: workspaceCode, 145 - showFullscreen: false 146 - }, '*'); 168 + if (event.data?.type === "rsc-embed:ready") { 169 + iframeRef.current?.contentWindow?.postMessage( 170 + { 171 + type: "rsc-embed:init", 172 + code: workspaceCode, 173 + showFullscreen: false, 174 + }, 175 + "*", 176 + ); 147 177 } 148 - if (event.data?.type === 'rsc-embed:code-changed') { 178 + if (event.data?.type === "rsc-embed:code-changed") { 149 179 setLiveCode(event.data.code); 150 180 } 151 181 }; 152 182 153 - window.addEventListener('message', handleMessage); 154 - return () => window.removeEventListener('message', handleMessage); 183 + window.addEventListener("message", handleMessage); 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 - iframeRef.current.contentWindow.postMessage({ 161 - type: 'rsc-embed:init', 162 - code: workspaceCode, 163 - showFullscreen: false 164 - }, '*'); 190 + iframeRef.current.contentWindow.postMessage( 191 + { 192 + type: "rsc-embed:init", 193 + code: workspaceCode, 194 + showFullscreen: false, 195 + }, 196 + "*", 197 + ); 165 198 } 166 199 }, [workspaceCode]); 167 200 ··· 171 204 }; 172 205 173 206 const isDirty = currentSample 174 - ? liveCode.server !== SAMPLES[currentSample].server || liveCode.client !== SAMPLES[currentSample].client 175 - : liveCode.server !== initialCode.server || liveCode.client !== initialCode.client; 207 + ? liveCode.server !== SAMPLES[currentSample].server || 208 + liveCode.client !== SAMPLES[currentSample].client 209 + : liveCode.server !== initialCode.server || 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 - url.searchParams.delete('c'); 188 - url.searchParams.set('s', key); 189 - window.history.pushState({}, '', url); 222 + url.searchParams.delete("c"); 223 + url.searchParams.set("s", key); 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 - <select value={currentSample || ''} onChange={handleSampleChange}> 234 + <select value={currentSample || ""} onChange={handleSampleChange}> 200 235 {!currentSample && <option value="">Custom</option>} 201 236 {Object.entries(SAMPLES).map(([key, sample]) => ( 202 - <option key={key} value={key}>{sample.name}</option> 237 + <option key={key} value={key}> 238 + {sample.name} 239 + </option> 203 240 ))} 204 241 </select> 205 - <button className="save-btn" onClick={handleSave} disabled={!isDirty} title="Save to URL"> 206 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 242 + <button 243 + className="save-btn" 244 + onClick={handleSave} 245 + disabled={!isDirty} 246 + title="Save to URL" 247 + > 248 + <svg 249 + width="16" 250 + height="16" 251 + viewBox="0 0 24 24" 252 + fill="none" 253 + stroke="currentColor" 254 + strokeWidth="2" 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 - <button className="embed-btn" onClick={() => setShowEmbedModal(true)} title="Embed"> 213 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> 261 + <button 262 + className="embed-btn" 263 + onClick={() => setShowEmbedModal(true)} 264 + title="Embed" 265 + > 266 + <svg 267 + width="16" 268 + height="16" 269 + viewBox="0 0 24 24" 270 + fill="none" 271 + stroke="currentColor" 272 + strokeWidth="2" 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 + <div className="header-links"> 281 + <a 282 + href="https://github.com/gaearon/rscexplorer" 283 + target="_blank" 284 + rel="noopener noreferrer" 285 + className="github-link" 286 + title="View on GitHub" 287 + > 288 + <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> 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 + </svg> 291 + </a> 292 + <a 293 + href="https://tangled.sh/danabra.mov/rscexplorer" 294 + target="_blank" 295 + rel="noopener noreferrer" 296 + className="tangled-link" 297 + title="View on Tangled" 298 + > 299 + <svg width="20" height="20" viewBox="0 0 26 26" fill="currentColor"> 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 + </svg> 302 + </a> 303 + </div> 220 304 <BuildSwitcher /> 221 305 </header> 222 306 <iframe 223 307 ref={iframeRef} 224 308 src="embed.html" 225 - style={{ flex: 1, border: 'none', width: '100%' }} 309 + style={{ flex: 1, border: "none", width: "100%" }} 226 310 /> 227 311 {showEmbedModal && ( 228 312 <EmbedModal code={liveCode} onClose={() => setShowEmbedModal(false)} />