← Commit history

Publish 0.14.0

Colby Knox ·df745a650a ·2mo ago ·parent 399ef7c
6 files changed +124−13
SKILL.md+14−5
@@ -147,7 +147,13 @@ text intruding into the dynamic marquee band, text clipped by the hero edge (HARD FAIL: locks Generate), near-edge insets, and 4+ line title wraps. Findings render in the gate and publish to `state.lint` ([{id, level: warn|fail, msg}]); the AI should fix warns without being asked.-Contrast checking is not in v1.+FULL-HERO IMPORTS are never reported all-clear: the linter discloses that+composed checks cannot see imported pixels, and an edge-density scan of the+lower band flags text-like structure there (heuristic, hero-studio#22).+Quoted prompt-looking text in the image zone warns too: sample prompts belong+in page metadata (sample_prompts), which the dynamic overlay previews with a+copy button; a baked prompt is pixels nobody can copy. Contrast checking is+not in v1.  ## Verification mode (what the pose gate shows) @@ -159,10 +165,13 @@ vitrine, then the type pill (where the human catches an AI's wrong app/skill/ component call); "Looks good" ticks that check and advances. The homepage- marquee overlay composites over the hero by default in the gate so the human SEES where the wiki URL, pill, and today's duplicated title land (annotated-with adom/wiki#80, which will drop the duplicate). The overlay BREATHES, tags-itself DYNAMIC, and hold-to-hides (press the band to see the pure export), so-dynamic text is never mistaken for baked text. Tour dialogs are draggable by-their header and remember where you put them. The header's focus button (zen)+with adom/wiki#80, which will drop the duplicate). STAGE HYGIENE (hero-studio#23): the stage shows ONLY final-hero+pixels plus the dynamic overlay when previewing; every explanation lives in+the STAGEBAR above the stage, never on the artwork. The stagebar's segmented+toggle flips between "Exactly my PNG" and "With homepage overlay" (keyboard D;+hold-the-band still peeks); the dynamic layer breathes and carries an amber+dashed outline so it can never be mistaken for baked pixels. Tour dialogs are+draggable by their header and remember where you put them. The header's focus button (zen) hides the panel and requests fullscreen so the hero gets the whole surface; Esc restores. Free exploration without the checklist is lab mode, a plain open.
data/pages.json+2−2
@@ -84,7 +84,7 @@   "score": 0,   "stars": 0,   "installs30d": 0,-  "brief": "Fair rolls in your terminal: dice expressions, coin flips, random picks.",+  "brief": "Roll dice expressions from the terminal with visible fair math: 2d6+1, d20, coin flips, and random picks.",   "tags": [    "cli",    "dice",@@ -229,7 +229,7 @@   "score": 539,   "stars": 0,   "installs30d": 82,-  "brief": "adom-lbr , the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad \u21c4 Altium \u21c4 EAGLE/Fusion) through one canonical adom-lbr JSON , symbol + footp",+  "brief": "adom-lbr , the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON , symbol + footp",   "tags": [    "eda",    "altium",
package.json+1−1
@@ -2,7 +2,7 @@   "slug": "hero-studio",   "title": "Hero Studio",   "type": "app",-  "version": "0.13.0",+  "version": "0.14.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": [
page.json+1−1
@@ -2,7 +2,7 @@   "slug": "hero-studio",   "title": "Hero Studio",   "type": "app",-  "version": "0.13.0",+  "version": "0.14.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": [
pose.mjs+1−1
@@ -135,7 +135,7 @@ await jf('state', {   body: JSON.stringify({     mode: poseMode, page: ref,     // the gate presentation: verification chrome + a fresh checklist-    verify: true, tour: true, marqueePreview: true,+    verify: true, tour: true, marqueePreview: true, guides: false,     confirm: { title: false, subtitle: false, vitrine: false, type: false },     flags: { title: false, subtitle: false, vitrine: false, type: false },     lint: [],
public/index.html+105−3
@@ -295,8 +295,18 @@ label.chk input { accent-color:var(--accent); width:14px; height:14px; cursor:po /* dynamic-vs-baked (#16): the overlay breathes, tags itself, and hold-hides */ .hero.show-mq #mqoverlay { animation:mqbreathe 3.4s ease-in-out infinite; cursor:pointer; } @keyframes mqbreathe { 0%,100% { opacity:1; } 50% { opacity:.5; } }-#mqoverlay::after { content:'DYNAMIC \00b7 homepage adds this, NOT in your PNG \00b7 hold to hide'; position:absolute; right:1.2cqw; top:.6cqw; font-family:'JetBrains Mono',monospace; font-size:.85cqw; letter-spacing:.06em; color:#e8a13c; } .hero.mq-hold #mqoverlay { display:none !important; }+#mqoverlay .mqo-prompt { display:flex; gap:.8cqw; align-items:center; margin-top:.6cqw; font-family:'JetBrains Mono',monospace; font-size:1.05cqw; color:#c9d1d9; }+#mqoverlay .mqo-prompt button { font-size:.95cqw; padding:.2cqw .8cqw; border-radius:.5cqw; border:1px solid rgba(232,161,60,.6); background:rgba(232,161,60,.12); color:#e8a13c; cursor:pointer; }+.hero.show-mq #mqoverlay { outline:2px dashed rgba(232,161,60,.85); outline-offset:-2px; background:rgba(232,161,60,.045); }+/* the stagebar (#21/#23): stage chrome lives ABOVE the stage, never on it */+#stagebar { display:none; align-items:center; gap:10px; margin:10px 0 8px; }+#stagebar.on { display:flex; }+#stagebar .seg { display:flex; border:1px solid #30363d; border-radius:8px; overflow:hidden; }+#stagebar .seg button { background:#0d1117; color:var(--text); border:0; padding:6px 12px; font-size:12.5px; cursor:pointer; }+#stagebar .seg button.on { background:rgba(0,184,176,.18); color:var(--accent-bright); }+#stagebar .legend { font-size:12px; color:#8b949e; line-height:1.35; }+#stagebar .legend b { color:#e8a13c; font-weight:600; } .obadge {   display:flex; align-items:center; gap:.42cqw; font-family:'JetBrains Mono',monospace;   font-size:.95cqw; color:#aeb8c4;@@ -728,6 +738,13 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh       <div id="lintbox"></div>       <button class="btn btn-primary" id="v-generate" disabled>Generate hero PNG</button>     </div>+    <div id="stagebar">+      <div class="seg">+        <button id="sb-png">Exactly my PNG</button>+        <button id="sb-mq">With homepage overlay</button>+      </div>+      <div class="legend"><b>amber-outlined = dynamic</b>: the homepage adds it, it is NOT in your PNG. Press D (or hold the band) to flip. The lower band stays calm in the image because the marquee overlays its own info there.</div>+    </div>     <div id="hero" class="hero" data-theme="midnight" data-layout="bleed-bottom">       <div class="bg"></div> @@ -791,6 +808,7 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh         <h3 class="mqo-title" id="mqo-title"></h3>         <p class="mqo-brief" id="mqo-brief"></p>         <span class="mqo-owner" id="mqo-owner"></span>+        <div class="mqo-prompt" id="mqo-prompt" style="display:none"><span id="mqo-prompt-text"></span><button id="mqo-prompt-copy">copy</button></div>       </div>       <div class="pill-tr" id="pill-tr" data-tip="Type pill: bakes into the PNG top right (app / skill / component). Toggle under Image badges"><span class="pill app" id="l-pill">APP</span></div>       <div class="slug-br" id="slug-br" data-tip="Wiki slug: bakes into the PNG bottom right. Toggle under Image badges"><a class="l-slug" id="l-slug" target="_blank" href="#"></a></div>@@ -822,7 +840,7 @@ body[data-render] #hero { width:1600px; max-width:none; aspect-ratio:auto; heigh         </div>       </div> -      <div class="guide"><span>marquee overlays its info over this lower band; keep it calm</span></div>+      <div class="guide"></div>     </div>      <div id="contexts">@@ -996,6 +1014,11 @@ function render() {   }   $('verifybar').style.display = state.verify && !approvedHere ? 'block' : 'none'; +  // stagebar (#21/#23): stage chrome above the stage, never on it+  $('stagebar').classList.toggle('on', !!state.verify || !!state.marqueePreview);+  $('sb-png').classList.toggle('on', !state.marqueePreview);+  $('sb-mq').classList.toggle('on', !!state.marqueePreview);+   // walkthrough + procedural background + measured layout lint + page fields   showTourStep();   scheduleLint();@@ -1015,7 +1038,12 @@ function render() {     // planned fix so nobody thinks it is an accident.     $('mqo-title').textContent = d.title;     $('mqo-brief').textContent = firstSentence(d.brief || p.brief || '', 120);-    $('mqo-owner').textContent = 'wiki.adom.inc/' + d.owner + '/' + d.slug + '   (overlay as the homepage renders TODAY; adom/wiki#80 will drop this duplicated title)';+    $('mqo-owner').textContent = 'wiki.adom.inc/' + d.owner + '/' + d.slug;+    // sample prompt: DYNAMIC by design (#22): metadata the page renders with a+    // copy button; if it were pixels nobody could copy it+    const sp = (Array.isArray(d.sample_prompts) && d.sample_prompts[0] && d.sample_prompts[0].prompt) || state.samplePrompt || '';+    $('mqo-prompt').style.display = sp ? 'flex' : 'none';+    $('mqo-prompt-text').textContent = sp ? '"' + sp + '"' : '';   }   $('chk-mqpreview').checked = !!state.marqueePreview; @@ -1522,6 +1550,26 @@ function runLint() {     }   } +  // 4b. full-hero imports (#22): the composed checks cannot see imported+  // pixels; say so instead of reporting all clear, and run the band scan.+  const vit2 = state.vitrine || {};+  if (vit2.full && vit2.src) {+    scanImportBand(vit2.src);+    items.push({ id: 'import-unlinted', level: 'warn', el: 'fullimport-img', msg: 'full-hero import: composed-layout checks cannot inspect imported pixels; verify the lower band and margins by eye before ratifying' });+    if (bandScan.status === 'done' && bandScan.texty) {+      items.push({ id: 'import-band-text', level: 'warn', el: 'fullimport-img', msg: 'the imported image appears to carry text or graphics in the lower dynamic band (heuristic edge scan): the homepage overlays its own info there, keep the band calm' });+    }+  }++  // 4c. prompt-looking text baked into pixels (#22): sample prompts belong in+  // page metadata (sample_prompts), rendered dynamically with a copy button.+  for (const t of texts) {+    const txt = ($(t.id).textContent || '');+    if (/["\u201c\u201d'][^"\u201c\u201d']{12,}["\u201c\u201d']/.test(txt)) {+      items.push({ id: 'prompt-' + t.id, level: 'warn', el: t.id, msg: t.name + ' contains quoted prompt-looking text; a baked prompt cannot be copied. Put it in sample_prompts (page metadata) and the page renders it dynamically with a copy button' });+    }+  }+   // 4. title wrap count: a 4-line title is almost never intended.   const tEl = $('el-title');   if (vis(tEl)) {@@ -1592,6 +1640,60 @@ $('tp-flag').addEventListener('click', () => { }); $('tp-skip').addEventListener('click', () => { clearTourGlow(); $('tourpop').style.display = 'none'; send({ tour: false }); }); +$('mqo-prompt-copy').addEventListener('click', async (e) => {+  e.stopPropagation();+  const txt = $('mqo-prompt-text').textContent.replace(/^"|"$/g, '');+  try { await navigator.clipboard.writeText(txt); } catch {}+  handleToast({ text: 'sample prompt copied', kind: 'success', seq: Date.now() });+});++/* full-hero import band scan (#22.1): composed-layout lint cannot see imported+   pixels, so the class of hero most likely to violate the band skipped every+   check. A cheap edge-density scan of the bottom 24% flags text-like structure+   in the band; it is a heuristic and says so. */+let bandScan = { key: '', status: 'idle', texty: false };+function scanImportBand(src) {+  const key = src.length + ':' + src.slice(40, 72);+  if (bandScan.key === key) return;+  bandScan = { key, status: 'pending', texty: false };+  const img = new Image();+  img.onload = () => {+    try {+      const w = Math.min(800, img.width);+      const bandH = Math.max(10, Math.round(img.height * 0.24 * (w / img.width)));+      const c = document.createElement('canvas');+      c.width = w; c.height = bandH;+      const g = c.getContext('2d');+      g.drawImage(img, 0, img.height * 0.76, img.width, img.height * 0.24, 0, 0, w, bandH);+      const d = g.getImageData(0, 0, w, bandH).data;+      let edges = 0, n = 0;+      for (let y = 0; y < bandH; y++) {+        for (let x = 1; x < w; x++) {+          const i = (y * w + x) * 4, j = i - 4;+          const a = d[i] * .3 + d[i + 1] * .59 + d[i + 2] * .11;+          const b = d[j] * .3 + d[j + 1] * .59 + d[j + 2] * .11;+          if (Math.abs(a - b) > 60) edges++;+          n++;+        }+      }+      bandScan.texty = n > 0 && (edges / n) > 0.0015;+      bandScan.status = 'done';+    } catch { bandScan.status = 'failed'; }+    scheduleLint();+  };+  img.onerror = () => { bandScan.status = 'failed'; scheduleLint(); };+  img.src = src;+}++/* the stagebar (#21/#23): explicit dynamic on/off, keyboard D, legend off-stage */+$('sb-png').addEventListener('click', () => send({ marqueePreview: false }));+$('sb-mq').addEventListener('click', () => send({ marqueePreview: true }));+document.addEventListener('keydown', (e) => {+  if (e.key.toLowerCase() === 'd' && !e.target.closest('input,textarea')) {+    send({ marqueePreview: !state.marqueePreview });+  }+});+ /* hold-to-hide the dynamic overlay (#16): press the band to see the pure    export, release to bring the overlay back */ (() => {