← Commit history

Publish 0.11.2

Colby Knox ·aab2f4618a ·2mo ago ·parent 522deb3
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 });