
Napkin
- 2.1k installs
- 37.1k repo stars
- Updated July 28, 2026
- github/awesome-copilot
napkin is an agent skill that Visual whiteboard collaboration for Copilot CLI. Creates an interactive whiteboard that opens in your browser — draw, sk.
About
Napkin Visual Whiteboard for Copilot CLI Napkin gives users a browser based whiteboard where they can draw sketch and add sticky notes to think through ideas visually The agent reads back the whiteboard contents via a PNG snapshot and optional JSON data and responds conversationally with analysis suggestions and next steps The target audience is lawyers PMs and business stakeholders not software developers Keep everything approachable and jargon free When the user invokes this skill saying things like let s napkin open a napkin start a whiteboard or using the slash command do the following 1 Copy the bundled HTML template from the skill assets to the user s Desktop The template lives at assets napkin html relative to this SKILL md file Copy it to Desktop napkin html If Desktop napkin html already exists ask the user whether they want to open the existing one or start fresh before overwriting
- description: 'Visual whiteboard collaboration for Copilot CLI. Creates an interactive whiteboard that opens in your brow
- Napkin gives users a browser-based whiteboard where they can draw, sketch, and add sticky notes to think through ideas v
- The target audience is lawyers, PMs, and business stakeholders — not software developers. Keep everything approachable a
- Follow napkin SKILL.md steps and documented constraints.
- Follow napkin SKILL.md steps and documented constraints.
Napkin by the numbers
- 2,066 all-time installs (skills.sh)
- +34 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #547 of 16,659 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
napkin capabilities & compatibility
- Capabilities
- description: 'visual whiteboard collaboration fo · napkin gives users a browser based whiteboard wh · the target audience is lawyers, pms, and busines · follow napkin skill.md steps and documented cons
- Use cases
- orchestration
What napkin says it does
description: 'Visual whiteboard collaboration for Copilot CLI. Creates an interactive whiteboard that opens in your browser — draw, sketch, add sticky notes, then share everything back with Copilot. C
Napkin gives users a browser-based whiteboard where they can draw, sketch, and add sticky notes to think through ideas visually. The agent reads back the whiteboard contents (via a PNG snapshot and op
The target audience is lawyers, PMs, and business stakeholders — not software developers. Keep everything approachable and jargon-free.
npx skills add https://github.com/github/awesome-copilot --skill napkinAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.1k |
|---|---|
| repo stars | ★ 37.1k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | github/awesome-copilot ↗ |
When should an agent use napkin and what problem does it solve?
Visual whiteboard collaboration for Copilot CLI. Creates an interactive whiteboard that opens in your browser — draw, sketch, add sticky notes, then share everything back with Copilot. Copilot sees yo
Who is it for?
Developers invoking napkin as documented in the skill source.
Skip if: Skip when requirements fall outside napkin documented scope.
When should I use this skill?
Visual whiteboard collaboration for Copilot CLI. Creates an interactive whiteboard that opens in your browser — draw, sketch, add sticky notes, then share everything back with Copilot. Copilot sees yo
What you get
Outputs aligned with the napkin SKILL.md workflow and stated deliverables.
- PNG whiteboard snapshot
- optional JSON export
- agent analysis response
Files
Napkin — Visual Whiteboard for Copilot CLI
Napkin gives users a browser-based whiteboard where they can draw, sketch, and add sticky notes to think through ideas visually. The agent reads back the whiteboard contents (via a PNG snapshot and optional JSON data) and responds conversationally with analysis, suggestions, and next steps.
The target audience is lawyers, PMs, and business stakeholders — not software developers. Keep everything approachable and jargon-free.
---
Activation
When the user invokes this skill — saying things like "let's napkin," "open a napkin," "start a whiteboard," or using the slash command — do the following:
1. Copy the bundled HTML template from the skill assets to the user's Desktop.
- The template lives at
assets/napkin.htmlrelative to this SKILL.md file. - Copy it to
~/Desktop/napkin.html. - If
~/Desktop/napkin.htmlalready exists, ask the user whether they want to open the existing one or start fresh before overwriting.
2. Open it in the default browser:
- macOS:
open ~/Desktop/napkin.html - Linux:
xdg-open ~/Desktop/napkin.html - Windows:
start ~/Desktop/napkin.html
3. Tell the user what to do next. Say something warm and simple:
Your napkin is open in your browser!
Draw, sketch, or add sticky notes — whatever helps you think through your idea.
When you're ready for my input, click the green "Share with Copilot" button on the whiteboard, then come back here and say "check the napkin."---
Reading the Napkin
When the user says "check the napkin," "look at the napkin," "what do you think," "read my napkin," or anything similar, follow these steps:
Step 1 — Read the PNG snapshot (primary)
Look for a PNG file called napkin-snapshot.png. Check these locations in order (the browser saves it to the user's default download folder, which varies):
1. ~/Downloads/napkin-snapshot.png 2. ~/Desktop/napkin-snapshot.png
Use the view tool to read the PNG. This sends the image as base64-encoded data to the model, which can visually interpret it. The PNG is the primary way the agent understands what the user drew — it captures freehand sketches, arrows, spatial layout, annotations, circled or crossed-out items, and anything else on the canvas.
If the PNG is not found in either location, do NOT silently skip it. Instead, tell the user:
I don't see a snapshot from your napkin yet. Here's what to do:
1. Go to your whiteboard in the browser
2. Click the green "Share with Copilot" button
3. Come back here and say "check the napkin" again
The button saves a screenshot that I can look at.Step 2 — Read the clipboard for structured JSON (supplementary)
Also try to grab structured JSON data from the system clipboard. The whiteboard copies this automatically alongside the PNG.
- macOS:
pbpaste - Linux:
xclip -selection clipboard -o - Windows:
powershell -command "Get-Clipboard"
The JSON contains the exact text content of sticky notes and text labels, their positions, and their colors. This supplements the PNG by giving you precise text that might be hard to read from a screenshot.
If the clipboard doesn't contain JSON data, that's fine — the PNG alone gives the model plenty to work with. Do not treat a missing clipboard as an error.
Step 3 — Interpret both sources together
Synthesize the visual snapshot and the structured text into a coherent understanding of what the user is thinking or planning:
- From the PNG: Describe what you see — sketches, diagrams, flowcharts, groupings, arrows, spatial layout, annotations, circled items, crossed-out items, emphasis marks.
- From the JSON: Read the exact text content of sticky notes and labels, noting their positions and colors.
- Combine both into a single, conversational interpretation.
Step 4 — Respond conversationally
Do not dump raw data or a technical summary. Respond as a collaborator who looked at someone's whiteboard sketch. Examples:
- "I can see you've sketched out a three-stage process — it looks like you're thinking about [X] flowing into [Y] and then [Z]. The sticky note in the corner says '[text]' — is that a concern you want me to address?"
- "It looks like you've grouped these four ideas together on the left side and separated them from the two items on the right. Are you thinking of these as two different categories?"
- "I see you drew arrows connecting [A] to [B] to [C] — is this the workflow you're envisioning?"
Step 5 — Ask what's next
Always end by offering a next step:
- "Want me to build on this?"
- "Should I turn this into a structured document?"
- "Want me to add my suggestions to the napkin?"
---
Responding on the Napkin
When the user wants the agent to add content back to the whiteboard:
- The agent cannot directly modify the HTML file's canvas state — that's managed by JavaScript running in the browser.
- Instead, offer practical alternatives:
- Provide the response right here in the CLI, and suggest the user add it to the napkin manually.
- Offer to create a separate document (markdown, memo, checklist, etc.) based on what was interpreted from the napkin.
- If it makes sense, create an updated copy of
napkin.htmlwith pre-loaded content.
---
Tone and Style
- Use the same approachable, non-technical tone as the noob-mode skill.
- Never use developer jargon without explaining it in plain English.
- Treat the napkin as a creative, collaborative space — not a formal input mechanism.
- Be encouraging about the user's sketches regardless of artistic quality.
- Frame responses as "building on your thinking," not "analyzing your input."
---
Error Handling
PNG snapshot not found:
I don't see a snapshot from your napkin yet. Here's what to do:
1. Go to your whiteboard in the browser
2. Click the green "Share with Copilot" button
3. Come back here and say "check the napkin" again
The button saves a screenshot that I can look at.Whiteboard file doesn't exist on Desktop:
It looks like we haven't started a napkin yet. Want me to open one for you?---
Important Notes
- The PNG interpretation is the primary channel. Multimodal models can read and interpret the base64 image data returned by the
viewtool. - The JSON clipboard data is supplementary — it provides precise text but does not capture freehand drawings.
- Always check for the PNG first. If it isn't found, prompt the user to click "Share with Copilot."
- If the clipboard doesn't have JSON data, proceed with the PNG alone.
- The HTML template is located at
assets/napkin.htmlrelative to this SKILL.md file. - If the noob-mode skill is also active, use its risk indicator format (green/yellow/red) when requesting file or bash permissions.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Napkin — Whiteboard for Copilot</title>
<style>
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #f5f5f5;
user-select: none;
-webkit-user-select: none;
}
/* ── Toolbar ───────────────────────────────────────────────────── */
#toolbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 72px;
background: #fafafa;
border-bottom: 1px solid #e0e0e0;
display: flex;
align-items: center;
padding: 0 12px;
gap: 4px;
z-index: 1000;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.toolbar-group {
display: flex;
align-items: center;
gap: 2px;
padding: 0 6px;
}
.toolbar-group + .toolbar-group {
border-left: 1px solid #e0e0e0;
margin-left: 4px;
padding-left: 10px;
}
.tool-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
border: 2px solid transparent;
border-radius: 10px;
background: transparent;
cursor: pointer;
transition: all 0.15s ease;
padding: 4px 2px 2px;
}
.tool-btn:hover {
background: #eee;
}
.tool-btn.active {
background: #e3f2fd;
border-color: #1e88e5;
}
.tool-btn .icon {
font-size: 20px;
line-height: 1;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.tool-btn .label {
font-size: 9px;
color: #666;
margin-top: 2px;
white-space: nowrap;
font-weight: 500;
letter-spacing: 0.02em;
}
.tool-btn.active .label {
color: #1e88e5;
}
/* Color picker */
.color-picker {
display: flex;
gap: 3px;
align-items: center;
padding: 0 4px;
}
.color-swatch {
width: 22px;
height: 22px;
border-radius: 50%;
border: 2px solid #ddd;
cursor: pointer;
transition: transform 0.1s ease;
}
.color-swatch:hover {
transform: scale(1.15);
}
.color-swatch.active {
border-color: #333;
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #333;
}
/* Stroke width buttons */
.stroke-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: 2px solid transparent;
border-radius: 8px;
background: transparent;
cursor: pointer;
}
.stroke-btn:hover {
background: #eee;
}
.stroke-btn.active {
background: #e3f2fd;
border-color: #1e88e5;
}
.stroke-btn .stroke-line {
background: #333;
border-radius: 4px;
width: 20px;
}
.stroke-btn .label {
font-size: 8px;
color: #888;
margin-top: 2px;
}
/* Share button */
.share-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: #0d9488;
color: #fff;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease, transform 0.1s ease;
margin-left: auto;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(13,148,136,0.3);
font-family: inherit;
}
.share-btn:hover {
background: #0f766e;
transform: translateY(-1px);
}
.share-btn:active {
transform: translateY(0);
}
.share-btn .icon {
font-size: 18px;
}
/* Help button */
.help-btn {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid #ccc;
background: #fff;
color: #888;
font-size: 16px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin-left: 8px;
flex-shrink: 0;
font-family: inherit;
}
.help-btn:hover {
border-color: #999;
color: #555;
}
/* ── Canvas area ───────────────────────────────────────────────── */
#canvas-container {
position: fixed;
top: 72px;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
background: #f0f0f0;
cursor: crosshair;
}
#canvas-container.panning {
cursor: grab;
}
#canvas-container.panning:active {
cursor: grabbing;
}
#drawing-canvas {
position: absolute;
background: #fff;
box-shadow: 0 2px 20px rgba(0,0,0,0.08);
}
/* ── Sticky notes ──────────────────────────────────────────────── */
.sticky-note {
position: absolute;
min-width: 140px;
min-height: 100px;
border-radius: 4px;
box-shadow: 2px 3px 12px rgba(0,0,0,0.12), 0 1px 4px rgba(0,0,0,0.06);
display: flex;
flex-direction: column;
z-index: 500;
font-family: inherit;
}
.sticky-note .note-header {
height: 24px;
border-radius: 4px 4px 0 0;
cursor: move;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 4px;
flex-shrink: 0;
opacity: 0.8;
}
.sticky-note .note-delete {
width: 18px;
height: 18px;
border: none;
background: rgba(0,0,0,0.15);
color: rgba(0,0,0,0.5);
border-radius: 50%;
font-size: 12px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.15s;
font-family: inherit;
}
.sticky-note:hover .note-delete {
opacity: 1;
}
.sticky-note .note-delete:hover {
background: rgba(0,0,0,0.3);
color: rgba(0,0,0,0.8);
}
.sticky-note .note-body {
flex: 1;
padding: 8px 12px 12px;
font-size: 14px;
line-height: 1.4;
outline: none;
cursor: text;
overflow-wrap: break-word;
word-break: break-word;
white-space: pre-wrap;
border-radius: 0 0 4px 4px;
min-height: 76px;
}
.sticky-note .note-resize {
position: absolute;
bottom: 0;
right: 0;
width: 16px;
height: 16px;
cursor: nwse-resize;
opacity: 0;
transition: opacity 0.15s;
}
.sticky-note:hover .note-resize {
opacity: 0.4;
}
.sticky-note .note-resize::after {
content: '';
position: absolute;
bottom: 3px;
right: 3px;
width: 8px;
height: 8px;
border-right: 2px solid rgba(0,0,0,0.3);
border-bottom: 2px solid rgba(0,0,0,0.3);
}
/* Sticky note colors */
.sticky-yellow { background: #fff9c4; }
.sticky-yellow .note-header { background: #fff176; }
.sticky-pink { background: #fce4ec; }
.sticky-pink .note-header { background: #f48fb1; }
.sticky-blue { background: #e3f2fd; }
.sticky-blue .note-header { background: #90caf9; }
.sticky-green { background: #e8f5e9; }
.sticky-green .note-header { background: #a5d6a7; }
/* Sticky note color picker in toolbar */
.note-color-picker {
display: none;
position: absolute;
top: 60px;
background: #fff;
border-radius: 10px;
padding: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
gap: 6px;
z-index: 1001;
}
.note-color-picker.show {
display: flex;
}
.note-color-opt {
width: 30px;
height: 30px;
border-radius: 6px;
border: 2px solid #ddd;
cursor: pointer;
}
.note-color-opt:hover {
border-color: #999;
}
/* ── Text labels on canvas ─────────────────────────────────────── */
.canvas-text-label {
position: absolute;
font-size: 16px;
color: #333;
outline: none;
cursor: text;
padding: 2px 4px;
min-width: 20px;
min-height: 20px;
white-space: pre-wrap;
z-index: 400;
border: 1px dashed transparent;
border-radius: 3px;
font-family: inherit;
background: transparent;
}
.canvas-text-label:focus {
border-color: #90caf9;
background: rgba(255,255,255,0.85);
}
/* ── Overlays ──────────────────────────────────────────────────── */
.overlay-backdrop {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.overlay-backdrop.hidden {
display: none;
}
.overlay-card {
background: #fff;
border-radius: 16px;
padding: 40px 44px;
max-width: 500px;
width: 90%;
box-shadow: 0 16px 48px rgba(0,0,0,0.18);
text-align: center;
}
.overlay-card h1 {
font-size: 26px;
font-weight: 700;
color: #222;
margin-bottom: 8px;
}
.overlay-card .subtitle {
font-size: 15px;
color: #666;
margin-bottom: 24px;
}
.overlay-card .steps {
text-align: left;
margin: 0 auto 28px;
max-width: 380px;
}
.overlay-card .steps .step {
display: flex;
gap: 12px;
margin-bottom: 14px;
font-size: 14px;
line-height: 1.5;
color: #444;
}
.overlay-card .steps .step-num {
flex-shrink: 0;
width: 26px;
height: 26px;
background: #0d9488;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
}
.overlay-card .cta-btn {
display: inline-block;
padding: 14px 32px;
background: #0d9488;
color: #fff;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease;
font-family: inherit;
}
.overlay-card .cta-btn:hover {
background: #0f766e;
}
/* Share confirmation */
.overlay-card .confirm-icon {
font-size: 48px;
margin-bottom: 12px;
}
.overlay-card .confirm-detail {
text-align: left;
background: #f5f5f5;
border-radius: 10px;
padding: 16px 20px;
margin: 16px 0 24px;
font-size: 13px;
line-height: 1.7;
color: #555;
}
.overlay-card .confirm-detail .clipboard-hint {
display: inline-block;
background: #e8f5e9;
color: #2e7d32;
padding: 2px 8px;
border-radius: 4px;
font-family: monospace;
font-size: 13px;
margin-top: 4px;
}
/* ── Keyboard shortcuts panel ──────────────────────────────────── */
.shortcuts-panel {
position: fixed;
bottom: 16px;
right: 16px;
background: #fff;
border-radius: 12px;
padding: 16px 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
z-index: 1001;
font-size: 12px;
display: none;
min-width: 220px;
}
.shortcuts-panel.show {
display: block;
}
.shortcuts-panel h3 {
font-size: 13px;
font-weight: 700;
margin-bottom: 10px;
color: #333;
}
.shortcuts-panel .shortcut-row {
display: flex;
justify-content: space-between;
padding: 3px 0;
color: #555;
}
.shortcuts-panel .shortcut-row kbd {
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
padding: 1px 6px;
font-family: monospace;
font-size: 11px;
color: #444;
}
.shortcuts-panel .close-shortcuts {
position: absolute;
top: 8px;
right: 10px;
border: none;
background: none;
cursor: pointer;
font-size: 16px;
color: #999;
}
/* ── Zoom indicator ────────────────────────────────────────────── */
.zoom-indicator {
position: fixed;
bottom: 16px;
left: 16px;
display: flex;
align-items: center;
gap: 6px;
background: #fff;
border-radius: 8px;
padding: 6px 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
font-size: 12px;
color: #555;
z-index: 1001;
}
.zoom-indicator button {
width: 26px;
height: 26px;
border: 1px solid #ddd;
border-radius: 6px;
background: #fff;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
color: #555;
font-family: inherit;
}
.zoom-indicator button:hover {
background: #f5f5f5;
}
/* ── Toast notification ────────────────────────────────────────── */
.toast {
position: fixed;
bottom: 60px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: #333;
color: #fff;
padding: 10px 20px;
border-radius: 8px;
font-size: 13px;
opacity: 0;
transition: all 0.3s ease;
z-index: 9998;
pointer-events: none;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
</style>
</head>
<body>
<!-- ── Toolbar ──────────────────────────────────────────────────── -->
<div id="toolbar">
<!-- Drawing tools -->
<div class="toolbar-group">
<button class="tool-btn active" data-tool="select" title="Select / Move (V)">
<span class="icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 3l14 9-7 2-4 7z"/></svg>
</span>
<span class="label">Select</span>
</button>
<button class="tool-btn" data-tool="pen" title="Pen (P)">
<span class="icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
</span>
<span class="label">Pen</span>
</button>
<button class="tool-btn" data-tool="line" title="Line (L)">
<span class="icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="19" x2="19" y2="5"/></svg>
</span>
<span class="label">Line</span>
</button>
<button class="tool-btn" data-tool="arrow" title="Arrow (A)">
<span class="icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="19" x2="19" y2="5"/><polyline points="10 5 19 5 19 14"/></svg>
</span>
<span class="label">Arrow</span>
</button>
<button class="tool-btn" data-tool="rect" title="Rectangle (R)">
<span class="icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/></svg>
</span>
<span class="label">Rect</span>
</button>
<button class="tool-btn" data-tool="ellipse" title="Circle (C)">
<span class="icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="12" rx="10" ry="8"/></svg>
</span>
<span class="label">Circle</span>
</button>
<button class="tool-btn" data-tool="eraser" title="Eraser (E)">
<span class="icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 20H7L3 16l9-9 8 8-4 5z"/><path d="M6 11l8 8"/></svg>
</span>
<span class="label">Eraser</span>
</button>
</div>
<!-- Text & Notes -->
<div class="toolbar-group">
<button class="tool-btn" data-tool="text" title="Text (T)">
<span class="icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 7 4 4 20 4 20 7"/><line x1="12" y1="4" x2="12" y2="20"/><line x1="8" y1="20" x2="16" y2="20"/></svg>
</span>
<span class="label">Text</span>
</button>
<button class="tool-btn" data-tool="note" id="note-tool-btn" title="Sticky Note (N)">
<span class="icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M14 3v8h8"/></svg>
</span>
<span class="label">Note</span>
</button>
<div class="note-color-picker" id="note-color-picker">
<div class="note-color-opt" data-note-color="yellow" style="background:#fff9c4;" title="Yellow"></div>
<div class="note-color-opt" data-note-color="pink" style="background:#fce4ec;" title="Pink"></div>
<div class="note-color-opt" data-note-color="blue" style="background:#e3f2fd;" title="Blue"></div>
<div class="note-color-opt" data-note-color="green" style="background:#e8f5e9;" title="Green"></div>
</div>
</div>
<!-- Color & stroke -->
<div class="toolbar-group">
<div class="color-picker">
<div class="color-swatch active" data-color="#222222" style="background:#222222;" title="Black"></div>
<div class="color-swatch" data-color="#e53935" style="background:#e53935;" title="Red"></div>
<div class="color-swatch" data-color="#1e88e5" style="background:#1e88e5;" title="Blue"></div>
<div class="color-swatch" data-color="#43a047" style="background:#43a047;" title="Green"></div>
<div class="color-swatch" data-color="#fb8c00" style="background:#fb8c00;" title="Orange"></div>
<div class="color-swatch" data-color="#8e24aa" style="background:#8e24aa;" title="Purple"></div>
</div>
</div>
<div class="toolbar-group">
<button class="stroke-btn active" data-stroke="2" title="Thin">
<div class="stroke-line" style="height:2px;"></div>
<span class="label">Thin</span>
</button>
<button class="stroke-btn" data-stroke="4" title="Medium">
<div class="stroke-line" style="height:4px;"></div>
<span class="label">Med</span>
</button>
<button class="stroke-btn" data-stroke="7" title="Thick">
<div class="stroke-line" style="height:7px;"></div>
<span class="label">Thick</span>
</button>
</div>
<!-- Undo / Redo -->
<div class="toolbar-group">
<button class="tool-btn" id="undo-btn" title="Undo (Ctrl/Cmd+Z)">
<span class="icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 105.64-10.36L1 10"/></svg>
</span>
<span class="label">Undo</span>
</button>
<button class="tool-btn" id="redo-btn" title="Redo (Ctrl/Cmd+Shift+Z)">
<span class="icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-5.64-10.36L23 10"/></svg>
</span>
<span class="label">Redo</span>
</button>
</div>
<!-- Share button -->
<button class="share-btn" id="share-btn">
<span class="icon">✉</span>
Share with Copilot
</button>
<!-- Help -->
<button class="help-btn" id="help-btn" title="Help">?</button>
</div>
<!-- ── Canvas ──────────────────────────────────────────────────── -->
<div id="canvas-container">
<canvas id="drawing-canvas"></canvas>
</div>
<!-- ── Onboarding overlay ──────────────────────────────────────── -->
<div class="overlay-backdrop" id="onboarding-overlay">
<div class="overlay-card">
<h1>Welcome to Napkin!</h1>
<p class="subtitle">Your whiteboard for brainstorming with Copilot.</p>
<div class="steps">
<div class="step">
<div class="step-num">1</div>
<div>Draw, sketch, or add sticky notes — whatever helps you think</div>
</div>
<div class="step">
<div class="step-num">2</div>
<div>When you're ready, click <strong>"Share with Copilot"</strong> (the green button)</div>
</div>
<div class="step">
<div class="step-num">3</div>
<div>Go back to your terminal and say <strong>"check the napkin"</strong></div>
</div>
<div class="step">
<div class="step-num">4</div>
<div>Copilot will look at your whiteboard and respond</div>
</div>
</div>
<p style="font-size:14px;color:#888;margin-bottom:20px;">That's it. Let's go!</p>
<button class="cta-btn" id="onboarding-dismiss">Got it — start drawing</button>
</div>
</div>
<!-- ── Share confirmation overlay ──────────────────────────────── -->
<div class="overlay-backdrop hidden" id="share-overlay">
<div class="overlay-card">
<div class="confirm-icon">✔️</div>
<h1>Shared with Copilot!</h1>
<div class="confirm-detail">
💾 A screenshot was saved (check your Downloads or Desktop).<br>
📋 The text content was copied to your clipboard.<br><br>
Go back to Copilot CLI and say:<br>
<span class="clipboard-hint">"check the napkin"</span>
</div>
<button class="cta-btn" id="share-overlay-close">Got it</button>
</div>
</div>
<!-- ── Keyboard shortcuts panel ────────────────────────────────── -->
<div class="shortcuts-panel" id="shortcuts-panel">
<button class="close-shortcuts" id="close-shortcuts">×</button>
<h3>Keyboard Shortcuts</h3>
<div class="shortcut-row"><span>Select / Move</span><kbd>V</kbd></div>
<div class="shortcut-row"><span>Pen</span><kbd>P</kbd></div>
<div class="shortcut-row"><span>Rectangle</span><kbd>R</kbd></div>
<div class="shortcut-row"><span>Circle</span><kbd>C</kbd></div>
<div class="shortcut-row"><span>Arrow</span><kbd>A</kbd></div>
<div class="shortcut-row"><span>Line</span><kbd>L</kbd></div>
<div class="shortcut-row"><span>Text</span><kbd>T</kbd></div>
<div class="shortcut-row"><span>Sticky Note</span><kbd>N</kbd></div>
<div class="shortcut-row"><span>Eraser</span><kbd>E</kbd></div>
<div class="shortcut-row"><span>Undo</span><kbd>Ctrl/Cmd+Z</kbd></div>
<div class="shortcut-row"><span>Redo</span><kbd>Ctrl/Cmd+Shift+Z</kbd></div>
<div class="shortcut-row"><span>Pan canvas</span><kbd>Space+Drag</kbd></div>
</div>
<!-- ── Zoom indicator ──────────────────────────────────────────── -->
<div class="zoom-indicator">
<button id="zoom-out-btn" title="Zoom out">−</button>
<span id="zoom-level">100%</span>
<button id="zoom-in-btn" title="Zoom in">+</button>
<button id="fit-btn" title="Fit to content" style="font-size:11px;width:auto;padding:0 8px;">Fit</button>
</div>
<!-- ── Toast ───────────────────────────────────────────────────── -->
<div class="toast" id="toast"></div>
<script>
// ═══════════════════════════════════════════════════════════════════
// NAPKIN — Self-contained whiteboard for Copilot collaboration
// ═══════════════════════════════════════════════════════════════════
(function () {
'use strict';
// ── DOM references ───────────────────────────────────────────────
const container = document.getElementById('canvas-container');
const canvas = document.getElementById('drawing-canvas');
const ctx = canvas.getContext('2d');
const toolbar = document.getElementById('toolbar');
const toastEl = document.getElementById('toast');
const onboarding = document.getElementById('onboarding-overlay');
const shareOverlay = document.getElementById('share-overlay');
const noteColorPicker = document.getElementById('note-color-picker');
// ── State ────────────────────────────────────────────────────────
const CANVAS_W = 3840;
const CANVAS_H = 2160;
let currentTool = 'select';
let currentColor = '#222222';
let currentStroke = 2;
let noteColor = 'yellow';
// View transform
let viewX = 0, viewY = 0, viewScale = 1;
// Drawing state
let isDrawing = false;
let isPanning = false;
let spaceHeld = false;
let eraserDidErase = false;
let panStartX = 0, panStartY = 0;
let panViewStartX = 0, panViewStartY = 0;
// Objects
let drawingObjects = []; // { type, points?, x?, y?, ... }
let stickyNotes = []; // { id, text, x, y, w, h, color }
let textLabels = []; // { id, text, x, y, fontSize }
// Current in-progress drawing
let currentPath = null;
// Undo/redo stacks
let undoStack = [];
let redoStack = [];
// Unique ID counter
let idCounter = Date.now();
function uid() { return 'n' + (idCounter++); }
// ── Utility ──────────────────────────────────────────────────────
function screenToCanvas(sx, sy) {
const rect = container.getBoundingClientRect();
return {
x: (sx - rect.left - viewX) / viewScale,
y: (sy - rect.top - viewY) / viewScale
};
}
function showToast(msg, duration) {
toastEl.textContent = msg;
toastEl.classList.add('show');
clearTimeout(showToast._t);
showToast._t = setTimeout(() => toastEl.classList.remove('show'), duration || 2500);
}
// ── Onboarding ───────────────────────────────────────────────────
function initOnboarding() {
if (localStorage.getItem('napkin_onboarded')) {
onboarding.classList.add('hidden');
}
document.getElementById('onboarding-dismiss').addEventListener('click', () => {
onboarding.classList.add('hidden');
localStorage.setItem('napkin_onboarded', '1');
});
document.getElementById('help-btn').addEventListener('click', () => {
onboarding.classList.remove('hidden');
});
}
// ── Canvas setup ─────────────────────────────────────────────────
function initCanvas() {
canvas.width = CANVAS_W;
canvas.height = CANVAS_H;
centerView();
render();
}
function centerView() {
const cw = container.clientWidth;
const ch = container.clientHeight;
viewScale = Math.min(cw / CANVAS_W, ch / CANVAS_H, 1) * 0.9;
viewX = (cw - CANVAS_W * viewScale) / 2;
viewY = (ch - CANVAS_H * viewScale) / 2;
updateCanvasTransform();
}
function updateCanvasTransform() {
canvas.style.left = viewX + 'px';
canvas.style.top = viewY + 'px';
canvas.style.width = (CANVAS_W * viewScale) + 'px';
canvas.style.height = (CANVAS_H * viewScale) + 'px';
document.getElementById('zoom-level').textContent = Math.round(viewScale * 100) + '%';
// Reposition sticky notes and text labels
repositionOverlays();
}
function repositionOverlays() {
document.querySelectorAll('.sticky-note').forEach(el => {
const note = stickyNotes.find(n => n.id === el.dataset.noteId);
if (!note) return;
el.style.left = (viewX + note.x * viewScale) + 'px';
el.style.top = (viewY + note.y * viewScale) + 'px';
el.style.width = (note.w * viewScale) + 'px';
el.style.height = (note.h * viewScale) + 'px';
el.style.fontSize = (14 * viewScale) + 'px';
});
document.querySelectorAll('.canvas-text-label').forEach(el => {
const lbl = textLabels.find(l => l.id === el.dataset.labelId);
if (!lbl) return;
el.style.left = (viewX + lbl.x * viewScale) + 'px';
el.style.top = (viewY + lbl.y * viewScale) + 'px';
el.style.fontSize = (lbl.fontSize * viewScale) + 'px';
});
}
// ── Render canvas objects ────────────────────────────────────────
function render() {
ctx.clearRect(0, 0, CANVAS_W, CANVAS_H);
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
// Draw grid (very subtle)
ctx.strokeStyle = '#f0f0f0';
ctx.lineWidth = 1;
for (let x = 0; x < CANVAS_W; x += 40) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, CANVAS_H); ctx.stroke();
}
for (let y = 0; y < CANVAS_H; y += 40) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(CANVAS_W, y); ctx.stroke();
}
// Draw all objects
drawingObjects.forEach(obj => drawObject(ctx, obj));
// Draw current in-progress path
if (currentPath) {
drawObject(ctx, currentPath);
}
}
function drawObject(c, obj) {
c.lineCap = 'round';
c.lineJoin = 'round';
switch (obj.type) {
case 'pen': {
if (obj.points.length < 2) return;
c.strokeStyle = obj.color;
c.lineWidth = obj.stroke;
c.beginPath();
c.moveTo(obj.points[0].x, obj.points[0].y);
for (let i = 1; i < obj.points.length; i++) {
c.lineTo(obj.points[i].x, obj.points[i].y);
}
c.stroke();
break;
}
case 'line': {
c.strokeStyle = obj.color;
c.lineWidth = obj.stroke;
c.beginPath();
c.moveTo(obj.x1, obj.y1);
c.lineTo(obj.x2, obj.y2);
c.stroke();
break;
}
case 'arrow': {
c.strokeStyle = obj.color;
c.lineWidth = obj.stroke;
c.fillStyle = obj.color;
c.beginPath();
c.moveTo(obj.x1, obj.y1);
c.lineTo(obj.x2, obj.y2);
c.stroke();
// Arrowhead
const angle = Math.atan2(obj.y2 - obj.y1, obj.x2 - obj.x1);
const headLen = 12 + obj.stroke * 2;
c.beginPath();
c.moveTo(obj.x2, obj.y2);
c.lineTo(obj.x2 - headLen * Math.cos(angle - 0.4), obj.y2 - headLen * Math.sin(angle - 0.4));
c.lineTo(obj.x2 - headLen * Math.cos(angle + 0.4), obj.y2 - headLen * Math.sin(angle + 0.4));
c.closePath();
c.fill();
break;
}
case 'rect': {
c.strokeStyle = obj.color;
c.lineWidth = obj.stroke;
c.beginPath();
c.rect(obj.x, obj.y, obj.w, obj.h);
c.stroke();
break;
}
case 'ellipse': {
c.strokeStyle = obj.color;
c.lineWidth = obj.stroke;
c.beginPath();
const cx = obj.x + obj.w / 2;
const cy = obj.y + obj.h / 2;
c.ellipse(cx, cy, Math.abs(obj.w / 2), Math.abs(obj.h / 2), 0, 0, Math.PI * 2);
c.stroke();
break;
}
}
}
// ── Shape recognition ────────────────────────────────────────────
function recognizeShape(points) {
if (points.length < 10) return null;
const first = points[0];
const last = points[points.length - 1];
const dist = Math.hypot(last.x - first.x, last.y - first.y);
// Bounding box
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
points.forEach(p => {
if (p.x < minX) minX = p.x;
if (p.y < minY) minY = p.y;
if (p.x > maxX) maxX = p.x;
if (p.y > maxY) maxY = p.y;
});
const bw = maxX - minX;
const bh = maxY - minY;
const diagonal = Math.hypot(bw, bh);
// Check if path closes (endpoints near each other relative to size)
if (dist > diagonal * 0.25) return null;
// Compute total path length
let pathLen = 0;
for (let i = 1; i < points.length; i++) {
pathLen += Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y);
}
// Skip tiny shapes
if (bw < 20 || bh < 20) return null;
// Check rectangularity by analyzing corner angles
const cx = (minX + maxX) / 2;
const cy = (minY + maxY) / 2;
// Measure how well points fit an ellipse vs a rectangle
let ellipseError = 0;
let rectError = 0;
const rx = bw / 2;
const ry = bh / 2;
points.forEach(p => {
// Ellipse error: distance from ellipse boundary
const dx = (p.x - cx) / rx;
const dy = (p.y - cy) / ry;
const r = Math.sqrt(dx * dx + dy * dy);
ellipseError += Math.abs(r - 1);
// Rectangle error: distance from nearest rectangle edge
const distToLeft = Math.abs(p.x - minX);
const distToRight = Math.abs(p.x - maxX);
const distToTop = Math.abs(p.y - minY);
const distToBottom = Math.abs(p.y - maxY);
rectError += Math.min(distToLeft, distToRight, distToTop, distToBottom);
});
ellipseError /= points.length;
rectError /= points.length;
// Normalize errors
const normEllipse = ellipseError;
const normRect = rectError / Math.max(bw, bh) * 4;
if (normEllipse < 0.35 && normEllipse < normRect) {
return { type: 'ellipse', x: minX, y: minY, w: bw, h: bh };
}
if (normRect < 0.25) {
return { type: 'rect', x: minX, y: minY, w: bw, h: bh };
}
return null;
}
// ── roundRect fallback for older browsers ──────────────────────
function safeRoundRect(ctx, x, y, w, h, radii) {
if (typeof ctx.roundRect === 'function') {
ctx.roundRect(x, y, w, h, radii);
return;
}
const r = Array.isArray(radii) ? radii : [radii, radii, radii, radii];
const [tl, tr, br, bl] = r.length === 4 ? r : r.length === 2 ? [r[0], r[1], r[0], r[1]] : [r[0], r[0], r[0], r[0]];
ctx.moveTo(x + tl, y);
ctx.lineTo(x + w - tr, y);
ctx.arcTo(x + w, y, x + w, y + tr, tr);
ctx.lineTo(x + w, y + h - br);
ctx.arcTo(x + w, y + h, x + w - br, y + h, br);
ctx.lineTo(x + bl, y + h);
ctx.arcTo(x, y + h, x, y + h - bl, bl);
ctx.lineTo(x, y + tl);
ctx.arcTo(x, y, x + tl, y, tl);
ctx.closePath();
}
// ── Eraser ───────────────────────────────────────────────────────
function eraseAt(cx, cy, radius) {
const r2 = radius * radius;
const before = drawingObjects.length;
drawingObjects = drawingObjects.filter(obj => {
switch (obj.type) {
case 'pen':
return !obj.points.some(p => (p.x - cx) ** 2 + (p.y - cy) ** 2 < r2);
case 'line':
case 'arrow':
return distToSegment(cx, cy, obj.x1, obj.y1, obj.x2, obj.y2) > radius;
case 'rect':
return !(cx > obj.x - radius && cx < obj.x + obj.w + radius &&
cy > obj.y - radius && cy < obj.y + obj.h + radius);
case 'ellipse': {
const ecx = obj.x + obj.w / 2;
const ecy = obj.y + obj.h / 2;
const dx = (cx - ecx) / (Math.abs(obj.w) / 2 + radius);
const dy = (cy - ecy) / (Math.abs(obj.h) / 2 + radius);
return (dx * dx + dy * dy) > 1.0;
}
default: return true;
}
});
if (drawingObjects.length !== before) {
eraserDidErase = true;
render();
return true;
}
return false;
}
function distToSegment(px, py, x1, y1, x2, y2) {
const dx = x2 - x1, dy = y2 - y1;
const lenSq = dx * dx + dy * dy;
if (lenSq === 0) return Math.hypot(px - x1, py - y1);
let t = ((px - x1) * dx + (py - y1) * dy) / lenSq;
t = Math.max(0, Math.min(1, t));
return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy));
}
// ── Undo / Redo ──────────────────────────────────────────────────
function saveState() {
undoStack.push({
objects: JSON.parse(JSON.stringify(drawingObjects)),
notes: JSON.parse(JSON.stringify(stickyNotes)),
labels: JSON.parse(JSON.stringify(textLabels))
});
if (undoStack.length > 60) undoStack.shift();
redoStack = [];
scheduleAutoSave();
}
function undo() {
if (undoStack.length === 0) return;
redoStack.push({
objects: JSON.parse(JSON.stringify(drawingObjects)),
notes: JSON.parse(JSON.stringify(stickyNotes)),
labels: JSON.parse(JSON.stringify(textLabels))
});
const state = undoStack.pop();
drawingObjects = state.objects;
stickyNotes = state.notes;
textLabels = state.labels;
rebuildOverlays();
render();
scheduleAutoSave();
}
function redo() {
if (redoStack.length === 0) return;
undoStack.push({
objects: JSON.parse(JSON.stringify(drawingObjects)),
notes: JSON.parse(JSON.stringify(stickyNotes)),
labels: JSON.parse(JSON.stringify(textLabels))
});
const state = redoStack.pop();
drawingObjects = state.objects;
stickyNotes = state.notes;
textLabels = state.labels;
rebuildOverlays();
render();
scheduleAutoSave();
}
document.getElementById('undo-btn').addEventListener('click', undo);
document.getElementById('redo-btn').addEventListener('click', redo);
// ── Tool selection ───────────────────────────────────────────────
function setTool(tool) {
currentTool = tool;
document.querySelectorAll('.tool-btn[data-tool]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tool === tool);
});
container.style.cursor = tool === 'select' ? 'default' :
tool === 'eraser' ? 'cell' : 'crosshair';
noteColorPicker.classList.remove('show');
}
toolbar.addEventListener('click', e => {
const btn = e.target.closest('.tool-btn[data-tool]');
if (!btn) return;
const tool = btn.dataset.tool;
if (tool === 'note') {
noteColorPicker.classList.toggle('show');
const rect = btn.getBoundingClientRect();
noteColorPicker.style.left = rect.left + 'px';
} else {
setTool(tool);
}
});
// Note color picker
noteColorPicker.addEventListener('click', e => {
const opt = e.target.closest('.note-color-opt');
if (!opt) return;
noteColor = opt.dataset.noteColor;
noteColorPicker.classList.remove('show');
setTool('note');
});
// Color swatches
document.querySelectorAll('.color-swatch').forEach(s => {
s.addEventListener('click', () => {
document.querySelectorAll('.color-swatch').forEach(el => el.classList.remove('active'));
s.classList.add('active');
currentColor = s.dataset.color;
});
});
// Stroke buttons
document.querySelectorAll('.stroke-btn').forEach(s => {
s.addEventListener('click', () => {
document.querySelectorAll('.stroke-btn').forEach(el => el.classList.remove('active'));
s.classList.add('active');
currentStroke = parseInt(s.dataset.stroke, 10);
});
});
// ── Mouse / pointer events on canvas ─────────────────────────────
let drawStartX, drawStartY;
container.addEventListener('pointerdown', e => {
if (e.target.closest('#toolbar') || e.target.closest('.sticky-note') ||
e.target.closest('.canvas-text-label') || e.target.closest('.overlay-backdrop') ||
e.target.closest('.shortcuts-panel') || e.target.closest('.zoom-indicator')) return;
const pt = screenToCanvas(e.clientX, e.clientY);
// Pan with space or middle button
if (spaceHeld || e.button === 1) {
isPanning = true;
panStartX = e.clientX;
panStartY = e.clientY;
panViewStartX = viewX;
panViewStartY = viewY;
container.classList.add('panning');
e.preventDefault();
return;
}
if (e.button !== 0) return;
switch (currentTool) {
case 'pen':
case 'eraser': {
isDrawing = true;
if (currentTool === 'pen') {
currentPath = { type: 'pen', points: [pt], color: currentColor, stroke: currentStroke };
} else {
eraserDidErase = false;
const redoStackBeforeEraser = redoStack.slice();
saveState();
eraseAt(pt.x, pt.y, 16);
if (!eraserDidErase) {
redoStack = redoStackBeforeEraser;
}
}
break;
}
case 'line':
case 'arrow':
case 'rect':
case 'ellipse': {
isDrawing = true;
drawStartX = pt.x;
drawStartY = pt.y;
if (currentTool === 'line' || currentTool === 'arrow') {
currentPath = { type: currentTool, x1: pt.x, y1: pt.y, x2: pt.x, y2: pt.y, color: currentColor, stroke: currentStroke };
} else {
currentPath = { type: currentTool, x: pt.x, y: pt.y, w: 0, h: 0, color: currentColor, stroke: currentStroke };
}
break;
}
case 'text': {
createTextLabel(pt.x, pt.y);
break;
}
case 'note': {
createStickyNote(pt.x, pt.y);
setTool('select');
break;
}
case 'select': {
// In select mode, clicking empty canvas does nothing special
break;
}
}
});
container.addEventListener('pointermove', e => {
if (isPanning) {
viewX = panViewStartX + (e.clientX - panStartX);
viewY = panViewStartY + (e.clientY - panStartY);
updateCanvasTransform();
return;
}
if (!isDrawing) return;
const pt = screenToCanvas(e.clientX, e.clientY);
switch (currentTool) {
case 'pen': {
if (currentPath) {
currentPath.points.push(pt);
render();
}
break;
}
case 'eraser': {
eraseAt(pt.x, pt.y, 16);
break;
}
case 'line':
case 'arrow': {
if (currentPath) {
currentPath.x2 = pt.x;
currentPath.y2 = pt.y;
render();
}
break;
}
case 'rect':
case 'ellipse': {
if (currentPath) {
currentPath.x = Math.min(drawStartX, pt.x);
currentPath.y = Math.min(drawStartY, pt.y);
currentPath.w = Math.abs(pt.x - drawStartX);
currentPath.h = Math.abs(pt.y - drawStartY);
render();
}
break;
}
}
});
function finishDrawing() {
if (isPanning) {
isPanning = false;
container.classList.remove('panning');
return;
}
if (!isDrawing) return;
isDrawing = false;
if (currentTool === 'eraser') {
if (!eraserDidErase) {
// Nothing was erased — pop the pre-erase state we saved on pointerdown
undoStack.pop();
}
return;
}
if (!currentPath) return;
// Shape recognition for pen tool
if (currentPath.type === 'pen') {
const recognized = recognizeShape(currentPath.points);
if (recognized) {
currentPath = {
...recognized,
color: currentPath.color,
stroke: currentPath.stroke
};
}
}
// Don't save degenerate shapes
if (currentPath.type === 'pen' && currentPath.points.length < 2) {
currentPath = null;
return;
}
if ((currentPath.type === 'rect' || currentPath.type === 'ellipse') &&
(Math.abs(currentPath.w) < 3 && Math.abs(currentPath.h) < 3)) {
currentPath = null;
render();
return;
}
if ((currentPath.type === 'line' || currentPath.type === 'arrow') &&
Math.hypot(currentPath.x2 - currentPath.x1, currentPath.y2 - currentPath.y1) < 3) {
currentPath = null;
render();
return;
}
saveState();
drawingObjects.push(currentPath);
currentPath = null;
render();
scheduleAutoSave();
}
container.addEventListener('pointerup', finishDrawing);
container.addEventListener('pointerleave', finishDrawing);
// ── Zoom ─────────────────────────────────────────────────────────
container.addEventListener('wheel', e => {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.92 : 1.08;
zoomAt(e.clientX, e.clientY, delta);
}, { passive: false });
function zoomAt(sx, sy, factor) {
const rect = container.getBoundingClientRect();
const mx = sx - rect.left;
const my = sy - rect.top;
const newScale = Math.min(Math.max(viewScale * factor, 0.1), 5);
const scaleRatio = newScale / viewScale;
viewX = mx - (mx - viewX) * scaleRatio;
viewY = my - (my - viewY) * scaleRatio;
viewScale = newScale;
updateCanvasTransform();
}
document.getElementById('zoom-in-btn').addEventListener('click', () => {
const rect = container.getBoundingClientRect();
zoomAt(rect.left + rect.width / 2, rect.top + rect.height / 2, 1.2);
});
document.getElementById('zoom-out-btn').addEventListener('click', () => {
const rect = container.getBoundingClientRect();
zoomAt(rect.left + rect.width / 2, rect.top + rect.height / 2, 0.8);
});
document.getElementById('fit-btn').addEventListener('click', fitToContent);
function fitToContent() {
// Find bounding box of all content
let minX = CANVAS_W, minY = CANVAS_H, maxX = 0, maxY = 0;
let hasContent = false;
drawingObjects.forEach(obj => {
hasContent = true;
switch (obj.type) {
case 'pen':
obj.points.forEach(p => {
minX = Math.min(minX, p.x); minY = Math.min(minY, p.y);
maxX = Math.max(maxX, p.x); maxY = Math.max(maxY, p.y);
});
break;
case 'line': case 'arrow':
minX = Math.min(minX, obj.x1, obj.x2); minY = Math.min(minY, obj.y1, obj.y2);
maxX = Math.max(maxX, obj.x1, obj.x2); maxY = Math.max(maxY, obj.y1, obj.y2);
break;
case 'rect': case 'ellipse':
minX = Math.min(minX, obj.x); minY = Math.min(minY, obj.y);
maxX = Math.max(maxX, obj.x + obj.w); maxY = Math.max(maxY, obj.y + obj.h);
break;
}
});
stickyNotes.forEach(n => {
hasContent = true;
minX = Math.min(minX, n.x); minY = Math.min(minY, n.y);
maxX = Math.max(maxX, n.x + n.w); maxY = Math.max(maxY, n.y + n.h);
});
textLabels.forEach(l => {
hasContent = true;
minX = Math.min(minX, l.x); minY = Math.min(minY, l.y);
maxX = Math.max(maxX, l.x + 200); maxY = Math.max(maxY, l.y + 30);
});
if (!hasContent) {
centerView();
return;
}
const pad = 80;
minX -= pad; minY -= pad; maxX += pad; maxY += pad;
const cw = container.clientWidth;
const ch = container.clientHeight;
viewScale = Math.min(cw / (maxX - minX), ch / (maxY - minY), 2);
viewX = (cw - (maxX - minX) * viewScale) / 2 - minX * viewScale;
viewY = (ch - (maxY - minY) * viewScale) / 2 - minY * viewScale;
updateCanvasTransform();
}
// ── Keyboard ─────────────────────────────────────────────────────
document.addEventListener('keydown', e => {
// Don't capture when typing in inputs
if (e.target.isContentEditable || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
if (e.key === 'Escape') e.target.blur();
return;
}
if (e.key === ' ') {
e.preventDefault();
spaceHeld = true;
container.classList.add('panning');
}
// Ctrl/Cmd shortcuts
if (e.metaKey || e.ctrlKey) {
if (e.key === 'z' && !e.shiftKey) { e.preventDefault(); undo(); }
if (e.key === 'z' && e.shiftKey) { e.preventDefault(); redo(); }
if (e.key === 'Z') { e.preventDefault(); redo(); }
return;
}
if (e.key === 'Delete' || e.key === 'Backspace') {
// No specific selection handling in v1 beyond sticky notes
return;
}
const keyMap = { v: 'select', p: 'pen', r: 'rect', c: 'ellipse', a: 'arrow', l: 'line', t: 'text', n: 'note', e: 'eraser' };
if (keyMap[e.key]) {
if (e.key === 'n') {
noteColorPicker.classList.toggle('show');
const btn = document.getElementById('note-tool-btn');
const rect = btn.getBoundingClientRect();
noteColorPicker.style.left = rect.left + 'px';
} else {
setTool(keyMap[e.key]);
}
}
});
document.addEventListener('keyup', e => {
if (e.key === ' ') {
spaceHeld = false;
if (!isPanning) container.classList.remove('panning');
}
});
// Shortcuts panel
document.getElementById('close-shortcuts').addEventListener('click', () => {
document.getElementById('shortcuts-panel').classList.remove('show');
});
// Show shortcuts with ? key when not typing
document.addEventListener('keydown', e => {
if (e.target.isContentEditable || e.target.tagName === 'INPUT') return;
if (e.key === '?') {
document.getElementById('shortcuts-panel').classList.toggle('show');
}
});
// ── Sticky notes ─────────────────────────────────────────────────
function createStickyNote(x, y) {
const note = {
id: uid(),
text: '',
x: x,
y: y,
w: 200,
h: 160,
color: noteColor
};
saveState();
stickyNotes.push(note);
renderStickyNote(note, true);
scheduleAutoSave();
}
function renderStickyNote(note, focusAfter) {
const el = document.createElement('div');
el.className = 'sticky-note sticky-' + note.color;
el.dataset.noteId = note.id;
el.style.left = (viewX + note.x * viewScale) + 'px';
el.style.top = (viewY + note.y * viewScale) + 'px';
el.style.width = (note.w * viewScale) + 'px';
el.style.height = (note.h * viewScale) + 'px';
el.style.fontSize = (14 * viewScale) + 'px';
const header = document.createElement('div');
header.className = 'note-header';
const del = document.createElement('button');
del.className = 'note-delete';
del.textContent = '\u00d7';
del.addEventListener('click', () => {
saveState();
stickyNotes = stickyNotes.filter(n => n.id !== note.id);
el.remove();
scheduleAutoSave();
});
header.appendChild(del);
const body = document.createElement('div');
body.className = 'note-body';
body.contentEditable = 'true';
body.textContent = note.text;
body.addEventListener('input', () => {
note.text = body.textContent;
scheduleAutoSave();
});
body.addEventListener('blur', () => {
note.text = body.textContent;
scheduleAutoSave();
});
const resize = document.createElement('div');
resize.className = 'note-resize';
el.appendChild(header);
el.appendChild(body);
el.appendChild(resize);
container.appendChild(el);
// Drag header to move
let dragOffX, dragOffY, isDragging = false;
header.addEventListener('pointerdown', e => {
isDragging = true;
const rect = el.getBoundingClientRect();
dragOffX = e.clientX - rect.left;
dragOffY = e.clientY - rect.top;
e.preventDefault();
header.setPointerCapture(e.pointerId);
});
header.addEventListener('pointermove', e => {
if (!isDragging) return;
const cRect = container.getBoundingClientRect();
const newLeft = e.clientX - cRect.left - dragOffX;
const newTop = e.clientY - cRect.top - dragOffY;
note.x = (newLeft - viewX) / viewScale;
note.y = (newTop - viewY) / viewScale;
el.style.left = newLeft + 'px';
el.style.top = newTop + 'px';
});
header.addEventListener('pointerup', () => {
if (isDragging) scheduleAutoSave();
isDragging = false;
});
// Resize handle
let isResizing = false, resizeStartW, resizeStartH, resizeStartMx, resizeStartMy;
resize.addEventListener('pointerdown', e => {
isResizing = true;
resizeStartW = note.w;
resizeStartH = note.h;
resizeStartMx = e.clientX;
resizeStartMy = e.clientY;
e.preventDefault();
e.stopPropagation();
resize.setPointerCapture(e.pointerId);
});
resize.addEventListener('pointermove', e => {
if (!isResizing) return;
const dw = (e.clientX - resizeStartMx) / viewScale;
const dh = (e.clientY - resizeStartMy) / viewScale;
note.w = Math.max(120, resizeStartW + dw);
note.h = Math.max(80, resizeStartH + dh);
el.style.width = (note.w * viewScale) + 'px';
el.style.height = (note.h * viewScale) + 'px';
});
resize.addEventListener('pointerup', () => {
if (isResizing) scheduleAutoSave();
isResizing = false;
});
if (focusAfter) {
setTimeout(() => body.focus(), 50);
}
}
// ── Text labels ──────────────────────────────────────────────────
function createTextLabel(x, y) {
const lbl = {
id: uid(),
text: '',
x: x,
y: y,
fontSize: 16
};
saveState();
textLabels.push(lbl);
renderTextLabel(lbl, true);
setTool('select');
scheduleAutoSave();
}
function renderTextLabel(lbl, focusAfter) {
const el = document.createElement('div');
el.className = 'canvas-text-label';
el.dataset.labelId = lbl.id;
el.contentEditable = 'true';
el.style.left = (viewX + lbl.x * viewScale) + 'px';
el.style.top = (viewY + lbl.y * viewScale) + 'px';
el.style.fontSize = (lbl.fontSize * viewScale) + 'px';
el.textContent = lbl.text;
el.addEventListener('input', () => {
lbl.text = el.textContent;
scheduleAutoSave();
});
el.addEventListener('blur', () => {
lbl.text = el.textContent;
if (!lbl.text.trim()) {
textLabels = textLabels.filter(l => l.id !== lbl.id);
el.remove();
}
scheduleAutoSave();
});
container.appendChild(el);
if (focusAfter) {
setTimeout(() => el.focus(), 50);
}
}
// ── Rebuild overlays from data (for undo/redo) ───────────────────
function rebuildOverlays() {
document.querySelectorAll('.sticky-note').forEach(el => el.remove());
document.querySelectorAll('.canvas-text-label').forEach(el => el.remove());
stickyNotes.forEach(n => renderStickyNote(n, false));
textLabels.forEach(l => renderTextLabel(l, false));
}
// ── Auto-save to localStorage ────────────────────────────────────
let autoSaveTimer = null;
function scheduleAutoSave() {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(autoSave, 2000);
}
function autoSave() {
try {
const state = {
objects: drawingObjects,
notes: stickyNotes,
labels: textLabels
};
localStorage.setItem('napkin_state', JSON.stringify(state));
} catch (e) {
// localStorage might be full; silently ignore
}
}
// Periodic save every 10 seconds
setInterval(autoSave, 10000);
function loadState() {
try {
const raw = localStorage.getItem('napkin_state');
if (!raw) return;
const state = JSON.parse(raw);
if (state.objects) drawingObjects = state.objects;
if (state.notes) stickyNotes = state.notes;
if (state.labels) textLabels = state.labels;
rebuildOverlays();
render();
} catch (e) {
// corrupted state, ignore
}
}
// ── Share with Copilot ───────────────────────────────────────────
document.getElementById('share-btn').addEventListener('click', async () => {
try {
// Create an offscreen canvas for export
const exportCanvas = document.createElement('canvas');
exportCanvas.width = CANVAS_W;
exportCanvas.height = CANVAS_H;
const ectx = exportCanvas.getContext('2d');
// White background
ectx.fillStyle = '#fff';
ectx.fillRect(0, 0, CANVAS_W, CANVAS_H);
// Draw all drawing objects
drawingObjects.forEach(obj => drawObject(ectx, obj));
// Draw sticky notes onto export canvas
stickyNotes.forEach(note => {
const colors = {
yellow: { bg: '#fff9c4', header: '#fff176' },
pink: { bg: '#fce4ec', header: '#f48fb1' },
blue: { bg: '#e3f2fd', header: '#90caf9' },
green: { bg: '#e8f5e9', header: '#a5d6a7' }
};
const c = colors[note.color] || colors.yellow;
// Shadow
ectx.shadowColor = 'rgba(0,0,0,0.12)';
ectx.shadowBlur = 12;
ectx.shadowOffsetX = 2;
ectx.shadowOffsetY = 3;
// Body
ectx.fillStyle = c.bg;
ectx.beginPath();
safeRoundRect(ectx, note.x, note.y, note.w, note.h, 4);
ectx.fill();
// Reset shadow
ectx.shadowColor = 'transparent';
ectx.shadowBlur = 0;
ectx.shadowOffsetX = 0;
ectx.shadowOffsetY = 0;
// Header
ectx.fillStyle = c.header;
ectx.beginPath();
safeRoundRect(ectx, note.x, note.y, note.w, 24, [4, 4, 0, 0]);
ectx.fill();
// Text
if (note.text) {
ectx.fillStyle = '#333';
ectx.font = '14px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
const lines = wrapText(ectx, note.text, note.w - 24);
lines.forEach((line, i) => {
ectx.fillText(line, note.x + 12, note.y + 44 + i * 20);
});
}
});
// Draw text labels
textLabels.forEach(lbl => {
if (!lbl.text) return;
ectx.fillStyle = '#333';
ectx.font = lbl.fontSize + 'px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
ectx.fillText(lbl.text, lbl.x, lbl.y + lbl.fontSize);
});
// Export PNG
const dataUrl = exportCanvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = 'napkin-snapshot.png';
link.href = dataUrl;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Build JSON
const json = {
version: 1,
timestamp: new Date().toISOString(),
notes: stickyNotes.map(n => ({
id: n.id, text: n.text, x: n.x, y: n.y, color: n.color, width: n.w, height: n.h
})),
textLabels: textLabels.map(l => ({
id: l.id, text: l.text, x: l.x, y: l.y, fontSize: l.fontSize
})),
canvasSize: { width: CANVAS_W, height: CANVAS_H }
};
// Copy JSON to clipboard
try {
await navigator.clipboard.writeText(JSON.stringify(json, null, 2));
} catch (clipErr) {
// Fallback for file:// protocol or older browsers
const ta = document.createElement('textarea');
ta.value = JSON.stringify(json, null, 2);
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
// Show confirmation
shareOverlay.classList.remove('hidden');
} catch (err) {
showToast('Export failed: ' + err.message, 4000);
}
});
document.getElementById('share-overlay-close').addEventListener('click', () => {
shareOverlay.classList.add('hidden');
});
function wrapText(c, text, maxWidth) {
const words = text.split(/\s+/);
const lines = [];
let currentLine = '';
words.forEach(word => {
const test = currentLine ? currentLine + ' ' + word : word;
if (c.measureText(test).width > maxWidth && currentLine) {
lines.push(currentLine);
currentLine = word;
} else {
currentLine = test;
}
});
if (currentLine) lines.push(currentLine);
return lines;
}
// ── Touch support for pinch zoom ─────────────────────────────────
let lastPinchDist = 0;
let lastPinchCX = 0, lastPinchCY = 0;
container.addEventListener('touchstart', e => {
if (e.touches.length === 2) {
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
lastPinchDist = Math.hypot(dx, dy);
lastPinchCX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
lastPinchCY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
}
}, { passive: true });
container.addEventListener('touchmove', e => {
if (e.touches.length === 2) {
e.preventDefault();
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const dist = Math.hypot(dx, dy);
const cx = (e.touches[0].clientX + e.touches[1].clientX) / 2;
const cy = (e.touches[0].clientY + e.touches[1].clientY) / 2;
if (lastPinchDist > 0) {
const factor = dist / lastPinchDist;
zoomAt(cx, cy, factor);
viewX += cx - lastPinchCX;
viewY += cy - lastPinchCY;
updateCanvasTransform();
}
lastPinchDist = dist;
lastPinchCX = cx;
lastPinchCY = cy;
}
}, { passive: false });
container.addEventListener('touchend', () => {
lastPinchDist = 0;
}, { passive: true });
// ── Close overlays on escape ─────────────────────────────────────
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
onboarding.classList.add('hidden');
shareOverlay.classList.add('hidden');
noteColorPicker.classList.remove('show');
document.getElementById('shortcuts-panel').classList.remove('show');
}
});
// Close note color picker on outside click
document.addEventListener('pointerdown', e => {
if (!e.target.closest('#note-color-picker') && !e.target.closest('#note-tool-btn')) {
noteColorPicker.classList.remove('show');
}
});
// ── Window resize ────────────────────────────────────────────────
window.addEventListener('resize', () => {
updateCanvasTransform();
});
// ── Init ─────────────────────────────────────────────────────────
initOnboarding();
initCanvas();
loadState();
})();
</script>
</body>
</html>
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="460" viewBox="0 0 800 460">
<defs>
<linearGradient id="termBg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#2D2B55"/>
<stop offset="100%" stop-color="#1E1B3A"/>
</linearGradient>
<linearGradient id="browserGlow" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#0D9488" stop-opacity="0.15"/>
<stop offset="100%" stop-color="#0D9488" stop-opacity="0.05"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="800" height="460" rx="12" fill="#F8FAFB"/>
<!-- Step indicator -->
<circle cx="46" cy="28" r="16" fill="#0D9488"/>
<text x="46" y="33" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="14" font-weight="bold" fill="white">1</text>
<text x="72" y="33" font-family="Arial, Helvetica, sans-serif" font-size="15" font-weight="bold" fill="#334155">Type "let's napkin" to start</text>
<!-- Terminal Window -->
<g transform="translate(40, 55)">
<!-- Terminal chrome -->
<rect width="480" height="280" rx="10" fill="#1E1B3A" stroke="#3D3A5C" stroke-width="1"/>
<!-- Title bar -->
<rect width="480" height="36" rx="10" fill="#2D2B55"/>
<rect x="0" y="26" width="480" height="10" fill="#2D2B55"/>
<!-- Window buttons -->
<circle cx="20" cy="18" r="6" fill="#FF5F57"/>
<circle cx="40" cy="18" r="6" fill="#FFBD2E"/>
<circle cx="60" cy="18" r="6" fill="#28C840"/>
<!-- Title -->
<text x="240" y="22" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#8B87B0">Terminal</text>
<!-- Terminal content -->
<!-- Prompt line 1 -->
<text x="20" y="68" font-family="monospace" font-size="13" fill="#8B87B0">~/Projects</text>
<text x="115" y="68" font-family="monospace" font-size="13" fill="#6C6A8A">$</text>
<text x="130" y="68" font-family="monospace" font-size="13" fill="#E0DEF4">copilot</text>
<!-- Prompt line 2 - the napkin command -->
<text x="20" y="100" font-family="monospace" font-size="13" fill="#0D9488">❯</text>
<text x="36" y="100" font-family="monospace" font-size="13" fill="#E0DEF4">let's napkin</text>
<!-- Copilot response -->
<text x="20" y="140" font-family="monospace" font-size="13" fill="#0D9488">✓</text>
<text x="36" y="140" font-family="monospace" font-size="13" fill="#A5F3FC">Opening your napkin whiteboard...</text>
<!-- Blank line then response -->
<text x="20" y="172" font-family="monospace" font-size="13" fill="#C4B5FD">Copilot</text>
<text x="20" y="196" font-family="monospace" font-size="13" fill="#E0DEF4">Your napkin is open in your browser!</text>
<text x="20" y="218" font-family="monospace" font-size="13" fill="#E0DEF4">Sketch your ideas, then click</text>
<text x="20" y="240" font-family="monospace" font-size="13" fill="#34D399">"Share with Copilot"</text>
<text x="215" y="240" font-family="monospace" font-size="13" fill="#E0DEF4">when ready.</text>
<!-- Cursor -->
<rect x="20" y="260" width="8" height="14" fill="#0D9488" opacity="0.8"/>
</g>
<!-- Browser opening indicator -->
<g transform="translate(560, 100)">
<!-- Mini browser window -->
<rect width="200" height="160" rx="8" fill="white" stroke="#D1D5DB" stroke-width="1.5"/>
<!-- Browser chrome -->
<rect width="200" height="28" rx="8" fill="#F1F5F9"/>
<rect x="0" y="20" width="200" height="8" fill="#F1F5F9"/>
<!-- Browser dots -->
<circle cx="14" cy="14" r="4" fill="#E2E8F0"/>
<circle cx="28" cy="14" r="4" fill="#E2E8F0"/>
<circle cx="42" cy="14" r="4" fill="#E2E8F0"/>
<!-- Address bar -->
<rect x="56" y="7" width="132" height="14" rx="3" fill="white" stroke="#E2E8F0" stroke-width="1"/>
<text x="72" y="17" font-family="Arial, Helvetica, sans-serif" font-size="7" fill="#94A3B8">napkin.html</text>
<!-- Canvas preview -->
<rect x="10" y="38" width="180" height="110" rx="3" fill="#FAFAFA"/>
<!-- Mini toolbar -->
<rect x="40" y="44" width="120" height="14" rx="3" fill="#F1F5F9" stroke="#E2E8F0" stroke-width="0.5"/>
<!-- Mini green button -->
<rect x="130" y="130" width="50" height="12" rx="3" fill="#0D9488"/>
<text x="155" y="139" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="5" fill="white">Share</text>
<!-- Whiteboard grid dots -->
<circle cx="50" cy="80" r="1" fill="#E2E8F0"/>
<circle cx="80" cy="80" r="1" fill="#E2E8F0"/>
<circle cx="110" cy="80" r="1" fill="#E2E8F0"/>
<circle cx="140" cy="80" r="1" fill="#E2E8F0"/>
<circle cx="50" cy="105" r="1" fill="#E2E8F0"/>
<circle cx="80" cy="105" r="1" fill="#E2E8F0"/>
<circle cx="110" cy="105" r="1" fill="#E2E8F0"/>
<circle cx="140" cy="105" r="1" fill="#E2E8F0"/>
</g>
<!-- Curved arrow from terminal to browser -->
<path d="M525,200 C555,180 555,160 570,150" fill="none" stroke="#0D9488" stroke-width="2.5" stroke-dasharray="6,4"/>
<polygon points="568,144 576,150 568,156" fill="#0D9488"/>
<!-- Arrow label -->
<rect x="530" y="270" width="190" height="36" rx="6" fill="#0D9488" opacity="0.1"/>
<text x="625" y="285" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#0D9488" font-weight="bold">Browser opens</text>
<text x="625" y="300" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#0D9488">automatically</text>
<!-- Bottom caption -->
<rect x="40" y="365" width="720" height="70" rx="8" fill="white" stroke="#E2E8F0" stroke-width="1"/>
<text x="60" y="393" font-family="Arial, Helvetica, sans-serif" font-size="13" fill="#64748B" font-weight="bold">How it works:</text>
<text x="60" y="418" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#94A3B8">Type "let's napkin" (or "open the napkin") in your Copilot CLI session.</text>
<text x="60" y="436" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#94A3B8">A whiteboard will open in your default browser — no setup needed.</text>
</svg><svg xmlns="http://www.w3.org/2000/svg" width="800" height="480" viewBox="0 0 800 480">
<defs>
<linearGradient id="overlayBg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="white"/>
<stop offset="100%" stop-color="#F8FAFB"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="800" height="480" rx="12" fill="#F8FAFB"/>
<!-- Step indicator -->
<circle cx="46" cy="28" r="16" fill="#0D9488"/>
<text x="46" y="33" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="14" font-weight="bold" fill="white">2</text>
<text x="72" y="33" font-family="Arial, Helvetica, sans-serif" font-size="15" font-weight="bold" fill="#334155">The whiteboard opens in your browser</text>
<!-- Browser Window -->
<g transform="translate(30, 52)">
<!-- Browser outer frame -->
<rect width="740" height="400" rx="10" fill="white" stroke="#D1D5DB" stroke-width="1.5"/>
<!-- Browser chrome -->
<rect width="740" height="38" rx="10" fill="#F1F5F9"/>
<rect x="0" y="28" width="740" height="10" fill="#F1F5F9"/>
<!-- Window buttons -->
<circle cx="18" cy="19" r="5.5" fill="#FF5F57"/>
<circle cx="36" cy="19" r="5.5" fill="#FFBD2E"/>
<circle cx="54" cy="19" r="5.5" fill="#28C840"/>
<!-- Address bar -->
<rect x="80" y="8" width="580" height="22" rx="6" fill="white" stroke="#E2E8F0" stroke-width="1"/>
<text x="96" y="23" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#94A3B8">file:///Users/.../napkin.html</text>
<!-- Lock icon substitute -->
<circle cx="90" cy="19" r="3" fill="none" stroke="#94A3B8" stroke-width="1"/>
<!-- Whiteboard area -->
<rect x="1" y="38" width="738" height="361" fill="white" rx="0"/>
<!-- Toolbar -->
<rect x="120" y="50" width="500" height="40" rx="20" fill="white" stroke="#E2E8F0" stroke-width="1.5"/>
<!-- Tool buttons -->
<!-- Pen -->
<g transform="translate(155, 58)">
<rect width="60" height="24" rx="12" fill="#F1F5F9"/>
<line x1="16" y1="18" x2="26" y2="6" stroke="#64748B" stroke-width="2" stroke-linecap="round"/>
<circle cx="16" cy="18" r="2" fill="#64748B"/>
<text x="34" y="16" font-family="Arial, Helvetica, sans-serif" font-size="9" fill="#64748B">Pen</text>
</g>
<!-- Shapes -->
<g transform="translate(230, 58)">
<rect width="76" height="24" rx="12" fill="#F1F5F9"/>
<rect x="10" y="6" width="12" height="12" rx="2" fill="none" stroke="#64748B" stroke-width="1.5"/>
<text x="30" y="16" font-family="Arial, Helvetica, sans-serif" font-size="9" fill="#64748B">Shapes</text>
</g>
<!-- Arrow -->
<g transform="translate(320, 58)">
<rect width="70" height="24" rx="12" fill="#F1F5F9"/>
<line x1="12" y1="16" x2="24" y2="8" stroke="#64748B" stroke-width="1.5" stroke-linecap="round"/>
<polygon points="22,5 28,8 22,11" fill="#64748B"/>
<text x="34" y="16" font-family="Arial, Helvetica, sans-serif" font-size="9" fill="#64748B">Arrow</text>
</g>
<!-- Sticky Note -->
<g transform="translate(404, 58)">
<rect width="80" height="24" rx="12" fill="#F1F5F9"/>
<rect x="10" y="6" width="12" height="12" rx="1" fill="#FEF3C7" stroke="#F59E0B" stroke-width="0.8"/>
<text x="28" y="16" font-family="Arial, Helvetica, sans-serif" font-size="9" fill="#64748B">Sticky Note</text>
</g>
<!-- Eraser -->
<g transform="translate(498, 58)">
<rect width="70" height="24" rx="12" fill="#F1F5F9"/>
<rect x="10" y="8" width="14" height="10" rx="2" fill="none" stroke="#64748B" stroke-width="1.5"/>
<text x="30" y="16" font-family="Arial, Helvetica, sans-serif" font-size="9" fill="#64748B">Eraser</text>
</g>
<!-- Share with Copilot button -->
<rect x="540" y="356" width="180" height="36" rx="18" fill="#0D9488"/>
<text x="582" y="378" font-family="Arial, Helvetica, sans-serif" font-size="13" fill="white" font-weight="bold">Share with Copilot</text>
<!-- Subtle grid dots on canvas -->
<g fill="#F1F5F9" opacity="0.8">
<circle cx="100" cy="140" r="1.5"/>
<circle cx="160" cy="140" r="1.5"/>
<circle cx="220" cy="140" r="1.5"/>
<circle cx="280" cy="140" r="1.5"/>
<circle cx="340" cy="140" r="1.5"/>
<circle cx="400" cy="140" r="1.5"/>
<circle cx="460" cy="140" r="1.5"/>
<circle cx="520" cy="140" r="1.5"/>
<circle cx="580" cy="140" r="1.5"/>
<circle cx="640" cy="140" r="1.5"/>
<circle cx="100" cy="200" r="1.5"/>
<circle cx="160" cy="200" r="1.5"/>
<circle cx="220" cy="200" r="1.5"/>
<circle cx="280" cy="200" r="1.5"/>
<circle cx="340" cy="200" r="1.5"/>
<circle cx="400" cy="200" r="1.5"/>
<circle cx="460" cy="200" r="1.5"/>
<circle cx="520" cy="200" r="1.5"/>
<circle cx="580" cy="200" r="1.5"/>
<circle cx="640" cy="200" r="1.5"/>
<circle cx="100" cy="260" r="1.5"/>
<circle cx="160" cy="260" r="1.5"/>
<circle cx="220" cy="260" r="1.5"/>
<circle cx="280" cy="260" r="1.5"/>
<circle cx="340" cy="260" r="1.5"/>
<circle cx="400" cy="260" r="1.5"/>
<circle cx="460" cy="260" r="1.5"/>
<circle cx="520" cy="260" r="1.5"/>
<circle cx="580" cy="260" r="1.5"/>
<circle cx="640" cy="260" r="1.5"/>
<circle cx="100" cy="320" r="1.5"/>
<circle cx="160" cy="320" r="1.5"/>
<circle cx="220" cy="320" r="1.5"/>
<circle cx="280" cy="320" r="1.5"/>
<circle cx="340" cy="320" r="1.5"/>
<circle cx="400" cy="320" r="1.5"/>
<circle cx="460" cy="320" r="1.5"/>
<circle cx="520" cy="320" r="1.5"/>
<circle cx="580" cy="320" r="1.5"/>
<circle cx="640" cy="320" r="1.5"/>
</g>
<!-- Onboarding Overlay -->
<!-- Semi-transparent backdrop -->
<rect x="1" y="38" width="738" height="361" fill="black" opacity="0.15" rx="0"/>
<!-- Overlay card -->
<rect x="195" y="135" width="350" height="200" rx="16" fill="white" stroke="#E2E8F0" stroke-width="1"/>
<!-- Pencil icon -->
<circle cx="370" cy="175" r="22" fill="#F0FDFA"/>
<line x1="360" y1="185" x2="380" y2="165" stroke="#0D9488" stroke-width="3" stroke-linecap="round"/>
<circle cx="360" cy="185" r="3" fill="#0D9488"/>
<!-- Welcome text -->
<text x="370" y="218" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="18" font-weight="bold" fill="#1E293B">Welcome to your Napkin!</text>
<text x="370" y="244" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#64748B">Sketch your ideas here — draw, write, add sticky notes.</text>
<text x="370" y="262" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#64748B">When you're done, click "Share with Copilot"</text>
<text x="370" y="280" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#64748B">to send your sketch back to the CLI.</text>
<!-- Got it button -->
<rect x="320" y="296" width="100" height="32" rx="16" fill="#0D9488"/>
<text x="370" y="316" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="13" font-weight="bold" fill="white">Got it!</text>
</g>
<!-- Callout annotations -->
<!-- Toolbar callout -->
<rect x="608" y="68" width="90" height="26" rx="6" fill="#0D9488" opacity="0.12"/>
<text x="653" y="85" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="10" fill="#0D9488" font-weight="bold">Drawing tools</text>
<path d="M608,82 L590,82" fill="none" stroke="#0D9488" stroke-width="1.5" stroke-dasharray="3,2"/>
<polygon points="592,79 586,82 592,85" fill="#0D9488"/>
<!-- Share button callout -->
<path d="M726,396 C746,396 756,416 756,436" fill="none" stroke="#0D9488" stroke-width="1.5" stroke-dasharray="3,2"/>
<polygon points="753,434 756,442 759,434" fill="#0D9488"/>
<rect x="690" y="444" width="104" height="26" rx="6" fill="#0D9488" opacity="0.12"/>
<text x="742" y="461" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="10" fill="#0D9488" font-weight="bold">Send to Copilot</text>
</svg><svg xmlns="http://www.w3.org/2000/svg" width="800" height="480" viewBox="0 0 800 480">
<defs>
<linearGradient id="stickyYellow" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#FEF9C3"/>
<stop offset="100%" stop-color="#FEF3C7"/>
</linearGradient>
<linearGradient id="stickyPink" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#FCE7F3"/>
<stop offset="100%" stop-color="#FBCFE8"/>
</linearGradient>
<linearGradient id="stickyBlue" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#DBEAFE"/>
<stop offset="100%" stop-color="#BFDBFE"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="800" height="480" rx="12" fill="#F8FAFB"/>
<!-- Step indicator -->
<circle cx="46" cy="28" r="16" fill="#0D9488"/>
<text x="46" y="33" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="14" font-weight="bold" fill="white">3</text>
<text x="72" y="33" font-family="Arial, Helvetica, sans-serif" font-size="15" font-weight="bold" fill="#334155">Sketch your ideas on the whiteboard</text>
<!-- Browser Window -->
<g transform="translate(30, 52)">
<!-- Browser frame -->
<rect width="740" height="390" rx="10" fill="white" stroke="#D1D5DB" stroke-width="1.5"/>
<!-- Browser chrome -->
<rect width="740" height="38" rx="10" fill="#F1F5F9"/>
<rect x="0" y="28" width="740" height="10" fill="#F1F5F9"/>
<circle cx="18" cy="19" r="5.5" fill="#FF5F57"/>
<circle cx="36" cy="19" r="5.5" fill="#FFBD2E"/>
<circle cx="54" cy="19" r="5.5" fill="#28C840"/>
<rect x="80" y="8" width="580" height="22" rx="6" fill="white" stroke="#E2E8F0" stroke-width="1"/>
<text x="96" y="23" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#94A3B8">napkin.html</text>
<!-- Whiteboard canvas -->
<rect x="1" y="38" width="738" height="351" fill="white"/>
<!-- Toolbar -->
<rect x="120" y="48" width="500" height="36" rx="18" fill="white" stroke="#E2E8F0" stroke-width="1.5"/>
<!-- Pen tool ACTIVE -->
<g transform="translate(155, 54)">
<rect width="60" height="24" rx="12" fill="#0D9488"/>
<line x1="16" y1="18" x2="26" y2="6" stroke="white" stroke-width="2" stroke-linecap="round"/>
<circle cx="16" cy="18" r="2" fill="white"/>
<text x="34" y="16" font-family="Arial, Helvetica, sans-serif" font-size="9" fill="white" font-weight="bold">Pen</text>
</g>
<!-- Other tools -->
<g transform="translate(230, 54)">
<rect width="76" height="24" rx="12" fill="#F1F5F9"/>
<rect x="10" y="6" width="12" height="12" rx="2" fill="none" stroke="#64748B" stroke-width="1.5"/>
<text x="30" y="16" font-family="Arial, Helvetica, sans-serif" font-size="9" fill="#64748B">Shapes</text>
</g>
<g transform="translate(320, 54)">
<rect width="70" height="24" rx="12" fill="#F1F5F9"/>
<line x1="12" y1="16" x2="24" y2="8" stroke="#64748B" stroke-width="1.5" stroke-linecap="round"/>
<polygon points="22,5 28,8 22,11" fill="#64748B"/>
<text x="34" y="16" font-family="Arial, Helvetica, sans-serif" font-size="9" fill="#64748B">Arrow</text>
</g>
<g transform="translate(404, 54)">
<rect width="80" height="24" rx="12" fill="#F1F5F9"/>
<rect x="10" y="6" width="12" height="12" rx="1" fill="#FEF3C7" stroke="#F59E0B" stroke-width="0.8"/>
<text x="28" y="16" font-family="Arial, Helvetica, sans-serif" font-size="9" fill="#64748B">Sticky Note</text>
</g>
<g transform="translate(498, 54)">
<rect width="70" height="24" rx="12" fill="#F1F5F9"/>
<rect x="10" y="8" width="14" height="10" rx="2" fill="none" stroke="#64748B" stroke-width="1.5"/>
<text x="30" y="16" font-family="Arial, Helvetica, sans-serif" font-size="9" fill="#64748B">Eraser</text>
</g>
<!-- ======= USER DRAWINGS ======= -->
<!-- Flowchart Box 1: "Draft" -->
<rect x="80" y="130" width="130" height="60" rx="8" fill="white" stroke="#475569" stroke-width="2"/>
<text x="145" y="158" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="14" fill="#1E293B" font-weight="bold">Draft</text>
<text x="145" y="176" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#64748B">proposal</text>
<!-- Arrow 1 to 2 -->
<line x1="210" y1="160" x2="270" y2="160" stroke="#475569" stroke-width="2.5" stroke-linecap="round"/>
<polygon points="268,154 280,160 268,166" fill="#475569"/>
<!-- Flowchart Box 2: "Review" -->
<rect x="280" y="130" width="130" height="60" rx="8" fill="white" stroke="#475569" stroke-width="2"/>
<text x="345" y="158" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="14" fill="#1E293B" font-weight="bold">Review</text>
<text x="345" y="176" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#64748B">with team</text>
<!-- Arrow 2 to 3 -->
<line x1="410" y1="160" x2="470" y2="160" stroke="#475569" stroke-width="2.5" stroke-linecap="round"/>
<polygon points="468,154 480,160 468,166" fill="#475569"/>
<!-- Flowchart Box 3: "Ship" -->
<rect x="480" y="130" width="130" height="60" rx="8" fill="white" stroke="#475569" stroke-width="2"/>
<text x="545" y="158" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="14" fill="#1E293B" font-weight="bold">Ship</text>
<text x="545" y="176" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#64748B">to customer</text>
<!-- Sticky Note 1: Yellow -->
<g transform="translate(75, 225) rotate(-2)">
<rect width="140" height="90" rx="3" fill="url(#stickyYellow)" stroke="#F59E0B" stroke-width="0.8"/>
<text x="14" y="28" font-family="Arial, Helvetica, sans-serif" font-size="13" fill="#92400E" font-weight="bold">Need legal</text>
<text x="14" y="48" font-family="Arial, Helvetica, sans-serif" font-size="13" fill="#92400E" font-weight="bold">review</text>
<text x="14" y="72" font-family="Arial, Helvetica, sans-serif" font-size="10" fill="#B45309">before shipping!</text>
</g>
<!-- Sticky Note 2: Pink -->
<g transform="translate(260, 240) rotate(1.5)">
<rect width="130" height="80" rx="3" fill="url(#stickyPink)" stroke="#EC4899" stroke-width="0.8"/>
<text x="14" y="28" font-family="Arial, Helvetica, sans-serif" font-size="13" fill="#9D174D" font-weight="bold">Q2 deadline</text>
<text x="14" y="50" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#BE185D">June 30th</text>
</g>
<!-- Sticky Note 3: Blue -->
<g transform="translate(440, 230) rotate(-1)">
<rect width="140" height="85" rx="3" fill="url(#stickyBlue)" stroke="#3B82F6" stroke-width="0.8"/>
<text x="14" y="28" font-family="Arial, Helvetica, sans-serif" font-size="13" fill="#1E40AF" font-weight="bold">Check with</text>
<text x="14" y="48" font-family="Arial, Helvetica, sans-serif" font-size="13" fill="#1E40AF" font-weight="bold">Sarah</text>
<text x="14" y="70" font-family="Arial, Helvetica, sans-serif" font-size="10" fill="#2563EB">re: compliance</text>
</g>
<!-- Freehand circle around "Review" box -->
<ellipse cx="345" cy="160" rx="85" ry="45" fill="none" stroke="#EF4444" stroke-width="2" stroke-dasharray="6,3" opacity="0.7"/>
<!-- Freehand underline under "Ship" -->
<path d="M490,186 C510,192 540,190 600,188" fill="none" stroke="#F59E0B" stroke-width="2.5" stroke-linecap="round" opacity="0.7"/>
<!-- Small star/asterisk near Review box -->
<text x="420" y="128" font-family="Arial, Helvetica, sans-serif" font-size="20" fill="#EF4444" font-weight="bold">*</text>
<text x="430" y="128" font-family="Arial, Helvetica, sans-serif" font-size="10" fill="#EF4444">important!</text>
<!-- Freehand question marks -->
<text x="620" y="170" font-family="Arial, Helvetica, sans-serif" font-size="18" fill="#94A3B8">?</text>
<text x="636" y="162" font-family="Arial, Helvetica, sans-serif" font-size="14" fill="#CBD5E1">?</text>
<!-- Share button (still visible) -->
<rect x="540" y="348" width="180" height="34" rx="17" fill="#0D9488"/>
<text x="582" y="369" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="white" font-weight="bold">Share with Copilot</text>
</g>
<!-- Callout annotation -->
<rect x="30" y="448" width="440" height="26" rx="8" fill="#0D9488" opacity="0.1"/>
<text x="42" y="465" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#0D9488" font-weight="bold">Draw, sketch, and add notes — whatever helps you think</text>
</svg><svg xmlns="http://www.w3.org/2000/svg" width="800" height="480" viewBox="0 0 800 480">
<defs>
<linearGradient id="popupShadow" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#0D9488" stop-opacity="0.08"/>
<stop offset="100%" stop-color="#0D9488" stop-opacity="0.02"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="800" height="480" rx="12" fill="#F8FAFB"/>
<!-- Step indicator -->
<circle cx="46" cy="28" r="16" fill="#0D9488"/>
<text x="46" y="33" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="14" font-weight="bold" fill="white">4</text>
<text x="72" y="33" font-family="Arial, Helvetica, sans-serif" font-size="15" font-weight="bold" fill="#334155">Share your sketch with Copilot</text>
<!-- Browser Window -->
<g transform="translate(30, 52)">
<!-- Browser frame -->
<rect width="740" height="340" rx="10" fill="white" stroke="#D1D5DB" stroke-width="1.5"/>
<!-- Browser chrome -->
<rect width="740" height="38" rx="10" fill="#F1F5F9"/>
<rect x="0" y="28" width="740" height="10" fill="#F1F5F9"/>
<circle cx="18" cy="19" r="5.5" fill="#FF5F57"/>
<circle cx="36" cy="19" r="5.5" fill="#FFBD2E"/>
<circle cx="54" cy="19" r="5.5" fill="#28C840"/>
<rect x="80" y="8" width="580" height="22" rx="6" fill="white" stroke="#E2E8F0" stroke-width="1"/>
<text x="96" y="23" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#94A3B8">napkin.html</text>
<!-- Whiteboard canvas (slightly dimmed to show overlay context) -->
<rect x="1" y="38" width="738" height="301" fill="#FAFAFA"/>
<!-- Simplified whiteboard content (faded to focus on the popup) -->
<!-- Mini flowchart -->
<rect x="80" y="100" width="100" height="45" rx="6" fill="white" stroke="#CBD5E1" stroke-width="1.5" opacity="0.6"/>
<text x="130" y="127" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#94A3B8">Draft</text>
<line x1="180" y1="122" x2="230" y2="122" stroke="#CBD5E1" stroke-width="1.5" opacity="0.6"/>
<polygon points="228,118 236,122 228,126" fill="#CBD5E1" opacity="0.6"/>
<rect x="236" y="100" width="100" height="45" rx="6" fill="white" stroke="#CBD5E1" stroke-width="1.5" opacity="0.6"/>
<text x="286" y="127" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#94A3B8">Review</text>
<line x1="336" y1="122" x2="386" y2="122" stroke="#CBD5E1" stroke-width="1.5" opacity="0.6"/>
<polygon points="384,118 392,122 384,126" fill="#CBD5E1" opacity="0.6"/>
<rect x="392" y="100" width="100" height="45" rx="6" fill="white" stroke="#CBD5E1" stroke-width="1.5" opacity="0.6"/>
<text x="442" y="127" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#94A3B8">Ship</text>
<!-- Mini sticky notes (faded) -->
<rect x="90" y="170" width="100" height="60" rx="2" fill="#FEF9C3" opacity="0.5"/>
<rect x="260" y="175" width="90" height="55" rx="2" fill="#FCE7F3" opacity="0.5"/>
<rect x="420" y="168" width="100" height="58" rx="2" fill="#DBEAFE" opacity="0.5"/>
<!-- Share with Copilot button — HIGHLIGHTED with glow -->
<rect x="498" y="293" width="180" height="36" rx="18" fill="#059669"/>
<rect x="496" y="291" width="184" height="40" rx="20" fill="none" stroke="#0D9488" stroke-width="3" opacity="0.5"/>
<text x="540" y="315" font-family="Arial, Helvetica, sans-serif" font-size="13" fill="white" font-weight="bold">Share with Copilot</text>
<!-- Click cursor icon near button -->
<g transform="translate(660, 300)">
<path d="M0,0 L0,18 L5,14 L9,22 L13,20 L9,12 L15,12 Z" fill="#1E293B" stroke="white" stroke-width="1"/>
</g>
<!-- Confirmation Popup -->
<rect x="190" y="165" width="360" height="145" rx="14" fill="white" stroke="#0D9488" stroke-width="2"/>
<!-- Green check circle -->
<circle cx="370" cy="200" r="18" fill="#D1FAE5"/>
<path d="M360,200 L367,207 L381,193" fill="none" stroke="#059669" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Popup text -->
<text x="370" y="236" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="15" font-weight="bold" fill="#1E293B">Shared with Copilot!</text>
<text x="370" y="258" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#64748B">A screenshot was saved and your text was copied.</text>
<text x="370" y="276" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#64748B">Go back to your terminal to continue.</text>
<!-- Tiny icons for PNG + text -->
<rect x="285" y="286" width="50" height="16" rx="4" fill="#F0FDFA" stroke="#0D9488" stroke-width="0.8"/>
<text x="310" y="297" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="8" fill="#0D9488">PNG</text>
<rect x="345" y="286" width="50" height="16" rx="4" fill="#F0FDFA" stroke="#0D9488" stroke-width="0.8"/>
<text x="370" y="297" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="8" fill="#0D9488">TEXT</text>
<!-- Check marks -->
<text x="272" y="298" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#059669">✓</text>
<text x="332" y="298" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#059669">✓</text>
</g>
<!-- Annotation Arrow 1: Pointing to Share button -->
<g transform="translate(0, 0)">
<rect x="590" y="395" width="180" height="28" rx="8" fill="#0D9488" opacity="0.12"/>
<text x="680" y="413" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#0D9488" font-weight="bold">Click when you're ready</text>
<path d="M680,395 C680,380 670,365 660,355" fill="none" stroke="#0D9488" stroke-width="1.5" stroke-dasharray="4,3"/>
<polygon points="656,357 660,349 664,357" fill="#0D9488"/>
</g>
<!-- Annotation Arrow 2: Terminal instruction -->
<g transform="translate(0, 0)">
<rect x="30" y="415" width="490" height="52" rx="10" fill="white" stroke="#0D9488" stroke-width="1.5"/>
<!-- Terminal mini icon -->
<rect x="46" y="426" width="26" height="18" rx="3" fill="#1E1B3A"/>
<text x="52" y="439" font-family="monospace" font-size="8" fill="#0D9488">_</text>
<text x="82" y="437" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#334155" font-weight="bold">Next: Go back to your terminal and say:</text>
<rect x="82" y="446" width="172" height="18" rx="4" fill="#F0FDFA"/>
<text x="92" y="458" font-family="monospace" font-size="11" fill="#0D9488" font-weight="bold">check the napkin</text>
</g>
</svg><svg xmlns="http://www.w3.org/2000/svg" width="800" height="480" viewBox="0 0 800 480">
<defs>
<linearGradient id="termBg5" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#2D2B55"/>
<stop offset="100%" stop-color="#1E1B3A"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="800" height="480" rx="12" fill="#F8FAFB"/>
<!-- Step indicator -->
<circle cx="46" cy="28" r="16" fill="#0D9488"/>
<text x="46" y="33" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="14" font-weight="bold" fill="white">5</text>
<text x="72" y="33" font-family="Arial, Helvetica, sans-serif" font-size="15" font-weight="bold" fill="#334155">Copilot reads your napkin and responds</text>
<!-- Terminal Window -->
<g transform="translate(40, 55)">
<!-- Terminal chrome -->
<rect width="550" height="370" rx="10" fill="#1E1B3A" stroke="#3D3A5C" stroke-width="1"/>
<!-- Title bar -->
<rect width="550" height="36" rx="10" fill="#2D2B55"/>
<rect x="0" y="26" width="550" height="10" fill="#2D2B55"/>
<!-- Window buttons -->
<circle cx="20" cy="18" r="6" fill="#FF5F57"/>
<circle cx="40" cy="18" r="6" fill="#FFBD2E"/>
<circle cx="60" cy="18" r="6" fill="#28C840"/>
<text x="275" y="22" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="#8B87B0">Terminal</text>
<!-- User prompt -->
<text x="20" y="62" font-family="monospace" font-size="13" fill="#0D9488">❯</text>
<text x="36" y="62" font-family="monospace" font-size="13" fill="#E0DEF4">check the napkin</text>
<!-- Loading indicator -->
<text x="20" y="88" font-family="monospace" font-size="13" fill="#0D9488">✓</text>
<text x="36" y="88" font-family="monospace" font-size="13" fill="#A5F3FC">Reading your napkin...</text>
<!-- Copilot response header -->
<text x="20" y="120" font-family="monospace" font-size="13" fill="#C4B5FD">Copilot</text>
<!-- Response content - warm, conversational -->
<text x="20" y="148" font-family="monospace" font-size="12" fill="#E0DEF4">I can see your sketch! Here's what I'm picking up:</text>
<!-- Response body -->
<text x="20" y="178" font-family="monospace" font-size="12" fill="#E0DEF4">You've drawn a three-stage workflow:</text>
<text x="20" y="200" font-family="monospace" font-size="12" fill="#A5F3FC"> Draft</text>
<text x="80" y="200" font-family="monospace" font-size="12" fill="#64748B">→</text>
<text x="100" y="200" font-family="monospace" font-size="12" fill="#A5F3FC">Review</text>
<text x="165" y="200" font-family="monospace" font-size="12" fill="#64748B">→</text>
<text x="185" y="200" font-family="monospace" font-size="12" fill="#A5F3FC">Ship</text>
<text x="20" y="228" font-family="monospace" font-size="12" fill="#E0DEF4">Your sticky notes highlight some key items:</text>
<!-- Sticky note references -->
<text x="20" y="254" font-family="monospace" font-size="12" fill="#FBBF24">■</text>
<text x="36" y="254" font-family="monospace" font-size="12" fill="#FEF3C7">"Need legal review" — before the Ship phase</text>
<text x="20" y="276" font-family="monospace" font-size="12" fill="#F472B6">■</text>
<text x="36" y="276" font-family="monospace" font-size="12" fill="#FCE7F3">"Q2 deadline" — June 30th</text>
<text x="20" y="298" font-family="monospace" font-size="12" fill="#60A5FA">■</text>
<text x="36" y="298" font-family="monospace" font-size="12" fill="#DBEAFE">"Check with Sarah" — re: compliance</text>
<text x="20" y="328" font-family="monospace" font-size="12" fill="#E0DEF4">It looks like you're planning a review workflow</text>
<text x="20" y="348" font-family="monospace" font-size="12" fill="#E0DEF4">with a legal gate. Would you like me to help you</text>
<text x="20" y="368" font-family="monospace" font-size="12" fill="#E0DEF4">turn this into a checklist or project plan?</text>
<!-- Cursor -->
<rect x="20" y="385" width="8" height="14" fill="#0D9488" opacity="0.8"/>
</g>
<!-- Annotation callout -->
<g transform="translate(608, 75)">
<rect width="170" height="84" rx="10" fill="#0D9488" opacity="0.1"/>
<text x="85" y="24" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#0D9488" font-weight="bold">Copilot sees your</text>
<text x="85" y="42" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#0D9488" font-weight="bold">drawings and text,</text>
<text x="85" y="60" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#0D9488" font-weight="bold">then responds in</text>
<text x="85" y="78" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" fill="#0D9488" font-weight="bold">plain English</text>
</g>
<!-- Arrow from annotation to terminal content -->
<path d="M608,130 C598,140 596,150 594,160" fill="none" stroke="#0D9488" stroke-width="1.5" stroke-dasharray="4,3"/>
<polygon points="590,158 594,166 598,158" fill="#0D9488"/>
<!-- What Copilot understood panel -->
<g transform="translate(608, 190)">
<rect width="170" height="230" rx="10" fill="white" stroke="#E2E8F0" stroke-width="1"/>
<text x="85" y="24" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="11" font-weight="bold" fill="#334155">What Copilot understood:</text>
<line x1="15" y1="34" x2="155" y2="34" stroke="#E2E8F0" stroke-width="1"/>
<!-- Mini flowchart icon -->
<rect x="15" y="48" width="14" height="14" rx="2" fill="#F0FDFA" stroke="#0D9488" stroke-width="1"/>
<text x="35" y="59" font-family="Arial, Helvetica, sans-serif" font-size="10" fill="#475569">3-stage workflow</text>
<!-- Sticky note icon -->
<rect x="15" y="74" width="14" height="14" rx="1" fill="#FEF9C3" stroke="#F59E0B" stroke-width="0.8"/>
<text x="35" y="85" font-family="Arial, Helvetica, sans-serif" font-size="10" fill="#475569">Legal review needed</text>
<!-- Calendar icon -->
<rect x="15" y="100" width="14" height="14" rx="2" fill="#FCE7F3" stroke="#EC4899" stroke-width="0.8"/>
<text x="35" y="111" font-family="Arial, Helvetica, sans-serif" font-size="10" fill="#475569">Q2 deadline (June 30)</text>
<!-- Person icon -->
<rect x="15" y="126" width="14" height="14" rx="2" fill="#DBEAFE" stroke="#3B82F6" stroke-width="0.8"/>
<text x="35" y="137" font-family="Arial, Helvetica, sans-serif" font-size="10" fill="#475569">Follow up with Sarah</text>
<!-- Annotation icon -->
<rect x="15" y="152" width="14" height="14" rx="7" fill="#FEE2E2" stroke="#EF4444" stroke-width="0.8"/>
<text x="35" y="163" font-family="Arial, Helvetica, sans-serif" font-size="10" fill="#475569">Review step = priority</text>
<line x1="15" y1="180" x2="155" y2="180" stroke="#E2E8F0" stroke-width="1"/>
<text x="85" y="200" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="10" fill="#0D9488" font-weight="bold">Everything from your</text>
<text x="85" y="216" text-anchor="middle" font-family="Arial, Helvetica, sans-serif" font-size="10" fill="#0D9488" font-weight="bold">napkin — captured!</text>
</g>
</svg>Related skills
How it compares
Choose Napkin over text-only prompting when visual sketches communicate structure faster than prose, and you already work inside Copilot CLI.
FAQ
What is napkin?
Visual whiteboard collaboration for Copilot CLI. Creates an interactive whiteboard that opens in your browser — draw, sketch, add sticky notes, then share everything back with Copi
When should I use napkin?
Visual whiteboard collaboration for Copilot CLI. Creates an interactive whiteboard that opens in your browser — draw, sketch, add sticky notes, then share everything back with Copi
Is napkin safe to install?
Review the Security Audits panel on this page before production use.