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.19.9
7 files changed
+97−10
SKILL.md+22−2@@ -15,8 +15,17 @@ The studio opens as a bottom-dock WIZARD: seven steps (Package, Showcase image, Placement, Background, Headline and text, Finishing, Review and generate), one decision at a time, the hero always live above, a dashed ring marking what the current step edits. The old full control panel is one click-away (header "Classic") and keeps the component-mode extras. Component pages-ride the same wizard for now. The title ALWAYS bakes (it IS the message) and+away (header "Classic") and keeps the component-mode extras. Hardware+pages (--type hardware or its legacy spelling component) ride the same+wizard and the same composition: title, subtitle, and YOUR PART's render in+the showcase slot, floating bare by default (no window frame, bar, or+shadow; a fake app window around a 3D board render reads as a screenshot+of software). Load the render with `hero-studio vitrine --file+<render.png>` (a chip-thumbnailer `*-icon.png` is alpha-keyed and floats+cleanly); `--chrome none|fake` overrides the bare default. The studio's+"component" MODE in Classic is a sample-parts design lab (built-in photos+of a JST connector) that ignores the posed page and the vitrine; posing+never enters it. The title ALWAYS bakes (it IS the message) and its ink top-left is pixel-anchored at any font size (fixed leading + cap trim; the leading control is gone, the wire field remains inert). @@ -46,6 +55,17 @@ scales by gating the STYLE, not every part: look. parts.json rows: {ref, title, kind?, vitrine?: <render path>, headline?, subtitle?}. +EXISTING pages (a fleet that already lives on the wiki, e.g. 136 of the+137 basic parts) are not republished: push the PNG into the page repo and+point the page at it, `adom-wiki repo push <ref> --files docs/hero.png -m+"hero" && adom-wiki page hero <ref> --image docs/hero.png` (repo push+30/min, the hero call is a page edit at 10/min). The reference script does+this automatically when its probe finds the page (`--existing` / `--new`+force either path). Note that `page hero --image` accepts an unstamped+PNG today: the wiki's hero gate checks provenance at pkg publish, not on+this call, so the attestation a batch hero carries is your record, not an+enforced one; keep the human in the loop anyway.+ Generation is local (no wiki calls, no rate limits). Publishing is capped at 10 logical publishes/min PER ACCOUNT (sliding minute window; wiki#131 has the full table: repo push 30/min, page edit 10/min; IP adds nothing
docs/bulk-publish-reference.mjs+51−2@@ -15,12 +15,20 @@ Usage: node bulk-publish-reference.mjs --parts ./parts --org adom \ --style <saved-style> --human colby --quote "match the family style" \- [--pace 4] [--dry-run] [--studio-port 8846]+ [--pace 7] [--existing | --new] [--dry-run] [--studio-port 8846] Before running: pose ONE representative part, let a human Generate, then `hero-studio style save <name>`. That approval gates every hero this script produces (it rides each PNG's provenance). + New vs existing pages: a part whose page does NOT exist yet is scaffolded+ and `pkg publish`ed. A part whose page ALREADY exists (Arav's basic-parts+ fleet: 136 of 137) is not republished; its hero is pushed into the page+ repo (repo push, 30/min) and applied with `page hero --image` (a page+ edit, 10/min). Both paths get the same gated, provenance-stamped PNG.+ Pass --existing to force the update path, --new to force publish; the+ default probes each page once (`adom-wiki page stats`, a read).+ Rate limits (exact, from the wiki team on adom/wiki#131): sliding 60-second window PER ACCOUNT (IP does not add a cap for authed calls). pkg publish and page create are 10/min each, and a new-page publish@@ -44,6 +52,7 @@ const QUOTE = arg('--quote'); const PACE = Number(arg('--pace', 7)); const PORT = arg('--studio-port', process.env.HERO_STUDIO_PORT || '8846'); const DRY = has('--dry-run');+const FORCE_MODE = has('--existing') ? 'existing' : has('--new') ? 'new' : ''; if (!PARTS || !STYLE || !HUMAN || !QUOTE) { console.error('usage: bulk-publish-reference.mjs --parts <dir> --style <name> --human <who> --quote "<their words>" [--org adom] [--pace 4] [--dry-run]'); process.exit(2);@@ -97,7 +106,47 @@ for (const p of partsManifest) { if (state.done[p._dir] || state.failed[p.ref]) continue; const dir = path.join(PARTS, p._dir); const meta = JSON.parse(fs.readFileSync(path.join(dir, 'part.json'), 'utf8'));- if (DRY) { console.error(`[dry] would publish ${p.ref}`); continue; }+ if (DRY) { console.error(`[dry] would ${FORCE_MODE || 'probe then publish/update'} ${p.ref}`); continue; }+ // existing page: push the hero into its repo and point the page at it.+ // No scaffold, no publish; the page keeps its README, files, and history.+ let exists = FORCE_MODE === 'existing';+ if (!FORCE_MODE) {+ const probe = await run('adom-wiki', ['page', 'stats', p.ref, '--json'], dir);+ exists = probe.ok && /"status":\s*"ok"/.test(probe.out);+ }+ if (exists) {+ if (!fs.existsSync(path.join(dir, 'docs', 'hero.png'))) {+ console.error(` ${p.ref}: exists but no docs/hero.png was generated for it; skipping`);+ state.failed[p.ref] = 'no-hero';+ save();+ continue;+ }+ let applied = false;+ for (let attempt = 0, backoff = 30; attempt < 4 && !applied; attempt++) {+ const push = await run('adom-wiki', ['repo', 'push', p.ref, '--files', 'docs/hero.png', '-m', 'hero: gated style ' + STYLE], dir);+ const hero = push.ok ? await run('adom-wiki', ['page', 'hero', p.ref, '--image', 'docs/hero.png'], dir) : push;+ if (push.ok && hero.ok && !/"status":\s*"error"/.test(hero.out)) {+ applied = true;+ if (meta.source) await run('adom-wiki', ['page', 'edit', p.ref, '--source', meta.source], dir);+ state.done[p._dir] = { at: new Date().toISOString(), mode: 'existing' };+ delete state.failed[p.ref];+ console.error(` ${p.ref}: hero updated (existing page)`);+ } else if (/429|rate/i.test(push.out + hero.out)) {+ const ra = (push.out + hero.out).match(/retry[- ]after[:\s]+(\d+)/i);+ const wait = ra ? Math.max(Number(ra[1]), 5) : backoff;+ console.error(` ${p.ref}: rate-limited; waiting ${wait}s (${ra ? 'server Retry-After' : 'backoff'}, attempt ${attempt + 1})`);+ await sleep(wait);+ backoff *= 2;+ } else {+ console.error(` ${p.ref}: FAILED: ${(push.out + hero.out).split('\n').find((l) => /error/i.test(l)) || 'unknown'}`);+ state.failed[p.ref] = 'update';+ break;+ }+ }+ save();+ await sleep(PACE);+ continue;+ } // scaffold in place if package.json is absent if (!fs.existsSync(path.join(dir, 'package.json'))) { const pkg = {
package.json+1−1@@ -2,7 +2,7 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.19.8",+ "version": "0.19.9", "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.19.8",+ "version": "0.19.9", "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+9−1@@ -179,7 +179,15 @@ await jf('api/pages', { { owner, slug, title, type: typeList[0], types: typeList.length > 1 ? typeList : null, brief: flags.brief }, ).filter(([, v]) => v))), });-const poseMode = (typeList[0] === 'component') ? 'component' : 'app';+// Every type poses into the app composition (title + subtitle + showcase+// window): that is the surface the wizard drives and the only one that honors+// an imported vitrine. The studio's legacy 'component' mode is a sample-parts+// design lab (built-in photos of a JST connector, its own text zone) that+// ignores the posed page AND the vitrine, so posing --type component into it+// produced heroes of a completely different part (Arav, basic-parts rollout).+// It stays reachable from Classic for what it is. Hardware pages get a bare,+// frameless showcase by default (app.js chrome auto).+const poseMode = 'app'; const samePageRepose = !!(state && state.page === ref && state.vitrine && state.vitrine.src); if (samePageRepose && !('fresh-vitrine' in flags)) { console.error('note: re-posing ' + ref + ' with staged vitrine content preserved (' + Math.round(state.vitrine.src.length / 1024) + 'kB' + (state.vitrine.full ? ', full-hero import' : '') + '); pass --fresh-vitrine to start clean');
public/js/app.js+8−1@@ -206,7 +206,11 @@ function render() { // 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');+ // Hardware pages: an imported render is a physical part, not a program,+ // so auto = bare (no frame, bar, or shadow at all); a fake app window+ // around a 3D board render reads as a screenshot of software.+ const isHardware = canonType(d.type) === 'component';+ const chromeMode = vit.chrome || ((vit.src && !vit.full) ? (isHardware ? 'bare' : 'none') : 'fake'); hero.classList.toggle('no-chrome', chromeMode === 'none'); hero.classList.toggle('bare-chrome', chromeMode === 'bare'); $('vitrine-chrome').checked = chromeMode === 'fake';@@ -627,6 +631,9 @@ function clearTourGlow() { } function showTourStep() { clearTourGlow();+ // the walkthrough is classic-only: in wizard mode the review step owns+ // spotlighting, and two rings on the stage read as one ring too many+ if (document.body.classList.contains('wizard')) { $('tourpop').style.display = 'none'; return; } const confirm = state.confirm || {}; const flags = state.flags || {}; // a flagged step is reviewed (verdict: needs fixing), so the tour moves on;
public/js/wizard.js+5−2@@ -126,7 +126,9 @@ const WIZ_STEPS = [ }, { id: 'showcase', title: 'Showcase image',- hint: 'The screenshot shown in your hero. Use your real app, not a mock.',+ hint: () => (canonType(wp().type) === 'component'+ ? 'The picture of your part. Use its real render or photo; it floats bare, no app window.'+ : 'The screenshot shown in your hero. Use your real app, not a mock.'), summary: () => (state.vitrine && state.vitrine.src ? (state.vitrine.full ? 'Full-hero import' : 'Screenshot') : 'Drawn mock'), options: () => [ { v: 'page', label: 'From the wiki page', sub: 'The screenshot already on the page' },@@ -567,7 +569,7 @@ function wizardRender() { wizRenderRail(); $('wintro-ref').textContent = state.page; $('wd-title').textContent = (wizStepIndex(wizStep) + 1) + '. ' + s.title;- $('wd-hint').textContent = s.hint;+ $('wd-hint').textContent = typeof s.hint === 'function' ? s.hint() : s.hint; const next = WIZ_STEPS[wizStepIndex(wizStep) + 1]; $('wbtn-next').textContent = next ? 'Next: ' + next.title : 'Done'; $('wbtn-next').style.display = next && !s.review ? '' : 'none';@@ -649,6 +651,7 @@ $('wiz-classic').addEventListener('click', () => { document.body.classList.toggle('classic', toClassic); $('wiz-classic').textContent = toClassic ? 'Wizard' : 'Classic'; if (!toClassic) wizardRender();+ showTourStep(); // hand the spotlight to whichever mode we just entered }); /* boot: wizard is the default surface; a pose (verify) starts at Showcase