# Share Card v2 + Markdown Upgrade — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Upgrade markdown rendering across the entire serve UI, rebuild share card as DOM-based independent ES module with 6-theme thumbnail picker and html2canvas export.

**Architecture:** Replace regex-based `renderMarkdown()` with `marked` (CDN). Extract share card from serve-ui.html into `src/share-card/share-card.js` as standalone ES module, inline back into serve-ui.html as IIFE. Share card renders via DOM (not Canvas), exports via html2canvas lazy-loaded from CDN.

**Tech Stack:** marked (CDN), html2canvas (CDN, lazy), vanilla JS ES module, vitest

**Spec:** `docs/superpowers/specs/2026-03-21-share-v2-design.md`

---

### Task 1: Upgrade renderMarkdown to marked

**Files:**
- Modify: `src/commands/serve-ui.html:3-7` (add marked CDN script tag in head)
- Modify: `src/commands/serve-ui.html:1270-1278` (replace renderMarkdown)
- Modify: `src/commands/serve-ui.html` CSS section (add markdown element styles)

- [ ] **Step 1: Add marked CDN script tag**

In `serve-ui.html`, after line 6 (`<title>memex</title>`), add:

```html
<script src="https://cdn.jsdelivr.net/npm/marked@15/marked.min.js"></script>
```

- [ ] **Step 2: Add CSS for markdown elements**

In the `<style>` section, after the `.card-body-inner pre code` rules (around line 530), add styles for tables, images, lists, blockquotes, headings, and hr inside `.card-body-inner`.

```css
.card-body-inner h1, .card-body-inner h2, .card-body-inner h3,
.card-body-inner h4, .card-body-inner h5, .card-body-inner h6 {
  margin: 16px 0 8px;
  font-weight: 600;
  color: var(--label);
}
.card-body-inner h1 { font-size: 20px; }
.card-body-inner h2 { font-size: 17px; }
.card-body-inner h3 { font-size: 15px; }
.card-body-inner h4, .card-body-inner h5, .card-body-inner h6 { font-size: 13px; }
.card-body-inner ul, .card-body-inner ol {
  margin: 8px 0;
  padding-left: 20px;
}
.card-body-inner li { margin-bottom: 4px; line-height: 1.6; }
.card-body-inner blockquote {
  margin: 8px 0;
  padding: 4px 12px;
  border-left: 3px solid var(--blue);
  color: var(--label-2);
}
.card-body-inner table {
  width: 100%;
  border-collapse: collapse;
  margin: 8px 0;
  font-size: 12px;
}
.card-body-inner th, .card-body-inner td {
  padding: 6px 10px;
  border: 1px solid var(--border);
  text-align: left;
}
.card-body-inner th {
  background: rgba(0,0,0,0.03);
  font-weight: 600;
}
.card-body-inner tr:nth-child(even) td {
  background: rgba(0,0,0,0.015);
}
.card-body-inner img {
  max-width: 100%;
  border-radius: 6px;
  margin: 8px 0;
}
.card-body-inner hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: 12px 0;
}
```

Also add midnight theme overrides after the existing midnight rules (around line 136):

```css
[data-theme="midnight"] .card-body-inner th { background: rgba(255,255,255,0.06); }
[data-theme="midnight"] .card-body-inner th,
[data-theme="midnight"] .card-body-inner td { border-color: rgba(255,255,255,0.08); }
[data-theme="midnight"] .card-body-inner tr:nth-child(even) td { background: rgba(255,255,255,0.03); }
[data-theme="midnight"] .card-body-inner blockquote { border-left-color: #5ac8fa; }
[data-theme="midnight"] .card-body-inner h1, [data-theme="midnight"] .card-body-inner h2,
[data-theme="midnight"] .card-body-inner h3 { color: rgba(255,255,255,0.9); }
```

- [ ] **Step 3: Replace renderMarkdown function**

Replace lines 1270-1278 (`function renderMarkdown(text) { ... }`) with:

```js
function renderMarkdown(text) {
  const preserved = text.replace(/\[\[([^\]]+)\]\]/g, '%%MEMEXLINK:$1%%');
  let html = marked.parse(preserved, { breaks: true });
  html = html.replace(/%%MEMEXLINK:(.+?)%%/g,
    '<span class="chip" data-link="$1">[[$1]]</span>');
  return html;
}
```

Note: `esc()` is no longer called before `marked.parse()` — marked handles escaping internally. The `esc()` function stays because it's used elsewhere (card titles, etc.).

- [ ] **Step 4: Verify manually**

Run: `npm run build && MEMEX_HOME=~/.memex node dist/cli.js serve`

Open the UI, expand a card with markdown content (tables, lists, bold, code blocks). Verify rendering in both timeline and graph views.

- [ ] **Step 5: Commit**

```bash
git add src/commands/serve-ui.html
git commit -m "feat: upgrade renderMarkdown to marked library

Replaces regex-based markdown with marked.parse() for full
markdown support (tables, images, lists, blockquotes, headings).
Adds CSS for all new markdown elements including midnight theme."
```

---

### Task 2: Create share-card.js ES module

**Files:**
- Create: `src/share-card/share-card.js`

- [ ] **Step 1: Create share-card directory**

Run: `mkdir -p src/share-card`

- [ ] **Step 2: Write share-card.js**

Create `src/share-card/share-card.js` with the full module. This is the core of the feature. The file contains:

1. `themes` object — 6 theme definitions
2. `STYLES` string — scoped CSS for `.memex-sc-*` classes
3. `createShareCard(container, options)` — main factory function
4. Internal functions: `buildDOM()`, `buildPicker()`, `applyTheme()`, `render()`, `lazyLoadHtml2Canvas()`, `exportPng()`

```js
// share-card.js — Independent ES module for memex share cards
// Zero dependencies. markdownRenderer and html2canvas are injected/lazy-loaded.

const themes = {
  clean: {
    name: 'Clean',
    background: '#ffffff',
    text: '#1d1d1f',
    secondary: '#666666',
    accent: '#007aff',
    chipBg: 'rgba(0,122,255,0.08)',
    chipText: '#007aff',
    border: 'rgba(0,0,0,0.08)',
    brand: '#999999',
    contentBg: 'rgba(255,255,255,0.9)',
  },
  aurora: {
    name: 'Aurora',
    background: 'radial-gradient(138% 32% at 70% 33%, #fff 2%, rgba(255,160,247,0.3) 50%, rgba(212,245,255,0.5)), #fff',
    text: '#1d1d1f',
    secondary: '#666666',
    accent: '#007aff',
    chipBg: 'rgba(0,122,255,0.08)',
    chipText: '#007aff',
    border: 'rgba(0,0,0,0.06)',
    brand: '#999999',
    contentBg: 'transparent',
  },
  spectrum: {
    name: 'Spectrum',
    background: 'linear-gradient(145deg, #c676ff 0%, #654cff 41%, #405eff 75%, #007fff 99%)',
    text: '#ffffff',
    secondary: 'rgba(255,255,255,0.7)',
    accent: '#a0d4ff',
    chipBg: 'rgba(255,255,255,0.15)',
    chipText: 'rgba(255,255,255,0.9)',
    border: 'rgba(255,255,255,0.15)',
    brand: 'rgba(255,255,255,0.4)',
    contentBg: 'rgba(0,0,0,0.15)',
  },
  ocean: {
    name: 'Ocean',
    background: '#235ff5',
    text: '#ffffff',
    secondary: 'rgba(255,255,255,0.7)',
    accent: '#a0d4ff',
    chipBg: 'rgba(255,255,255,0.15)',
    chipText: 'rgba(255,255,255,0.9)',
    border: 'rgba(255,255,255,0.15)',
    brand: 'rgba(255,255,255,0.4)',
    contentBg: 'rgba(0,0,0,0.1)',
  },
  ember: {
    name: 'Ember',
    background: '#fb7933',
    text: '#ffffff',
    secondary: 'rgba(255,255,255,0.75)',
    accent: '#ffd6b0',
    chipBg: 'rgba(255,255,255,0.18)',
    chipText: 'rgba(255,255,255,0.9)',
    border: 'rgba(255,255,255,0.18)',
    brand: 'rgba(255,255,255,0.45)',
    contentBg: 'rgba(0,0,0,0.08)',
  },
  frost: {
    name: 'Frost',
    background: '#e7f1fa',
    text: '#1d1d1f',
    secondary: '#5a7a9a',
    accent: '#007aff',
    chipBg: 'rgba(0,122,255,0.08)',
    chipText: '#007aff',
    border: 'rgba(17,31,44,0.08)',
    brand: '#8aa0b8',
    contentBg: 'rgba(255,255,255,0.5)',
  },
};

const STYLES = `
.memex-sc-root { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
.memex-sc-picker { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 16px; max-width: 420px; }
.memex-sc-thumb {
  aspect-ratio: 3/4; border-radius: 8px; padding: 8px; cursor: pointer;
  border: 2px solid transparent; transition: border-color 0.15s, transform 0.15s;
  display: flex; flex-direction: column; gap: 3px; box-sizing: border-box;
}
.memex-sc-thumb:hover { transform: scale(1.05); }
.memex-sc-thumb.active { border-color: #007aff; }
.memex-sc-skel { border-radius: 2px; }
.memex-sc-card {
  width: 420px; border-radius: 16px; overflow: hidden;
  box-shadow: 0 8px 32px rgba(0,0,0,0.12);
}
.memex-sc-card-inner { padding: 30px; }
.memex-sc-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.memex-sc-source {
  font-size: 10px; font-weight: 600; letter-spacing: 0.5px;
  padding: 3px 10px; border-radius: 10px;
}
.memex-sc-date { font-size: 12px; }
.memex-sc-title { font-size: 18px; font-weight: 700; margin-bottom: 12px; line-height: 1.4; }
.memex-sc-body {
  font-size: 14px; line-height: 1.7; margin-bottom: 16px; border-radius: 8px; padding: 12px;
}
.memex-sc-body p { margin-bottom: 8px; }
.memex-sc-body p:last-child { margin-bottom: 0; }
.memex-sc-body pre { padding: 8px 12px; border-radius: 6px; overflow-x: auto; font-size: 12px; margin: 8px 0; }
.memex-sc-body code { font-size: 12px; padding: 1px 5px; border-radius: 3px; }
.memex-sc-body pre code { padding: 0; background: none; }
.memex-sc-body table { width: 100%; border-collapse: collapse; margin: 8px 0; font-size: 12px; }
.memex-sc-body th, .memex-sc-body td { padding: 6px 10px; text-align: left; }
.memex-sc-body th { font-weight: 600; }
.memex-sc-body img { max-width: 100%; border-radius: 6px; margin: 8px 0; }
.memex-sc-body ul, .memex-sc-body ol { padding-left: 20px; margin: 8px 0; }
.memex-sc-body li { margin-bottom: 4px; }
.memex-sc-body blockquote { margin: 8px 0; padding: 4px 12px; border-left: 3px solid; }
.memex-sc-body h1, .memex-sc-body h2, .memex-sc-body h3 { margin: 12px 0 6px; font-weight: 600; }
.memex-sc-body h1 { font-size: 18px; }
.memex-sc-body h2 { font-size: 16px; }
.memex-sc-body h3 { font-size: 14px; }
.memex-sc-body hr { border: none; margin: 12px 0; }
.memex-sc-links { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 16px; }
.memex-sc-chip { font-size: 11px; padding: 3px 10px; border-radius: 11px; }
.memex-sc-divider { height: 1px; margin-bottom: 16px; }
.memex-sc-footer { display: flex; justify-content: space-between; align-items: center; }
.memex-sc-stats { font-size: 10px; letter-spacing: 1px; }
.memex-sc-brand { font-size: 11px; font-weight: 600; }
.memex-sc-actions { display: flex; gap: 8px; justify-content: center; margin-top: 16px; }
.memex-sc-btn {
  padding: 8px 20px; border: none; border-radius: 8px;
  font-size: 13px; font-weight: 600; cursor: pointer;
  font-family: inherit; transition: all 0.15s;
}
.memex-sc-btn.primary { background: #007aff; color: #fff; }
.memex-sc-btn.primary:hover { background: #0066d6; }
.memex-sc-btn.secondary { background: rgba(0,0,0,0.06); color: #666; }
.memex-sc-btn.secondary:hover { background: rgba(0,0,0,0.1); }
`;

let html2canvasPromise = null;

function lazyLoadHtml2Canvas() {
  if (window.html2canvas) return Promise.resolve(window.html2canvas);
  if (html2canvasPromise) return html2canvasPromise;
  html2canvasPromise = new Promise((resolve, reject) => {
    const s = document.createElement('script');
    s.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1/dist/html2canvas.min.js';
    s.onload = () => resolve(window.html2canvas);
    s.onerror = () => reject(new Error('Failed to load html2canvas'));
    document.head.appendChild(s);
  });
  return html2canvasPromise;
}

function buildSkeleton(t) {
  // Builds skeleton bars mimicking card layout for thumbnail preview
  const light = t.text === '#ffffff' || t.text === 'rgba(255,255,255,0.9)';
  const barColor = light ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.12)';
  return `
    <div class="memex-sc-skel" style="width:60%;height:5px;background:${barColor};margin-bottom:6px"></div>
    <div class="memex-sc-skel" style="width:100%;height:3px;background:${barColor};margin-bottom:3px"></div>
    <div class="memex-sc-skel" style="width:100%;height:3px;background:${barColor};margin-bottom:3px"></div>
    <div class="memex-sc-skel" style="width:40%;height:3px;background:${barColor};margin-bottom:6px"></div>
    <div class="memex-sc-skel" style="width:100%;height:20px;background:${barColor};border-radius:2px"></div>
  `;
}

export function createShareCard(container, options = {}) {
  const {
    data = {},
    theme: initialTheme = 'aurora',
    markdownRenderer = (text) => text,
    onExport = null,
    onCancel = null,
  } = options;

  let currentTheme = initialTheme;
  let currentData = { ...data };

  // Inject styles once
  if (!document.getElementById('memex-sc-styles')) {
    const style = document.createElement('style');
    style.id = 'memex-sc-styles';
    style.textContent = STYLES;
    document.head.appendChild(style);
  }

  // Build picker
  let pickerHtml = '<div class="memex-sc-picker">';
  for (const [key, t] of Object.entries(themes)) {
    const border = t.border !== 'rgba(0,0,0,0.08)' ? '' : 'border:1px solid rgba(0,0,0,0.08);';
    pickerHtml += `<div class="memex-sc-thumb${key === currentTheme ? ' active' : ''}" data-theme="${key}" style="background:${t.background};${border}">${buildSkeleton(t)}</div>`;
  }
  pickerHtml += '</div>';

  function renderCard() {
    const t = themes[currentTheme];
    const bodyHtml = markdownRenderer(currentData.body || '');
    const dateStr = currentData.created ? currentData.created.slice(0, 10).replace(/-/g, '/') : '';
    const src = (currentData.source || 'note').toUpperCase();
    const links = (currentData.links || [])
      .map(l => `<span class="memex-sc-chip" style="background:${t.chipBg};color:${t.chipText}">[[${l}]]</span>`)
      .join('');
    const stats = currentData.stats
      ? `${currentData.stats.totalCards || 0} CARDS \u00b7 ${currentData.stats.totalDays || 0} DAYS`
      : '';

    // Dynamic styles for dark-on-light themes in body
    const bodyBg = t.contentBg;
    const codeBg = t.text === '#ffffff' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.04)';
    const tableBorder = t.border;
    const thBg = t.text === '#ffffff' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.03)';
    const hrBorder = t.border;

    return `
      <div class="memex-sc-card" style="background:${t.background}">
        <div class="memex-sc-card-inner">
          <div class="memex-sc-header">
            <span class="memex-sc-source" style="background:${t.chipBg};color:${t.chipText}">${src}</span>
            <span class="memex-sc-date" style="color:${t.secondary}">${dateStr}</span>
          </div>
          <div class="memex-sc-title" style="color:${t.text}">${currentData.title || currentData.slug || ''}</div>
          <div class="memex-sc-body" style="color:${t.secondary};background:${bodyBg};
            --sc-code-bg:${codeBg};--sc-table-border:${tableBorder};--sc-th-bg:${thBg};--sc-hr:${hrBorder}">
            <style>
              .memex-sc-body code { background: var(--sc-code-bg); }
              .memex-sc-body pre { background: var(--sc-code-bg); }
              .memex-sc-body th, .memex-sc-body td { border: 1px solid var(--sc-table-border); }
              .memex-sc-body th { background: var(--sc-th-bg); }
              .memex-sc-body blockquote { border-left-color: ${t.accent}; }
              .memex-sc-body hr { border-top: 1px solid var(--sc-hr); }
            </style>
            ${bodyHtml}
          </div>
          ${links ? `<div class="memex-sc-links">${links}</div>` : ''}
          <div class="memex-sc-divider" style="background:${t.border}"></div>
          <div class="memex-sc-footer">
            <span class="memex-sc-stats" style="color:${t.brand}">${stats}</span>
            <span class="memex-sc-brand" style="color:${t.brand}">memex</span>
          </div>
        </div>
      </div>
    `;
  }

  // Render full component
  function render() {
    const cardHtml = renderCard();
    const actionsHtml = `<div class="memex-sc-actions">
      ${onCancel ? '<button class="memex-sc-btn secondary" data-action="cancel">Cancel</button>' : ''}
      <button class="memex-sc-btn primary" data-action="export">Download</button>
    </div>`;
    container.innerHTML = `<div class="memex-sc-root">${pickerHtml}${cardHtml}${actionsHtml}</div>`;
    bindEvents();
  }

  function bindEvents() {
    // Picker clicks
    container.querySelectorAll('.memex-sc-thumb').forEach(thumb => {
      thumb.addEventListener('click', () => {
        currentTheme = thumb.dataset.theme;
        container.querySelectorAll('.memex-sc-thumb').forEach(t => t.classList.remove('active'));
        thumb.classList.add('active');
        // Re-render only the card, not the picker
        const cardEl = container.querySelector('.memex-sc-card');
        const t = themes[currentTheme];
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = renderCard();
        cardEl.replaceWith(tempDiv.firstElementChild);
      });
    });
    // Action buttons
    container.querySelector('[data-action="export"]')?.addEventListener('click', () => exportPng());
    container.querySelector('[data-action="cancel"]')?.addEventListener('click', () => { if (onCancel) onCancel(); });
  }

  async function exportPng() {
    const cardEl = container.querySelector('.memex-sc-card');
    if (!cardEl) return;
    const h2c = await lazyLoadHtml2Canvas();
    const canvas = await h2c(cardEl, { scale: 2, useCORS: true, backgroundColor: null });
    canvas.toBlob(blob => {
      if (onExport) {
        onExport(blob, (currentData.slug || 'memex-card') + '.png');
      } else {
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = (currentData.slug || 'memex-card') + '.png';
        a.click();
        URL.revokeObjectURL(url);
      }
    }, 'image/png');
  }

  render();

  // Public controller
  return {
    setTheme(name) {
      if (!themes[name]) return;
      currentTheme = name;
      render();
    },
    setData(newData) {
      currentData = { ...newData };
      render();
    },
    export: exportPng,
    destroy() {
      container.innerHTML = '';
    },
  };
}
```

- [ ] **Step 3: Commit**

```bash
git add src/share-card/share-card.js
git commit -m "feat: create share-card.js independent ES module

Standalone share card component with 6 themes (clean, aurora,
spectrum, ocean, ember, frost), thumbnail picker, DOM rendering,
and html2canvas lazy-loaded export. Zero dependencies —
markdownRenderer is injected by consumer."
```

---

### Task 3: Integrate share-card.js into serve-ui.html

**Files:**
- Modify: `src/commands/serve-ui.html:889-901` (share modal HTML)
- Modify: `src/commands/serve-ui.html:611-684` (share CSS)
- Modify: `src/commands/serve-ui.html:1300-1513` (share JS — remove Canvas renderer, inline share-card.js)

- [ ] **Step 1: Update share modal HTML**

Replace lines 889-901 (the share overlay HTML) with:

```html
<!-- Share modal -->
<div class="share-overlay" id="shareOverlay" onclick="if(event.target===this)closeShare()">
  <div id="shareRoot" style="max-width:480px;width:100%;padding:20px"></div>
</div>
```

- [ ] **Step 2: Update share CSS**

Keep `.share-overlay` styles (lines 630-642). Remove the Canvas-specific styles that are no longer needed:
- Remove `.share-preview` (line 643-646) — was for `<canvas>`
- Remove `.share-themes` and `.share-theme-btn` (lines 671-684) — replaced by share-card.js picker
- Keep `.share-actions` and `.share-action-btn` styles only if used elsewhere, otherwise remove (lines 647-670)

Update `.share-action-btn.secondary` for the overlay context:

```css
.share-overlay .memex-sc-btn.secondary {
  background: rgba(255,255,255,0.1);
  color: rgba(255,255,255,0.7);
}
.share-overlay .memex-sc-btn.secondary:hover { background: rgba(255,255,255,0.15); }
```

- [ ] **Step 3: Inline share-card.js as IIFE**

Replace the entire share card renderer script block (lines 1300-1513, the `<!-- Share card renderer -->` section) with:

```html
<!-- Share card (inlined from src/share-card/share-card.js) -->
<script>
(function() {
  // === PASTE share-card.js contents here, removing `export` keyword ===
  // ... (full contents of share-card.js with `export function` → `function`)

  // Expose to global for serve-ui
  window.createShareCard = createShareCard;
})();
</script>
```

Copy the full contents of `src/share-card/share-card.js` into this IIFE block. Change `export function createShareCard` to just `function createShareCard`.

- [ ] **Step 4: Rewrite shareCard/closeShare/downloadShare functions**

In the main UI script section (after the inlined share-card module), replace the existing `window.shareCard`, `window.setShareTheme`, `window.downloadShare`, `window.closeShare` with:

```js
let shareInstance = null;

window.shareCard = async function(slug) {
  const res = await fetch('/api/cards/' + encodeURIComponent(slug));
  const raw = await res.text();
  const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
  let title = slug, created = '', source = '', links = [];
  let body = raw;
  if (fmMatch) {
    const fm = fmMatch[1];
    body = fmMatch[2].trim();
    const titleM = fm.match(/title:\s*['"]?(.+?)['"]?\s*$/m);
    const createdM = fm.match(/created:\s*['"]?(.+?)['"]?\s*$/m);
    const sourceM = fm.match(/source:\s*['"]?(.+?)['"]?\s*$/m);
    if (titleM) title = titleM[1];
    if (createdM) created = createdM[1];
    if (sourceM) source = sourceM[1];
  }
  const linkRe = /\[\[([^\]]+)\]\]/g;
  let m;
  while ((m = linkRe.exec(body)) !== null) links.push(m[1]);
  links = [...new Set(links)];

  const statsCards = document.getElementById('stat-cards').textContent;
  const statsDays = document.getElementById('stat-days').textContent;

  // Get current app theme and map to share theme
  const appTheme = document.documentElement.getAttribute('data-theme') || 'forest';
  const themeMap = { forest: 'aurora', sonoma: 'clean', sunset: 'ember', midnight: 'spectrum' };
  const shareTheme = themeMap[appTheme] || 'aurora';

  if (shareInstance) shareInstance.destroy();
  shareInstance = createShareCard(document.getElementById('shareRoot'), {
    data: {
      slug, title, body, created, source, links,
      stats: { totalCards: parseInt(statsCards) || 0, totalDays: parseInt(statsDays) || 0 },
    },
    theme: shareTheme,
    markdownRenderer: renderMarkdown,
    onCancel: closeShare,
  });

  document.getElementById('shareOverlay').classList.add('open');
};

window.closeShare = function() {
  document.getElementById('shareOverlay').classList.remove('open');
  if (shareInstance) { shareInstance.destroy(); shareInstance = null; }
};
```

Remove `window.setShareTheme` and `window.downloadShare` — these are now handled internally by share-card.js.

- [ ] **Step 5: Update postbuild script**

In `package.json`, the existing `"postbuild": "cp src/commands/serve-ui.html dist/commands/"` handles the HTML copy. No change needed. But ensure `src/share-card/share-card.js` is also copied to dist for standalone usage:

Add to package.json `files` array or postbuild:

```json
"postbuild": "cp src/commands/serve-ui.html dist/commands/ && cp -r src/share-card dist/"
```

- [ ] **Step 6: Verify end-to-end**

Run: `npm run build && MEMEX_HOME=~/.memex node dist/cli.js serve`

Test the full flow:
1. Open UI, expand a card, click "Share"
2. Verify thumbnail picker shows 6 themes with skeleton previews
3. Click different themes — card preview updates
4. Click "Download" — PNG downloads with correct theme
5. Click "Cancel" — modal closes
6. Test in both timeline and graph views

- [ ] **Step 7: Commit**

```bash
git add src/commands/serve-ui.html package.json
git commit -m "feat: integrate share-card.js into serve UI

Replace Canvas-based share renderer with DOM-based share-card.js.
Thumbnail grid picker with 6 themes. html2canvas for PNG export.
Full markdown rendering in share cards via marked."
```

---

### Task 4: Clean up and verify

**Files:**
- Verify: `src/commands/serve-ui.html` — no dead code
- Verify: `src/share-card/share-card.js` — works standalone

- [ ] **Step 1: Run existing tests**

Run: `npm test`

Expected: All 79+ tests pass. The serve tests don't test the share UI directly (they test API endpoints), so they should pass without modification.

- [ ] **Step 2: Check for dead code**

Search serve-ui.html for any remaining references to the old share implementation:
- `shareCanvas` — should be gone
- `shareSlug` — should be gone
- `shareData` — should be gone
- `renderShareCard` — should be gone
- `share-theme-btn` — should be gone

If any remain, remove them.

- [ ] **Step 3: Verify standalone share-card.js**

Create a quick test HTML file (not committed) to verify the module works independently:

```html
<script src="https://cdn.jsdelivr.net/npm/marked@15/marked.min.js"></script>
<script type="module">
  import { createShareCard } from './src/share-card/share-card.js';
  createShareCard(document.getElementById('app'), {
    data: { title: 'Test', body: '## Hello\n\n- item 1\n- item 2', created: '2026-03-21', source: 'retro', links: ['foo'], stats: { totalCards: 5, totalDays: 2 } },
    markdownRenderer: (text) => marked.parse(text, { breaks: true }),
  });
</script>
<div id="app"></div>
```

Open in browser, verify card renders with picker and export works.

- [ ] **Step 4: Final commit (if cleanup needed)**

```bash
git add src/commands/serve-ui.html
git commit -m "chore: clean up dead share code from serve-ui.html"
```
