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.7.0
6 files changed
+59−6
SKILL.md+5−2@@ -58,8 +58,11 @@ without the checklist is lab mode, which is what a plain open gets. ## The vitrine, and putting real content in it The floating app window in the hero is called the VITRINE (one word,-everywhere). By default it renders a CSS mock; the Vitrine content section-imports a real screenshot instead, so apps show their real UI.+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. 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
package.json+2−2@@ -2,8 +2,8 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.6.1",- "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 renders a CSS mock, a real imported screenshot, 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.",+ "version": "0.7.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": [ "hero",
page.json+2−2@@ -2,8 +2,8 @@ "slug": "hero-studio", "title": "Hero Studio", "type": "app",- "version": "0.6.1",- "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 renders a CSS mock, a real imported screenshot, 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.",+ "version": "0.7.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": [ "hero",
pose.mjs+4@@ -68,6 +68,10 @@ await jf('state', { }), }); +// 2b. best-effort: pre-load a real screenshot from the package's wiki page+// 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.
public/index.html+7@@ -542,6 +542,7 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh <h3>Vitrine content</h3> <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>+ <button class="btn" id="vitrine-pull">Pull real screenshot from the page</button> <button class="btn" id="vitrine-clear">Clear import</button> <div class="hint">Import a real screenshot into the vitrine instead of the CSS mock (apps show their real UI). Full-hero mode is the re-stamp path: bring in an existing hero image, ratify it here as the human, and the export carries the studio provenance stamp. The type pill and slug still bake on top.</div> </div>@@ -1116,6 +1117,12 @@ $('vitrine-file').addEventListener('change', e => { }); $('vitrine-full').addEventListener('change', e => send({ vitrine: { full: e.target.checked } })); $('vitrine-clear').addEventListener('click', () => send({ vitrine: { src: '', full: false } }));+$('vitrine-pull').addEventListener('click', () => {+ fetch('vitrine-from-page?page=' + encodeURIComponent(state.page), { method: 'POST' })+ .then(r => r.json())+ .then(j => handleToast({ text: j.ok ? 'Vitrine loaded: ' + j.picked : 'No page screenshot: ' + j.error, kind: j.ok ? 'success' : 'warn', seq: Date.now() }))+ .catch(() => {});+}); /* owner brand controls */ $('brand-accent').addEventListener('input', e => send({ brand: { accent: e.target.value } }));
server.js+39@@ -121,6 +121,45 @@ const server = http.createServer((req, res) => { res.end(JSON.stringify({ ok: true })); }); }+ 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';+ const headers = {};+ try { headers.Authorization = 'Bearer ' + fs.readFileSync('/var/run/adom/api-key', 'utf8').trim(); } catch {}+ const finish = (code, payload) => { res.writeHead(code, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(payload)); };+ (async () => {+ const lr = await fetch(REG + '/api/pages/' + ref + '/files', { headers });+ if (!lr.ok) return finish(200, { ok: false, error: 'files listing ' + lr.status });+ const j = await lr.json();+ const raw = j.files || j.data || j;+ const entries = Array.isArray(raw) ? raw : [];+ const paths = entries.filter((f) => f.type !== 'tree').map((f) => (typeof f === 'string' ? f : f.path || f.name || '')).filter(Boolean);+ // the files listing is top-level only; descend one level into the+ // directories that plausibly hold screenshots (plain-text listings)+ const dirs = entries.filter((f) => f.type === 'tree' && /^(screenshots|docs|images|assets|media)$/i.test(f.path || f.name || ''));+ for (const d of dirs) {+ const dn = d.path || d.name;+ const dr = await fetch(REG + '/api/pages/' + ref + '/files/' + dn, { headers });+ if (!dr.ok) continue;+ const lines = (await dr.text()).split('\n').map((x) => x.trim()).filter((x) => x && !x.startsWith('tree '));+ for (const ln of lines) paths.push(dn + '/' + ln);+ }+ const img = (re) => paths.find((x) => re.test(x.toLowerCase()) && !/hero/.test(x.toLowerCase()));+ // representative real UI shot: screenshots/ first, then docs/ images, then any image+ const pick = img(/^screenshots\/.*\.(png|jpe?g|webp)$/) || img(/^docs\/.*\.(png|jpe?g|webp)$/) || img(/\.(png|jpe?g|webp)$/);+ if (!pick) return finish(200, { ok: false, error: 'no non-hero image on the page' });+ const ir = await fetch(REG + '/api/pages/' + ref + '/files/' + pick, { headers });+ if (!ir.ok) return finish(200, { ok: false, error: 'image fetch ' + ir.status });+ 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 };+ logLine('info', 'vitrine <- ' + ref + '/' + pick + ' (' + Math.round(buf.length / 1024) + ' KB)');+ broadcast('state', state);+ finish(200, { ok: true, picked: pick, bytes: buf.length });+ })().catch((e) => finish(200, { ok: false, error: String(e).slice(0, 200) }));+ return;+ } if (p === '/api/pages' && req.method === 'POST') { return readBody(req, (err, body) => { if (err || !body || !body.owner || !body.slug) {