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.10.0
6 files changed
+72−10
SKILL.md+4−1@@ -155,7 +155,10 @@ State fields: `page` (owner/slug), `layout`, `theme`, `gradAngle`, `titleMode`, `confirm.title/subtitle/vitrine/type` (the checklist), `vitrine.src/full/cover` (imported content; src is a data URL; cover crops instead of letterboxing), `marqueePreview` (composite today's homepage-marquee overlay for judging-composition and title redundancy; never exported), `guides`.+composition and title redundancy; never exported), `fontSize.title/sub/desc`+(cqw; author-adjustable image-zone type, defaults tuned for dock legibility),+`approved` ({page, file, ts}: the persistent gate-closed record, set on+Generate and shown as the green Approved banner), `guides`. ## Showing the studio manually
package.json+1−1@@ -2,7 +2,7 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.9.0",+ "version": "0.10.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.9.0",+ "version": "0.10.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+3−2@@ -1,5 +1,5 @@ #!/usr/bin/env node-// hero-studio pose <owner/slug> [--title T] [--brief B] [--type app|skill] [--timeout SECS]+// hero-studio pose <owner/slug> [--title T] [--brief B] [--type app|skill|component] [--timeout SECS] // // The human gate. Launches Hero Studio in front of the human, posed for the // given package, then BLOCKS until a human clicks Generate in the UI. Prints@@ -66,10 +66,11 @@ await jf('api/pages', { { owner, slug, title, type: flags.type || 'app', brief: flags.brief }, ).filter(([, v]) => v))), });+const poseMode = (flags.type === 'component') ? 'component' : 'app'; await jf('state', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({- mode: 'app', page: ref,+ mode: poseMode, page: ref, // the gate presentation: verification chrome + a fresh checklist verify: true, tour: true, marqueePreview: true, confirm: { title: false, subtitle: false, vitrine: false, type: false },
public/index.html+54−4@@ -199,16 +199,16 @@ 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:8cqw; line-height:1.02;+ font-family:'Familjen Grotesk'; font-weight:700; font-size:var(--fs-title, 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;+.h-sub { font-size:var(--fs-sub, 2.5cqw); line-height:1.45; color:#e8eef4; margin-bottom:1.2cqw; max-width:30cqw; text-shadow:0 1px 6px rgba(0,0,0,.45); } .hero[data-theme=gradient] .h-sub { color:#f6fafd; } .h-sub .accent { background:var(--grad); -webkit-background-clip:text; background-clip:text; color:transparent; font-weight:500; } .h-sub b { color:#e6edf3; font-weight:500; }-.h-desc { font-size:1.35cqw; line-height:1.5; color:#8b949e; max-width:30cqw; }+.h-desc { font-size:var(--fs-desc, 1.35cqw); line-height:1.5; color:#8b949e; max-width:30cqw; } /* ---------- the app-window mock (the "screenshot") ---------- */ .window {@@ -434,6 +434,11 @@ body[data-mode=component] .mode-app { display:none; } .brandrow label { font-size:12px; color:var(--text-2); width:84px; } .brandrow input[type=color] { width:44px; height:26px; border:1px solid var(--border); border-radius:6px; background:var(--surface); padding:1px; cursor:pointer; } +#approvedbar { display:none; margin:0 0 14px; border:1px solid #2ea06b; background:rgba(46,160,107,.09);+ border-radius:10px; padding:14px 16px; }+#approvedbar .ab-head { font-family:'Familjen Grotesk'; font-weight:700; color:#4ade80; margin-bottom:6px; }+#approvedbar .ab-body { font-size:13px; color:var(--text); line-height:1.5; margin-bottom:10px; }+#genmodal .gen-approved { font-family:'Familjen Grotesk'; font-weight:700; color:#4ade80; margin-bottom:6px; } #verifybar { display:none; margin:0 0 14px; border:1px solid var(--accent); background:rgba(0,184,176,.07); border-radius:10px; padding:14px 16px; } #verifybar .vhead { font-size:13px; color:var(--text); margin-bottom:10px; line-height:1.5; } #verifybar .vchk { display:block; margin:6px 0; }@@ -475,6 +480,7 @@ body[data-mode=component] .mode-app { display:none; } /* 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,+body[data-render] #approvedbar, 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@@ -638,6 +644,10 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh <label class="chk"><input type="checkbox" id="chk-title"> Title (big headline)</label> <label class="chk"><input type="checkbox" id="chk-subtitle"> Value-prop subtitle</label> <label class="chk"><input type="checkbox" id="chk-description"> Detail line</label>+ <div class="brandrow"><label>Title size</label><input type="range" id="fs-title" min="5" max="11" step="0.1" value="8"><span class="hint" id="fs-title-val">8</span></div>+ <div class="brandrow"><label>Subtitle size</label><input type="range" id="fs-sub" min="1.8" max="3.5" step="0.1" value="2.5"><span class="hint" id="fs-sub-val">2.5</span></div>+ <div class="brandrow"><label>Detail size</label><input type="range" id="fs-desc" min="1.0" max="2.0" step="0.05" value="1.35"><span class="hint" id="fs-desc-val">1.35</span></div>+ <div class="hint">The defaults are tuned to read at dock size and set you up for success. You are the author: adjust if your name or copy needs it, thoughtfully.</div> <h3>Title placement</h3> <div class="optgrid" id="grp-titlemode">@@ -659,6 +669,11 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh </div> <div id="stage">+ <div id="approvedbar">+ <div class="ab-head">Hero approved</div>+ <div class="ab-body" id="ab-body"></div>+ <button class="btn" id="ab-again">Generate a new version</button>+ </div> <div id="verifybar"> <div class="vhead"><b>Hero verification.</b> The registry requires a human to approve this hero; an AI cannot do it for you. Check each item below, adjust anything that reads wrong (left panel), then Generate. Only this studio's export carries the provenance stamp the gate accepts.</div> <label class="chk vchk"><input type="checkbox" id="v-title"> Title reads right</label>@@ -796,6 +811,7 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh <div id="toasts"></div> <div id="genmodal"> <div class="genbox">+ <div class="gen-approved">Hero approved</div> <img id="gen-img" alt="Generated hero"> <div class="genrow"> <div class="hint" id="gen-path"></div>@@ -885,7 +901,6 @@ function render() { // verification chrome (the pose gate sets state.verify); per-item confirms const confirm = state.confirm || {};- $('verifybar').style.display = state.verify ? 'block' : 'none'; // stylesheet default is none; '' would fall back to it $('v-title').checked = !!confirm.title; $('v-sub').checked = !!confirm.subtitle; $('v-vitrine').checked = !!confirm.vitrine;@@ -895,6 +910,26 @@ function render() { $('v-type-label').classList.toggle('warn', !modeMatchesType); $('v-generate').disabled = !(confirm.title && confirm.subtitle && confirm.vitrine && confirm.type); + // author-adjustable image-zone type (hero-studio#4); cqw so preview == render+ const fs = state.fontSize || {};+ hero.style.setProperty('--fs-title', (fs.title || 8) + 'cqw');+ hero.style.setProperty('--fs-sub', (fs.sub || 2.5) + 'cqw');+ hero.style.setProperty('--fs-desc', (fs.desc || 1.35) + 'cqw');+ for (const [id, val, dflt] of [['fs-title', fs.title, 8], ['fs-sub', fs.sub, 2.5], ['fs-desc', fs.desc, 1.35]]) {+ $(id).value = val || dflt;+ $(id + '-val').textContent = (val || dflt);+ }++ // the gate visibly CLOSES (hero-studio#5): approval is persistent state+ const ap = state.approved;+ const approvedHere = !!(ap && ap.page === state.page);+ $('approvedbar').style.display = approvedHere ? 'block' : 'none';+ if (approvedHere) {+ $('ab-body').textContent = ap.file + ', generated ' + new Date(ap.ts).toLocaleString() ++ '. The gate is satisfied; the PNG carries the provenance stamp. Apply it with: adom-wiki page hero ' + state.page + ' --image <repo path>';+ }+ $('verifybar').style.display = state.verify && !approvedHere ? 'block' : 'none';+ // walkthrough + procedural background showTourStep(); if (state.theme === 'circuitry') requestAnimationFrame(drawCircuitry);@@ -1201,6 +1236,7 @@ $('btn-reset').addEventListener('click', () => { send({ mode:'app', layout:'bleed-bottom', theme:'midnight', titleMode:'image-only', gradAngle:155, comp:{ id:'sm02b-srss-tb', imgSrc:'photo', layout:'photo-right', mpn:'under-title' }, show:{ logo:true, title:true, subtitle:true, description:false },+ fontSize:{ title:8, sub:2.5, desc:1.35 }, overlay:{ pill:true, scores:true, author:true, slug:true }, guides:true }); toast('Reset to the proposed default: overlay title, image keeps value-prop', 'success'); });@@ -1372,6 +1408,20 @@ function drawCircuitry() { } $('btn-reroll').addEventListener('click', () => { fetch('reroll', { method: 'POST' }).catch(() => {}); }); +/* image-zone type sizes: instant local feedback, debounced send */+let fsTimer = null;+for (const [id, key] of [['fs-title', 'title'], ['fs-sub', 'sub'], ['fs-desc', 'desc']]) {+ $(id).addEventListener('input', e => {+ const v = Number(e.target.value);+ $(id + '-val').textContent = v;+ hero.style.setProperty('--' + id.replace('fs-', 'fs-'), v + 'cqw');+ hero.style.setProperty('--fs-' + key.replace('sub', 'sub').replace('desc', 'desc'), v + 'cqw');+ clearTimeout(fsTimer);+ fsTimer = setTimeout(() => send({ fontSize: { [key]: v } }), 150);+ });+}+$('ab-again').addEventListener('click', runGenerate);+ /* 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+9−1@@ -7,6 +7,9 @@ const { spawn, execFile } = require('child_process'); const PORT = 8846; const ROOT = path.join(__dirname, 'public'); const PAGES = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'pages.json'), 'utf8'));+// The provenance stamp reads the REAL version so it stays truthful across+// releases (hero-studio#2: a hardcoded '0.5.0' cost Arav a wrong-release chase).+const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')).version; const COMPONENTS = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'components.json'), 'utf8')); let state = {@@ -26,6 +29,8 @@ let state = { 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+ fontSize: { title: 8, sub: 2.5, desc: 1.35 }, // cqw; author-adjustable, defaults tuned for dock legibility+ approved: null, // { page, file, ts } set on Generate: the persistent gate-closed record gradAngle: 155, guides: true, };@@ -249,7 +254,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), circuitSeed: state.theme === 'circuitry' ? state.circuitSeed : undefined } });+ const meta = JSON.stringify({ generator: 'hero-studio', version: PKG_VERSION, ts, state: { mode: state.mode, page: state.page, comp: state.comp, theme: state.theme, layout: state.layout, gradAngle: state.gradAngle, brand: state.brand, fontSize: state.fontSize, 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' +@@ -269,6 +274,9 @@ const server = http.createServer((req, res) => { try { fs.copyFileSync(outFile, path.join(outDir, 'latest.png')); } catch {} genSeq += 1; lastGen = { seq: genSeq, ok: true, file, url: 'generated/' + file, latest: 'generated/latest.png', ts, slug };+ // hero-studio#5: approval must be a persistent state, not a toast.+ state.approved = { page: state.page, file, ts };+ broadcast('state', state); logLine('info', 'generated ' + file); broadcast('generated', lastGen); broadcast('toast', { text: 'Hero generated: ' + file, kind: 'success', seq: ++toastSeq });