skills/build-session-deck/SKILL.md
Generate a complete /present system for a project demo — a hub that lists presentations, and per-presentation Present / Speaker / Clicker views plus a Prep learning center. Self-contained HTML deck with a PowerPoint-style presenter view (speaker notes + timer + next-slide), a phone PWA remote (clicker or speaker mode) synced cross-device, a scannable QR to open it, ArrowUp/Down font zoom, and a companion study page covering the tech + industry + business-case domains. Use when presenting a project/demo to a team, an interviewer, or a weekly review. Triggers on "build session presentation", "demo deck", "present what I built", "presentation hub", "/present", "prep / learning center for a demo", "slides for the demo", "weekly demo".
npx skillsauth add RonanCodes/ronan-skills build-session-deckInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
A self-contained HTML deck for presenting what you built. One file, opens in any browser, looks great, and has a real presenter view: speaker notes, a timer, and a next-slide preview on your laptop while the clean slide shows on the projector — synced live with no server.
A presenter (or a stand-in presentee) needs to get up to speed fast on the material before they present or field questions. So alongside the deck, generate a companion Prep page (a learning center) that teaches the concepts behind the demo. Structure it by the domains that matter for THIS pitch, typically:
Make it skimmable and optionally interactive (expandable Q&A, "could they ask…" prompts, a quick self-quiz). Source it from real material (the repo, design docs, research notes), not invented. It pairs with the deck: the deck is what you show, the Prep is what you know. Surface it from the presentation hub (below) as a "Prep" / "Study up" option.
Serve presentations from a hub route (e.g. /present): list the available presentations, pick one, then from that presentation's page offer Present / Speaker / Clicker views (QR beside the clicker for the phone remote) plus the Prep learning center. One entry point, many decks, each with its show-views and its study-view.
The canonical experience, build all of it:
/present hub — lists the available presentations (cards). There may be several over time, so this is a picker, not a single deck. Each card → that presentation's page.?...) — clean fullscreen audience slides.?mode=speaker) — slide + notes + timer + next.?mode=clicker) — phone remote (split-tap), shown with a QR code beside it so you scan and go.?mode=remote) — a chooser on the phone: Clicker or Speaker.
N/total · title), and a Notes toggle. Tapping "Notes" reveals the current slide's speaker notes plus a "Next:" one-line summary of the upcoming slide, inline above the thumb zones on the same screen — never a navigation to a separate page. Tap again to hide. The Prev/Next zones stay live underneath so you keep clicking with notes open.ArrowUp / ArrowDown scale all slide text (root font-scale, clamped, persisted in localStorage). Left/Right stay navigation.manifest + apple-mobile-web-app-capable so the remote adds to the home screen as a standalone app, with an icon.Reference implementation: the Makely repo (housapp-bieden) under apps/web/public/present/ (hub + makely.html deck) — copy its structure.
A single project demo is often better as several themed decks than one long one. Support splitting a presentation into N named themes, each a self-contained deck (own slides + speaker notes) under the same /present hub, wired so you move between them from the clicker alone.
present/makely-business.html, -build.html, -way-of-working.html).?mode= and room params so the clicker stays connected) and that deck ends with its own chooser offering the remaining one(s). So from the remote you traverse the whole set and back, no mouse, no hub round-trip. Define the set as an ordered THEMES array each deck knows, so "next" is deterministic and every deck can render the chooser.room stable across decks so the laptop + phone stay in lockstep through the jump.A strong default theme set for pitching any project you built (offer it by name when the user is presenting a project/product):
Fill each from real project material (repo, design docs, the About page, research notes), never invented.
The deck runs in two modes from the same file, same machine:
deck.html) — clean fullscreen slides. Drag this window to the projector (extended display).deck.html?mode=speaker, or press S) — current slide + speaker notes + next-slide preview + elapsed timer + clock. Keep on your laptop.They sync through BroadcastChannel (same-origin, same browser, same machine): moving slides in either window moves both. No backend, no Durable Object.
For a phone as a thumb clicker (or any second device), BroadcastChannel can't reach across devices — you need a network relay. Don't build a Durable Object from scratch: if the app already has Supabase, use Supabase Realtime broadcast as the relay (zero new infra, the anon key is public-safe). Otherwise a tiny WebSocket/Durable Object is the fallback.
?mode=clicker screen: a full-height split with a left "‹ Prev" zone and a right "Next ›" zone, huge thumb targets, touch-action:manipulation + user-select:none so taps don't zoom/select, 100dvh + env(safe-area-inset-*) for notch safety, and a status line showing N / total · current title so the presenter never looks up. Add a manifest + apple-mobile-web-app-capable so it adds to the home screen.BroadcastChannel links the two laptop windows; Realtime links the phone. A go() that navigates locally broadcasts to both; an apply() that receives a peer event updates without re-broadcasting (prevents loops; set Realtime broadcast:{self:false}). A room param namespaces channels so two presenters don't collide.import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const sb = createClient(URL, ANON);
const room = sb.channel('deck:'+ROOM, { config:{ broadcast:{ self:false } } });
room.on('broadcast', { event:'nav' }, ({payload}) => apply(payload.i)); // receive: no re-broadcast
room.subscribe();
const net = (i) => room.send({ type:'broadcast', event:'nav', payload:{ i } });
function go(n){ apply(n); bc.postMessage({i}); net(i); } // local nav → both relays
A Durable Object / dedicated WebSocket is only worth it when there's no Supabase and you need many concurrent rooms — note it as the upgrade path, don't default to it.
CLAUDE.md/README, git log for what shipped, any design docs (vault wiki/concepts/*), the live URLs, the data model, and the tech choices. For a demo deck, accuracy > polish — every claim should trace to something real.<repo>/.deck/<slug>.html (gitignored) or a path the user names, and open it.Scale up or down to the audience. An interview build-session wants 8-12 slides; a weekly team demo wants 5-7.
stroke="currentColor"), official vendor / simple-icons SVGs for tech logos, or clean text labels. An emoji on a pitch deck reads as unfinished; a real icon reads as built. If a glyph is decorative and adds no meaning (e.g. a coloured dot before a tech name), drop it entirely rather than reaching for an emoji.SLIDES + notes)Self-contained. Slides + speaker notes live in one SLIDES array. Keyboard: ←/→/Space navigate, S opens speaker view, F fullscreen. BroadcastChannel keeps both windows in lockstep.
<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"><title>DECK TITLE</title>
<style>
:root{--bg:#0f1115;--fg:#f5f5f4;--muted:#a1a1aa;--accent:#7c3aed;}
*{box-sizing:border-box;margin:0} html,body{height:100%}
body{background:var(--bg);color:var(--fg);font:18px/1.5 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto}
#stage{height:100vh;display:flex;align-items:center;justify-content:center;padding:6vmin;text-align:center}
.slide{display:none;max-width:1100px;animation:in .4s ease} .slide.active{display:block}
@keyframes in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
h1{font-size:clamp(40px,7vmin,84px);line-height:1.05;letter-spacing:-.02em;font-weight:800}
h2{font-size:clamp(30px,5vmin,56px);line-height:1.1;letter-spacing:-.01em;font-weight:700;margin-bottom:.4em}
p,li{font-size:clamp(18px,2.6vmin,28px);color:var(--muted)} ul{margin:.6em auto;max-width:800px;text-align:left}
li{margin:.4em 0} .eyebrow{text-transform:uppercase;letter-spacing:.18em;font-size:14px;color:var(--accent);font-weight:700;margin-bottom:1em}
.accent{color:var(--fg)} a{color:var(--accent)}
#bar{position:fixed;bottom:0;left:0;height:4px;background:var(--accent);transition:width .3s} #num{position:fixed;bottom:14px;right:18px;color:var(--muted);font-size:14px}
/* speaker view */
body.speaker #stage{display:none} #spk{display:none}
body.speaker #spk{display:grid;grid-template-columns:1.4fr 1fr;grid-template-rows:1fr auto;gap:18px;height:100vh;padding:24px}
#spk .now{background:#000;border-radius:14px;display:flex;align-items:center;justify-content:center;padding:30px;overflow:hidden}
#spk .now .slide{display:block;transform:scale(.85)} #spk .notes{overflow:auto;background:#17181d;border-radius:14px;padding:24px;font-size:22px;line-height:1.5;color:#e7e7ea}
#spk .notes h3{color:var(--accent);font-size:14px;text-transform:uppercase;letter-spacing:.14em;margin-bottom:.6em}
#spk .next{grid-column:1;background:#17181d;border-radius:14px;padding:16px;color:var(--muted);font-size:16px}
#spk .meta{grid-column:2;display:flex;gap:18px;align-items:center;justify-content:flex-end;font-variant-numeric:tabular-nums;font-size:30px}
#spk .meta small{display:block;color:var(--muted);font-size:13px;text-transform:uppercase;letter-spacing:.12em}
</style></head><body>
<div id="stage"></div>
<div id="spk"><div class="now" id="spkNow"></div><div class="notes"><h3>Speaker notes</h3><div id="spkNotes"></div></div>
<div class="next" id="spkNext"></div><div class="meta"><div><small>Elapsed</small><span id="elapsed">00:00</span></div><div><small>Time</small><span id="clock">--:--</span></div></div></div>
<div id="bar"></div><div id="num"></div>
<script>
// FILL THIS: one entry per slide. html = the slide markup, notes = what to say.
const SLIDES = [
{ html:`<p class="eyebrow">Build session demo</p><h1>Project <span class="accent">Name</span></h1><p>One striking line.</p>`,
notes:`Open strong. Say the one-liner. Set the frame: 'here's what I built and why.'` },
{ html:`<h2>The problem</h2><ul><li>Who hurts.</li><li>Why now.</li></ul>`, notes:`Make them feel the pain before the solution.` },
// ...more slides...
];
const ch = new BroadcastChannel('deck');
let i = 0; const isSpeaker = new URLSearchParams(location.search).get('mode')==='speaker';
if(isSpeaker) document.body.classList.add('speaker');
function renderStage(idx){ const s=SLIDES[idx]; document.getElementById('stage').innerHTML=`<div class="slide active">${s.html}</div>`; }
function renderSpeaker(idx){ const s=SLIDES[idx], n=SLIDES[idx+1];
document.getElementById('spkNow').innerHTML=`<div class="slide">${s.html}</div>`;
document.getElementById('spkNotes').innerHTML=(s.notes||'').replace(/\n/g,'<br>');
document.getElementById('spkNext').innerHTML = n?`<b>Next:</b> ${n.html.replace(/<[^>]+>/g,' ').slice(0,90)}…`:'<b>Last slide.</b>'; }
function paint(){ if(isSpeaker) renderSpeaker(i); else renderStage(i);
document.getElementById('bar').style.width=((i+1)/SLIDES.length*100)+'%';
document.getElementById('num').textContent=(i+1)+' / '+SLIDES.length; }
function go(n,broadcast=true){ i=Math.max(0,Math.min(SLIDES.length-1,n)); paint(); if(broadcast) ch.postMessage({i}); }
ch.onmessage=e=>{ if(typeof e.data.i==='number'){ i=e.data.i; paint(); } };
let zoom = +(localStorage.getItem('deckZoom')||1);
function applyZoom(){ document.getElementById('stage').style.fontSize=(zoom*100)+'%'; localStorage.setItem('deckZoom',zoom); }
addEventListener('keydown',e=>{ if(e.key==='ArrowRight'||e.key===' ')go(i+1);
else if(e.key==='ArrowLeft')go(i-1);
else if(e.key==='ArrowUp'){ zoom=Math.min(2,zoom+0.1); applyZoom(); } // font zoom (persisted)
else if(e.key==='ArrowDown'){ zoom=Math.max(0.6,zoom-0.1); applyZoom(); }
else if(e.key==='s'||e.key==='S')window.open(location.pathname+'?mode=speaker','speaker','width=1100,height=700');
else if(e.key==='f'||e.key==='F'){document.fullscreenElement?document.exitFullscreen():document.documentElement.requestFullscreen();} });
applyZoom();
// timer + clock (speaker view)
const t0=Date.now(); setInterval(()=>{ const s=Math.floor((Date.now()-t0)/1000);
const el=document.getElementById('elapsed'); if(el)el.textContent=String(Math.floor(s/60)).padStart(2,'0')+':'+String(s%60).padStart(2,'0');
const c=document.getElementById('clock'); if(c){const d=new Date();c.textContent=String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');} },1000);
go(0,false);
</script></body></html>
Proven on the Makely build-session deck:
Decks skew too wordy. Push for trimmed copy (one idea per slide), official tech-stack SVG icons (vendor or simple-icons), and at least one image per section. Store generic tech icons in this skill's assets/; keep domain imagery in a resources vault (an LLM-wiki vault of icons/images that decks + UI skills reference) so it is reusable. Nudge the user to create that resources vault.
zoom, never transform: scale(). Transform-scale animates a per-slide transition (the deck visibly "jumps" to the right size on each navigation) and does not reflow, so text overflows when enlarged. zoom: var(--zoom) on the slide reflows responsively with no transition jump. Keep --zoom global (on :root) + persisted in localStorage so it is identical across every slide and across reloads.L), persisted. Presenters need to brighten a deck for a bright room. Drive ALL surface/border colors from CSS vars (--bg --fg --muted --surface --line) and add a body.light{} override toggled on L. Never hardcode dark hex on cards/pills or light mode breaks.git push to main auto-deploys (no vercel CLI). The custom domain can lag the edge cache a few minutes; the raw *.vercel.app URL is always fresh for verification.S, syncs via BroadcastChannel, shows notes + timer + next.ArrowUp/ArrowDown zoom slide text (persisted); Left/Right navigate./present hub lists presentations; each has Present / Speaker / Clicker + Prep.testing
--- name: linear-pipeline description: The Fable orchestrator for a single dispatched Linear ticket. Holds almost no context itself; it receives `--issue <ID> --detached`, decides the stage sequence, and fans out a sub-agent per stage, passing forward only each stage's artifact (never re-derived, never inlined into its own context). Step zero, before any planning or stage routing, is a boundary triage against `canon/security-boundary.md` (#199): a match tags Ronan Connolly and stops the run, no
development
--- name: in-your-face description: Capture a chat-only answer into a durable artifact (markdown + HTML, PDF when cheap) and launch it automatically so the user cannot miss it. Use when user says "in your face", "don't let me lose this", "save that answer", "make that durable", or right after answering a substantive side question (a recipe, comparison, how-to, or generated prompt) that would otherwise die with the context. category: workflow argument-hint: [--no-open] [--vault <short>] [hint of
tools
One-shot headless OpenAI Codex CLI calls for background/admin AI tasks — summaries, classification, extraction, admin glue. The default engine for anything that runs AI constantly in the background (daemon-driven, per-event), because it bills the flat ChatGPT subscription instead of Claude usage or per-token API spend, and it keeps working while Claude is rate-limited. NEVER for coding — coding stays Claude. Use when a skill or daemon needs a cheap always-on AI call, when the user says "use codex", "ask codex", "codex as backup", or when building a background summarizer/classifier into a listener or loop. Reads auth from ~/.codex/auth.json (ChatGPT account, no API key).
research
Turn a warranty rejection, repair quote, or RMA email into a cited decision brief — legal read (NL/EU consumer law), is the part user-serviceable, live part and new-unit prices, repair-vs-DIY-vs-new economics, before-you-send-it checklist, deadlines. Use when the user pastes or screenshots a repair quote, warranty rejection, "not covered" email, onderzoekskosten fee, or asks "should I repair or replace this".