Skip to content

Instantly share code, notes, and snippets.

@maxparm
Last active March 27, 2026 18:16
Show Gist options
  • Select an option

  • Save maxparm/aa9a16f053c7011ff998b931358a8804 to your computer and use it in GitHub Desktop.

Select an option

Save maxparm/aa9a16f053c7011ff998b931358a8804 to your computer and use it in GitHub Desktop.
Hack Week: Draft Topics — The Missing Layer Between Meetings
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Draft Topics: The Platform Primitive Topicflow Needs</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, sans-serif;
background: #0f0f13;
color: #e4e4e7;
overflow: hidden;
height: 100vh;
}
.deck {
display: flex;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
height: 100vh;
}
.slide {
min-width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
padding: 6vh 10vw;
position: relative;
}
.progress {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: linear-gradient(90deg, #7c3aed, #a78bfa);
transition: width 0.5s ease;
z-index: 100;
}
.counter {
position: fixed;
bottom: 2vh;
right: 3vw;
font-size: 0.8rem;
color: #52525b;
z-index: 100;
font-variant-numeric: tabular-nums;
}
.nav-hint {
position: fixed;
bottom: 2vh;
left: 3vw;
font-size: 0.75rem;
color: #3f3f46;
z-index: 100;
}
/* Typography */
h1 {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.03em;
margin-bottom: 1.5rem;
}
h2 {
font-size: clamp(1.8rem, 3.5vw, 2.8rem);
font-weight: 700;
line-height: 1.15;
letter-spacing: -0.02em;
margin-bottom: 1.5rem;
}
h3 {
font-size: 1.1rem;
font-weight: 600;
color: #a78bfa;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 0.75rem;
}
p, li {
font-size: clamp(1rem, 1.5vw, 1.25rem);
line-height: 1.7;
color: #a1a1aa;
max-width: 48rem;
}
.subtitle {
font-size: clamp(1.1rem, 2vw, 1.4rem);
color: #71717a;
font-weight: 400;
max-width: 40rem;
}
.accent { color: #a78bfa; }
.accent-green { color: #4ade80; }
.accent-amber { color: #fbbf24; }
.accent-rose { color: #fb7185; }
.dim { color: #52525b; }
.bright { color: #e4e4e7; }
/* Lists */
ul { list-style: none; padding: 0; }
ul li {
padding: 0.5rem 0;
padding-left: 1.5rem;
position: relative;
}
ul li::before {
content: '';
position: absolute;
left: 0;
top: 1rem;
width: 6px;
height: 6px;
border-radius: 50%;
background: #7c3aed;
}
/* Flow diagram */
.flow {
display: flex;
align-items: center;
gap: 0;
margin: 2.5rem 0;
flex-wrap: wrap;
}
.flow-node {
padding: 1rem 1.5rem;
border-radius: 12px;
font-weight: 600;
font-size: 0.95rem;
white-space: nowrap;
}
.flow-arrow {
color: #3f3f46;
font-size: 1.5rem;
padding: 0 0.5rem;
}
.flow-node.purple { background: #7c3aed22; border: 1px solid #7c3aed55; color: #c4b5fd; }
.flow-node.green { background: #22c55e18; border: 1px solid #22c55e44; color: #86efac; }
.flow-node.amber { background: #f59e0b18; border: 1px solid #f59e0b44; color: #fcd34d; }
.flow-node.zinc { background: #27272a; border: 1px solid #3f3f46; color: #a1a1aa; }
.flow-node.rose { background: #f4336618; border: 1px solid #f4336644; color: #fca5a5; }
/* Callout */
.callout {
border-left: 3px solid #7c3aed;
padding: 1rem 1.5rem;
margin: 2rem 0;
background: #7c3aed08;
border-radius: 0 8px 8px 0;
max-width: 40rem;
}
.callout p {
color: #c4b5fd;
font-size: 1.15rem;
font-style: italic;
font-weight: 400;
}
/* Stacked items */
.stack {
display: flex;
flex-direction: column;
gap: 1rem;
margin: 1.5rem 0;
max-width: 36rem;
}
.stack-item {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.stack-icon {
font-size: 1.5rem;
flex-shrink: 0;
width: 2rem;
text-align: center;
}
.stack-item h4 {
font-size: 1rem;
font-weight: 600;
color: #e4e4e7;
margin-bottom: 0.2rem;
}
.stack-item p {
font-size: 0.9rem;
line-height: 1.5;
color: #71717a;
}
/* Title slide */
.slide.title-slide { justify-content: center; }
.slide.title-slide h1 { font-size: clamp(3rem, 6vw, 5rem); }
.tag {
display: inline-block;
padding: 0.3rem 0.75rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1.5rem;
}
.tag.purple { background: #7c3aed22; color: #a78bfa; border: 1px solid #7c3aed44; }
/* Object definition card */
.object-def {
background: #18181b;
border: 1px solid #27272a;
border-radius: 12px;
padding: 1.5rem 2rem;
margin: 2rem 0;
max-width: 36rem;
font-family: 'Inter', monospace;
}
.object-def .obj-name {
color: #a78bfa;
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 1rem;
}
.object-def .obj-field {
display: flex;
gap: 0.75rem;
padding: 0.3rem 0;
font-size: 0.9rem;
}
.object-def .obj-key {
color: #71717a;
min-width: 9rem;
}
.object-def .obj-val {
color: #d4d4d8;
}
/* Sources funnel diagram */
.sources-diagram {
display: flex;
align-items: center;
gap: 1.5rem;
margin: 2rem 0;
}
.sources-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.source-item {
padding: 0.6rem 1.2rem;
border-radius: 10px;
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
}
.source-item.s1 { background: #f4336618; border: 1px solid #f4336644; color: #fca5a5; }
.source-item.s2 { background: #7c3aed18; border: 1px solid #7c3aed44; color: #c4b5fd; }
.source-item.s3 { background: #22c55e12; border: 1px solid #22c55e33; color: #86efac; }
.source-item.s4 { background: #27272a; border: 1px solid #3f3f46; color: #a1a1aa; }
.funnel-arrow {
display: flex;
flex-direction: column;
align-items: center;
color: #3f3f46;
font-size: 2rem;
gap: 0;
line-height: 1;
}
.draft-box {
padding: 1.25rem 2rem;
border-radius: 12px;
background: #f59e0b12;
border: 2px solid #f59e0b55;
text-align: center;
}
.draft-box h4 {
color: #fcd34d;
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 0.3rem;
}
.draft-box p {
color: #71717a;
font-size: 0.8rem;
line-height: 1.4;
}
.manager-box {
padding: 1rem 1.5rem;
border-radius: 12px;
background: #27272a;
border: 1px solid #3f3f46;
text-align: center;
}
.manager-box h4 {
color: #e4e4e7;
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 0.2rem;
}
.manager-box p {
color: #52525b;
font-size: 0.75rem;
}
.meeting-box {
padding: 1.25rem 2rem;
border-radius: 12px;
background: #22c55e12;
border: 2px solid #22c55e44;
text-align: center;
}
.meeting-box h4 {
color: #86efac;
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 0.3rem;
}
.meeting-box p {
color: #71717a;
font-size: 0.8rem;
line-height: 1.4;
}
/* Kanban mockup */
.kanban-mock {
display: flex;
gap: 1rem;
margin: 2rem 0;
overflow-x: auto;
padding-bottom: 0.5rem;
}
.kanban-col {
min-width: 180px;
border-radius: 10px;
padding: 0.75rem;
flex-shrink: 0;
}
.kanban-col.ai { background: #7c3aed12; border: 1px solid #7c3aed33; }
.kanban-col.draft { background: #18181b; border: 1px solid #27272a; }
.kanban-col.meeting { background: #18181b; border: 1px solid #27272a; }
.kanban-col-title {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.4rem;
}
.kanban-col.ai .kanban-col-title { color: #a78bfa; }
.kanban-col.draft .kanban-col-title { color: #71717a; }
.kanban-col.meeting .kanban-col-title { color: #71717a; }
.kanban-card {
background: #27272a;
border: 1px solid #3f3f46;
border-radius: 8px;
padding: 0.6rem 0.75rem;
font-size: 0.78rem;
color: #d4d4d8;
margin-bottom: 0.4rem;
line-height: 1.4;
}
.kanban-col.ai .kanban-card {
background: #7c3aed15;
border-color: #7c3aed33;
}
/* Loop / proved it */
.loop-diagram {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 2rem 0;
flex-wrap: wrap;
}
.loop-step {
padding: 0.85rem 1.25rem;
border-radius: 10px;
font-weight: 600;
font-size: 0.85rem;
text-align: center;
min-width: 120px;
}
.loop-step.step-ai { background: #7c3aed18; border: 1px solid #7c3aed44; color: #c4b5fd; }
.loop-step.step-draft { background: #f59e0b12; border: 1px solid #f59e0b44; color: #fcd34d; }
.loop-step.step-meeting { background: #22c55e12; border: 1px solid #22c55e44; color: #86efac; }
.loop-step.step-signal { background: #27272a; border: 1px solid #3f3f46; color: #a1a1aa; }
.loop-arrow {
color: #3f3f46;
font-size: 1.3rem;
}
.loop-return {
display: flex;
align-items: center;
gap: 0.5rem;
margin-top: 0.75rem;
color: #52525b;
font-size: 0.85rem;
}
.loop-return .return-line {
flex: 1;
height: 1px;
background: #27272a;
}
/* Stat callout */
.stat {
display: inline-block;
font-size: clamp(3rem, 5vw, 4.5rem);
font-weight: 800;
color: #a78bfa;
letter-spacing: -0.03em;
line-height: 1;
margin-bottom: 0.5rem;
}
.stat-label {
font-size: clamp(1.1rem, 2vw, 1.5rem);
color: #71717a;
font-weight: 400;
max-width: 32rem;
}
/* Screenshot placeholder */
.screenshot-placeholder {
background: #18181b;
border: 2px dashed #27272a;
border-radius: 12px;
padding: 3rem 2rem;
text-align: center;
margin: 1.5rem 0;
max-width: 56rem;
}
.screenshot-placeholder p {
color: #3f3f46;
font-size: 0.9rem;
text-align: center;
max-width: none;
}
</style>
</head>
<body>
<div class="progress" id="progress"></div>
<div class="counter" id="counter"></div>
<div class="nav-hint">Arrow keys or click to navigate</div>
<div class="deck" id="deck">
<!-- SLIDE 1: Title -->
<section class="slide title-slide">
<span class="tag purple">Hack Week 2026</span>
<h1>Draft Topics</h1>
<p class="subtitle">Topicflow needs a meeting-free object. Here's why, and here's what I built.</p>
</section>
<!-- SLIDE 2: The Problem (one slide, sharp) -->
<section class="slide">
<h3>The Problem</h3>
<div class="stat">0</div>
<p class="stat-label">external systems can create an object in Topicflow today without knowing which meeting to put it in.</p>
<div class="callout" style="margin-top: 2.5rem;">
<p>Imagine if Linear required every issue to be scheduled into a Sprint the moment it was created. No backlog. No triage. Just: pick a Sprint or it doesn't exist.</p>
</div>
<p style="margin-top: 1.5rem;">That's us. A Slack bot, an AI agent, a webhook &mdash; they all hit the same wall: <em>you can't create a topic without a meeting.</em></p>
</section>
<!-- SLIDE 3: The Primitive -->
<section class="slide">
<h3>The Primitive</h3>
<h2>A topic that exists<br><span class="accent">without a meeting</span></h2>
<div class="object-def">
<div class="obj-name">DraftTopic</div>
<div class="obj-field"><span class="obj-key">title</span><span class="obj-val">"Follow up on oncall rotation"</span></div>
<div class="obj-field"><span class="obj-key">relationship</span><span class="obj-val">Manager &harr; Report</span></div>
<div class="obj-field"><span class="obj-key">notes</span><span class="obj-val">Rich text (facilitator / subject)</span></div>
<div class="obj-field"><span class="obj-key">meeting</span><span class="obj-val accent-rose" style="color: #fb7185;">null</span></div>
</div>
<p>Scoped to a relationship, not a meeting. The manager decides <em>if</em> and <em>when</em> it reaches the agenda. The creator just needs to know who it's about.</p>
</section>
<!-- SLIDE 4: What This Unlocks (the money diagram) -->
<section class="slide">
<h3>What This Unlocks</h3>
<h2>Many sources, <span class="accent">one inbox</span>,<br>manager decides</h2>
<div class="sources-diagram">
<div class="sources-list">
<div class="source-item s1">Slack bot</div>
<div class="source-item s2">AI coaching agent</div>
<div class="source-item s3">Integration / webhook</div>
<div class="source-item s4">Manager (manual)</div>
</div>
<div class="funnel-arrow">&rarr;</div>
<div class="draft-box">
<h4>Draft Topics</h4>
<p>Refine, add notes,<br>decide importance</p>
</div>
<div class="funnel-arrow">&rarr;</div>
<div class="manager-box">
<h4>Manager curates</h4>
<p>promote / dismiss / wait</p>
</div>
<div class="funnel-arrow">&rarr;</div>
<div class="meeting-box">
<h4>Meeting Agenda</h4>
<p>Only what's ready,<br>when it's ready</p>
</div>
</div>
<p>Creation is decoupled from scheduling. Any tool writes to the inbox. The manager owns the agenda.</p>
</section>
<!-- SLIDE 5: Demo — what I built -->
<section class="slide">
<h3>What I Built This Week</h3>
<h2>A kanban board for<br><span class="accent">every manager &harr; report pair</span></h2>
<div class="kanban-mock">
<div class="kanban-col ai">
<div class="kanban-col-title">&#x2728; AI Insights</div>
<div class="kanban-card">Talk about career growth &mdash; no discussion in 3 months</div>
<div class="kanban-card">Recognize recent shipping velocity</div>
</div>
<div class="kanban-col draft">
<div class="kanban-col-title">&#x1F4DD; Draft Topics</div>
<div class="kanban-card">Discuss Q2 project ownership</div>
<div class="kanban-card">Follow up on conference talk idea</div>
</div>
<div class="kanban-col meeting">
<div class="kanban-col-title">&#x1F4C5; Next 1-on-1 &mdash; Apr 1</div>
<div class="kanban-card">Q1 retro &amp; wins</div>
</div>
<div class="kanban-col meeting">
<div class="kanban-col-title">&#x1F4C5; Last 1-on-1 &mdash; Mar 25</div>
<div class="kanban-card">Team reorg update</div>
<div class="kanban-card">On-call rotation concerns</div>
</div>
</div>
<p><span class="bright">AI Insights</span> column loads coaching suggestions on demand. <span class="bright">"Add to next meeting"</span> promotes a draft with one click. Below the board: collapsible <span class="bright">Discussed</span>, <span class="bright">Undiscussed</span>, and <span class="bright">Dismissed</span> lists.</p>
<p style="margin-top: 0.5rem; color: #52525b; font-size: 0.85rem;">Backend: new DraftTopic fields on BaseTopic + kanban_meetings resolver. Frontend: full working board at /kanban.</p>
</section>
<!-- SLIDE 6: The loop I proved -->
<section class="slide">
<h3>The Loop I Proved</h3>
<h2>AI insight &rarr; draft &rarr; agenda &rarr; <span class="accent">signal</span></h2>
<div class="loop-diagram">
<div class="loop-step step-ai">AI surfaces<br>coaching insight</div>
<div class="loop-arrow">&rarr;</div>
<div class="loop-step step-draft">Manager accepts<br>as draft topic</div>
<div class="loop-arrow">&rarr;</div>
<div class="loop-step step-meeting">Promoted to<br>meeting agenda</div>
<div class="loop-arrow">&rarr;</div>
<div class="loop-step step-signal">Discussed?<br>Dismissed?</div>
</div>
<div class="loop-return">
<span>&larr;</span>
<span class="return-line"></span>
<span style="color: #71717a;">Outcome feeds back into AI &mdash; learns what this manager cares about</span>
</div>
<ul style="margin-top: 2rem;">
<li>This loop <span class="bright">works end-to-end</span> in the hack week build</li>
<li>Every accept, dismiss, and promotion is a <span class="bright">signal</span> the current system doesn't capture</li>
<li>Replace "AI" with "Slack bot" or "webhook" and the loop still works &mdash; the primitive is the same</li>
</ul>
</section>
<!-- SLIDE 7: Strategic — why this matters -->
<section class="slide">
<h3>Why This Matters</h3>
<h2>Every feature gets better when it can <span class="accent">read and write drafts</span></h2>
<div class="stack">
<div class="stack-item">
<div class="stack-icon">&#x1F50C;</div>
<div>
<h4>Integrations become possible</h4>
<p>Slack, GitHub, Jira, HRIS systems &mdash; all can write draft topics without understanding our calendar or meeting model.</p>
</div>
</div>
<div class="stack-item">
<div class="stack-icon">&#x1F916;</div>
<div>
<h4>AI agents get a landing pad</h4>
<p>Agents detect events (missed goals, PR patterns, recognition moments) and push drafts. The manager curates, not the bot.</p>
</div>
</div>
<div class="stack-item">
<div class="stack-icon">&#x1F4CB;</div>
<div>
<h4>Reviews write themselves</h4>
<p>Draft history + notes = a real-time log of manager observations. No more "try to remember 6 months of context."</p>
</div>
</div>
<div class="stack-item">
<div class="stack-icon">&#x1F4C8;</div>
<div>
<h4>Meeting prep goes from shallow to deep</h4>
<p>AI sees accumulated context between meetings, not just what was discussed last time.</p>
</div>
</div>
</div>
</section>
<!-- SLIDE 8: What's Next -->
<section class="slide">
<h3>What's Next</h3>
<h2>From hack week to <span class="accent">roadmap</span></h2>
<div class="stack">
<div class="stack-item">
<div class="stack-icon">&#x1F50C;</div>
<div>
<h4>Slack bot: create drafts from chat</h4>
<p><span style="color: #d4d4d8; font-family: monospace; font-size: 0.85rem;">/topicflow draft @sarah Follow up on oncall rotation</span></p>
</div>
</div>
<div class="stack-item">
<div class="stack-icon">&#x1F517;</div>
<div>
<h4>Public API / webhook endpoint</h4>
<p>Any third-party system can push a draft topic. HR tools, project trackers, performance platforms.</p>
</div>
</div>
<div class="stack-item">
<div class="stack-icon">&#x1F916;</div>
<div>
<h4>AI agent auto-drafting</h4>
<p>Proactive agents that detect signals and create drafts without manager intervention.</p>
</div>
</div>
<div class="stack-item">
<div class="stack-icon">&#x1F4CA;</div>
<div>
<h4>Feed draft history into reviews</h4>
<p>The accumulated log of topics, notes, and outcomes becomes source material for performance reviews.</p>
</div>
</div>
</div>
</section>
<!-- SLIDE 9: Close -->
<section class="slide title-slide">
<div class="stat" style="margin-bottom: 1rem;">0 &rarr; &infin;</div>
<h2>Today, zero external systems can create<br>a topic in Topicflow.</h2>
<p class="subtitle" style="margin-top: 1rem;">Draft topics change that number to <em>all of them</em>.<br>The manager stays in control. The platform opens up.</p>
<p style="margin-top: 3rem; color: #52525b; font-size: 0.9rem;">hackweek &middot; march 2026 &middot; maxime</p>
</section>
</div>
<script>
let current = 0;
const deck = document.getElementById('deck');
const progress = document.getElementById('progress');
const counter = document.getElementById('counter');
const slides = document.querySelectorAll('.slide');
const total = slides.length;
function goTo(n) {
current = Math.max(0, Math.min(n, total - 1));
deck.style.transform = `translateX(-${current * 100}vw)`;
progress.style.width = `${((current + 1) / total) * 100}%`;
counter.textContent = `${current + 1} / ${total}`;
}
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); goTo(current + 1); }
if (e.key === 'ArrowLeft') { e.preventDefault(); goTo(current - 1); }
});
document.addEventListener('click', (e) => {
if (e.clientX > window.innerWidth / 2) goTo(current + 1);
else goTo(current - 1);
});
let touchStartX = 0;
document.addEventListener('touchstart', (e) => { touchStartX = e.touches[0].clientX; });
document.addEventListener('touchend', (e) => {
const diff = touchStartX - e.changedTouches[0].clientX;
if (Math.abs(diff) > 50) { diff > 0 ? goTo(current + 1) : goTo(current - 1); }
});
goTo(0);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment