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
Publish 0.9.0
7 files changed
+343−44
SKILL.md+50−12@@ -17,11 +17,17 @@ gate, which opens the studio in front of the human and BLOCKS until they click Generate: ```bash-hero-studio pose <owner>/<slug> [--title "T"] [--brief "one-liner"] [--timeout 900]+hero-studio pose <owner>/<slug> --surface webview|pup [--title "T"] [--brief "one-liner"] [--timeout 900] # blocks; exits 0 with {file, path, latest} JSON on stdout when the HUMAN generates, # exit 2 on timeout, exit 1 on setup errors ``` +`--surface` is REQUIRED: check the user's stored preference first with+`adom-cli hydrogen probe` (window_mode: webview | pup) and pass that. Never+open both surfaces for the same content. The pup path reuses a `hero-studio`+session (navigate, not a duplicate window) and falls back to webview with a+warning if Adom Desktop is unreachable.+ `pose` starts the server if needed, registers the package with the studio (no restart needed), poses it, opens the "Hero Studio" webview tab with proxy mode off (proxy mode silently blocks all page JS), and waits. When it returns, apply the human's image, and@@ -60,19 +66,51 @@ pose sibling B, confirm, Generate. ## Verification mode (what the pose gate shows) When opened by the pose gate, the studio presents verification chrome above the-hero: an explainer, a per-item checklist the human ratifies deliberately (title,-subtitle, the vitrine, and a page-type/mode match check), and a prominent-Generate button that unlocks only when every item is confirmed. Free exploration-without the checklist is lab mode, which is what a plain open gets.--## The vitrine, and putting real content in it+hero: an explainer, a per-item checklist, and a Generate button that unlocks+only when every item is confirmed (clicking it locked explains why). A+WALKTHROUGH spotlights each item in place: "this is your title", subtitle,+vitrine, then the type pill (where the human catches an AI's wrong app/skill/+component call); "Looks good" ticks that check and advances. The homepage-+marquee overlay composites over the hero by default in the gate so the human+SEES where the wiki URL, pill, and today's duplicated title land (annotated+with adom/wiki#80, which will drop the duplicate). Free exploration without+the checklist is lab mode, a plain open.++## The vitrine: real content is MANDATORY, not a suggestion The floating app window in the hero is called the VITRINE (one word,-everywhere). The pose flow auto-loads a real screenshot from the package's wiki-page (screenshots/ first, then docs/) so apps show their real UI by default; the-Vitrine content section can also import one manually or pull again with the-"Pull real screenshot from the page" button. The CSS mock only shows when the-page has no images.+everywhere). A generic mock vitrine in a published hero is a failure. The pose+flow auto-pulls a screenshot from the package's wiki page; when that fails it+prints REPRIMAND [VITRINE_IS_GENERIC] with the exact recovery, which you MUST+follow before letting the human generate:++1. Capture the app's REAL UI at the slot size pose printed (also live in the+ left panel, click-to-copy), so nothing crops. In pup:+ `adom-desktop browser_open_window '{"sessionId":"vitrine-shot","profile":"vitrine-shot","url":"<app url>"}'`+ then size the viewport to the slot aspect and+ `adom-desktop browser_screenshot '{"sessionId":"vitrine-shot"}'`.+2. Load it: `hero-studio vitrine --file <shot.png>` (the open studio updates+ live within a second).++The CSS mock is acceptable ONLY when the page genuinely has no UI.++## Driving a live session from the CLI++The studio holds state on the server with an SSE + poll link to the open page,+so every command lands in front of the human as you run it:++```bash+hero-studio set theme=aurora gradAngle=220 # or: set '{"layout":"showcase"}'+hero-studio vitrine --file shot.png [--full] [--cover]+hero-studio state # read everything, incl. slotSize+hero-studio toast "trying the showcase layout" --kind info+hero-studio reroll # new procedural circuitry pattern+```++Layouts now include `showcase` (the vitrine gets ~65% of the hero behind a+gradient scrim, Adom-USB style). Themes add `aurora`, `mesh`, and `circuitry`,+which is procedurally generated from a seed so every hero is gently different;+the seed rides the provenance stamp and `reroll` re-rolls it. Full-hero import is the ratification path for an existing image: import the image, check "Use the imported image as the FULL hero", ratify the checklist as
bin/hero-studio+1@@ -6,5 +6,6 @@ SELF="$(readlink -f "$0")" DIR="$(cd "$(dirname "$SELF")/.." && pwd)" case "$1" in pose) shift; exec node "$DIR/pose.mjs" "$@" ;;+ vitrine|set|state|toast|reroll) exec node "$DIR/drive.mjs" "$@" ;; *) exec node "$DIR/server.js" "$@" ;; esac
package.json+1−1@@ -2,7 +2,7 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.8.1",+ "version": "0.9.0", "description": "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. Adom-org pages get the house wordmark and palette automatically; every other owner brings their own accent and background colors, with placement fixed for all. The floating app window is the vitrine: it auto-loads a real screenshot from the package's wiki page (screenshots/, docs/), takes a manual import, or a full-hero import (the human-ratified re-stamp path for existing heroes). The pose gate opens in verification mode: per-item checklist (title, subtitle, vitrine, type match) and a Generate that unlocks only fully confirmed. Includes vitrine bleeds, brand tile themes, gradient angles, pixel-faithful wiki marquee and index-card previews, and a component-hero mode with a photo/raytrace/phong fallback ladder.", "license": "MIT", "tags": [
page.json+1−1@@ -2,7 +2,7 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.8.1",+ "version": "0.9.0", "description": "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. Adom-org pages get the house wordmark and palette automatically; every other owner brings their own accent and background colors, with placement fixed for all. The floating app window is the vitrine: it auto-loads a real screenshot from the package's wiki page (screenshots/, docs/), takes a manual import, or a full-hero import (the human-ratified re-stamp path for existing heroes). The pose gate opens in verification mode: per-item checklist (title, subtitle, vitrine, type match) and a Generate that unlocks only fully confirmed. Includes vitrine bleeds, brand tile themes, gradient angles, pixel-faithful wiki marquee and index-card previews, and a component-hero mode with a photo/raytrace/phong fallback ladder.", "license": "MIT", "tags": [
pose.mjs+75−18@@ -24,7 +24,15 @@ for (let i = 0; i < args.length; i++) { } const ref = pos[0]; if (!ref || !ref.includes('/')) {- console.error('usage: hero-studio pose <owner/slug> [--title T] [--brief B] [--type app|skill] [--timeout SECS]');+ console.error('usage: hero-studio pose <owner/slug> --surface webview|pup [--title T] [--brief B] [--type app|skill] [--timeout SECS]');+ process.exit(1);+}+const surface = flags.surface;+if (surface !== 'webview' && surface !== 'pup') {+ console.error('ERROR [SURFACE_REQUIRED]: say which surface the human is looking at: --surface webview | --surface pup');+ console.error(" why: opening the wrong surface means the human never sees the gate. Check their stored preference first:");+ console.error(" adom-cli hydrogen probe # -> { window_mode: \"webview\" | \"pup\" }");+ console.error(' then pass that value. Never open both surfaces for the same content.'); process.exit(1); } const [owner, slug] = ref.split('/');@@ -63,7 +71,7 @@ await jf('state', { body: JSON.stringify({ mode: 'app', page: ref, // the gate presentation: verification chrome + a fresh checklist- verify: true,+ verify: true, tour: true, marqueePreview: true, confirm: { title: false, subtitle: false, vitrine: false, type: false }, // clear the previous package's vitrine so a failed auto-pull can never // leave sibling A's screenshot in sibling B's hero. Artwork state (theme,@@ -77,24 +85,73 @@ await jf('state', { // into the vitrine, so the mock only shows when the page has no images try { await jf('vitrine-from-page?page=' + ref, { method: 'POST' }); } catch {} -// 3. open the studio IN FRONT OF THE HUMAN: a Hydrogen webview tab, with-// proxy mode OFF (proxy mode silently blocks all page JS, which leaves the-// studio dead) and never in the VSCode editor pane.+// 3. open the studio IN FRONT OF THE HUMAN on the surface they actually use. const proxyBase = (process.env.VSCODE_PROXY_URI || '').replace('{{port}}', '8846');-if (!proxyBase) { console.error('VSCODE_PROXY_URI not set; cannot open an editor tab'); process.exit(1); }-const tabsRaw = await sh('adom-cli', ['hydrogen', 'workspace', 'tabs']);+if (!proxyBase) { console.error('VSCODE_PROXY_URI not set; cannot build the studio URL'); process.exit(1); } let tabName = 'Hero Studio';-let panelId = null;-try {- const tabs = JSON.parse(tabsRaw).tabs || [];- const existing = tabs.find((t) => (t.name || '').includes('Hero Studio'));- const anyWebview = tabs.find((t) => t.name !== 'Visual Studio Code');- if (existing) { tabName = existing.name; panelId = existing.panelId; }- else if (anyWebview) panelId = anyWebview.panelId;-} catch {}-if (!panelId) { console.error('no webview pane found to open the studio in'); process.exit(1); }-await sh('adom-cli', ['hydrogen', 'webview', 'open-or-refresh', '--name', tabName, '--url', proxyBase, '--panel-id', panelId]);-await sh('adom-cli', ['hydrogen', 'webview', 'set-proxy', 'false', '--name', tabName]);+if (surface === 'pup') {+ // Adom Desktop pup window. Reuse an existing hero-studio session; never+ // spawn a duplicate window (browser_status first, then navigate-or-open).+ const status = await sh('adom-desktop', ['browser_status', '{}']);+ const hasSession = !!(status && status.includes('hero-studio'));+ const opened = hasSession+ ? await sh('adom-desktop', ['browser_navigate', JSON.stringify({ sessionId: 'hero-studio', url: proxyBase })])+ : await sh('adom-desktop', ['browser_open_window', JSON.stringify({ sessionId: 'hero-studio', profile: 'hero-studio', url: proxyBase })]);+ if (opened === null) {+ console.error('WARN [PUP_UNREACHABLE]: Adom Desktop did not answer; falling back to a webview tab so the gate still reaches the human.');+ } else {+ await sh('adom-desktop', ['browser_alert_window', JSON.stringify({ sessionId: 'hero-studio' })]);+ }+ if (opened === null) {+ await openWebview();+ }+} else {+ await openWebview();+}+async function openWebview() {+ // Hydrogen webview tab, proxy mode OFF (proxy mode silently blocks all page+ // JS, which leaves the studio dead), never the VS Code pane.+ const tabsRaw = await sh('adom-cli', ['hydrogen', 'workspace', 'tabs']);+ let panelId = null;+ try {+ const tabs = JSON.parse(tabsRaw).tabs || [];+ const existing = tabs.find((t) => (t.name || '').includes('Hero Studio'));+ const anyWebview = tabs.find((t) => t.name !== 'Visual Studio Code');+ if (existing) { tabName = existing.name; panelId = existing.panelId; }+ else if (anyWebview) panelId = anyWebview.panelId;+ } catch {}+ if (!panelId) { console.error('no webview pane found to open the studio in'); process.exit(1); }+ await sh('adom-cli', ['hydrogen', 'webview', 'open-or-refresh', '--name', tabName, '--url', proxyBase, '--panel-id', panelId]);+ await sh('adom-cli', ['hydrogen', 'webview', 'set-proxy', 'false', '--name', tabName]);+}++// 3b. THE VITRINE MANDATE. Wait for the UI to measure the slot, then tell the+// AI exactly what a real capture requires. A generic mock vitrine is a+// reprimand, not a shrug.+let slot = null;+for (let i = 0; i < 8; i++) {+ await new Promise((r) => setTimeout(r, 750));+ try {+ const st = await jf('state');+ if (st.slotSize && st.slotSize.w) { slot = st.slotSize; state = st; break; }+ state = st;+ } catch {}+}+const slotTxt = slot ? slot.w + 'x' + slot.h + ' (aspect ' + (slot.w / slot.h).toFixed(2) + ')' : 'the size shown in the left panel';+if (!(state.vitrine && state.vitrine.src)) {+ console.error('');+ console.error('REPRIMAND [VITRINE_IS_GENERIC]: the vitrine is showing the GENERIC MOCK.');+ console.error(' You MUST capture a real screenshot of ' + ref + "'s actual UI and load it:");+ console.error(' 1. open the app in a pup window and size the capture to ' + slotTxt + ' so nothing crops:');+ console.error(' adom-desktop browser_open_window \'{"sessionId":"vitrine-shot","profile":"vitrine-shot","url":"<the app\'s URL>"}\'');+ console.error(' adom-desktop browser_eval \'{"sessionId":"vitrine-shot","expr":"window.resizeTo(' + (slot ? slot.w / 2 : 1000) + ',' + (slot ? slot.h / 2 : 640) + ')"}\'');+ console.error(' adom-desktop browser_screenshot \'{"sessionId":"vitrine-shot"}\'');+ console.error(' 2. load it into the studio: hero-studio vitrine --file <that screenshot.png>');+ console.error(' Do NOT let the human generate over the generic mock unless the page truly has no UI.');+ console.error('');+} else {+ console.error('vitrine: loaded (' + (state.vitrine.src.length / 1024 | 0) + ' KB). For a sharper capture, shoot at ' + slotTxt + ' and run: hero-studio vitrine --file <shot.png>');+} // 4. block until a HUMAN clicks Generate (a new _gen seq) const baseline = (state._gen && state._gen.seq) || 0;
public/index.html+188−11@@ -199,8 +199,8 @@ label.chk input { accent-color:var(--accent); width:14px; height:14px; cursor:po mask:url('adom-logo.svg') no-repeat left center / contain; } .h-title {- font-family:'Familjen Grotesk'; font-weight:700; font-size:6.4cqw; line-height:1.04;- letter-spacing:-.1cqw; color:#e6edf3; margin-bottom:1.8cqw;+ font-family:'Familjen Grotesk'; font-weight:700; font-size:8cqw; line-height:1.02;+ letter-spacing:-.14cqw; color:#e6edf3; margin-bottom:1.8cqw; } .h-title { max-width:30cqw; } .h-sub { font-size:2.5cqw; line-height:1.45; color:#e8eef4; margin-bottom:1.2cqw;@@ -224,6 +224,12 @@ label.chk input { accent-color:var(--accent); width:14px; height:14px; cursor:po .hero[data-layout=bleed-corner-tr] .window { right:-7cqw; top:-6cqw; width:56cqw; height:44cqw; border-radius:0 0 0 .9cqw; } .hero[data-layout=contained] .window { right:4.5cqw; top:6cqw; width:50cqw; height:33cqw; border-radius:.9cqw; } .hero[data-layout=mirror] .window { left:4.5cqw; top:6cqw; width:56cqw; height:64cqw; }+.hero[data-layout=showcase] .window { right:-4cqw; top:4cqw; width:66cqw; height:58cqw; border-radius:.9cqw 0 0 .9cqw; }+.hero[data-layout=showcase]::after {+ content:''; position:absolute; inset:0; z-index:2; pointer-events:none;+ background:linear-gradient(100deg, rgba(5,9,14,.88) 0%, rgba(5,9,14,.55) 30%, rgba(5,9,14,0) 55%);+}+.hero[data-layout=showcase] .textzone { z-index:3; width:30cqw; } .titlebar { flex:0 0 2.1cqw; background:#181f28; border-bottom:1px solid rgba(255,255,255,.06); display:flex; align-items:center; padding:0 .9cqw; gap:.6cqw;@@ -434,9 +440,42 @@ body[data-mode=component] .mode-app { display:none; } #verifybar .vchk .warn { color:#e8a13c; } #v-generate { margin-top:10px; font-size:14px; padding:10px 18px; } #v-generate:disabled { opacity:.45; cursor:not-allowed; }+/* new background families so heroes stop looking identical */+.hero[data-theme=aurora] {+ background:+ radial-gradient(90cqw 60cqw at 12% -10%, rgba(0,184,177,.28), transparent 60%),+ radial-gradient(70cqw 55cqw at 95% 20%, rgba(88,101,242,.22), transparent 65%),+ radial-gradient(80cqw 70cqw at 55% 115%, rgba(140,107,247,.20), transparent 60%),+ #0a0f16;+}+.hero[data-theme=mesh] {+ background:+ conic-gradient(from var(--ga) at 28% 30%, rgba(0,184,177,.16), transparent 32%),+ conic-gradient(from calc(var(--ga) + 120deg) at 75% 65%, rgba(88,166,255,.13), transparent 36%),+ conic-gradient(from calc(var(--ga) + 240deg) at 45% 90%, rgba(140,107,247,.12), transparent 30%),+ #0b1016;+}+.hero[data-theme=circuitry] { background:#080d13; }+#circuitcanvas { display:none; position:absolute; inset:0; width:100%; height:100%; z-index:0; }+.hero[data-theme=circuitry] #circuitcanvas { display:block; }+.sw-aurora { background:radial-gradient(8px 6px at 30% 20%, rgba(0,230,220,.8), transparent), radial-gradient(10px 8px at 70% 80%, rgba(140,107,247,.7), transparent), #0a0f16; }+.sw-mesh { background:conic-gradient(from 155deg at 40% 40%, rgba(0,184,177,.5), transparent 40%), #0b1016; }+.sw-circuitry { background:repeating-linear-gradient(45deg, rgba(0,184,177,.5) 0 1px, transparent 1px 5px), #080d13; }++/* verification walkthrough popout */+#tourpop { display:none; position:fixed; z-index:900; max-width:300px;+ background:var(--elevated); border:1px solid var(--accent); border-radius:10px;+ padding:12px 14px; box-shadow:0 12px 40px rgba(0,0,0,.6); font-size:13px; }+#tourpop .tp-title { font-family:'Familjen Grotesk'; font-weight:700; margin-bottom:6px; color:var(--accent-bright); }+#tourpop .tp-body { color:var(--text); line-height:1.45; margin-bottom:10px; }+#tourpop .tp-row { display:flex; gap:8px; }+.tour-glow { outline:3px solid var(--accent-bright) !important; outline-offset:3px; border-radius:4px;+ box-shadow:0 0 24px rgba(0,230,220,.35) !important; }+ /* render mode: ?render=1 strips all chrome for the server-side PNG shot */ body[data-render] header, body[data-render] #panel, body[data-render] #contexts,-body[data-render] #toasts, body[data-render] .guide, body[data-render] #verifybar { display:none !important; }+body[data-render] #toasts, body[data-render] .guide, body[data-render] #verifybar,+body[data-render] #tourpop { display:none !important; } /* Per Colby (2026-07-23): the image carries the title, the type pill (top right), and the full slug (bottom right); the pill and slug BAKE into the PNG. The page shows the image bare. Only the homepage marquee overlays@@ -551,6 +590,7 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh <button class="opt" data-v="bleed-corner-tr">Corner upper-right</button> <button class="opt" data-v="contained">Contained</button> <button class="opt" data-v="mirror">Mirror (text right)</button>+ <button class="opt" data-v="showcase" data-tip="Adom-USB style: the vitrine gets ~65% of the hero behind a gradient scrim">Showcase (65% vitrine)</button> </div> <div class="hint">The shot may run under the lower third; the overlay scrim covers it. That is the new rule.</div> @@ -574,7 +614,11 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh <div class="swatch sw-pill" data-v="pill" data-tip="Pill field: scattered capsule outlines from the brand tiling folder"></div> <div class="swatch sw-grid" data-v="grid" data-tip="Square grid: fine outlined-square field from the brand tiling folder"></div> <div class="swatch sw-grad" data-v="gradient" data-tip="Gradient: bold teal to blue to purple, for flagship launches"></div>+ <div class="swatch sw-aurora" data-v="aurora" data-tip="Aurora: layered soft radials"></div>+ <div class="swatch sw-mesh" data-v="mesh" data-tip="Mesh: conic color fields, angle follows the gradient slider"></div>+ <div class="swatch sw-circuitry" data-v="circuitry" data-tip="Circuitry: procedurally generated PCB traces, gently different every roll"></div> </div>+ <button class="btn" id="btn-reroll" style="display:none" data-tip="Re-roll the procedural pattern">Reroll circuitry pattern</button> <h3>Gradient angle</h3> <input type="range" id="rng-angle" min="0" max="360" step="5" value="155" data-tip="Rotate the base gradient so each hero feels unique while staying on-brand">@@ -679,6 +723,7 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh </div> </div> + <canvas id="circuitcanvas"></canvas> <img class="fullimport-img" id="fullimport-img" alt=""> <div id="mqoverlay"> <div class="mqo-badges" id="mqo-badges"></div>@@ -739,6 +784,14 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh </div> </main> +<div id="tourpop">+ <div class="tp-title" id="tp-title"></div>+ <div class="tp-body" id="tp-body"></div>+ <div class="tp-row">+ <button class="btn btn-primary" id="tp-ok">Looks good</button>+ <button class="btn" id="tp-skip">Skip tour</button>+ </div>+</div> <div id="tip"></div> <div id="toasts"></div> <div id="genmodal">@@ -842,6 +895,11 @@ function render() { $('v-type-label').classList.toggle('warn', !modeMatchesType); $('v-generate').disabled = !(confirm.title && confirm.subtitle && confirm.vitrine && confirm.type); + // walkthrough + procedural background+ showTourStep();+ if (state.theme === 'circuitry') requestAnimationFrame(drawCircuitry);+ $('btn-reroll').style.display = state.theme === 'circuitry' ? '' : 'none';+ // marquee overlay preview (ask 5): today's REAL homepage treatment, // composited over the hero so redundancy and balance are judgeable hero.classList.toggle('show-mq', !!state.marqueePreview && mode === 'app');@@ -849,11 +907,12 @@ function render() { $('mqo-badges').innerHTML = '<span class="badge badge-' + d.type + '">' + d.type + '</span>' + (d.score > 0 ? '<span class="ts-slide-score">' + TREND13 + ' ' + d.score + '</span>' : '') + (d.stars > 0 ? '<span class="ts-slide-stars">' + STAR12 + ' ' + d.stars + '</span>' : '');- // adom/wiki#80: hero tiles carry no overlay title/brief; the baked title- // is the single title, so the preview shows badges + owner only- $('mqo-title').textContent = '';- $('mqo-brief').textContent = '';- $('mqo-owner').textContent = d.owner + '/' + d.slug;+ // TODAY-REALITY on purpose (Colby): the homepage still overlays its own+ // title, so show the doubling instead of hiding it; the tag names the+ // planned fix so nobody thinks it is an accident.+ $('mqo-title').textContent = d.title;+ $('mqo-brief').textContent = firstSentence(d.brief || p.brief || '', 120);+ $('mqo-owner').textContent = 'wiki.adom.inc/' + d.owner + '/' + d.slug + ' (overlay as the homepage renders TODAY; adom/wiki#80 will drop this duplicated title)'; } $('chk-mqpreview').checked = !!state.marqueePreview; @@ -871,6 +930,8 @@ function render() { el.dataset.size = w + 'x' + h; el.textContent = 'shoot the vitrine screenshot at ' + w + 'x' + h + ' (aspect ' + (wb.width / wb.height).toFixed(2) + ') for this placement; click to copy';+ const cur = state.slotSize || {};+ if (cur.w !== w || cur.h !== h) send({ slotSize: { w: w, h: h } }); }); // vitrine content: real screenshot in the window, or full-hero import@@ -1123,7 +1184,7 @@ function handleGen(g) { } $('gen-close').addEventListener('click', e => { e.preventDefault(); $('genmodal').classList.remove('show'); }); $('genmodal').addEventListener('click', e => { if (e.target === $('genmodal')) $('genmodal').classList.remove('show'); });-$('btn-generate').addEventListener('click', () => {+function runGenerate() { const b = $('btn-generate'); b.disabled = true; b.textContent = 'Generating...'; toast('Rendering the hero on the server', 'info');@@ -1132,7 +1193,9 @@ $('btn-generate').addEventListener('click', () => { if (g.ok) handleGen(g); // success toast arrives via the live channel else toast('Generate failed: ' + (g.error || 'unknown') + (g.hint ? '. ' + g.hint : ''), 'error'); }).catch(() => { b.disabled = false; b.textContent = 'Generate hero PNG'; toast('Generate failed: network error', 'error'); });-});+}+$('btn-generate').addEventListener('click', runGenerate);+$('v-generate').addEventListener('click', runGenerate); $('btn-reset').addEventListener('click', () => { send({ mode:'app', layout:'bleed-bottom', theme:'midnight', titleMode:'image-only', gradAngle:155,@@ -1171,7 +1234,12 @@ $('btn-reset').addEventListener('click', () => { [['v-title','title'],['v-sub','subtitle'],['v-vitrine','vitrine'],['v-type','type']].forEach(([el, key]) => { $(el).addEventListener('change', e => send({ confirm: { [key]: e.target.checked } })); });-$('v-generate').addEventListener('click', () => $('btn-generate').click());+$('verifybar').addEventListener('click', e => {+ if (e.target.id !== 'v-generate' && !$('v-generate').disabled) return;+ if ($('v-generate').disabled && (e.target.id === 'v-generate' || e.target.closest('#v-generate'))) {+ handleToast({ text: 'Confirm all four checks first; Generate unlocks when every item is ratified', kind: 'warn', seq: Date.now() });+ }+}); $('vitrine-file').addEventListener('change', e => { const f = e.target.files && e.target.files[0]; if (!f) return;@@ -1195,6 +1263,115 @@ $('vitrine-pull').addEventListener('click', () => { .catch(() => {}); }); +/* verification walkthrough: spotlight each checklist item in place */+const TOUR_STEPS = [+ { key: 'title', el: 'el-title', title: 'This is your title',+ body: 'It bakes into the image at this exact size. Make sure the words are right and it reads at dock size.' },+ { key: 'subtitle', el: 'el-sub', title: 'This is your subtitle',+ body: 'The one-line value prop. Make sure you are happy with the wording and it is legible on this background.' },+ { key: 'vitrine', el: 'vitrine-img', fallbackEl: 'circuitcanvas', winSel: '.window', title: 'This is your vitrine',+ body: 'The app window. It should show YOUR app, not a generic chart. If it looks generic, tell your AI to capture a real screenshot at the slot size shown in the left panel.' },+ { key: 'type', el: 'pill-tr', title: 'This is your type pill',+ body: 'app / skill / component / bootstrap. AIs pick this wrong often; if it says the wrong thing, yell at yours now, before it bakes.' },+];+let tourIdx = -1;+function tourTarget(step) {+ if (step.winSel) { const w = hero.querySelector(step.winSel); if (w && w.offsetParent) return w; }+ const el = $(step.el);+ if (el && el.offsetParent) return el;+ return step.fallbackEl ? $(step.fallbackEl) : null;+}+function clearTourGlow() {+ document.querySelectorAll('.tour-glow').forEach(n => n.classList.remove('tour-glow'));+}+function showTourStep() {+ clearTourGlow();+ const confirm = state.confirm || {};+ const remaining = TOUR_STEPS.filter(st => !confirm[st.key]);+ if (!state.tour || !state.verify || remaining.length === 0) {+ $('tourpop').style.display = 'none';+ if (state.tour && remaining.length === 0) send({ tour: false });+ return;+ }+ const step = remaining[0];+ tourIdx = TOUR_STEPS.indexOf(step);+ const target = tourTarget(step);+ if (!target) { $('tourpop').style.display = 'none'; return; }+ target.classList.add('tour-glow');+ const r = target.getBoundingClientRect();+ const pop = $('tourpop');+ $('tp-title').textContent = step.title;+ $('tp-body').textContent = step.body;+ pop.style.display = 'block';+ const top = Math.max(12, Math.min(window.innerHeight - 180, r.bottom + 10));+ const left = Math.max(12, Math.min(window.innerWidth - 320, r.left));+ pop.style.top = top + 'px';+ pop.style.left = left + 'px';+}+$('tp-ok').addEventListener('click', () => {+ const step = TOUR_STEPS[tourIdx];+ if (step) send({ confirm: { [step.key]: true } });+});+$('tp-skip').addEventListener('click', () => { clearTourGlow(); $('tourpop').style.display = 'none'; send({ tour: false }); });++/* procedural circuitry background (seeded, gently different every roll) */+function mulberry32(a) {+ return function () {+ a |= 0; a = (a + 0x6D2B79F5) | 0;+ let t = Math.imul(a ^ (a >>> 15), 1 | a);+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;+ };+}+let lastCircuitKey = '';+function drawCircuitry() {+ const cv = $('circuitcanvas');+ const rect = hero.getBoundingClientRect();+ if (rect.width < 10) return;+ const key = state.circuitSeed + 'x' + Math.round(rect.width);+ if (key === lastCircuitKey) return;+ lastCircuitKey = key;+ const W = (cv.width = Math.round(rect.width * 2));+ const H = (cv.height = Math.round(rect.height * 2));+ const g = cv.getContext('2d');+ const rnd = mulberry32(state.circuitSeed || 1);+ g.clearRect(0, 0, W, H);+ const grid = Math.round(W / 46);+ const traces = 34;+ for (let i = 0; i < traces; i++) {+ const hue = rnd();+ g.strokeStyle = hue < 0.72 ? 'rgba(0,184,177,' + (0.05 + rnd() * 0.10).toFixed(3) + ')'+ : hue < 0.9 ? 'rgba(88,166,255,' + (0.05 + rnd() * 0.08).toFixed(3) + ')'+ : 'rgba(140,107,247,' + (0.05 + rnd() * 0.08).toFixed(3) + ')';+ g.lineWidth = rnd() < 0.85 ? 2 : 4;+ g.lineCap = 'round';+ let x = Math.round(rnd() * (W / grid)) * grid;+ let y = Math.round(rnd() * (H / grid)) * grid;+ g.beginPath();+ g.arc(x, y, g.lineWidth * 1.6, 0, Math.PI * 2);+ g.stroke();+ g.beginPath();+ g.moveTo(x, y);+ let dir = Math.floor(rnd() * 4); // 0 r, 1 d, 2 l, 3 u+ const segs = 3 + Math.floor(rnd() * 5);+ for (let sVar = 0; sVar < segs; sVar++) {+ const len = grid * (1 + Math.floor(rnd() * 5));+ // 45-degree bend half the time, PCB style+ if (rnd() < 0.5) dir = (dir + (rnd() < 0.5 ? 1 : 3)) % 4;+ const diag = rnd() < 0.4;+ const dx = dir === 0 ? len : dir === 2 ? -len : diag ? len * 0.7 : 0;+ const dy = dir === 1 ? len : dir === 3 ? -len : diag ? len * 0.7 : 0;+ x += dx; y += dy;+ g.lineTo(x, y);+ }+ g.stroke();+ g.beginPath();+ g.arc(x, y, g.lineWidth * 1.6, 0, Math.PI * 2);+ g.stroke();+ }+}+$('btn-reroll').addEventListener('click', () => { fetch('reroll', { method: 'POST' }).catch(() => {}); });+ /* owner brand controls */ $('brand-accent').addEventListener('input', e => send({ brand: { accent: e.target.value } })); $('brand-bg').addEventListener('input', e => send({ brand: { bg: e.target.value } }));
server.js+27−1@@ -23,6 +23,9 @@ let state = { confirm: { title: false, subtitle: false, vitrine: false, type: false }, vitrine: { src: '', full: false, cover: false }, // imported screenshot (data URL); full = whole-hero re-stamp; cover = crop-to-fill marqueePreview: false, // composite today's homepage-marquee overlay over the hero (preview only)+ tour: false, // verification walkthrough: spotlight title -> subtitle -> vitrine -> type+ slotSize: null, // measured vitrine slot in export px, posted back by the UI ({w,h})+ circuitSeed: Date.now() % 2147483647, // procedural circuitry theme seed; /reroll re-rolls gradAngle: 155, guides: true, };@@ -122,6 +125,29 @@ const server = http.createServer((req, res) => { res.end(JSON.stringify({ ok: true })); }); }+ if (p === '/vitrine-file' && req.method === 'POST') {+ // The CLI's explicit vitrine command: hero-studio vitrine --file <png>.+ // Local trusted tool, so a filesystem path is the contract.+ return readBody(req, (err, body) => {+ const finish = (code, payload) => { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(payload)); };+ if (err || !body || !body.path) return finish(400, { ok: false, error: 'path required' });+ let buf;+ try { buf = fs.readFileSync(body.path); } catch (e) { return finish(200, { ok: false, error: 'cannot read ' + body.path + ': ' + e.message }); }+ if (buf.length > 12 * 1024 * 1024) return finish(200, { ok: false, error: 'image too large (12 MB cap)' });+ const low = body.path.toLowerCase();+ const mime = low.endsWith('.webp') ? 'image/webp' : low.match(/\.jpe?g$/) ? 'image/jpeg' : 'image/png';+ state.vitrine = { src: 'data:' + mime + ';base64,' + buf.toString('base64'), full: !!body.full, cover: !!body.cover };+ logLine('info', 'vitrine <- ' + body.path + ' (' + Math.round(buf.length / 1024) + ' KB, cli)');+ broadcast('state', state);+ finish(200, { ok: true, bytes: buf.length, slot: state.slotSize });+ });+ }+ if (p === '/reroll' && req.method === 'POST') {+ state.circuitSeed = (state.circuitSeed * 48271 + Date.now()) % 2147483647;+ broadcast('state', state);+ res.writeHead(200, { 'Content-Type': 'application/json' });+ return res.end(JSON.stringify({ ok: true, seed: state.circuitSeed }));+ } if (p === '/vitrine-from-page' && req.method === 'POST') { const ref = url.searchParams.get('page') || state.page; const REG = process.env.ADOM_WIKI_REGISTRY || 'https://wiki.adom.inc';@@ -223,7 +249,7 @@ const server = http.createServer((req, res) => { return finish({ ok: false, error: errBuf.trim().split('\n').pop() || 'render exited ' + code, hint: 'set CHROME and PUPPETEER_CORE env vars for the server process' }); } // downscale to the hero standard 2000x1250 and stamp provenance metadata- const meta = JSON.stringify({ generator: 'hero-studio', version: '0.5.0', ts, state: { mode: state.mode, page: state.page, comp: state.comp, theme: state.theme, layout: state.layout, gradAngle: state.gradAngle, brand: state.brand, verify: state.verify, confirm: state.confirm, vitrine_used: !!(state.vitrine && state.vitrine.src), vitrine_full: !!(state.vitrine && state.vitrine.full) } });+ const meta = JSON.stringify({ generator: 'hero-studio', version: '0.5.0', ts, state: { mode: state.mode, page: state.page, comp: state.comp, theme: state.theme, layout: state.layout, gradAngle: state.gradAngle, brand: state.brand, verify: state.verify, confirm: state.confirm, vitrine_used: !!(state.vitrine && state.vitrine.src), vitrine_full: !!(state.vitrine && state.vitrine.full), circuitSeed: state.theme === 'circuitry' ? state.circuitSeed : undefined } }); const py = 'import sys\n' + 'from PIL import Image\n' + 'from PIL.PngImagePlugin import PngInfo\n' +