app
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
← Commit history
Merge PR #182: Preview text edits immediately while debouncing saves
3 files changed
+50−3
public/js/app.js+19−2@@ -51,6 +51,22 @@ const post = (path, body) => RENDER_VIEW : 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'); }@@ -1747,6 +1763,7 @@ let hlTimer = null; $('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']]) {@@ -1791,7 +1808,7 @@ Promise.all([ } 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)); });@@ -1807,7 +1824,7 @@ Promise.all([ 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); });
public/js/wizard.js+3−1@@ -212,7 +212,8 @@ const WIZ_STEPS = [ 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';@@ -256,6 +257,7 @@ const WIZ_STEPS = [ 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(() => {
test/circuit-traces.mjs+28@@ -163,6 +163,34 @@ try { 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);