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.11.2
6 files changed
+59−12
SKILL.md+7−1@@ -139,12 +139,18 @@ 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 vitrine --file shot.png [--full] [--cover] [--chrome none|fake] hero-studio state # read everything, incl. slotSize hero-studio toast "trying the showcase layout" --kind info hero-studio reroll # new procedural circuitry pattern ``` +The vitrine's FAKE titlebar is auto-off whenever an imported screenshot is+present: a real window shot carries its own chrome, and stacking the fake bar+above it reads as fake (hero-studio#11). The CSS mock keeps the fake bar.+Override either way with `--chrome none|fake`, the "Draw the fake window+titlebar" checkbox, or `set '{"vitrine":{"chrome":"none"}}'`.+ 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;
drive.mjs+6−2@@ -3,7 +3,7 @@ // The open studio follows within a second (SSE + poll), so the human watches // the pose change as commands land. //-// hero-studio vitrine --file <png> [--full] [--cover] load a real screenshot+// hero-studio vitrine --file <png> [--full] [--cover] [--chrome none|fake] load a real screenshot // hero-studio set '<json>' | key=value [...] patch posing state // hero-studio state print current state // hero-studio toast "<text>" [--kind info|success|warn] narrate to the human@@ -29,12 +29,16 @@ const die = (msg) => { console.error(msg); process.exit(1); }; try { if (verb === 'vitrine') { const fi = args.indexOf('--file');- if (fi < 0 || !args[fi + 1]) die('usage: hero-studio vitrine --file <png> [--full] [--cover]');+ if (fi < 0 || !args[fi + 1]) die('usage: hero-studio vitrine --file <png> [--full] [--cover] [--chrome none|fake]'); const file = path.resolve(args[fi + 1]);+ const ci = args.indexOf('--chrome');+ const chrome = ci >= 0 ? args[ci + 1] : '';+ if (chrome && chrome !== 'none' && chrome !== 'fake') die('--chrome takes none|fake (default: auto, which is none for imported screenshots)'); const out = await jf('vitrine-file', { path: file, full: args.includes('--full'), cover: args.includes('--cover'),+ chrome, }); if (!out.ok) die('ERROR: ' + out.error + (out.error && out.error.includes('cannot read') ? '\n hint: the studio server reads the path locally; pass an absolute path on this machine' : '')); const slot = out.slot ? ' (slot wants ' + out.slot.w + 'x' + out.slot.h + ')' : '';
package.json+1−1@@ -2,7 +2,7 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.11.1",+ "version": "0.11.2", "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.11.1",+ "version": "0.11.2", "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": [
public/index.html+41−4@@ -474,6 +474,8 @@ body[data-mode=component] .mode-app { display:none; } #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-above { position:relative !important; z-index:901 !important; }+.hero.no-chrome .window .titlebar { display:none; } .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; } @@ -606,6 +608,7 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh <input type="file" id="vitrine-file" accept="image/*"> <label class="chk"><input type="checkbox" id="vitrine-full"> Use the imported image as the FULL hero</label> <label class="chk"><input type="checkbox" id="vitrine-cover"> Crop to fill (cover) instead of letterboxing a mismatched shot</label>+ <label class="chk" data-tip="Real window screenshots already have a real titlebar; stacking the fake one above it reads as fake. Auto-off when an import is present; the CSS mock keeps it."><input type="checkbox" id="vitrine-chrome"> Draw the fake window titlebar</label> <label class="chk"><input type="checkbox" id="chk-mqpreview"> Show the homepage-marquee overlay over the hero (judge composition and title redundancy; never exported)</label> <button class="btn" id="vitrine-pull">Pull real screenshot from the page</button> <button class="btn" id="vitrine-clear">Clear import</button>@@ -974,6 +977,12 @@ function render() { hero.classList.toggle('vitrine-cover', !!vit.cover); $('vitrine-cover').checked = !!vit.cover; hero.classList.toggle('has-vitrine', !!vit.src && !vit.full);+ // fake titlebar: real window screenshots carry their own chrome, so a+ // second bar above it reads as an obvious fake (#11). Auto = off whenever+ // an import is present; the CSS mock keeps it. Explicit choice wins.+ const chromeMode = vit.chrome || ((vit.src && !vit.full) ? 'none' : 'fake');+ hero.classList.toggle('no-chrome', chromeMode === 'none');+ $('vitrine-chrome').checked = chromeMode === 'fake'; hero.classList.toggle('fullimport', !!vit.src && !!vit.full); $('vitrine-img').src = (!vit.full && vit.src) || ''; $('fullimport-img').src = (vit.full && vit.src) || '';@@ -1285,6 +1294,7 @@ $('vitrine-file').addEventListener('change', e => { }); $('vitrine-full').addEventListener('change', e => send({ vitrine: { full: e.target.checked } })); $('vitrine-cover').addEventListener('change', e => send({ vitrine: { cover: e.target.checked } }));+$('vitrine-chrome').addEventListener('change', e => send({ vitrine: { chrome: e.target.checked ? 'fake' : 'none' } })); $('chk-mqpreview').addEventListener('change', e => send({ marqueePreview: e.target.checked })); $('slot-size').addEventListener('click', () => { const sz = $('slot-size').dataset.size;@@ -1339,11 +1349,38 @@ function showTourStep() { $('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';+ placeTourPop(pop, target, r); }+/* The card must NEVER cover the element it asks the human to verify: a+ coach-mark over its own subject collects blind confirmations (#10).+ Try below / above / right / left; take the first spot that fits the+ viewport and does not intersect the target. If no spot exists (target+ nearly fills the window), lift the target above the card instead. */+function placeTourPop(pop, target, r) {+ const W = window.innerWidth, H = window.innerHeight, M = 12, GAP = 14;+ const pw = pop.offsetWidth, ph = pop.offsetHeight;+ const candidates = [+ { top: r.bottom + GAP, left: r.left },+ { top: r.top - ph - GAP, left: r.left },+ { top: r.top, left: r.right + GAP },+ { top: r.top, left: r.left - pw - GAP },+ ];+ const fits = c => c.top >= M && c.left >= M && c.top + ph <= H - M && c.left + pw <= W - M;+ const hides = c => !(c.left + pw <= r.left || c.left >= r.right || c.top + ph <= r.top || c.top >= r.bottom);+ let spot = candidates.find(c => fits(c) && !hides(c));+ if (!spot) {+ // clamp into the viewport on the roomier vertical side of the target+ const left = Math.max(M, Math.min(W - pw - M, r.left));+ spot = (H - r.bottom >= r.top)+ ? { top: Math.max(M, Math.min(H - ph - M, r.bottom + GAP)), left }+ : { top: Math.max(M, r.top - ph - GAP), left };+ }+ document.querySelectorAll('.tour-above').forEach(n => n.classList.remove('tour-above'));+ if (hides(spot)) target.classList.add('tour-above');+ pop.style.top = spot.top + 'px';+ pop.style.left = spot.left + 'px';+}+window.addEventListener('resize', () => { if (state.tour && state.verify) showTourStep(); }); $('tp-ok').addEventListener('click', () => { const step = TOUR_STEPS[tourIdx]; if (step) send({ confirm: { [step.key]: true } });
server.js+3−3@@ -24,7 +24,7 @@ let state = { brand: { accent: '', bg: '' }, // non-adom owner palette; empty = studio default verify: false, // the pose gate turns this on: verification chrome + checklist 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+ vitrine: { src: '', full: false, cover: false, chrome: '' }, // imported screenshot (data URL); full = whole-hero re-stamp; cover = crop-to-fill; chrome '' = auto (fake bar for the mock, none for imports), 'fake'|'none' = explicit 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})@@ -141,7 +141,7 @@ const server = http.createServer((req, res) => { 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 };+ state.vitrine = { src: 'data:' + mime + ';base64,' + buf.toString('base64'), full: !!body.full, cover: !!body.cover, chrome: body.chrome || (state.vitrine && state.vitrine.chrome) || '' }; 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 });@@ -185,7 +185,7 @@ const server = http.createServer((req, res) => { const buf = Buffer.from(await ir.arrayBuffer()); if (buf.length > 8 * 1024 * 1024) return finish(200, { ok: false, error: 'image too large' }); const mime = pick.endsWith('.webp') ? 'image/webp' : pick.match(/\.jpe?g$/) ? 'image/jpeg' : 'image/png';- state.vitrine = { src: 'data:' + mime + ';base64,' + buf.toString('base64'), full: false };+ state.vitrine = { src: 'data:' + mime + ';base64,' + buf.toString('base64'), full: false, chrome: (state.vitrine && state.vitrine.chrome) || '' }; logLine('info', 'vitrine <- ' + ref + '/' + pick + ' (' + Math.round(buf.length / 1024) + ' KB)'); broadcast('state', state); finish(200, { ok: true, picked: pick, bytes: buf.length });