Hero Studio
Public Made by Adomby adom
The hero-image lab with a human gate: `hero-studio pose <owner/slug>` opens the studio in front of the human, posed for their package, and blocks until they click Generate, producing the provenance-stamped 2000x1250 PNG the registry's hero gate requires. The image carries the title and artwork; the exported image also bakes the type pill (top right) and full wiki slug (bottom right); the page shows the hero bare, and only the homepage marquee overlays its own dynamic info. Every hero wears its P
Preview text edits immediately while debouncing saves #4
Headline and subtitle edits now update the hero immediately, so the circuit contours start rerouting on each input instead of waiting for the 350 ms save debounce. The existing debounce still controls persistence and change-log traffic. Pending text drafts survive older SSE/poll responses until the server acknowledges the same value. This covers the wizard's headline/subtitle and Classic's headline input; the circuit geometry is unchanged.
Colby (@kcknox), please review this small follow-up to the already merged PR #3. Verified the complete latest contour revision is present in the published 0.23.0 source: circuit adapter, app, wizard, server defaults, README and circuit tests all match byte-for-byte. No replacement of the merged integration is needed.
Validation in the required Ubuntu WSL live mirror: 23 syntax checks and 43 circuit/UI checks pass, including immediate preview before persistence, stale-state protection, saved text reaching the API, later server edits taking effect, and editor/export geometry parity. The existing read-only render-view suite also passes (11 checks). git diff --check passes.
This proposes only public/js/app.js, public/js/wizard.js, and test/circuit-traces.mjs against 0.23.0 (e13ab87c70fbedfa9f598bede7066a0bf3f9416b). No direct push, merge, release or version change.
Diff Skip to comments
@@ -1,1820 +1,1839 @@⋯ 50 unchanged lines ⋯ : fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(withThread(body)) }); const send = (patch) => post('state', patch).catch(() => {}); +// Paint typing immediately; keep the existing debounce for persistence. An+// older SSE/poll response must not roll back text whose save is still pending.+const textDrafts = {};+function previewText(patch) {+ Object.assign(textDrafts, patch);+ Object.assign(state, patch);+ render();+}+function receiveState(next) {+ for (const [key, value] of Object.entries(textDrafts)) {+ if (next[key] === value) delete textDrafts[key];+ else next[key] = value;+ }+ state = next;+}+ function canonType(t) { return t === 'hardware' ? 'component' : (t || 'app'); } // the LAUNCH spelling for display + bake: the wiki's public name is hardware function wireLabel(t) { return canonType(t) === 'component' ? 'hardware' : (t || 'app'); }⋯ 1690 unchanged lines ⋯ $('headline-input').addEventListener('input', e => { clearTimeout(hlTimer); const v = e.target.value;+ previewText({ headline: v }); hlTimer = setTimeout(() => { send({ headline: v }); setTimeout(() => runFit(null), 250); }, 350); }); for (const [id, key] of [['fs-title', 'title'], ['fs-sub', 'sub'], ['fs-desc', 'desc'], ['fs-leading', 'leading'], ['fs-gap', 'gap']]) {⋯ 38 unchanged lines ⋯ } if (RENDER_VIEW) return; // render view: the state it loaded is the state it renders const es = new EventSource('events');- es.addEventListener('state', e => { state = JSON.parse(e.data); syncPagesIfStale(state); render(); });+ es.addEventListener('state', e => { receiveState(JSON.parse(e.data)); syncPagesIfStale(state); render(); }); es.addEventListener('fit', e => { let m = null; try { m = (JSON.parse(e.data) || {}).max; } catch {} setTimeout(() => runFit(m), 80); }); es.addEventListener('toast', e => { handleToast(JSON.parse(e.data)); }); es.addEventListener('generated', e => { handleGen(JSON.parse(e.data)); });⋯ 9 unchanged lines ⋯ if (!st) return; handleToast(st._toast); handleGen(st._gen);- if (JSON.stringify(st) !== JSON.stringify(state)) { state = st; render(); }+ if (JSON.stringify(st) !== JSON.stringify(state)) { receiveState(st); render(); } }).catch(()=>{}); }, 1000); });⋯ 4 unchanged lines ⋯ window._abDismissed = (state.approved || {}).ts; $('approvedbar').style.display = 'none'; });++@@ -1,979 +1,982 @@⋯ 211 unchanged lines ⋯ let ht = null; hin.addEventListener('input', () => { clearTimeout(ht);- ht = setTimeout(() => { wizSend({ headline: hin.value }); setTimeout(() => runFit(null), 300); }, 350);+ previewText({ headline: hin.value });+ ht = setTimeout(() => { send({ headline: hin.value }); setTimeout(() => runFit(null), 300); }, 350); }); const fitb = document.createElement('button'); fitb.className = 'btn small'; fitb.textContent = 'Make it as big as fits';⋯ 37 unchanged lines ⋯ const liveWords = () => (sin.value || firstSentence(wp().brief, 80)).split(/\s+/).filter(Boolean); let st = null; sin.addEventListener('input', () => {+ previewText({ subtitle: sin.value }); renderPick(); clearTimeout(st); st = setTimeout(() => {⋯ 715 unchanged lines ⋯ positionWizRing(); }); })();+@@ -1,182 +1,211 @@⋯ 162 unchanged lines ⋯ await ui.bringToFront(); const status=await (await fetch(base+'/changes')).json(); ok('change ledger records seed',JSON.stringify(status).includes('circuitSeed'));+ await ui.evaluate(()=>{wizStep='headline';wizardRender();});+ ok('wizard headline paints before its debounced save',await ui.$eval('.wiz-headline input.hin',el=>{+ el.focus();el.value='Instant circuit';el.dispatchEvent(new Event('input',{bubbles:true}));+ return document.querySelector('#el-title').textContent==='Instant circuit' && window.__tracePending;+ }));+ // An unrelated server update can arrive while a text save is pending.+ await patch({guides:false});+ ok('incoming state does not roll back the active draft',await ui.$eval('#el-title',el=>el.textContent==='Instant circuit'));+ await ui.waitForFunction(()=>!window.__tracePending && Object.keys(textDrafts).length===0);+ ok('headline save reaches the API and reroutes',+ (await (await fetch(base+'/state')).json()).headline==='Instant circuit' && uiField!==await ui.$eval('#trace-art',im=>im.src));+ ok('wizard subtitle paints immediately',await ui.$$eval('.wiz-headline input.hin',els=>{+ const el=els[1];el.focus();el.value='Updates as you type';el.dispatchEvent(new Event('input',{bubbles:true}));+ return document.querySelector('#el-sub').textContent==='Updates as you type';+ }));+ await ui.waitForFunction(()=>Object.keys(textDrafts).length===0);+ ok('subtitle save reaches the API',(await (await fetch(base+'/state')).json()).subtitle==='Updates as you type');+ await ui.click('#wiz-classic');+ ok('Classic headline also paints immediately',await ui.$eval('#headline-input',el=>{+ el.focus();el.value='Fast in Classic';el.dispatchEvent(new Event('input',{bubbles:true}));+ return document.querySelector('#el-title').textContent==='Fast in Classic';+ }));+ await ui.waitForFunction(()=>Object.keys(textDrafts).length===0);+ await ui.evaluate(()=>{document.activeElement.blur();});+ await patch({headline:'Traces with purpose',subtitle:'Make room for the story',fontSize:{title:8}});+ await ui.waitForFunction(()=>state.headline==='Traces with purpose' && !window.__tracePending);+ ok('saved drafts yield to later server edits',await ui.$eval('#el-title',el=>el.textContent==='Traces with purpose'));+ await ui.evaluate(()=>{document.body.classList.add('wizard');wizStep='background';wizardRender();}); if(process.env.TRACE_SHOWCASE) { await patch({showcase:{src:'data:image/png;base64,'+fs.readFileSync(process.env.TRACE_SHOWCASE).toString('base64'),full:false},headline:'Circuit traces',subtitle:'Made for your composition',fontSize:{title:8},guide:{status:'confirmed',brand:{colors:{primary:'#00b8b1',background:'#0d1117'}}}}); await ui.waitForFunction(()=>state.headline==='Circuit traces' && !window.__tracePending);⋯ 11 unchanged lines ⋯ } finally { if(browser) await browser.close(); server.kill(); registry.close(); }+
Reviews
Comments
Log in to comment.
No comments yet.