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.20.5
5 files changed
+110−10
data/pages.json+14@@ -1,4 +1,18 @@ [+ {+ "type": "component",+ "title": "Nametag Blank Large LED",+ "author": "",+ "score": 0,+ "stars": 0,+ "installs30d": 0,+ "brief": "A large LED nametag blank board for badge builds.",+ "tags": [],+ "updated_at": "2026-08-28T19:08:26.378Z",+ "owner": "kcknox",+ "slug": "ztest-hero52",+ "aiThread": "cli thread hero52 verify"+ }, { "type": "hardware", "title": "Ztest Batch B",
package.json+1−1@@ -2,7 +2,7 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.20.4",+ "version": "0.20.5", "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 showcase: 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, showcase, type match) and a Generate that unlocks only fully confirmed. Includes showcase 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.20.4",+ "version": "0.20.5", "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 showcase: 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, showcase, type match) and a Generate that unlocks only fully confirmed. Includes showcase 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+79−7@@ -25,7 +25,7 @@ const flags = {}; const pos = []; // Valueless flags MUST be declared: the old parser consumed the next token as // a value, so `--steal --headline "X"` silently dropped the headline (#42).-const BOOL_FLAGS = new Set(['steal', 'fresh-showcase', 'no-fallback', 'keep-alive']);+const BOOL_FLAGS = new Set(['steal', 'fresh-showcase', 'no-fallback', 'keep-alive', 'seed-only', 'no-local']); for (let i = 0; i < args.length; i++) { if (!args[i].startsWith('--')) { pos.push(args[i]); continue; } const name = args[i].slice(2);@@ -167,16 +167,56 @@ if (!state) { if (!state) { console.error('hero-studio server did not come up on :' + port); process.exit(1); } } +// 2-local. The LOCAL PACKAGE subject source (hero-studio#52): a first hero is+// a PUBLISH PREREQUISITE, so at hero time the page often does not exist on the+// wiki yet -- but everything the studio needs is sitting in the invoking+// directory. When ./package.json (or page.json) matches the posed slug, its+// fields seed the subject and its images become showcase candidates.+// Precedence: local package -> wiki page -> generic mock ("prefer what is+// about to be published over what is already published"); explicit --title/+// --brief flags outrank everything. --no-local opts out.+let localPkg = null;+let localImages = [];+if (!('no-local' in flags)) {+ for (const mf of ['package.json', 'page.json']) {+ try {+ const m = JSON.parse(fs.readFileSync(path.join(process.cwd(), mf), 'utf8'));+ if ((m.slug || m.name || '').replace(/^.*\//, '') === slug) { localPkg = m; break; }+ } catch {}+ }+ if (localPkg) {+ const seen = new Set();+ const addImgs = (dir, excl) => {+ try {+ for (const f of fs.readdirSync(path.join(process.cwd(), dir))) {+ if (!/\.(png|jpe?g|webp)$/i.test(f)) continue;+ if (excl && excl.test(f)) continue;+ const rel = path.join(dir, f);+ if (!seen.has(rel)) { seen.add(rel); localImages.push(path.join(process.cwd(), rel)); }+ }+ } catch {}+ };+ // UI/part screenshots first (the old hero.png is the thing being REPLACED,+ // so it goes last, never first)+ addImgs('screenshots', /^hero\./i);+ addImgs('docs');+ try { if (fs.existsSync(path.join(process.cwd(), slug + '.png'))) localImages.push(path.join(process.cwd(), slug + '.png')); } catch {}+ console.error('local package subject: ' + (localPkg.title || slug) + ' (' + (localImages.length || 'no') + ' local image candidate(s))');+ }+}+ // 2. make sure the studio knows this package, then pose it. // --type accepts a comma list for multi-type pages (hero-studio#19, // e.g. --type app,service): the first entry drives the mode, the full set // renders as a pill group.-const title = flags.title || slug.split('-').map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');-const typeList = String(flags.type || 'app').split(',').map((t) => t.trim()).filter(Boolean);+const localType = localPkg && (localPkg.type === 'hardware' ? 'component' : localPkg.type);+const title = flags.title || (localPkg && localPkg.title) || slug.split('-').map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');+const localBrief = localPkg && (localPkg.brief || localPkg.description);+const typeList = String(flags.type || localType || 'app').split(',').map((t) => t.trim()).filter(Boolean); await jf('api/pages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.fromEntries(Object.entries(- { owner, slug, title, type: typeList[0], types: typeList.length > 1 ? typeList : null, brief: flags.brief },+ { owner, slug, title, type: typeList[0], types: typeList.length > 1 ? typeList : null, brief: flags.brief || localBrief }, ).filter(([, v]) => v))), }); // Every type poses into the app composition (title + subtitle + showcase@@ -196,6 +236,9 @@ await jf('state', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mode: poseMode, page: ref,+ // #53: the legacy component lab's built-in sample part (a real JST MPN)+ // must never ride into a posed session; a posed page's comp slot is inert+ comp: { id: null }, // the gate presentation: verification chrome + a fresh checklist verify: true, tour: true, marqueePreview: true, guides: false, confirm: { title: false, subtitle: false, showcase: false, type: false },@@ -215,12 +258,41 @@ await jf('state', { }), }); -// 2a-live. re-pull title/brief/type from the wiki (#30/#33): the seeded row-// above is only a fallback; the LIVE page wins when it exists.+// 2a-live. re-pull title/brief/type from the wiki (#30/#33). For a page with+// NO local package, the live page wins as before. With a local package the+// PRECEDENCE INVERTS (#52): local fields are what is about to be published,+// so they are re-asserted over the live row after the pull (which still runs+// for the metadata the local manifest lacks: score, stars, author). try { const lv = await jf('refresh-page?page=' + ref, { method: 'POST' });- if (lv && lv.ok) console.error('live page: "' + lv.title + '"');+ if (lv && lv.ok) console.error('live page: "' + lv.title + '"' + (localPkg ? ' (local package fields win for this pose)' : '')); } catch {}+if (localPkg) {+ await jf('api/pages', {+ method: 'POST', headers: { 'Content-Type': 'application/json' },+ body: JSON.stringify(Object.fromEntries(Object.entries(+ { owner, slug, title, type: typeList[0], brief: flags.brief || localBrief },+ ).filter(([, v]) => v))),+ });+}+// 2a-showcase. Stage the best local image as the showcase (unless staged work+// is being preserved on a same-page re-pose, or the caller staged their own).+if (localImages.length && !(samePageRepose && !('fresh-showcase' in flags))) {+ const pick = localImages[0];+ const sc = await jf('showcase-file', {+ method: 'POST', headers: { 'Content-Type': 'application/json' },+ body: JSON.stringify({ path: pick }),+ });+ if (sc && sc.ok) {+ console.error('local showcase staged: ' + path.relative(process.cwd(), pick) + (localImages.length > 1 ? ' (alternatives: ' + localImages.slice(1, 4).map((f) => path.relative(process.cwd(), f)).join(', ') + '; swap with `hero-studio showcase --file <path>`)' : ''));+ }+}+// Test hook (#52 verification): everything above ran; skip the human gate.+if ('seed-only' in flags) {+ const st = await jf('state');+ console.log(JSON.stringify({ ok: true, seedOnly: true, page: st.page, resolved: st.resolved, showcaseBytes: (st.showcase && st.showcase.src || '').length, localImages: localImages.map((f) => path.relative(process.cwd(), f)) }));+ process.exit(0);+} // 2b. fresh change ledger for THIS review session (#20): everything the human // edits at the gate (page fields + pose deltas) accumulates from here and
public/js/app.js+15−1@@ -463,7 +463,14 @@ function toast(text, kind) { } /* wire controls: server owns the truth, SSE echoes back */-document.querySelectorAll('#grp-mode .opt').forEach(b => b.addEventListener('click', () => send({ mode: b.dataset.v })));+document.querySelectorAll('#grp-mode .opt').forEach(b => b.addEventListener('click', () => {+ // #53: entering the component design lab with no part re-seeds its built-in+ // sample EXPLICITLY (the lab's photos are of this part); a posed page never+ // carries it implicitly (pose nulls comp.id).+ const payload = { mode: b.dataset.v };+ if (b.dataset.v === 'component' && !(state.comp && state.comp.id)) payload.comp = { id: 'sm02b-srss-tb' };+ send(payload);+})); $('sel-comp').addEventListener('change', e => send({ comp: { id: e.target.value } })); document.querySelectorAll('#grp-imgsrc .opt').forEach(b => b.addEventListener('click', () => send({ comp: { imgSrc: b.dataset.v } }))); document.querySelectorAll('#grp-clayout .opt').forEach(b => b.addEventListener('click', () => send({ comp: { layout: b.dataset.v } })));@@ -827,6 +834,13 @@ function computeLint() { } } + // 4d. the built-in sample part composing a POSED page (#53): a real,+ // unrelated MPN in the slot where the page's own part belongs reads as a+ // bug to the person whose board it is, and shipped unlinted once.+ if (state.mode === 'component' && state.comp && state.comp.id === 'sm02b-srss-tb' && state.page) {+ items.push({ id: 'comp-demo-part', level: 'warn', el: null, msg: 'the composition shows the built-in SAMPLE part (SM02B-SRSS-TB, a JST connector), not ' + state.page + "'s part: set the real part (set {\"comp\":{\"id\":\"<slug>\"}}) or pose the app composition with a showcase image" });+ }+ // 4. title wrap count: a 4-line title is almost never intended. const tEl = $('el-title'); if (vis(tEl)) {