Buckets:
sabaridsnfuji/bco-repro-artifacts / repro-bundle /scripts /posterly /examples /hello_world /poster.html
| <!-- | |
| ============================================================ | |
| posterly — Hello World example | |
| PURPOSE: smallest poster that passes all four gates | |
| (preflight, measure, polish --strict, verify-final). | |
| CANVAS: A2 portrait (420 × 594 mm), single card, no MathJax, | |
| no external images — renders fully offline, single | |
| page, fills the canvas. | |
| The example is a single card FILLED with real content -- not | |
| stretched with `flex: 1` + blank space to fake a full page (see | |
| SKILL.md "Gate C -- the same trap, one card": a half-empty | |
| stretched card passes every gate yet reads as a failed poster). | |
| It stays single-card so it's robust to font / Chromium revision | |
| drift; multi-card alignment is the templates/ gallery's job, not | |
| this install-verification fixture's. | |
| Use: | |
| cd examples/hello_world | |
| python ../../tools/poster_check.py preflight poster.html | |
| python ../../tools/poster_check.py measure poster.html | |
| python ../../tools/poster_check.py polish poster.html --strict | |
| python ../../tools/render_preview.py poster.html | |
| python ../../tools/poster_check.py verify-final poster_preview.pdf --from-html poster.html | |
| Expect five lines confirming success. | |
| ============================================================ | |
| --> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>posterly — Hello World</title> | |
| <style> | |
| @page { size: A2 portrait; margin: 0; } | |
| :root { | |
| --accent: #2D5F8B; | |
| --accent-deep: #1F4566; | |
| --accent-light: #E8F1F8; | |
| --gold: #C9A24A; | |
| --text-primary: #1A1A1A; | |
| --text-muted: #777; | |
| --bg-page: #F6F3F0; | |
| --bg-card: #FFFFFF; | |
| --border-soft: #D8D8D8; | |
| --u: 1.6px; | |
| --font-sans: "Inter","Helvetica Neue",sans-serif; | |
| --font-serif: "Charter","Source Serif Pro","Georgia",serif; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| /* BASE DEFENSES (see templates/*_neutral.html for the annotated | |
| version): orphan/widow wrap protection on prose, balance on the | |
| centered title. Keep this block in any skeleton, incl. custom. */ | |
| p, li, dd, figcaption, | |
| .body-text, .caption, .callout, .section-title { text-wrap: pretty; } | |
| .title { text-wrap: balance; } | |
| html, body { background: #2b2b2b; font-family: var(--font-serif); color: var(--text-primary); } | |
| .poster { | |
| width: calc(420 * var(--u)); | |
| height: calc(594 * var(--u)); | |
| background: var(--bg-page); | |
| margin: 20px auto; | |
| padding: calc(8 * var(--u)) calc(12 * var(--u)); | |
| display: grid; | |
| grid-template-rows: auto 1fr auto auto; | |
| /* 10 mm row-gap → 38 px in print mode (--u: 1mm), comfortably inside | |
| the [30, 50] px target for measure's gap-to-footer-strip gate. */ | |
| gap: calc(10 * var(--u)); | |
| box-shadow: 0 0 40px rgba(0,0,0,0.4); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .poster::before { | |
| content: ""; | |
| position: absolute; top: 0; left: 0; right: 0; | |
| height: calc(4 * var(--u)); | |
| background: linear-gradient(90deg, var(--accent-deep), var(--accent), var(--gold)); | |
| } | |
| /* ---- header ---- */ | |
| .header { | |
| display: grid; | |
| grid-template-columns: 1fr auto; | |
| align-items: center; | |
| gap: calc(8 * var(--u)); | |
| padding: calc(2 * var(--u)) calc(2 * var(--u)) calc(4 * var(--u)); | |
| border-bottom: calc(1 * var(--u)) solid var(--accent); | |
| } | |
| .title-block { min-width: 0; } | |
| .title { | |
| font-family: var(--font-sans); | |
| font-weight: 800; | |
| font-size: calc(20 * var(--u)); | |
| line-height: 1.08; | |
| color: var(--accent-deep); | |
| letter-spacing: -0.4px; | |
| } | |
| .title .accent { color: var(--gold); } | |
| .subtitle { | |
| font-family: var(--font-sans); | |
| font-size: calc(9 * var(--u)); | |
| color: var(--text-muted); | |
| margin-top: calc(1 * var(--u)); | |
| } | |
| .authors { | |
| font-family: var(--font-sans); | |
| font-size: calc(8 * var(--u)); | |
| color: var(--accent); | |
| font-weight: 600; | |
| margin-top: calc(2 * var(--u)); | |
| } | |
| .qr-block { display: flex; flex-direction: column; align-items: center; gap: calc(1 * var(--u)); } | |
| .qr-block img { | |
| width: calc(28 * var(--u)); | |
| height: calc(28 * var(--u)); | |
| border: calc(0.5 * var(--u)) solid var(--accent); | |
| border-radius: calc(1.5 * var(--u)); | |
| background: white; | |
| padding: calc(0.5 * var(--u)); | |
| } | |
| .qr-label { font-family: var(--font-sans); font-size: calc(6 * var(--u)); color: var(--accent); font-weight: 600; } | |
| /* ---- body (single column, single card filling the row) ---- */ | |
| .body-grid { | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| min-height: 0; | |
| } | |
| .column { | |
| display: flex; | |
| flex-direction: column; | |
| min-height: 0; | |
| } | |
| .card { | |
| background: var(--bg-card); | |
| border-radius: calc(2 * var(--u)); | |
| padding: calc(6 * var(--u)) calc(8 * var(--u)); | |
| border: calc(0.5 * var(--u)) solid var(--border-soft); | |
| box-shadow: 0 calc(1 * var(--u)) calc(2 * var(--u)) rgba(0,0,0,0.04); | |
| border-left: calc(3 * var(--u)) solid var(--accent); | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .section-title { | |
| font-family: var(--font-sans); | |
| font-weight: 700; | |
| font-size: calc(11 * var(--u)); | |
| color: var(--accent-deep); | |
| margin-bottom: calc(2 * var(--u)); | |
| } | |
| .body-text { | |
| font-family: var(--font-serif); | |
| font-size: calc(7 * var(--u)); | |
| line-height: 1.35; | |
| color: var(--text-primary); | |
| } | |
| .body-text + .body-text { margin-top: calc(1.5 * var(--u)); } | |
| .keyword { color: var(--accent); font-weight: 700; } | |
| .card ul { padding-left: calc(11 * var(--u)); margin-top: calc(1 * var(--u)); } | |
| .card li { font-family: var(--font-serif); font-size: calc(7 * var(--u)); line-height: 1.35; margin-top: calc(0.5 * var(--u)); } | |
| .figure { margin-top: calc(3 * var(--u)); display: flex; flex-direction: column; align-items: center; } | |
| /* 80% width on an AR=2 figure → tall figures would be too big, but | |
| AR=2 wide fits comfortably AND satisfies polish Gate A's wide-min | |
| ratio (must be >= 65% of card width). */ | |
| /* Natural aspect ratio (AR=2), no max-height cap: the figure carries | |
| real vertical space instead of leaving the card half-blank, and | |
| stays >= 65% width so polish Gate A doesn't flag it as too small. */ | |
| .figure img { width: 70%; display: block; } | |
| .figure .caption { font-family: var(--font-sans); font-size: calc(6 * var(--u)); color: var(--text-muted); margin-top: calc(1 * var(--u)); text-align: center; line-height: 1.2; } | |
| /* ---- footer-strip ---- */ | |
| .footer-strip { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: calc(4 * var(--u)); | |
| background: var(--accent-deep); | |
| color: white; | |
| border-radius: calc(2 * var(--u)); | |
| padding: calc(3 * var(--u)) calc(5 * var(--u)); | |
| } | |
| .hs-stat { display: flex; flex-direction: column; align-items: center; gap: calc(0.5 * var(--u)); } | |
| .hs-stat .num { | |
| font-family: var(--font-sans); | |
| font-weight: 800; | |
| font-size: calc(12 * var(--u)); | |
| color: var(--gold); | |
| white-space: nowrap; | |
| } | |
| .hs-stat .lbl { | |
| font-family: var(--font-sans); | |
| font-size: calc(6 * var(--u)); | |
| color: var(--accent-light); | |
| text-align: center; | |
| } | |
| /* ---- footer ---- */ | |
| .footer { | |
| display: grid; | |
| grid-template-columns: 1fr auto; | |
| align-items: center; | |
| gap: calc(8 * var(--u)); | |
| font-family: var(--font-sans); | |
| font-size: calc(6 * var(--u)); | |
| color: var(--text-muted); | |
| padding: calc(1 * var(--u)) calc(2 * var(--u)); | |
| } | |
| .footer .right { color: var(--accent); font-weight: 600; } | |
| /* ========================================================= | |
| PRINT OVERRIDE — KEEP LAST so it wins source-order ties. | |
| Without `:root { --u: 1mm }` the poster keeps the 1.6 px / | |
| unit screen scale and renders into ~42 % of A2 — the column | |
| spread gate would still pass on the shrunken poster, but | |
| the printed PDF would show a giant gray border. measure | |
| hard-fails this via `--min-canvas-fill`. The explicit | |
| `html, body { height: 100% }` block dodges Chromium's | |
| well-known "round content height to next page" quirk that | |
| otherwise emits a phantom second page. | |
| ========================================================= */ | |
| @media print { | |
| :root { --u: 1mm; } | |
| html, body { | |
| background: white; | |
| margin: 0; | |
| padding: 0; | |
| width: 100%; | |
| height: 100%; | |
| overflow: hidden; | |
| } | |
| .poster { margin: 0; box-shadow: none; page-break-after: avoid; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="poster" data-measure-role="poster"> | |
| <header class="header" data-measure-role="header"> | |
| <div class="title-block"> | |
| <h1 class="title">Hello, <span class="accent">posterly</span></h1> | |
| <div class="subtitle">A minimal poster that passes every gate.</div> | |
| <div class="authors">A. Researcher · B. Collaborator · C. Author<sup>✉</sup></div> | |
| </div> | |
| <div class="qr-block"> | |
| <!-- Inline SVG QR placeholder so the template renders offline. | |
| Replace with a real QR before printing: | |
| $ qrencode -o qr.png -s 12 "<your-url>" | |
| $ python -c "import qrcode; qrcode.make('<your-url>').save('qr.png')" --> | |
| <img src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' fill='%23fff'/><g fill='%23222'><rect x='4' y='4' width='26' height='26'/><rect x='70' y='4' width='26' height='26'/><rect x='4' y='70' width='26' height='26'/></g><g fill='%23fff'><rect x='10' y='10' width='14' height='14'/><rect x='76' y='10' width='14' height='14'/><rect x='10' y='76' width='14' height='14'/></g><g fill='%23222'><rect x='14' y='14' width='6' height='6'/><rect x='80' y='14' width='6' height='6'/><rect x='14' y='80' width='6' height='6'/><rect x='38' y='10' width='4' height='4'/><rect x='48' y='10' width='4' height='4'/><rect x='58' y='10' width='4' height='4'/><rect x='34' y='34' width='4' height='4'/><rect x='44' y='38' width='4' height='4'/><rect x='54' y='34' width='4' height='4'/><rect x='62' y='40' width='4' height='4'/><rect x='38' y='46' width='4' height='4'/><rect x='50' y='52' width='4' height='4'/><rect x='62' y='56' width='4' height='4'/><rect x='34' y='60' width='4' height='4'/><rect x='44' y='66' width='4' height='4'/><rect x='56' y='62' width='4' height='4'/><rect x='80' y='80' width='4' height='4'/><rect x='88' y='86' width='4' height='4'/></g><text x='50' y='54' text-anchor='middle' font-family='sans-serif' font-size='6' font-weight='bold' fill='%23888'>QR</text></svg>" alt="QR placeholder"> | |
| <div class="qr-label">Paper</div> | |
| </div> | |
| </header> | |
| <div class="body-grid" data-measure-role="body"> | |
| <div class="column" data-measure-role="column"> | |
| <div class="card" data-measure-role="card"> | |
| <div class="section-title">What this is</div> | |
| <p class="body-text"> | |
| <span class="keyword">posterly</span> is a Claude Code skill that builds academic conference posters as a single HTML/CSS file rendered to print-ready PDF via headless Chromium. You iterate by <em>measuring</em>, not eyeballing — the screen preview lies; only <code>emulate_media("print")</code> at the exact viewport tells the truth. | |
| </p> | |
| <div class="figure"> | |
| <img alt="A bar chart placeholder" src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 240 120'><rect width='240' height='120' fill='%23F6F3F0'/><g fill='%232D5F8B'><rect x='15' y='80' width='28' height='30'/><rect x='55' y='55' width='28' height='55'/><rect x='95' y='35' width='28' height='75'/><rect x='135' y='25' width='28' height='85'/></g><g fill='%23C9A24A'><rect x='175' y='12' width='28' height='98'/></g><line x1='10' y1='110' x2='220' y2='110' stroke='%23555' stroke-width='1'/><text x='189' y='8' font-family='sans-serif' font-size='8' fill='%23C9A24A' font-weight='bold' text-anchor='middle'>ours</text></svg>"> | |
| <div class="caption">Hypothetical headline result. Last bar is “ours” in gold.</div> | |
| </div> | |
| <div class="section-title" style="margin-top: calc(4 * var(--u))">How the gates work</div> | |
| <ul> | |
| <li><b>preflight</b> — static lint: LaTeX residue, raw <code><</code> inside math, missing local images, role validation.</li> | |
| <li><b>measure</b> — print-emulated geometry: column-bottom spread (< 5 px), gap to next strip ([30, 50] px), poster bbox aligns to page within ±2 px.</li> | |
| <li><b>polish</b> — soft visual checks: figure-AR sizing, broken images, typography orphans, space-between fill.</li> | |
| <li><b>verify-final</b> — <code>pdfinfo</code>-based PDF sanity: page count, dimensions, file size.</li> | |
| </ul> | |
| <div class="section-title" style="margin-top: calc(4 * var(--u))">Why measure, not eyeball</div> | |
| <p class="body-text"> | |
| The screen preview <em>lies</em>: <code>@media print</code> uses a different unit scale, web fonts substitute, and a column that looks aligned on screen can sit 20 px off in the printed PDF. <span class="keyword">posterly</span> renders at the exact print viewport and reports numbers — so “looks aligned” becomes <code>spread = 0.0 px</code>, or it doesn't. | |
| </p> | |
| <ul> | |
| <li><b>Fill, don't stretch.</b> A card padded out with <code>flex: 1</code> clears the bottom-edge gate while half of it is blank — balance it with real content, never whitespace.</li> | |
| <li><b>Autocrop figures</b> so the image fills its card instead of floating in a gray margin.</li> | |
| <li><b>Guard trailing glyphs</b> (<code>×</code>, <code>↑</code>, <code>%</code>) with <code>white-space: nowrap</code> so a lone symbol never orphans onto its own line.</li> | |
| </ul> | |
| <div class="section-title" style="margin-top: calc(4 * var(--u))">What's in the box</div> | |
| <ul> | |
| <li><b>templates/</b> — portrait 2-column, landscape 4-column, and hero gallery, each pre-wired with the measurement roles.</li> | |
| <li><b>tools/</b> — the four-gate <code>poster_check.py</code> CLI plus a standalone <code>render_preview.py</code>.</li> | |
| <li><b>examples/</b> — this file: the smallest poster that clears every gate, your install check.</li> | |
| </ul> | |
| <p class="body-text" style="margin-top: calc(3 * var(--u))"> | |
| This example demonstrates every measurement role the gates look for: <span class="keyword">poster, header, body, column, card, footer-strip, footer</span>. For real multi-column posters, copy a template from <code>templates/</code> instead. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="footer-strip" data-measure-role="footer-strip"> | |
| <div class="hs-stat"> | |
| <div class="num">4 gates</div> | |
| <div class="lbl">preflight, measure, polish, verify-final</div> | |
| </div> | |
| <div class="hs-stat"> | |
| <div class="num">< 5 px</div> | |
| <div class="lbl">column-bottom spread target</div> | |
| </div> | |
| <div class="hs-stat"> | |
| <div class="num">A0–A4</div> | |
| <div class="lbl">named canvas sizes supported</div> | |
| </div> | |
| </div> | |
| <footer class="footer" data-measure-role="footer"> | |
| <div>posterly — build academic posters as HTML, render to print-ready PDF.</div> | |
| <div class="right">github.com/Chenruishuo/posterly</div> | |
| </footer> | |
| </div> | |
| </body> | |
| </html> | |
Xet Storage Details
- Size:
- 15.5 kB
- Xet hash:
- ef1e4c2be4186209e373ba80192074bb4d6af53ac89763499945d4bd43a3d9b6
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.