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

improve ux

+312 -130
+1 -1
src/client/embed.tsx
··· 5 import { EmbedApp } from "./ui/EmbedApp.tsx"; 6 7 const container = document.getElementById("embed-root")!; 8 - const root = createRoot(container); 9 root.render(<EmbedApp />);
··· 5 import { EmbedApp } from "./ui/EmbedApp.tsx"; 6 7 const container = document.getElementById("embed-root")!; 8 + const root = createRoot(container!); 9 root.render(<EmbedApp />);
+3 -8
src/client/index.tsx
··· 4 import { createRoot } from "react-dom/client"; 5 import { App } from "./ui/App.tsx"; 6 7 - document.addEventListener("DOMContentLoaded", () => { 8 - const container = document.getElementById("app"); 9 - if (!container) { 10 - throw new Error("Could not find #app element"); 11 - } 12 - const root = createRoot(container); 13 - root.render(<App />); 14 - });
··· 4 import { createRoot } from "react-dom/client"; 5 import { App } from "./ui/App.tsx"; 6 7 + const container = document.getElementById("app"); 8 + const root = createRoot(container!); 9 + root.render(<App />);
+5
src/client/ui/App.css
··· 197 border-color: #555; 198 } 199 200 .App-embedModal-tabs { 201 display: inline-flex; 202 background: var(--bg);
··· 197 border-color: #555; 198 } 199 200 + .App-embedModal-textarea::selection { 201 + background: rgba(255, 213, 79, 0.3); 202 + color: var(--text-bright); 203 + } 204 + 205 .App-embedModal-tabs { 206 display: inline-flex; 207 background: var(--bg);
+39 -22
src/client/ui/App.tsx
··· 1 - import React, { useState, useEffect, type ChangeEvent, type MouseEvent } from "react"; 2 import { version } from "react"; 3 import { SAMPLES, type Sample } from "../samples.ts"; 4 import REACT_VERSIONS from "../../../scripts/versions.json"; ··· 188 export function App(): React.ReactElement { 189 const [initialCode] = useState(getInitialCode); 190 const [currentSample, setCurrentSample] = useState<string | null>(initialCode.sampleKey); 191 - const [workspaceCode, setWorkspaceCode] = useState<CodeState>({ 192 server: initialCode.server, 193 client: initialCode.client, 194 }); 195 - const [liveCode, setLiveCode] = useState<CodeState>(workspaceCode); 196 const [showEmbedModal, setShowEmbedModal] = useState(false); 197 198 - // Listen for code changes from the embed iframe 199 useEffect(() => { 200 const handleMessage = (event: MessageEvent): void => { 201 const data = event.data as { type?: string; code?: CodeState }; ··· 203 setLiveCode(data.code); 204 } 205 }; 206 - 207 window.addEventListener("message", handleMessage); 208 return () => window.removeEventListener("message", handleMessage); 209 }, []); 210 211 - // Reset liveCode when workspaceCode changes (e.g., sample switch) 212 useEffect(() => { 213 - setLiveCode(workspaceCode); 214 - }, [workspaceCode]); 215 - 216 - const embedUrl = `embed.html?c=${encodeURIComponent(encodeCode(workspaceCode))}`; 217 - 218 - const handleSave = (): void => { 219 - saveToUrl(liveCode.server, liveCode.client); 220 - setCurrentSample(null); 221 - }; 222 - 223 - const isDirty = currentSample 224 - ? liveCode.server !== (SAMPLES[currentSample] as Sample).server || 225 - liveCode.client !== (SAMPLES[currentSample] as Sample).client 226 - : liveCode.server !== initialCode.server || liveCode.client !== initialCode.client; 227 228 const handleSampleChange = (e: ChangeEvent<HTMLSelectElement>): void => { 229 const key = e.target.value; ··· 233 server: sample.server, 234 client: sample.client, 235 }; 236 - setWorkspaceCode(newCode); 237 setCurrentSample(key); 238 const url = new URL(window.location.href); 239 url.searchParams.delete("c"); 240 url.searchParams.set("s", key); 241 window.history.pushState({}, "", url); 242 } 243 }; 244 245 return ( 246 <> ··· 322 </div> 323 <BuildSwitcher /> 324 </header> 325 - <iframe key={embedUrl} src={embedUrl} style={{ flex: 1, border: "none", width: "100%" }} /> 326 {showEmbedModal && <EmbedModal code={liveCode} onClose={() => setShowEmbedModal(false)} />} 327 </> 328 );
··· 1 + import React, { useState, useEffect, useRef, type ChangeEvent, type MouseEvent } from "react"; 2 import { version } from "react"; 3 import { SAMPLES, type Sample } from "../samples.ts"; 4 import REACT_VERSIONS from "../../../scripts/versions.json"; ··· 188 export function App(): React.ReactElement { 189 const [initialCode] = useState(getInitialCode); 190 const [currentSample, setCurrentSample] = useState<string | null>(initialCode.sampleKey); 191 + const [liveCode, setLiveCode] = useState<CodeState>({ 192 server: initialCode.server, 193 client: initialCode.client, 194 }); 195 const [showEmbedModal, setShowEmbedModal] = useState(false); 196 + const iframeRef = useRef<HTMLIFrameElement>(null); 197 198 + const [initialEmbedUrl] = useState( 199 + () => `embed.html?seamless=1&c=${encodeURIComponent(encodeCode(initialCode))}`, 200 + ); 201 + 202 useEffect(() => { 203 const handleMessage = (event: MessageEvent): void => { 204 const data = event.data as { type?: string; code?: CodeState }; ··· 206 setLiveCode(data.code); 207 } 208 }; 209 window.addEventListener("message", handleMessage); 210 return () => window.removeEventListener("message", handleMessage); 211 }, []); 212 213 useEffect(() => { 214 + const handlePopState = (): void => { 215 + const newCode = getInitialCode(); 216 + setLiveCode({ server: newCode.server, client: newCode.client }); 217 + setCurrentSample(newCode.sampleKey); 218 + iframeRef.current?.contentWindow?.postMessage( 219 + { type: "rscexplorer:reset", code: { server: newCode.server, client: newCode.client } }, 220 + "*", 221 + ); 222 + }; 223 + window.addEventListener("popstate", handlePopState); 224 + return () => window.removeEventListener("popstate", handlePopState); 225 + }, []); 226 227 const handleSampleChange = (e: ChangeEvent<HTMLSelectElement>): void => { 228 const key = e.target.value; ··· 232 server: sample.server, 233 client: sample.client, 234 }; 235 setCurrentSample(key); 236 + setLiveCode(newCode); 237 + iframeRef.current?.contentWindow?.postMessage( 238 + { type: "rscexplorer:reset", code: newCode }, 239 + "*", 240 + ); 241 const url = new URL(window.location.href); 242 url.searchParams.delete("c"); 243 url.searchParams.set("s", key); 244 window.history.pushState({}, "", url); 245 } 246 }; 247 + 248 + const handleSave = (): void => { 249 + saveToUrl(liveCode.server, liveCode.client); 250 + setCurrentSample(null); 251 + }; 252 + 253 + const isDirty = currentSample 254 + ? liveCode.server !== (SAMPLES[currentSample] as Sample).server || 255 + liveCode.client !== (SAMPLES[currentSample] as Sample).client 256 + : liveCode.server !== initialCode.server || liveCode.client !== initialCode.client; 257 258 return ( 259 <> ··· 335 </div> 336 <BuildSwitcher /> 337 </header> 338 + <iframe 339 + ref={iframeRef} 340 + src={initialEmbedUrl} 341 + style={{ flex: 1, border: "none", width: "100%" }} 342 + /> 343 {showEmbedModal && <EmbedModal code={liveCode} onClose={() => setShowEmbedModal(false)} />} 344 </> 345 );
+28
src/client/ui/CodeEditor.css
··· 21 .CodeEditor .cm-editor .cm-scroller { 22 overflow: auto !important; 23 }
··· 21 .CodeEditor .cm-editor .cm-scroller { 22 overflow: auto !important; 23 } 24 + 25 + /* Fallback textarea styled to match CodeMirror */ 26 + .CodeEditor-fallback { 27 + position: absolute; 28 + top: 0; 29 + left: 0; 30 + right: 0; 31 + bottom: 0; 32 + width: 100%; 33 + height: 100%; 34 + padding: 16px 18px; 35 + margin: 0; 36 + border: none; 37 + outline: none; 38 + resize: none; 39 + background: transparent; 40 + color: #b8b8b8; 41 + font-family: "SF Mono", "Fira Code", Menlo, monospace; 42 + font-size: 13px; 43 + line-height: 1.6; 44 + caret-color: #79b8ff; 45 + white-space: pre; 46 + overflow: auto; 47 + tab-size: 4; 48 + -moz-tab-size: 4; 49 + word-wrap: normal; 50 + overflow-wrap: normal; 51 + }
+42 -60
src/client/ui/CodeEditor.tsx
··· 1 - import React, { useRef, useEffect, useEffectEvent, useState } from "react"; 2 - import { EditorView, keymap } from "@codemirror/view"; 3 - import { javascript } from "@codemirror/lang-javascript"; 4 - import { syntaxHighlighting, HighlightStyle } from "@codemirror/language"; 5 - import { tags } from "@lezer/highlight"; 6 - import { history, historyKeymap, defaultKeymap } from "@codemirror/commands"; 7 - import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete"; 8 import { Pane } from "./Pane.tsx"; 9 import "./CodeEditor.css"; 10 11 - const highlightStyle = HighlightStyle.define([ 12 - { tag: tags.keyword, color: "#c678dd" }, 13 - { tag: tags.string, color: "#98c379" }, 14 - { tag: tags.number, color: "#d19a66" }, 15 - { tag: tags.comment, color: "#5c6370", fontStyle: "italic" }, 16 - { tag: tags.function(tags.variableName), color: "#61afef" }, 17 - { tag: tags.typeName, color: "#e5c07b" }, 18 - { tag: [tags.tagName, tags.angleBracket], color: "#e06c75" }, 19 - { tag: tags.attributeName, color: "#d19a66" }, 20 - { tag: tags.propertyName, color: "#abb2bf" }, 21 - ]); 22 - 23 - const minimalTheme = EditorView.theme( 24 - { 25 - "&": { height: "100%", fontSize: "13px", backgroundColor: "transparent" }, 26 - ".cm-scroller": { 27 - overflow: "auto", 28 - fontFamily: "'SF Mono', 'Fira Code', Menlo, monospace", 29 - lineHeight: "1.6", 30 - padding: "12px", 31 - }, 32 - ".cm-content": { caretColor: "#79b8ff" }, 33 - ".cm-cursor": { borderLeftColor: "#79b8ff", borderLeftWidth: "2px" }, 34 - ".cm-selectionBackground, &.cm-focused .cm-selectionBackground": { 35 - backgroundColor: "#3a3a3a", 36 - }, 37 - ".cm-activeLine": { backgroundColor: "transparent" }, 38 - ".cm-gutters": { display: "none" }, 39 - ".cm-line": { color: "#b8b8b8" }, 40 - }, 41 - { dark: true }, 42 - ); 43 44 type CodeEditorProps = { 45 defaultValue: string; ··· 49 50 export function CodeEditor({ defaultValue, onChange, label }: CodeEditorProps): React.ReactElement { 51 const [initialDefaultValue] = useState(defaultValue); 52 const containerRef = useRef<HTMLDivElement>(null); 53 54 const onEditorChange = useEffectEvent((doc: string) => { 55 onChange(doc); 56 }); 57 58 - useEffect(() => { 59 - if (!containerRef.current) return; 60 61 - const onChangeExtension = EditorView.updateListener.of((update) => { 62 - if (update.docChanged) { 63 - onEditorChange(update.state.doc.toString()); 64 } 65 - }); 66 67 - const editor = new EditorView({ 68 - doc: initialDefaultValue, 69 - extensions: [ 70 - minimalTheme, 71 - syntaxHighlighting(highlightStyle), 72 - javascript({ jsx: true }), 73 - history(), 74 - closeBrackets(), 75 - keymap.of([...defaultKeymap, ...historyKeymap, ...closeBracketsKeymap]), 76 - onChangeExtension, 77 - ], 78 - parent: containerRef.current, 79 - }); 80 81 - return () => editor.destroy(); 82 }, [initialDefaultValue]); 83 84 return ( 85 <Pane label={label}> 86 - <div className="CodeEditor" ref={containerRef} /> 87 </Pane> 88 ); 89 }
··· 1 + import React, { useRef, useLayoutEffect, useEffectEvent, useState } from "react"; 2 import { Pane } from "./Pane.tsx"; 3 import "./CodeEditor.css"; 4 5 + let cmModule: typeof import("./codemirror.ts") | null = null; 6 + const cmModulePromise = import("./codemirror.ts").then((mod) => { 7 + cmModule = mod; 8 + return mod; 9 + }); 10 11 type CodeEditorProps = { 12 defaultValue: string; ··· 16 17 export function CodeEditor({ defaultValue, onChange, label }: CodeEditorProps): React.ReactElement { 18 const [initialDefaultValue] = useState(defaultValue); 19 + const [cmLoaded, setCmLoaded] = useState(cmModule !== null); 20 const containerRef = useRef<HTMLDivElement>(null); 21 + const textareaRef = useRef<HTMLTextAreaElement>(null); 22 23 const onEditorChange = useEffectEvent((doc: string) => { 24 onChange(doc); 25 }); 26 27 + useLayoutEffect(() => { 28 + let editorHandle: { destroy: () => void } | null = null; 29 + let destroyed = false; 30 31 + function initEditor(cm: typeof import("./codemirror.ts")) { 32 + if (!destroyed) { 33 + editorHandle = cm.createEditor( 34 + containerRef.current!, 35 + textareaRef.current?.value ?? initialDefaultValue, 36 + onEditorChange, 37 + ); 38 + setCmLoaded(true); 39 } 40 + } 41 42 + if (cmModule) { 43 + initEditor(cmModule); 44 + } else { 45 + cmModulePromise.then(initEditor); 46 + } 47 48 + return () => { 49 + destroyed = true; 50 + editorHandle?.destroy(); 51 + }; 52 }, [initialDefaultValue]); 53 54 return ( 55 <Pane label={label}> 56 + <div className="CodeEditor" ref={containerRef}> 57 + {!cmLoaded && ( 58 + <textarea 59 + ref={textareaRef} 60 + className="CodeEditor-fallback" 61 + defaultValue={initialDefaultValue} 62 + onChange={(e) => onChange(e.target.value)} 63 + spellCheck={false} 64 + autoCapitalize="off" 65 + autoCorrect="off" 66 + /> 67 + )} 68 + </div> 69 </Pane> 70 ); 71 }
+63 -31
src/client/ui/EmbedApp.tsx
··· 1 - import React from "react"; 2 import { SAMPLES } from "../samples.ts"; 3 import { Workspace } from "./Workspace.tsx"; 4 import "./EmbedApp.css"; ··· 10 client: string; 11 }; 12 13 - function getCodeFromUrl(): CodeState { 14 const params = new URLSearchParams(window.location.search); 15 const encoded = params.get("c"); 16 17 if (encoded) { 18 try { 19 const json = decodeURIComponent(escape(atob(encoded))); 20 const parsed = JSON.parse(json) as { server?: string; client?: string }; 21 - return { 22 server: (parsed.server ?? DEFAULT_SAMPLE.server).trim(), 23 client: (parsed.client ?? DEFAULT_SAMPLE.client).trim(), 24 }; 25 } catch { 26 - // Fall through to defaults 27 } 28 } 29 30 - return { 31 - server: DEFAULT_SAMPLE.server, 32 - client: DEFAULT_SAMPLE.client, 33 - }; 34 } 35 36 function getFullscreenUrl(code: CodeState): string { ··· 40 } 41 42 export function EmbedApp(): React.ReactElement { 43 - const initialCode = getCodeFromUrl(); 44 45 const handleCodeChange = (server: string, client: string): void => { 46 if (window.parent !== window) { ··· 56 57 return ( 58 <> 59 - <div className="EmbedApp-header"> 60 - <span className="EmbedApp-title">RSC Explorer</span> 61 - <a 62 - href={getFullscreenUrl(initialCode)} 63 - target="_blank" 64 - rel="noopener noreferrer" 65 - className="EmbedApp-fullscreenLink" 66 - title="Open in RSC Explorer" 67 - > 68 - <svg 69 - width="14" 70 - height="14" 71 - viewBox="0 0 24 24" 72 - fill="none" 73 - stroke="currentColor" 74 - strokeWidth="2" 75 > 76 - <path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3" /> 77 - </svg> 78 - </a> 79 - </div> 80 <Workspace 81 - initialServerCode={initialCode.server} 82 - initialClientCode={initialCode.client} 83 onCodeChange={handleCodeChange} 84 /> 85 </>
··· 1 + import React, { useState, useEffect } from "react"; 2 import { SAMPLES } from "../samples.ts"; 3 import { Workspace } from "./Workspace.tsx"; 4 import "./EmbedApp.css"; ··· 10 client: string; 11 }; 12 13 + function getParamsFromUrl(): { code: CodeState; seamless: boolean } { 14 const params = new URLSearchParams(window.location.search); 15 const encoded = params.get("c"); 16 + const seamless = params.get("seamless") === "1"; 17 18 + let code: CodeState; 19 if (encoded) { 20 try { 21 const json = decodeURIComponent(escape(atob(encoded))); 22 const parsed = JSON.parse(json) as { server?: string; client?: string }; 23 + code = { 24 server: (parsed.server ?? DEFAULT_SAMPLE.server).trim(), 25 client: (parsed.client ?? DEFAULT_SAMPLE.client).trim(), 26 }; 27 } catch { 28 + code = { 29 + server: DEFAULT_SAMPLE.server, 30 + client: DEFAULT_SAMPLE.client, 31 + }; 32 } 33 + } else { 34 + code = { 35 + server: DEFAULT_SAMPLE.server, 36 + client: DEFAULT_SAMPLE.client, 37 + }; 38 } 39 40 + return { code, seamless }; 41 } 42 43 function getFullscreenUrl(code: CodeState): string { ··· 47 } 48 49 export function EmbedApp(): React.ReactElement { 50 + const [params] = useState(getParamsFromUrl); 51 + const [code, setCode] = useState(params.code); 52 + const seamless = params.seamless; 53 + 54 + // Listen for code updates from parent via postMessage 55 + useEffect(() => { 56 + const handleMessage = (event: MessageEvent): void => { 57 + const data = event.data as { type?: string; code?: CodeState }; 58 + if (data?.type === "rscexplorer:reset" && data.code) { 59 + const newServer = data.code.server.trim(); 60 + const newClient = data.code.client.trim(); 61 + setCode((prev) => { 62 + if (prev.server === newServer && prev.client === newClient) { 63 + return prev; 64 + } 65 + return { server: newServer, client: newClient }; 66 + }); 67 + } 68 + }; 69 + 70 + window.addEventListener("message", handleMessage); 71 + return () => window.removeEventListener("message", handleMessage); 72 + }, []); 73 74 const handleCodeChange = (server: string, client: string): void => { 75 if (window.parent !== window) { ··· 85 86 return ( 87 <> 88 + {!seamless && ( 89 + <div className="EmbedApp-header"> 90 + <span className="EmbedApp-title">RSC Explorer</span> 91 + <a 92 + href={getFullscreenUrl(code)} 93 + target="_blank" 94 + rel="noopener noreferrer" 95 + className="EmbedApp-fullscreenLink" 96 + title="Open in RSC Explorer" 97 > 98 + <svg 99 + width="14" 100 + height="14" 101 + viewBox="0 0 24 24" 102 + fill="none" 103 + stroke="currentColor" 104 + strokeWidth="2" 105 + > 106 + <path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3" /> 107 + </svg> 108 + </a> 109 + </div> 110 + )} 111 <Workspace 112 + key={`${code.server}:${code.client}`} 113 + initialServerCode={code.server} 114 + initialClientCode={code.client} 115 onCodeChange={handleCodeChange} 116 /> 117 </>
+1 -1
src/client/ui/FlightLog.tsx
··· 145 if (entries.length === 0) { 146 return ( 147 <div className="FlightLog-output"> 148 - <span className="FlightLog-empty FlightLog-empty--waiting">Compiling</span> 149 </div> 150 ); 151 }
··· 145 if (entries.length === 0) { 146 return ( 147 <div className="FlightLog-output"> 148 + <span className="FlightLog-empty FlightLog-empty--waiting">Loading</span> 149 </div> 150 ); 151 }
+2 -2
src/client/ui/LivePreview.tsx
··· 102 103 let statusText = ""; 104 if (isLoading) { 105 - statusText = "Compiling"; 106 } else if (isAtStart) { 107 statusText = "Ready"; 108 } else if (isAtEnd) { ··· 182 </div> 183 <div className="LivePreview-container"> 184 {isLoading ? ( 185 - <span className="LivePreview-empty">Compiling...</span> 186 ) : showPlaceholder ? ( 187 <span className="LivePreview-empty">{isAtStart ? "Step to begin..." : "Loading..."}</span> 188 ) : flightPromise ? (
··· 102 103 let statusText = ""; 104 if (isLoading) { 105 + statusText = "Loading"; 106 } else if (isAtStart) { 107 statusText = "Ready"; 108 } else if (isAtEnd) { ··· 182 </div> 183 <div className="LivePreview-container"> 184 {isLoading ? ( 185 + <span className="LivePreview-empty">Loading...</span> 186 ) : showPlaceholder ? ( 187 <span className="LivePreview-empty">{isAtStart ? "Step to begin..." : "Loading..."}</span> 188 ) : flightPromise ? (
+9 -2
src/client/ui/Workspace.tsx
··· 24 25 useEffect(() => { 26 const abort = new AbortController(); 27 WorkspaceSession.create(serverCode, clientCode, abort.signal).then((nextSession) => { 28 if (!abort.signal.aborted) { 29 setSession(nextSession); 30 } 31 }); 32 - return () => abort.abort(); 33 }, [serverCode, clientCode, resetKey]); 34 35 function handleServerChange(code: string) { ··· 68 {isLoading ? ( 69 <div className="Workspace-loadingOutput"> 70 <span className="Workspace-loadingEmpty Workspace-loadingEmpty--waiting"> 71 - Compiling 72 </span> 73 </div> 74 ) : isError ? (
··· 24 25 useEffect(() => { 26 const abort = new AbortController(); 27 + const timeoutId = setTimeout(() => { 28 + setSession(null); 29 + }, 1000); 30 WorkspaceSession.create(serverCode, clientCode, abort.signal).then((nextSession) => { 31 if (!abort.signal.aborted) { 32 + clearTimeout(timeoutId); 33 setSession(nextSession); 34 } 35 }); 36 + return () => { 37 + clearTimeout(timeoutId); 38 + abort.abort(); 39 + }; 40 }, [serverCode, clientCode, resetKey]); 41 42 function handleServerChange(code: string) { ··· 75 {isLoading ? ( 76 <div className="Workspace-loadingOutput"> 77 <span className="Workspace-loadingEmpty Workspace-loadingEmpty--waiting"> 78 + Loading 79 </span> 80 </div> 81 ) : isError ? (
+75
src/client/ui/codemirror.ts
···
··· 1 + import { EditorView, keymap } from "@codemirror/view"; 2 + import { javascript } from "@codemirror/lang-javascript"; 3 + import { syntaxHighlighting, HighlightStyle } from "@codemirror/language"; 4 + import { tags } from "@lezer/highlight"; 5 + import { history, historyKeymap, defaultKeymap } from "@codemirror/commands"; 6 + import { closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete"; 7 + 8 + const highlightStyle = HighlightStyle.define([ 9 + { tag: tags.keyword, color: "#c678dd" }, 10 + { tag: tags.string, color: "#98c379" }, 11 + { tag: tags.number, color: "#d19a66" }, 12 + { tag: tags.comment, color: "#5c6370", fontStyle: "italic" }, 13 + { tag: tags.function(tags.variableName), color: "#61afef" }, 14 + { tag: tags.typeName, color: "#e5c07b" }, 15 + { tag: [tags.tagName, tags.angleBracket], color: "#e06c75" }, 16 + { tag: tags.attributeName, color: "#d19a66" }, 17 + { tag: tags.propertyName, color: "#abb2bf" }, 18 + ]); 19 + 20 + const minimalTheme = EditorView.theme( 21 + { 22 + "&": { height: "100%", fontSize: "13px", backgroundColor: "transparent" }, 23 + ".cm-scroller": { 24 + overflow: "auto", 25 + fontFamily: "'SF Mono', 'Fira Code', Menlo, monospace", 26 + lineHeight: "1.6", 27 + padding: "12px", 28 + }, 29 + ".cm-content": { caretColor: "#79b8ff" }, 30 + ".cm-cursor": { borderLeftColor: "#79b8ff", borderLeftWidth: "2px" }, 31 + ".cm-selectionBackground, &.cm-focused .cm-selectionBackground": { 32 + backgroundColor: "#3a3a3a", 33 + }, 34 + ".cm-activeLine": { backgroundColor: "transparent" }, 35 + ".cm-gutters": { display: "none" }, 36 + ".cm-line": { color: "#b8b8b8" }, 37 + }, 38 + { dark: true }, 39 + ); 40 + 41 + export type EditorHandle = { 42 + view: EditorView; 43 + destroy: () => void; 44 + }; 45 + 46 + export function createEditor( 47 + container: HTMLElement, 48 + doc: string, 49 + onChange: (doc: string) => void, 50 + ): EditorHandle { 51 + const onChangeExtension = EditorView.updateListener.of((update) => { 52 + if (update.docChanged) { 53 + onChange(update.state.doc.toString()); 54 + } 55 + }); 56 + 57 + const view = new EditorView({ 58 + doc, 59 + extensions: [ 60 + minimalTheme, 61 + syntaxHighlighting(highlightStyle), 62 + javascript({ jsx: true }), 63 + history(), 64 + closeBrackets(), 65 + keymap.of([...defaultKeymap, ...historyKeymap, ...closeBracketsKeymap]), 66 + onChangeExtension, 67 + ], 68 + parent: container, 69 + }); 70 + 71 + return { 72 + view, 73 + destroy: () => view.destroy(), 74 + }; 75 + }
+8 -2
src/embed.ts
··· 27 type EmbedOptions = { 28 server: string; 29 client: string; 30 }; 31 32 type EmbedControl = { ··· 66 */ 67 export function mount( 68 container: string | HTMLElement, 69 - { server, client }: EmbedOptions, 70 ): EmbedControl { 71 const el = 72 typeof container === "string" ? document.querySelector<HTMLElement>(container) : container; ··· 75 throw new Error(`RSC Explorer: Container not found: ${container}`); 76 } 77 78 const iframe = document.createElement("iframe"); 79 - iframe.src = getEmbedUrl(); 80 iframe.style.cssText = 81 "width: 100%; height: 100%; border: 1px solid #e0e0e0; border-radius: 8px;"; 82
··· 27 type EmbedOptions = { 28 server: string; 29 client: string; 30 + seamless?: boolean; 31 }; 32 33 type EmbedControl = { ··· 67 */ 68 export function mount( 69 container: string | HTMLElement, 70 + { server, client, seamless }: EmbedOptions, 71 ): EmbedControl { 72 const el = 73 typeof container === "string" ? document.querySelector<HTMLElement>(container) : container; ··· 76 throw new Error(`RSC Explorer: Container not found: ${container}`); 77 } 78 79 + const embedUrl = new URL(getEmbedUrl()); 80 + if (seamless) { 81 + embedUrl.searchParams.set("seamless", "1"); 82 + } 83 + 84 const iframe = document.createElement("iframe"); 85 + iframe.src = embedUrl.href; 86 iframe.style.cssText = 87 "width: 100%; height: 100%; border: 1px solid #e0e0e0; border-radius: 8px;"; 88
+36 -1
vite.config.js
··· 68 }; 69 } 70 71 export default defineConfig(({ mode }) => ({ 72 - plugins: [react(), rolldownWorkerPlugin(), serveEmbedPlugin()], 73 server: { port: 3333 }, 74 define: { 75 "process.env.NODE_ENV": JSON.stringify(mode === "development" ? "development" : "production"),
··· 68 }; 69 } 70 71 + function preloadCodemirrorPlugin() { 72 + return { 73 + name: "preload-codemirror", 74 + transformIndexHtml(html, { bundle, filename }) { 75 + if (!bundle) return; // dev mode 76 + const tags = []; 77 + 78 + const cmChunk = Object.keys(bundle).find((k) => k.includes("codemirror")); 79 + if (cmChunk) { 80 + tags.push({ 81 + tag: "link", 82 + attrs: { rel: "modulepreload", href: "/" + cmChunk }, 83 + injectTo: "head", 84 + }); 85 + } 86 + 87 + // From index.html, prefetch embed resources for the iframe 88 + if (filename.endsWith("index.html")) { 89 + const embedChunk = Object.keys(bundle).find( 90 + (k) => k.startsWith("assets/embed") && k.endsWith(".js"), 91 + ); 92 + if (embedChunk) { 93 + tags.push({ 94 + tag: "link", 95 + attrs: { rel: "preload", href: "/" + embedChunk, as: "script", crossorigin: true }, 96 + injectTo: "head", 97 + }); 98 + } 99 + } 100 + 101 + return tags; 102 + }, 103 + }; 104 + } 105 + 106 export default defineConfig(({ mode }) => ({ 107 + plugins: [react(), rolldownWorkerPlugin(), serveEmbedPlugin(), preloadCodemirrorPlugin()], 108 server: { port: 3333 }, 109 define: { 110 "process.env.NODE_ENV": JSON.stringify(mode === "development" ? "development" : "production"),