Skip to content

Commit bcd31f5

Browse files
committed
chore(example): update examples
1 parent 4bf832c commit bcd31f5

6 files changed

Lines changed: 672 additions & 79 deletions

File tree

docs/browser-integration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,5 @@ The repository includes several browser examples:
289289
| Chat form | `examples/killer/one-line-chat-form.html` | Form generation with date-fns |
290290
| Sandbox worker | `examples/killer/one-line-sandbox-worker.html` | Worker-sandboxed source execution |
291291
| Hash runner | `examples/killer/hash-code-runner.html` | Execute plans from URL hashes |
292-
| Todo app | `examples/todo/react-shadcn-todo.html` | React + shadcn UI todo application |
292+
| Todo app | `examples/todo/react-shadcn-todo.html` | RuntimePlan-driven todo app with source module |
293+
| Todo hash app | `examples/todo/react-shadcn-todo-hash.html` | RuntimePlan todo app with URL hash sync |

examples/todo/react-shadcn-todo-hash.html

Lines changed: 168 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,17 @@
242242
color: var(--muted-foreground);
243243
}
244244

245+
.runtime-status {
246+
width: min(760px, calc(100% - 32px));
247+
margin: 14px auto 0;
248+
padding: 8px 12px;
249+
border: 1px solid var(--border);
250+
border-radius: 10px;
251+
background: color-mix(in srgb, var(--card) 94%, #ffffff 6%);
252+
color: var(--muted-foreground);
253+
font-size: 12px;
254+
}
255+
245256
@media (max-width: 640px) {
246257
.card-content,
247258
.card-header {
@@ -266,20 +277,40 @@
266277
</style>
267278
</head>
268279
<body>
280+
<div id="runtime-status" class="runtime-status">Booting Renderify runtime...</div>
269281
<div id="app"></div>
270282

271-
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
272-
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
273-
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
274-
<script type="text/babel">
275-
const { useEffect, useMemo, useState } = React;
276-
const STORAGE_KEY = "renderify-react-shadcn-todo-hash-demo";
283+
<script type="importmap">
284+
{
285+
"imports": {
286+
"@renderify/ir": "../../packages/ir/dist/ir.esm.js",
287+
"@renderify/security": "../../packages/security/dist/security.esm.js",
288+
"preact": "/node_modules/preact/dist/preact.module.js",
289+
"preact/hooks": "/node_modules/preact/hooks/dist/hooks.module.js",
290+
"preact/jsx-runtime": "/node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js",
291+
"preact/jsx-dev-runtime": "/node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js",
292+
"es-module-lexer": "/node_modules/.pnpm/es-module-lexer@1.7.0/node_modules/es-module-lexer/dist/lexer.js"
293+
}
294+
}
295+
</script>
296+
<script type="text/plain" id="todo-source">
297+
import { useEffect, useMemo, useState } from "preact/hooks";
298+
277299
const HASH_KEY = "state64";
300+
const DEFAULT_TODOS = [
301+
{ id: "seed-priority", text: "List the top 3 priorities for today", done: false },
302+
{ id: "seed-pr", text: "Review PR #142", done: true },
303+
{ id: "seed-share", text: "Share demo progress with the team", done: false },
304+
];
278305

279306
function uid() {
280307
return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
281308
}
282309

310+
function joinClasses(...parts) {
311+
return parts.filter(Boolean).join(" ");
312+
}
313+
283314
function encodeBase64Url(input) {
284315
const bytes = new TextEncoder().encode(String(input));
285316
let binary = "";
@@ -324,7 +355,6 @@
324355

325356
const parsed = JSON.parse(decodeBase64Url(payload));
326357
if (!parsed || typeof parsed !== "object") return null;
327-
328358
return {
329359
todos: normalizeTodos(parsed.todos),
330360
filter: normalizeFilter(parsed.filter),
@@ -344,34 +374,21 @@
344374
}
345375
}
346376

347-
function loadTodos() {
348-
try {
349-
const raw = localStorage.getItem(STORAGE_KEY);
350-
if (!raw) return [];
351-
return normalizeTodos(JSON.parse(raw));
352-
} catch {
353-
return [];
354-
}
355-
}
356-
357377
const initialHashState = readHashState();
358378

359-
function App() {
360-
const [todos, setTodos] = useState(initialHashState?.todos ?? loadTodos());
379+
export default function App() {
380+
const [todos, setTodos] = useState(initialHashState?.todos ?? DEFAULT_TODOS);
361381
const [value, setValue] = useState("");
362382
const [filter, setFilter] = useState(initialHashState?.filter ?? "all");
363383
const [flash, setFlash] = useState(initialHashState ? "State restored from hash" : "");
364384

365-
useEffect(() => {
366-
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
367-
}, [todos]);
368-
369385
useEffect(() => {
370386
writeHashState({ todos, filter });
371387
}, [todos, filter]);
372388

373389
useEffect(() => {
374-
const timer = setTimeout(() => setFlash(""), flash ? 1800 : 0);
390+
if (!flash) return undefined;
391+
const timer = setTimeout(() => setFlash(""), 1800);
375392
return () => clearTimeout(timer);
376393
}, [flash]);
377394

@@ -446,9 +463,9 @@
446463
<main className="app-shell">
447464
<section className="card">
448465
<header className="card-header">
449-
<h1 className="card-title">React + shadcn Todo (Hash Share)</h1>
466+
<h1 className="card-title">React + shadcn Todo (Hash Share RuntimePlan + JSX)</h1>
450467
<p className="card-description">
451-
State syncs automatically to the URL hash, so sharing the link reproduces this view.
468+
State syncs to URL hash and is rendered through Renderify runtime JSX source execution.
452469
</p>
453470
</header>
454471

@@ -458,7 +475,7 @@ <h1 className="card-title">React + shadcn Todo (Hash Share)</h1>
458475
className="input"
459476
placeholder="Add a task, e.g. design runtime streaming API"
460477
value={value}
461-
onChange={(event) => setValue(event.target.value)}
478+
onInput={(event) => setValue(event.target.value)}
462479
onKeyDown={onInputKeyDown}
463480
/>
464481
<button className="btn btn-primary" onClick={addTodo} disabled={!value.trim()}>
@@ -476,9 +493,9 @@ <h1 className="card-title">React + shadcn Todo (Hash Share)</h1>
476493
className="todo-check"
477494
type="checkbox"
478495
checked={todo.done}
479-
onChange={() => toggleTodo(todo.id)}
496+
onInput={() => toggleTodo(todo.id)}
480497
/>
481-
<span className={`todo-text ${todo.done ? "done" : ""}`}>
498+
<span className={joinClasses("todo-text", todo.done ? "done" : "")}>
482499
{todo.text}
483500
</span>
484501
<button
@@ -497,50 +514,160 @@ <h1 className="card-title">React + shadcn Todo (Hash Share)</h1>
497514
<div className="footer">
498515
<div className="row">
499516
<button
500-
className={`btn btn-outline ${filter === "all" ? "active" : ""}`}
517+
className={joinClasses("btn btn-outline", filter === "all" ? "active" : "")}
501518
onClick={() => setFilter("all")}
502519
>
503520
All
504521
</button>
505522
<button
506-
className={`btn btn-outline ${filter === "active" ? "active" : ""}`}
523+
className={joinClasses("btn btn-outline", filter === "active" ? "active" : "")}
507524
onClick={() => setFilter("active")}
508525
>
509526
Active
510527
</button>
511528
<button
512-
className={`btn btn-outline ${filter === "done" ? "active" : ""}`}
529+
className={joinClasses("btn btn-outline", filter === "done" ? "active" : "")}
513530
onClick={() => setFilter("done")}
514531
>
515532
Completed
516533
</button>
517534
</div>
518-
519535
<div className="row">
520536
<span className="badge">{leftCount} remaining</span>
521-
<button className="btn btn-danger" onClick={clearDone}>Clear completed</button>
522-
<button className="btn btn-ghost" onClick={resetDemo}>Reset demo</button>
523-
<button className="btn btn-outline" onClick={copyShareLink}>Copy share link</button>
537+
<button className="btn btn-danger" onClick={clearDone}>
538+
Clear completed
539+
</button>
540+
<button className="btn btn-ghost" onClick={resetDemo}>
541+
Reset demo
542+
</button>
543+
<button className="btn btn-outline" onClick={copyShareLink}>
544+
Copy share link
545+
</button>
524546
</div>
525547
</div>
526548

527549
<div className="hash-hint">
528-
Hash field: <code>#state64=&lt;base64url(json)&gt;</code>
550+
<span>Hash field: </span>
551+
<code>#state64=&lt;base64url(json)&gt;</code>
529552
{flash ? (
530-
<div style={{ marginTop: 6, color: "#0f766e", fontWeight: 600 }}>{flash}</div>
553+
<div
554+
style={{
555+
marginTop: "6px",
556+
color: "#0f766e",
557+
fontWeight: "600",
558+
}}
559+
>
560+
{flash}
561+
</div>
531562
) : null}
532563
</div>
533564

534-
<p className="muted" style={{ marginTop: 12, fontSize: 12 }}>
535-
Tip: state is also saved to LocalStorage. Manually editing the hash auto-reloads state.
565+
<p className="muted" style={{ marginTop: "12px", fontSize: "12px" }}>
566+
Tip: manually editing the hash updates state after hashchange.
536567
</p>
537568
</div>
538569
</section>
539570
</main>
540571
);
541572
}
573+
</script>
574+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
575+
<script type="module">
576+
import { renderPlanInBrowser } from "../../packages/runtime/dist/runtime.esm.js";
577+
578+
const appElement = document.getElementById("app");
579+
const statusElement = document.getElementById("runtime-status");
580+
const sourceElement = document.getElementById("todo-source");
581+
582+
function setStatus(message) {
583+
statusElement.textContent = message;
584+
}
585+
586+
function toErrorMessage(error) {
587+
if (error instanceof Error) {
588+
return error.message;
589+
}
590+
return String(error);
591+
}
592+
593+
function escapeHtml(value) {
594+
return String(value)
595+
.replace(/&/g, "&amp;")
596+
.replace(/</g, "&lt;")
597+
.replace(/>/g, "&gt;")
598+
.replace(/"/g, "&quot;")
599+
.replace(/'/g, "&#39;");
600+
}
601+
602+
const sourceCode = sourceElement?.textContent?.trim();
603+
if (!sourceCode) {
604+
throw new Error("todo-source payload is empty");
605+
}
606+
const localOrigin = window.location.origin;
607+
const moduleManifest = {
608+
preact: {
609+
resolvedUrl: `${localOrigin}/node_modules/preact/dist/preact.module.js`,
610+
},
611+
"preact/hooks": {
612+
resolvedUrl: `${localOrigin}/node_modules/preact/hooks/dist/hooks.module.js`,
613+
},
614+
"preact/jsx-runtime": {
615+
resolvedUrl: `${localOrigin}/node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js`,
616+
},
617+
"preact/jsx-dev-runtime": {
618+
resolvedUrl: `${localOrigin}/node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js`,
619+
},
620+
};
621+
622+
const plan = {
623+
specVersion: "runtime-plan/v1",
624+
id: "todo_runtimeplan_hash_demo",
625+
version: 1,
626+
root: {
627+
type: "element",
628+
tag: "section",
629+
children: [{ type: "text", value: "Preparing hash todo demo..." }],
630+
},
631+
imports: ["preact", "preact/hooks", "preact/jsx-runtime", "preact/jsx-dev-runtime"],
632+
moduleManifest,
633+
capabilities: {
634+
domWrite: true,
635+
},
636+
source: {
637+
language: "jsx",
638+
runtime: "preact",
639+
exportName: "default",
640+
code: sourceCode,
641+
},
642+
};
643+
644+
async function boot() {
645+
setStatus("Rendering RuntimePlan...");
646+
try {
647+
const result = await renderPlanInBrowser(plan, {
648+
target: "#app",
649+
autoPinLatestModuleManifest: false,
650+
securityInitialization: {
651+
profile: "balanced",
652+
overrides: {
653+
allowedNetworkHosts: ["ga.jspm.io", "cdn.jspm.io", window.location.host],
654+
},
655+
},
656+
});
657+
setStatus(`RuntimePlan rendered: ${result.execution.planId}`);
658+
window.__RENDERIFY_TODO_HASH_DEMO__ = {
659+
planId: result.execution.planId,
660+
diagnostics: result.execution.diagnostics,
661+
};
662+
} catch (error) {
663+
const message = toErrorMessage(error);
664+
setStatus(`RuntimePlan failed: ${message}`);
665+
appElement.innerHTML = `<div class="todo-empty">${escapeHtml(message)}</div>`;
666+
console.error(error);
667+
}
668+
}
542669

543-
ReactDOM.createRoot(document.getElementById("app")).render(<App />);
670+
void boot();
544671
</script>
545672
</body>
546673
</html>

0 commit comments

Comments
 (0)