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 {
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 = "" ;
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 ) ,
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
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=<base64url(json)></ code >
550+ < span > Hash field: </ span >
551+ < code > #state64=<base64url(json)></ 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, "&" )
596+ . replace ( / < / g, "<" )
597+ . replace ( / > / g, ">" )
598+ . replace ( / " / g, """ )
599+ . replace ( / ' / g, "'" ) ;
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