Installation

Install with CLI Recommended
gh skills-hub install napkin

Don't have the extension? Run gh extension install samueltauil/skills-hub first.

Download and extract to your repository:

.github/skills/napkin/

Extract the ZIP to .github/skills/ in your repo. The folder name must match napkin for Copilot to auto-discover it.

Skill Files (2)

SKILL.md 7.0 KB
---
name: napkin
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. Copilot sees your drawings and text, and responds with analysis, suggestions, and ideas.'
---

# 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.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.

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.html` with 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 `view` tool.
- 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.html` relative 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.
assets/
napkin.html 57.2 KB
<!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">&#9993;</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 &mdash; 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 &mdash; start drawing</button>
  </div>
</div>

<!-- ── Share confirmation overlay ──────────────────────────────── -->
<div class="overlay-backdrop hidden" id="share-overlay">
  <div class="overlay-card">
    <div class="confirm-icon">&#10004;&#65039;</div>
    <h1>Shared with Copilot!</h1>
    <div class="confirm-detail">
      &#128190; A screenshot was saved (check your Downloads or Desktop).<br>
      &#128203; 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">&times;</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">&minus;</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>

License (MIT)

View full license text
MIT License

Copyright GitHub, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.