← Commit history

Publish 0.7.0

Colby Knox ·a3ae0c8b41 ·2mo ago ·parent c8e4706
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) {