← Commit history
dev-skills/making-the-hero/SKILL.md+78−72
@@ -1,86 +1,92 @@ --- name: making-the-hero-description: "DEV skill: how to build the nb-bridge hero image - the full-hero-import recipe that finally worked, after every studio-native path produced a window-in-a-window John rejected. Read BEFORE regenerating the hero for adom/nb-bridge (or any bridge page whose vitrine is a real browser window). Covers: why the studio's vitrine container can never show a browser window cleanly, the real-nb-window rule (never pup!), the badged-taskbar capture, matching Pup's hero geometry, and the exact compose-render-import pipeline."+description: "DEV skill: the COMPLETE recipe for the nb-bridge hero image - build_nb_hero.py in this folder renders the whole 2000x1250 from a fresh real-nb-window capture, then hero-studio full-import stamps provenance at the human gate. Read this BEFORE touching the hero. Encodes every rule John set shipping v10 (2026-08-09) and every trap: the studio's .window wrapper, font-metric lies, occlusion-blank captures, the title-vs-headline split. Sibling recipe: pup-bridge dev-skills/pup-hero-image (we share its design constants)." --- Parent skill: **adom-native-browser** (maintainers). Publish flow: **publishing-the-bridge**. -# Making the nb-bridge hero+# The nb-bridge hero: build, gate, apply -The hero for adom/nb-bridge is built by the **full-hero import** path, not the studio's native-vitrine slot. This file is the recipe plus every rule John set while we iterated (2026-08-09).+Shipped as hero-v10 after ~6 hours of iteration with John at the gate. EVERYTHING below was paid+for; do not re-derive it. -## The one structural fact that forces full-hero import+## The pipeline (the only path that works) -Hero Studio wraps ANY vitrine image in its own `.window` container: `border 0.67px-rgba(255,255,255,.09)`, `border-radius 9px`, drop shadow + teal glow, and an opaque-`rgb(13,17,23)` background (transparency is lost). `--chrome none` removes only the fake-TITLEBAR, never this container. So an image that already IS a browser window always renders as a-window inside another window. John rejected that three times; do not try to outsmart it again.-Compose the entire 2000x1250 yourself and import with `hero-studio vitrine --file X --full`.-(Filed upstream as part of hero-studio#28 territory; until the studio offers a bare slot, full-import is THE path.)+```bash+# 1. a REAL nb window - never pup (pup is the sibling product; John rejected a pup window hard)+adom-bridge-cli --target <box> --ai-thread "<you>" nbrowser_wake_profile '{"profile":"chrome:[email protected]"}'+adom-bridge-cli ... nbrowser_open_window '{"sessionId":"nbhero","thread":"<you>","purpose":"hero capture",+  "profile":"chrome:[email protected]","url":"https://wiki.adom.inc/blob/app/nb-bridge/demo/nb.html"}'+adom-bridge-cli ... nbrowser_taskbar '{"sessionId":"nbhero","overlay":{"badge":"adom"}}'   # the nb signature+adom-bridge-cli ... nbrowser_window_state '{"sessionId":"nbhero","width":900,"height":772}' # taller aspect -> vitrine fills pup's height+adom-bridge-cli ... desktop_screenshot_window '{"hwnd":<from open>}'                        # RAW - keep the pink debugging banner! -## John's hero rules for nb (all landed the hard way)+# 2. build the full 2000x1250 (fonts/logo/taskbar-strip ship in assets/ - reproducible from a bare clone)+python3 dev-skills/making-the-hero/build_nb_hero.py --window <that capture> --out /tmp/hero_candidate.png -1. **The vitrine window MUST be a real nb window** - opened via `nbrowser_open_window` in a real-   signed-in profile (`chrome:[email protected]`). NEVER a pup window: pup is the sibling product-   (logged-out Chrome-for-Testing). The Work-profile pill + avatar in the toolbar is the proof.-2. **Badge it.** `nbrowser_taskbar {overlay:{badge:"adom"}}` before capturing the taskbar - the-   composited icon (user's photo + Adom mark + active indicator) is nb's visual signature.-3. **Page content: the giant `nb` wordmark**, dark mode, brand gradient. Serve it from the page-   repo (`demo/nb.html`) so the address bar shows a REAL `wiki.adom.inc` URL, never a `data:` URL.-4. **No window-in-a-window** (see above). The Chrome window floats directly on the hero-   background with its own shadow.-5. **Windows taskbar**: real, readable. Icons at 2x what you first think (~60px at 2000px hero),-   TRUE aspect (height-normalise each icon crop; squeezing slot crops into squares smooshes-   them). A floating SEGMENT like Pup's (a rounded dark bar with 3 icons: Chrome, Edge, badged-   nb) - not a full-width bar, no Start/Search needed. Nothing may be "cut off" below a taskbar:-   a taskbar is the bottom of a screen.-6. **Match Pup's geometry** (adom/pup-bridge hero-v9): window ~54% of hero width, chrome-   decoration ~7% of window width (capture the window ~930px wide so the toolbar reads),-   BUT give the right margin ~3% (Pup's own 2% is too tight per John) and align the pill, the-   window and the taskbar segment on ONE right edge.-7. **Upper-right pill says BRIDGE** - pose with `--type bridge` (works even though the studio-   `mode` enum only lists app/component). When the composed image carries its own baked pill,-   `hero-studio set overlay.pill=false` or the studio double-stamps it.-8. Title short form in the IMAGE ("Native Browser", like Pup's says "Pup"); the WIKI title stays-   "NB - Native Browser Bridge". NEVER click "Apply changes to wiki" at the gate - it would-   rename the page to the hero headline.+# 3. human gate: full-import (NEVER the studio's native vitrine slot - see traps)+HERO_STUDIO_PORT=<free> hero-studio pose adom/nb-bridge --type bridge --port <free> &   # --surface pup is broken (#31); open the gate in pup yourself+hero-studio vitrine --file /tmp/hero_candidate.png --full --port <port>+hero-studio set overlay.pill=false overlay.slug=false --port <port>                     # our pill is baked; studio double-stamps otherwise+# open the proxy URL in a pup window for John, maximize ONCE, flash - never foreground uninvited+# poll GET /state .approved; a timed-out pose process does NOT kill the server -## The pipeline (exact)+# 4. apply (order matters)+cp <generated png> screenshots/hero.png; cp <generated png> screenshots/hero-vN.png+adom-wiki repo push adom/nb-bridge --files screenshots/hero.png screenshots/hero-vN.png -m "..."+adom-wiki page hero adom/nb-bridge --image screenshots/hero.png     # works for us (pup's skill says broken for them - path layout differs)+adom-wiki repo push adom/nb-bridge --files page.json -m "..."       # LAST - pkg publish clobbers title+hero+# verify: hero_provenance=="studio" AND sha256(served blob) == sha256(generated) - then cache-busted refresh in John's pup window+``` -1. Wake + open: `nbrowser_wake_profile` then `nbrowser_open_window {profile:"chrome:[email protected]",-   url:"https://wiki.adom.inc/blob/app/nb-bridge/demo/nb.html"}`; size ~930x650 via-   `nbrowser_window_state` (decoration ratio).-2. Badge: `nbrowser_taskbar {sessionId, overlay:{badge:"adom"}}`.-3. Capture window: `desktop_screenshot_window {hwnd}`. If content is BLACK: Chrome throttles-   occluded windows - take `nbrowser_screenshot` (CDP) and paste the page bitmap into the frame,-   or just recapture while the window paints. Splice out the pink "started debugging this-   browser" banner rows (detect: full-width light rows y~110-260).-4. Capture taskbar: `desktop_screenshot_screen {monitor:0}` with a `reason`, crop the bottom-   ~6.5%, DELETE the full capture immediately (John's screen carries sensitive threads). The-   bar's true top edge is ~20px into that strip - profile rows to find it.-5. Compose 2000x1250 HTML (fonts from the hero-studio pkg: familjen-grotesk-700 + satoshi-400-   base64-embedded, adom-logo.svg inline; teal solid pill, title, accent subtitle, window img,-   taskbar segment). Push as `hero-build.html` to the page repo; open via pup at 2020x1290;-   `pup_screenshot_full_res {clip:{0,0,2000,1250}}`.-6. `hero-studio vitrine --file hero_candidate.png --full --port <N>` + `overlay.pill=false`,-   `overlay.slug=false`. Human ratifies + Generates (provenance stamp). Then: push PNG to-   `screenshots/hero.png` + `hero-vN.png`, `adom-wiki page hero adom/nb-bridge --image ...`,-   re-push complete `page.json` LAST, verify `hero_provenance:"studio"` and sha-match the served-   bytes.-7. Clean up: `nbrowser_close_window {includeSpawned:true}` + `pup_close_window` for every session-   you opened; delete `hero-build.html` from the repo if it is no longer wanted.+## John's rules (each cost a rejected round - all verbatim-enforced in the build script) -## Traps that burned a whole session+1. **Real nb window, never pup.** The vitrine window is opened by nbrowser_* in a real signed-in+   profile. The Work-profile pill + avatar + the pink "Adom Native Browser Extension started+   debugging this browser" banner ARE the product story - do not splice, retouch, or re-composite+   the capture. Raw pixels only. (Every "cleanup" I did made it read faker.)+2. **No window-in-a-window.** The window floats directly on the hero background with only a+   shadow. Corollary: never put the capture through the studio's vitrine slot (see traps).+3. **Taskbar = a small band, not the user's real full bar.** "A couple chrome icons and a couple+   edge icons" + the BADGED nb icon (user photo + Adom mark) underlined as active. 68px band,+   54px tiles, left-aligned - pup's exact band spec. NOT Start/Search/pinned-app zoo (rejected),+   NOT a stylized floating pod (rejected), and nothing may be "cut off" below a taskbar.+4. **Family geometry is law** - taken from pup's dev-skills/pup-hero-image (they measured+   Bridge's hero): vitrine y=140 w=1062 h=1050, RIGHT EDGE = PILL RIGHT EDGE = W-52; pill+   Familjen-700 26px pad 22x12 fill (0,206,194) inset 52; logo 236px at (90,64); bg #0d1117 ++   teal glow. kicad + fusion match. Do not eyeball; the constants are in the build script.+5. **Title**: hero headline is the short form "Native Browser" (page stays "NB - Native Browser+   Bridge" - the studio has ONE field for both, #33). Two lines, leading 0.92em ("tighter, one+   message"), as large as fits the column (200px; family 240px is for one-word names). Subtitle+   54px below (pup's offset; 36px was "a tinge close"), accent #4aa8ff on "Your real".+6. **Alignment is measured, not trusted.** Familjen reports NO left side bearing via+   PIL.textbbox, so metric compensation silently no-ops and the title sat 13px off the margin -+   John caught it from a zoomed phone crop after the linter said CLEAN. build_nb_hero.py renders+   each line to scratch, finds the actual first ink column, and offsets so every line's ink+   starts at exactly x=90. Verify after any font/text change: scan ink-left per line on the+   output PNG. -- pup `--surface pup` in `hero-studio pose` is broken (still calls dead `browser_*` verbs); pose-  with the port flag, open the gate in pup yourself, and poll `state.approved` - a timed-out pose-  process does NOT kill the studio server.-- The studio renders only the FIRST SENTENCE of `brief` as the hero subtitle. One sentence, commas.-- The studio caches page records per-port; a stale posed brief (old "abe" wording) survives-  republishes - POST /api/pages to refresh it.-- Screenshots cap at 1568px wide; taskbar icons are ~24px there. Upscale 2x + UnsharpMask, or the-  icons are mush.-- `state.lint` stays CLEAN through all of this - none of these mistakes are linted (filed-  hero-studio#28). The eyeball pass at the gate is the only real gate.+## Traps (the pain, so you skip it)++- **The studio's vitrine slot CANNOT show a browser window honestly**: every vitrine gets a+  `.window` wrapper (border, radius 9, shadow, glow, OPAQUE bg - alpha killed). `--chrome none`+  removes only the fake titlebar. Full-import is the only clean path (#34).+- **Occlusion-blank captures**: a background Chrome stops painting - desktop_screenshot_window+  returns the frame with BLACK content. Detect by max-saturation scan; nudge a paint with+  nbrowser_screenshot (CDP) or recapture while it draws. pup's Chrome ships anti-occlusion+  flags; the user's real Chrome does NOT.+- **Screenshots cap at 1568px wide** -> taskbar icons are ~24px native. Height-normalize each+  icon crop at TRUE aspect (squares smoosh them - John caught that too) and upscale ~2x with+  UnsharpMask. Icon crops in assets/taskbar_strip.png: chrome+profile 499-523, chrome 697-728,+  edge 576-606, badged-nb 899-934 (y 20-63).+- **The studio is a stale snapshot** (#33): its header/PAGE FIELDS title is a pose-time local+  record, NOT the live wiki title. Never click "Apply changes to wiki" - it renames the real+  page to the hero headline. Refresh a stale brief with POST /api/pages.+- **Full-import leaves studio overlays armed** - set overlay.pill=false or you ship two pills.+- **The subtitle renders only the FIRST SENTENCE of brief** - one sentence, commas.+- **--type bridge works** for the BRIDGE pill even though state.enums.mode only lists+  app/component (#27/#34).+- **Screen captures for taskbar material**: crop the strip immediately and DELETE the full+  capture unviewed - John's screen carries sensitive threads. The bar's true top is ~20px into+  a bottom-6.5% crop (row-profile to find it).+- The full hero-studio grievance ledger from this campaign: #28, #33, #34 (+ #25/#26/#27 and+  kicad's #29-#32). Read them before proposing studio-side fixes.
dev-skills/making-the-hero/assets/adom_logo_white.pngadded
⋯ 1 unchanged line ⋯
dev-skills/making-the-hero/assets/familjen-700.ttfadded
⋯ 1 unchanged line ⋯
dev-skills/making-the-hero/assets/satoshi-500.ttfadded
⋯ 1 unchanged line ⋯
dev-skills/making-the-hero/assets/taskbar_strip.pngadded
⋯ 1 unchanged line ⋯
dev-skills/making-the-hero/build_nb_hero.pyadded+110
@@ -0,0 +1,110 @@+#!/usr/bin/env python3+"""nb-bridge hero build - adapted from pup's canonical build_hero.py (same constants).+Vitrine = REAL nb window capture (untouched, banner incl.) + 68px taskbar band with+chrome/chrome/edge/edge/badged-nb tiles, active = the badged nb one."""+import os+from PIL import Image, ImageDraw, ImageFilter, ImageFont+HERE=os.path.dirname(os.path.abspath(__file__))+A=lambda *p: os.path.join(HERE,'assets',*p)++import argparse+ap=argparse.ArgumentParser(description="nb hero build - see SKILL.md for the capture recipe")+ap.add_argument("--window",required=True,help="RAW desktop_screenshot_window capture of a real nb window (banner incl., untouched)")+ap.add_argument("--out",default="hero_candidate.png")+args=ap.parse_args()+WIN=args.window+STRIP=A("taskbar_strip.png")+W,H=2000,1250; CQ=W/100.0++bg=Image.new('RGB',(W,H),(13,17,23))+glow=Image.new('L',(W,H),0)+ImageDraw.Draw(glow).ellipse([W*0.45,-H*0.3,W*1.25,H*0.9],fill=90)+glow=glow.filter(ImageFilter.GaussianBlur(240))+bg=Image.composite(Image.new('RGB',(W,H),(18,52,58)),bg,glow).convert('RGBA')++# vitrine geometry - pup's exact numbers+vy=140; vw=1062; vx=(W-int(2.6*CQ))-vw; VH=1050; TB=68+win_h=VH-TB++# panel: REAL window capture, width-fit, top-aligned; band at bottom; gap = panel bg+win=Image.open(WIN).convert('RGB')+s=vw/win.width+win=win.resize((vw,int(win.height*s)),Image.LANCZOS)+panel=Image.new('RGBA',(vw,VH),(19,22,27,255))+panel.paste(win.convert('RGBA'),(0,0))+# fade the window's bottom edge into the panel so the gap reads intentional+fh=40+al=Image.linear_gradient('L').resize((vw,fh))+sol=Image.new('RGBA',(vw,fh),(19,22,27,255)); sol.putalpha(al)+panel.alpha_composite(sol,(0,win.height-fh))++# taskbar band: 5 tiles left-aligned, active = badged nb (underline)+dp=ImageDraw.Draw(panel); tb_y=VH-TB+dp.rectangle([0,tb_y,vw,VH],fill=(24,26,31,255))+dp.line([0,tb_y,vw,tb_y],fill=(44,50,60,255))+strip=Image.open(STRIP).convert('RGB').crop((0,20,1568,63))+def tile(a,b):+    c=strip.crop((a,0,b,strip.height))+    s=54/c.height+    return c.resize((int(c.width*s),54),Image.LANCZOS).convert('RGBA')+tiles=[tile(499,523),tile(697,728),tile(576,606),tile(576,606),tile(899,934)]+bh,gap,x=54,14,14+for i,t in enumerate(tiles):+    by=tb_y+(TB-bh)//2+    panel.alpha_composite(t,(x,by))+    if i==4: dp.line([x+4,VH-4,x+t.width-4,VH-4],fill=(120,200,255),width=4)  # active = badged nb+    x+=t.width+gap++# rounded corners + drop shadow+R=22+mask=Image.new('L',(vw,VH),0); ImageDraw.Draw(mask).rounded_rectangle([0,0,vw,VH],R,fill=255)+sh=Image.new('RGBA',(W,H),(0,0,0,0)); shm=Image.new('L',(W,H),0)+ImageDraw.Draw(shm).rounded_rectangle([vx+10,vy+16,vx+vw+10,vy+VH+16],R,fill=150)+shm=shm.filter(ImageFilter.GaussianBlur(30)); sh.putalpha(shm)+bg=Image.alpha_composite(bg,sh); bg.paste(panel,(vx,vy),mask)++# left: logo, title (2 lines, max size that clears the vitrine), subtitle+d=ImageDraw.Draw(bg); inset=int(4.5*CQ)+logo=Image.open(A('adom_logo_white.png')).convert('RGBA'); lw=int(11.8*CQ)+logo=logo.resize((lw,int(logo.height*lw/logo.width)),Image.LANCZOS)+bg.alpha_composite(logo,(inset,int(3.2*CQ)))+maxw=vx-inset-46                       # clear the vitrine with a real gutter+size=int(12*CQ)+while size>100:+    f=ImageFont.truetype(A('familjen-700.ttf'),size)+    if max(d.textlength(w,font=f) for w in ("Native","Browser"))<=maxw: break+    size-=4+f_title=ImageFont.truetype(A('familjen-700.ttf'),size)+ty=int(13.15*CQ)+def ink_dx(txt,font):+    # render on scratch, measure where ink ACTUALLY starts vs the draw x+    t=Image.new('L',(1400,int(font.size*1.6)),0)+    ImageDraw.Draw(t).text((100,10),txt,font=font,fill=255)+    p=t.load()+    for x in range(100,1400):+        if any(p[x,y]>60 for y in range(0,t.height,2)): return x-100+    return 0+bbN=d.textbbox((0,0),"Native",font=f_title)+lh=int(size*0.92)+d.text((inset-ink_dx("Native",f_title),ty-bbN[1]),"Native",font=f_title,fill=(230,237,243,255))+d.text((inset-ink_dx("Browser",f_title),ty-bbN[1]+lh),"Browser",font=f_title,fill=(230,237,243,255))+f_sub=ImageFont.truetype(A('satoshi-500.ttf'),50)+BLU,WHT=(74,168,255,255),(232,238,244,255)+bb2=d.textbbox((0,0),'Browser',font=f_title)+sy=ty+lh+(bb2[3]-bb2[1])+54+sx=inset-ink_dx("Your real",f_sub)+d.text((sx,sy),"Your real",font=f_sub,fill=BLU)+w1=d.textlength("Your real",font=f_sub)+d.text((sx+w1,sy)," browser,",font=f_sub,fill=WHT)+d.text((inset-ink_dx("driven",f_sub),sy+int(50*1.35)),"driven by AI.",font=f_sub,fill=WHT)++# BRIDGE pill - pup's exact+pf=ImageFont.truetype(A('familjen-700.ttf'),26); txt="BRIDGE"+tb2=d.textbbox((0,0),txt,font=pf); tw,th=tb2[2]-tb2[0],tb2[3]-tb2[1]+padx,pady=22,12; pw2,ph2=tw+padx*2,th+pady*2+px_=W-int(2.6*CQ)-pw2; py_=int(2.6*CQ)+d.rounded_rectangle([px_,py_,px_+pw2,py_+ph2],radius=ph2//2,fill=(0,206,194,255))+d.text((px_+padx-tb2[0],py_+pady-tb2[1]),txt,font=pf,fill=(9,20,24,255))++bg.convert('RGB').save(args.out)+print(f"saved {args.out}. title {size}px, vitrine right {vx+vw} = pill right {W-int(2.6*CQ)}, window {win.size}, gap {win_h-win.height}px above band")