Skill: tom859174-sketch/visual-note-card (version 1.0.0)

--- name: visual-note-card description: Generate professional Chinese visual note cards (视觉笔记卡片/信息图) as single-page HTML infographics with automatic PNG export. Use this skill whenever the user asks to create a visual note, knowledge card, infographic, one-pager summary, visual summary, 知识卡片, 视觉笔记, 信息图, 一页纸总结, or any poster-style knowledge visualization. Also trigger when the user wants to summarize an article, blog post, book chapter, or concept into a structured visual card format, or when they reference an existing visual note and ask to create one in the same style. This skill produces both a self-contained HTML file and a high-quality PNG image, ready for sharing on social media or printing. --- # Visual Note Card Generator Create professional, information-dense visual note cards (视觉笔记/信息图) as self-contained HTML files with automatic PNG export via Playwright. The output is a structured, poster-style infographic with modular card layout, suitable for social media sharing or printing. ## Design System Before writing any code, read the reference template at `assets/template.html` for the canonical HTML/CSS structure. Then adapt the content to the user's topic. ### Core Visual Identity The design follows an **editorial knowledge card** aesthetic — high information density with clear visual hierarchy, inspired by premium magazine layouts and structured note-taking. **Color Palette (CSS Variables):** - `--primary`: Deep teal `#1a7a6d` — headers, badges, accent elements - `--primary-dark`: `#145f54` — bottom highlight bar - `--primary-deep`: `#0d3d36` — deep accent - `--accent`: Orange `#e8713a` — emphasis, secondary badges, important labels - `--bg`: Warm gray `#f0ebe4` — page background - `--bg-light`: `#f5f1ec` — light panel background - `--bg-card`: `#e8e3dc` — framework card background - `--black`: `#1a1a1a` — dark panel, primary text - `--white`: `#ffffff` Users may request alternate color schemes. If so, maintain the same structural contrast ratios: one warm neutral background, one strong primary color, one accent color, and black for dark panels. **Typography:** - English display: `Playfair Display` (serif, 700/900) — main title - Chinese body: `Noto Sans SC` (400/500/700/900) — all Chinese text - Monospace: `JetBrains Mono` (500/700) — labels, URLs, tags - Import via Google Fonts in the `<style>` block **Layout — Fixed Poster Dimensions:** - Container width: `1200px` - Padding: `36px 40px 28px` - Background: `var(--bg)` ### Mandatory Layout Sections (Top to Bottom) Every visual note card MUST include these sections in order: #### 1. Top Bar A single-line flex row with: - Left: Topic label in uppercase monospace (e.g., `AI PRODUCT ARCHITECTURE · SYSTEM DESIGN`) - Right: Source label (e.g., `OPENAI ENGINEERING BLOG`) #### 2. Main Title Area Two-column flex layout: - Left: Large bilingual title — English line in `Playfair Display 38px`, Chinese line in `Noto Sans SC 40px` colored with `var(--primary)` - Right: A 2–3 line **thesis statement** (核心观点) summarizing the card's argument, with the key phrase in `<strong>`. Optionally include a source URL below. #### 3. Framework Row (Flexible Grid, Recommended 4 Columns) A row of equal-width cards representing the core framework/model of the topic. The number of columns is flexible — choose based on the actual content: - **2 columns** — binary concepts, comparisons (e.g., Before vs After, Input vs Output) - **3 columns** — triads, triangular models (e.g., People-Process-Technology) - **4 columns** — **recommended default**, most common for frameworks (e.g., E-K-C-F, M-P-D-G) - **5 columns** — five-element models (e.g., HEART metrics, Five Forces) - **6 columns** — extended taxonomies (use sparingly, keep descriptions very short) Analyze the topic and choose the column count that best fits the natural structure. Don't force 4 columns if the concept has 3 or 5 natural parts. **CSS implementation:** Use `grid-template-columns: repeat(N, 1fr)` where N is the chosen column count. **Card structure (same regardless of column count):** - Each card has a colored square **letter badge** (first letter of the concept) + Chinese name - Below the badge: 1–2 lines of description with one `<strong>` keyword - For 5–6 columns, keep descriptions to 1 line to avoid overflow **Badge color rotation** (cycles through these in order): 1. `--primary` (teal) 2. `--primary` (teal) 3. `--accent` (orange) 4. `--primary-deep` (deep teal) 5. `--accent` (orange) — if 5th column exists 6. `--primary` (teal) — if 6th column exists The framework should be a **memorable acronym** (e.g., E-K-C-F, M-P-D-G). Invent one if the source doesn't provide it. #### 4. Two-Column Content Area A `grid-template-columns: 1fr 1fr` layout: **Left: Dark Panel** (`background: var(--black)`, white text) - Section title with emoji icon (⚡, 🔥, 🛠, etc.) - 2 sub-blocks, each with an orange title (`var(--accent)`) and a bulleted list (custom `■` bullets in teal) - List items use `<strong>` for key phrases (white color) - A bottom "conclusion" block with a divider line and a memorable quote/insight (key phrase in `#5ee6d0` mint color) **Right: Light Panel** (`background: var(--bg-light)`) - Section title with star icon (★) - 3–4 numbered insight items, each with: - A large serif number (`Playfair Display 36px`, teal) - Bold title line - 1–2 lines of description with `<strong>` keywords #### 5. Bottom Highlight Bar Full-width bar with `background: var(--primary-dark)`: - Left: The framework formula (e.g., `Architecture = M × P × D × G`) in mint highlight color `#5ee6d0` - Right: A closing thought in lighter text #### 6. Footer Flex row: - Left: Framework label in monospace - Right: Brand / framework name with a small teal dot separator ## Content Strategy When the user provides a topic (or an article URL/text): 1. **Extract or synthesize a 4-part framework** — find the core structural model. If one doesn't exist, create a meaningful decomposition with a memorable acronym. 2. **Write a provocative thesis** — the right-side subtitle should be a strong, opinionated claim, not a bland description. 3. **Dark panel = "Story + Transformation"** — use this for narrative content: problems, transitions, role changes, paradigm shifts. 4. **Light panel = "Pitfalls or Insights"** — use this for actionable numbered takeaways. 5. **Bottom formula** — distill the entire card into one equation-style summary. 6. **All text is Chinese** except for: the main English title line, technical terms, framework acronyms, and footer labels. ## Output ### Default: HTML + PNG By default, generate **both** an HTML file and a PNG image: 1. **Generate the HTML** — single self-contained `.html` file with all CSS inline. No external dependencies except Google Fonts and html2canvas CDN. Save to `/mnt/user-data/outputs/`. 2. **Generate the PNG** — run the bundled `scripts/html2png.py` script to render the HTML into a high-quality PNG image. ```bash python <skill-path>/scripts/html2png.py /mnt/user-data/outputs/card.html /mnt/user-data/outputs/card.png --scale=1.5 ``` Present **both** files to the user (PNG first, then HTML). The PNG is the primary deliverable for sharing; the HTML enables further browser-based export or editing. If the user explicitly asks for "只要 HTML" or "HTML only", skip the PNG step. ### PNG Generation Script: `scripts/html2png.py` A Playwright-based headless renderer. It opens the HTML in Chromium, waits for Google Fonts to load, hides the FAB button, then screenshots the `.poster` element. **Usage:** ```bash python html2png.py <input.html> [output.png] [--scale=N] ``` **Scale options:** - `--scale=1` — standard (1200px wide), smallest file - `--scale=1.5` — **default**, recommended for social media (1800px wide) - `--scale=2` — print quality (2400px wide) **Dependencies:** `playwright` (pip install playwright && playwright install chromium). Pre-installed on Claude's compute environment. ### Download Button (Mandatory in HTML) Every generated HTML card MUST include a **floating action button (FAB)** in the bottom-right corner with a dropdown menu for export options: - **标准 PNG** — 1× scale, quick sharing - **高清 PNG** — 1.5× scale, social media recommended - **超清 PNG** — 2× scale, for printing - **JPEG** — 1.5× scale, smaller file size Implementation: 1. Add `<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>` before `</body>` 2. The FAB (`div.fab-wrap`) is placed OUTSIDE `.poster` so it won't appear in the exported image 3. Clicking the FAB toggles a dropdown menu upward; clicking outside closes it 4. During capture, the FAB is hidden and a toast spinner ("正在导出…") is shown 5. The FAB icon toggles between a download arrow and a close × 6. The exported filename is derived from `document.title` + scale suffix See `assets/template.html` for the complete FAB HTML, CSS, and JS. ``` ## Example Prompts → Expected Behavior | User Says | Action | |-----------|--------| | "帮我做一张关于 RAG 架构的视觉笔记" | Generate HTML + PNG about RAG architecture | | "把这篇文章做成信息图" + article text | Extract key ideas, synthesize framework, generate HTML + PNG | | "生成一张同风格的卡片，主题是微服务" | Generate HTML + PNG about microservices | | "Create a visual note about product-market fit" | Generate bilingual HTML + PNG about PMF | | "只要 HTML，不要图片" | Generate HTML only, skip PNG | | "生成一张 2x 高清的 PNG" | Generate HTML + PNG with `--scale=2` |

path: assets/template.html
content: <!--
  VISUAL NOTE CARD TEMPLATE
  ==========================
  This is the canonical reference template. When generating a new card,
  copy this structure and replace all content placeholders (marked with
  {{PLACEHOLDER}}) with topic-specific content.

  DO NOT change the layout structure, class names, or CSS architecture.
  Only replace text content and adjust colors if the user requests a different palette.
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{CARD_TITLE}}</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Noto+Sans+SC:wght@400;500;700;900&family=JetBrains+Mono:wght@500;700&display=swap');

  * { margin: 0; padding: 0; box-sizing: border-box; }

  :root {
    /* === PRIMARY PALETTE === */
    --primary: #1a7a6d;
    --primary-dark: #145f54;
    --primary-deep: #0d3d36;
    --accent: #e8713a;
    --accent-dark: #d45a20;
    /* === BACKGROUNDS === */
    --bg: #f0ebe4;
    --bg-light: #f5f1ec;
    --bg-card: #e8e3dc;
    /* === NEUTRALS === */
    --black: #1a1a1a;
    --white: #ffffff;
    --gray: #6b6b6b;
    --gray-light: #999;
    /* === SPECIAL === */
    --highlight: #5ee6d0;  /* mint highlight for dark backgrounds */
  }

  body {
    background: var(--bg);
    font-family: 'Noto Sans SC', sans-serif;
    color: var(--black);
    padding: 0;
    display: flex;
    justify-content: center;
    align-items: flex-start;
    min-height: 100vh;
  }

  /* ============================================
     POSTER CONTAINER — fixed 1200px width
     ============================================ */
  .poster {
    width: 1200px;
    background: var(--bg);
    padding: 36px 40px 28px;
    position: relative;
  }

  /* ============================================
     SECTION 1: TOP BAR
     ============================================ */
  .top-bar {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 6px;
  }
  .top-bar .label-left,
  .top-bar .label-right {
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    letter-spacing: 2.5px;
    text-transform: uppercase;
    color: var(--gray);
    font-weight: 700;
  }
  .top-bar .label-right { text-align: right; }

  /* ============================================
     SECTION 2: MAIN TITLE
     ============================================ */
  .main-title {
    margin: 10px 0 4px;
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: 40px;
  }
  .main-title .left h1 {
    font-family: 'Playfair Display', serif;
    font-size: 38px;
    font-weight: 900;
    line-height: 1.15;
    color: var(--black);
  }
  .main-title .left h1 .cn {
    font-family: 'Noto Sans SC', sans-serif;
    font-weight: 900;
    font-size: 40px;
    color: var(--primary);
    display: block;
    margin-top: 2px;
  }
  .main-title .right-info {
    text-align: right;
    flex-shrink: 0;
    max-width: 320px;
  }
  .main-title .right-info .subtitle {
    font-size: 14.5px;
    font-weight: 700;
    line-height: 1.65;
    color: var(--black);
    margin-bottom: 8px;
  }
  .main-title .right-info .url {
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    color: var(--gray-light);
    letter-spacing: 0.5px;
  }

  /* ============================================
     SECTION 3: FRAMEWORK ROW (flexible columns)
     Default 4 columns. Override with inline style:
       style="grid-template-columns: repeat(N, 1fr)"
     where N = 2–6 based on content.
     ============================================ */
  .framework-row {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 10px;
    margin: 24px 0 18px;
  }
  .fw-card {
    background: var(--bg-card);
    padding: 16px 18px 14px;
    border-radius: 2px;
  }
  .fw-card .badge {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 10px;
  }
  .fw-card .badge .letter {
    background: var(--primary);
    color: var(--white);
    font-family: 'JetBrains Mono', monospace;
    font-weight: 700;
    font-size: 15px;
    width: 30px;
    height: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 3px;
  }
  /* Badge color rotation: 1=primary, 2=primary, 3=accent, 4=deep, 5=accent, 6=primary */
  .fw-card:nth-child(3) .badge .letter { background: var(--accent); }
  .fw-card:nth-child(4) .badge .letter { background: var(--primary-deep); }
  .fw-card:nth-child(5) .badge .letter { background: var(--accent); }
  .fw-card:nth-child(6) .badge .letter { background: var(--primary); }
  .fw-card .badge .name {
    font-weight: 900;
    font-size: 17px;
    color: var(--black);
  }
  .fw-card .desc {
    font-size: 13px;
    color: var(--gray);
    line-height: 1.6;
  }
  .fw-card .desc strong {
    color: var(--black);
    font-weight: 700;
  }

  /* ============================================
     SECTION 4: TWO-COLUMN CONTENT
     ============================================ */
  .content-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 14px;
    margin-bottom: 14px;
  }

  /* --- LEFT: DARK PANEL --- */
  .panel-dark {
    background: var(--black);
    color: var(--white);
    padding: 26px 28px 24px;
    border-radius: 3px;
  }
  .panel-dark .section-title {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 18px;
    font-size: 16px;
    font-weight: 900;
    letter-spacing: 0.5px;
  }
  .panel-dark .section-title .icon { font-size: 18px; }
  .panel-dark .block-title {
    color: var(--accent);
    font-weight: 900;
    font-size: 15px;
    margin-bottom: 10px;
    margin-top: 18px;
  }
  .panel-dark .block-title:first-of-type { margin-top: 0; }
  .panel-dark ul {
    list-style: none;
    padding: 0;
  }
  .panel-dark ul li {
    font-size: 13.5px;
    line-height: 1.75;
    padding-left: 16px;
    position: relative;
    color: #d4d4d4;
  }
  .panel-dark ul li::before {
    content: '■';
    position: absolute;
    left: 0;
    color: var(--primary);
    font-size: 8px;
    top: 3px;
  }
  .panel-dark ul li strong {
    color: #fff;
    font-weight: 700;
  }
  .panel-dark .conclusion {
    margin-top: 20px;
    padding-top: 16px;
    border-top: 1px solid #333;
  }
  .panel-dark .conclusion .label {
    font-weight: 900;
    font-size: 14.5px;
    color: var(--white);
    margin-bottom: 6px;
  }
  .panel-dark .conclusion p {
    font-size: 13px;
    color: #aaa;
    line-height: 1.7;
  }

  /* --- RIGHT: LIGHT PANEL --- */
  .panel-light {
    background: var(--bg-light);
    padding: 26px 28px 24px;
    border-radius: 3px;
  }
  .panel-light .section-title {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 20px;
    font-size: 16px;
    font-weight: 900;
    letter-spacing: 0.5px;
  }
  .panel-light .section-title .icon {
    color: var(--accent);
    font-size: 18px;
  }
  .insight-item {
    margin-bottom: 22px;
    display: flex;
    gap: 14px;
  }
  .insight-item:last-child { margin-bottom: 0; }
  .insight-num {
    font-family: 'Playfair Display', serif;
    font-size: 36px;
    font-weight: 900;
    color: var(--primary);
    line-height: 1;
    flex-shrink: 0;
    width: 36px;
    text-align: center;
  }
  .insight-content h4 {
    font-size: 15px;
    font-weight: 900;
    margin-bottom: 5px;
    line-height: 1.4;
    color: var(--black);
  }
  .insight-content p {
    font-size: 13px;
    color: var(--gray);
    line-height: 1.7;
  }
  .insight-content p strong {
    color: var(--black);
    font-weight: 700;
  }

  /* ============================================
     SECTION 5: BOTTOM HIGHLIGHT BAR
     ============================================ */
  .bottom-highlight {
    background: var(--primary-dark);
    color: var(--white);
    padding: 18px 24px;
    border-radius: 3px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 30px;
    margin-bottom: 14px;
  }
  .bottom-highlight .formula {
    font-weight: 900;
    font-size: 15px;
    line-height: 1.7;
  }
  .bottom-highlight .formula .eq {
    color: var(--highlight);
  }
  .bottom-highlight .note {
    font-size: 13px;
    line-height: 1.7;
    color: #b8dad4;
    max-width: 480px;
    text-align: right;
  }

  /* ============================================
     SECTION 6: FOOTER
     ============================================ */
  .footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-top: 4px;
  }
  .footer .left-url {
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    color: var(--gray-light);
    letter-spacing: 0.5px;
  }
  .footer .right-brand {
    display: flex;
    align-items: center;
    gap: 12px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 11px;
    letter-spacing: 1.5px;
    color: var(--gray);
    font-weight: 700;
    text-transform: uppercase;
  }
  .footer .right-brand .dot {
    width: 6px;
    height: 6px;
    background: var(--primary);
    border-radius: 50%;
  }
</style>
</head>
<body>
<div class="poster">

  <!-- ====== SECTION 1: TOP BAR ====== -->
  <div class="top-bar">
    <div class="label-left">{{TOPIC_LABEL_EN}} · {{SUBTOPIC_LABEL_EN}}</div>
    <div class="label-right">{{SOURCE_LABEL}}</div>
  </div>

  <!-- ====== SECTION 2: MAIN TITLE ====== -->
  <div class="main-title">
    <div class="left">
      <h1>
        {{TITLE_EN}}
        <span class="cn">{{TITLE_CN}}</span>
      </h1>
    </div>
    <div class="right-info">
      <div class="subtitle">{{THESIS_LINE_1}}<br>{{THESIS_LINE_2}}<br><strong>{{THESIS_KEYWORD}}</strong>{{THESIS_LINE_3}}</div>
      <!-- Optional: <div class="url">{{SOURCE_URL}}</div> -->
    </div>
  </div>

  <!-- ====== SECTION 3: FRAMEWORK ROW ======
       Default: 4 columns (no inline style needed).
       For other column counts, add inline style override:
         3 cols: style="grid-template-columns: repeat(3, 1fr)"
         5 cols: style="grid-template-columns: repeat(5, 1fr)"
         6 cols: style="grid-template-columns: repeat(6, 1fr)"
       Add or remove .fw-card blocks accordingly (2–6 cards).
  ====== -->
  <div class="framework-row">
    <div class="fw-card">
      <div class="badge">
        <div class="letter">{{FW1_LETTER}}</div>
        <div class="name">{{FW1_NAME}}</div>
      </div>
      <div class="desc">{{FW1_DESC_LINE1}}<br><strong>{{FW1_KEYWORD}}</strong>{{FW1_DESC_LINE2}}</div>
    </div>
    <div class="fw-card">
      <div class="badge">
        <div class="letter">{{FW2_LETTER}}</div>
        <div class="name">{{FW2_NAME}}</div>
      </div>
      <div class="desc">{{FW2_DESC_LINE1}}<br><strong>{{FW2_KEYWORD}}</strong>{{FW2_DESC_LINE2}}</div>
    </div>
    <div class="fw-card">
      <div class="badge">
        <div class="letter">{{FW3_LETTER}}</div>
        <div class="name">{{FW3_NAME}}</div>
      </div>
      <div class="desc">{{FW3_DESC_LINE1}}<br><strong>{{FW3_KEYWORD}}</strong>{{FW3_DESC_LINE2}}</div>
    </div>
    <div class="fw-card">
      <div class="badge">
        <div class="letter">{{FW4_LETTER}}</div>
        <div class="name">{{FW4_NAME}}</div>
      </div>
      <div class="desc">{{FW4_DESC_LINE1}}<br><strong>{{FW4_KEYWORD}}</strong>{{FW4_DESC_LINE2}}</div>
    </div>
    <!-- Optional 5th card: uncomment if needed
    <div class="fw-card">
      <div class="badge">
        <div class="letter">{{FW5_LETTER}}</div>
        <div class="name">{{FW5_NAME}}</div>
      </div>
      <div class="desc">{{FW5_DESC}}</div>
    </div>
    -->
    <!-- Optional 6th card: uncomment if needed
    <div class="fw-card">
      <div class="badge">
        <div class="letter">{{FW6_LETTER}}</div>
        <div class="name">{{FW6_NAME}}</div>
      </div>
      <div class="desc">{{FW6_DESC}}</div>
    </div>
    -->
  </div>

  <!-- ====== SECTION 4: TWO-COLUMN CONTENT ====== -->
  <div class="content-row">

    <!-- LEFT: DARK PANEL -->
    <div class="panel-dark">
      <div class="section-title">
        <span class="icon">{{DARK_ICON}}</span>
        {{DARK_SECTION_TITLE}}
      </div>

      <div class="block-title">{{BLOCK1_TITLE}}</div>
      <ul>
        <li>{{BLOCK1_ITEM1}}</li>
        <li>{{BLOCK1_ITEM2}}</li>
        <li>{{BLOCK1_ITEM3}}</li>
        <li>{{BLOCK1_ITEM4}}</li>
      </ul>

      <div class="block-title">{{BLOCK2_TITLE}}</div>
      <ul>
        <li>{{BLOCK2_ITEM1}}</li>
        <li>{{BLOCK2_ITEM2}}</li>
        <li>{{BLOCK2_ITEM3}}</li>
        <li>{{BLOCK2_ITEM4}}</li>
        <li>{{BLOCK2_ITEM5}}</li>
      </ul>

      <div class="conclusion">
        <div class="label">{{CONCLUSION_LABEL}}</div>
        <p>{{CONCLUSION_TEXT}} <strong style="color:#5ee6d0">{{CONCLUSION_HIGHLIGHT}}</strong></p>
      </div>
    </div>

    <!-- RIGHT: LIGHT PANEL -->
    <div class="panel-light">
      <div class="section-title">
        <span class="icon">★</span>
        {{LIGHT_SECTION_TITLE}}
      </div>

      <div class="insight-item">
        <div class="insight-num">1</div>
        <div class="insight-content">
          <h4>{{INSIGHT1_TITLE}}</h4>
          <p>{{INSIGHT1_DESC}}</p>
        </div>
      </div>

      <div class="insight-item">
        <div class="insight-num">2</div>
        <div class="insight-content">
          <h4>{{INSIGHT2_TITLE}}</h4>
          <p>{{INSIGHT2_DESC}}</p>
        </div>
      </div>

      <div class="insight-item">
        <div class="insight-num">3</div>
        <div class="insight-content">
          <h4>{{INSIGHT3_TITLE}}</h4>
          <p>{{INSIGHT3_DESC}}</p>
        </div>
      </div>

      <!-- Optional 4th insight — include if content warrants it -->
      <div class="insight-item">
        <div class="insight-num">4</div>
        <div class="insight-content">
          <h4>{{INSIGHT4_TITLE}}</h4>
          <p>{{INSIGHT4_DESC}}</p>
        </div>
      </div>
    </div>
  </div>

  <!-- ====== SECTION 5: BOTTOM HIGHLIGHT ====== -->
  <div class="bottom-highlight">
    <div class="formula">
      <span class="eq">{{FORMULA}}</span><br>
      {{FORMULA_SUBTEXT}}
    </div>
    <div class="note">
      {{CLOSING_THOUGHT}}
    </div>
  </div>

  <!-- ====== SECTION 6: FOOTER ====== -->
  <div class="footer">
    <div class="left-url">{{FOOTER_LEFT}}</div>
    <div class="right-brand">
      <span class="dot"></span>
      <span>{{FOOTER_BRAND}}</span>
      <span style="color:#aaa;font-weight:400">{{FOOTER_FRAMEWORK}} · {{FOOTER_YEAR}}</span>
    </div>
  </div>



</div>

  <!-- FLOATING DOWNLOAD FAB (hidden during export) -->
  <div class="fab-wrap" id="fabWrap">
    <div class="fab-dropdown" id="fabDropdown">
      <div class="fab-dropdown-title">导出图片</div>
      <button class="fab-option" onclick="exportImage(1,'png')">
        <span class="fab-option-icon">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>
        </span>
        <span class="fab-option-text">
          <strong>标准 PNG</strong>
          <small>1× · 适合快速分享</small>
        </span>
      </button>
      <button class="fab-option" onclick="exportImage(1.5,'png')">
        <span class="fab-option-icon">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>
        </span>
        <span class="fab-option-text">
          <strong>高清 PNG</strong>
          <small>1.5× · 社交媒体推荐</small>
        </span>
      </button>
      <button class="fab-option" onclick="exportImage(2,'png')">
        <span class="fab-option-icon">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>
        </span>
        <span class="fab-option-text">
          <strong>超清 PNG</strong>
          <small>2× · 适合打印</small>
        </span>
      </button>
      <div class="fab-divider"></div>
      <button class="fab-option" onclick="exportImage(1.5,'jpeg')">
        <span class="fab-option-icon">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
        </span>
        <span class="fab-option-text">
          <strong>JPEG</strong>
          <small>1.5× · 文件更小</small>
        </span>
      </button>
    </div>
    <button class="fab-btn" id="fabBtn" onclick="toggleDropdown(event)">
      <svg class="fab-icon fab-icon--download" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
        <polyline points="7 10 12 15 17 10"/>
        <line x1="12" y1="15" x2="12" y2="3"/>
      </svg>
      <svg class="fab-icon fab-icon--close" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
        <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
      </svg>
    </button>
  </div>

  <div class="export-toast" id="exportToast">
    <div class="export-toast-spinner"></div>
    <span>正在导出…</span>
  </div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script>
  const fabWrap = document.getElementById('fabWrap');
  const fabDropdown = document.getElementById('fabDropdown');
  const toast = document.getElementById('exportToast');
  let isOpen = false;
  function toggleDropdown(e) {
    e.stopPropagation();
    isOpen = !isOpen;
    fabWrap.classList.toggle('open', isOpen);
  }
  document.addEventListener('click', () => {
    if (isOpen) { isOpen = false; fabWrap.classList.remove('open'); }
  });
  fabDropdown.addEventListener('click', e => e.stopPropagation());
  async function exportImage(scale, format) {
    isOpen = false;
    fabWrap.classList.remove('open');
    fabWrap.style.display = 'none';
    toast.classList.add('show');
    const poster = document.querySelector('.poster');
    try {
      const canvas = await html2canvas(poster, {
        scale: scale, useCORS: true, allowTaint: true,
        backgroundColor: '#f0ebe4', logging: false,
        width: poster.scrollWidth, height: poster.scrollHeight,
      });
      const mime = format === 'jpeg' ? 'image/jpeg' : 'image/png';
      const ext = format === 'jpeg' ? '.jpg' : '.png';
      const link = document.createElement('a');
      link.download = document.title.replace(/\s+/g, '-') + '-' + scale + 'x' + ext;
      link.href = canvas.toDataURL(mime, 0.92);
      link.click();
    } catch (err) { alert('导出失败: ' + err.message); }
    finally { fabWrap.style.display = ''; toast.classList.remove('show'); }
  }
</script>

<style>
  .fab-wrap { position:fixed; bottom:32px; right:32px; z-index:9999; display:flex; flex-direction:column; align-items:flex-end; }
  .fab-btn { width:56px; height:56px; border-radius:50%; border:none; background:var(--primary,#1a7a6d); color:#fff; cursor:pointer; display:flex; align-items:center; justify-content:center; box-shadow:0 4px 16px rgba(0,0,0,.22),0 1px 4px rgba(0,0,0,.12); transition:all .25s cubic-bezier(.4,0,.2,1); }
  .fab-btn:hover { background:var(--primary-dark,#145f54); box-shadow:0 6px 24px rgba(0,0,0,.28); transform:scale(1.05); }
  .fab-btn:active { transform:scale(.96); }
  .fab-icon--close { display:none; }
  .fab-wrap.open .fab-icon--download { display:none; }
  .fab-wrap.open .fab-icon--close { display:block; }
  .fab-wrap.open .fab-btn { background:#1a1a1a; }
  .fab-dropdown { position:absolute; bottom:68px; right:0; width:240px; background:#fff; border-radius:14px; box-shadow:0 12px 40px rgba(0,0,0,.18); padding:8px 0; opacity:0; transform:translateY(10px) scale(.95); pointer-events:none; transition:all .2s cubic-bezier(.4,0,.2,1); }
  .fab-wrap.open .fab-dropdown { opacity:1; transform:translateY(0) scale(1); pointer-events:auto; }
  .fab-dropdown-title { padding:8px 16px 6px; font-family:'Noto Sans SC',sans-serif; font-size:11px; font-weight:700; color:#999; letter-spacing:1px; text-transform:uppercase; }
  .fab-divider { height:1px; background:#eee; margin:4px 12px; }
  .fab-option { display:flex; align-items:center; gap:12px; width:100%; padding:10px 16px; border:none; background:none; cursor:pointer; text-align:left; transition:background .15s; font-family:'Noto Sans SC',sans-serif; }
  .fab-option:hover { background:#f5f1ec; }
  .fab-option-icon { width:32px; height:32px; border-radius:8px; background:#f0ebe4; display:flex; align-items:center; justify-content:center; flex-shrink:0; color:var(--primary,#1a7a6d); transition:all .15s; }
  .fab-option:hover .fab-option-icon { background:var(--primary,#1a7a6d); color:#fff; }
  .fab-option-text { display:flex; flex-direction:column; gap:1px; }
  .fab-option-text strong { font-size:13.5px; font-weight:700; color:#1a1a1a; }
  .fab-option-text small { font-size:11px; color:#999; }
  .export-toast { position:fixed; bottom:32px; left:50%; transform:translateX(-50%) translateY(20px); background:#1a1a1a; color:#fff; font-family:'Noto Sans SC',sans-serif; font-size:14px; font-weight:600; padding:12px 24px; border-radius:12px; display:flex; align-items:center; gap:10px; opacity:0; pointer-events:none; transition:all .25s; z-index:10000; box-shadow:0 4px 20px rgba(0,0,0,.25); }
  .export-toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
  .export-toast-spinner { width:18px; height:18px; border:2.5px solid rgba(255,255,255,.25); border-top-color:#5ee6d0; border-radius:50%; animation:spin .7s linear infinite; }
  @keyframes spin { to { transform:rotate(360deg); } }
  @media print { .fab-wrap,.export-toast { display:none!important; } }

  .copyright-bar { display:flex; align-items:center; justify-content:center; gap:6px; padding:12px 0 2px; margin-top:10px; border-top:1px solid #ddd8d0; font-family:'JetBrains Mono',monospace; font-size:11px; color:#b5b0a8; }
  .copyright-bar svg { color:#c5c0b8; flex-shrink:0; }
  .copyright-bar a { color:#8aada7; text-decoration:none; transition:color .2s; }
  .copyright-bar a:hover { color:var(--primary,#1a7a6d); }
</style>
</body>
</html>


path: CONTRIBUTING.md
content: # Contributing

Thanks for your interest in contributing to Visual Note Card Generator!

## How to Contribute

1. **Fork** the repository
2. **Create** a feature branch (`git checkout -b feature/my-feature`)
3. **Commit** your changes (`git commit -m 'Add my feature'`)
4. **Push** to the branch (`git push origin feature/my-feature`)
5. **Open** a Pull Request

## Guidelines

- Keep the single-file HTML output approach — no build tools or frameworks
- Maintain the existing CSS variable system for theming
- Test generated cards in multiple browsers (Chrome, Firefox, Safari)
- If modifying the template, ensure the export (html2canvas) still works correctly

## Reporting Issues

Please open a GitHub issue with:
- A clear description of the problem
- Steps to reproduce
- Expected vs actual behavior
- Browser/environment info if relevant

## Code Style

- HTML/CSS follows the conventions in `assets/template.html`
- SKILL.md uses standard Claude Code skill frontmatter format


path: README.md
content: # Visual Note Card Generator

A custom skill for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and [OpenClaw](https://openclaw.ai/) that generates professional Chinese visual note cards (视觉笔记卡片/信息图) as single-page HTML infographics.

![Claude Code Skill](https://img.shields.io/badge/Claude_Code-Skill-blue)
![OpenClaw Compatible](https://img.shields.io/badge/OpenClaw-Compatible-purple)
![License](https://img.shields.io/badge/License-MIT-green)

## Overview

This skill turns any topic, article, or concept into a beautifully structured visual note card — a poster-style infographic optimized for social media sharing or printing. All output is a single self-contained HTML file with no external dependencies (except Google Fonts and html2canvas CDN).

### Features

- **Single HTML output** — no build tools, no frameworks, fully self-contained
- **Bilingual support** — Chinese body text with English display titles
- **Built-in export** — floating action button with PNG/JPEG export at multiple resolutions (1×, 1.5×, 2×)
- **Structured layout** — editorial knowledge card aesthetic with framework grid, dark/light panels, and highlight bar
- **Customizable palette** — default teal/orange theme with support for user-requested color schemes

### Example Output

![Demo](demo.png)

The card follows a fixed layout structure:

```
┌──────────────────────────────────────────┐
│ TOPIC LABEL              SOURCE LABEL    │  ← Top Bar
├────────────────────┬─────────────────────┤
│ English Title      │ Thesis statement    │  ← Title Area
│ 中文标题            │ with key insight    │
├─────┬─────┬─────┬──┴──────────────────────┤
│  M  │  P  │  D  │  G  │                  │  ← Framework Row (4 cards)
├─────┴─────┴─────┴─────┴──────────────────┤
│ ⚡ Dark Panel      │ ★ Light Panel        │  ← Two-Column Content
│ (narrative/story)  │ (numbered insights)  │
├──────────────────────────────────────────┤
│ Formula = M × P × D × G    Closing note  │  ← Highlight Bar
├──────────────────────────────────────────┤
│ FRAMEWORK LABEL              BRAND NAME   │  ← Footer
└──────────────────────────────────────────┘
```

## Installation

### Prerequisites

- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) or [OpenClaw](https://openclaw.ai/) installed and configured

### Install

Clone this repository into your skills directory:

**Claude Code:**

```bash
git clone https://github.com/beilunyang/visual-note-card-skills.git ~/.claude/skills/visual-note-card
```

**OpenClaw:**

```bash
git clone https://github.com/beilunyang/visual-note-card-skills.git ~/.openclaw/skills/visual-note-card
```

Both tools will automatically detect the skill and use it when you ask for visual notes or knowledge cards.

### Uninstall

**Claude Code:**

```bash
rm -rf ~/.claude/skills/visual-note-card
```

**OpenClaw:**

```bash
rm -rf ~/.openclaw/skills/visual-note-card
```

## Usage

Simply ask Claude Code or OpenClaw to create a visual note card:

```
# Chinese prompts
帮我做一张关于 RAG 架构的视觉笔记
把这篇文章做成信息图
生成一张知识卡片，主题是微服务

# English prompts
Create a visual note about product-market fit
Make a knowledge card summarizing this article
```

### What triggers this skill

The skill activates when you mention:
- 视觉笔记 / 知识卡片 / 信息图 / 一页纸总结
- visual note / knowledge card / infographic / one-pager summary
- Any request to summarize content into a structured visual card format

## Project Structure

```
.
├── SKILL.md              # Skill definition and design system specification
├── assets/
│   └── template.html     # Canonical HTML/CSS reference template
├── LICENSE
├── CONTRIBUTING.md
└── README.md
```

## Customization

### Color Palette

The default palette uses CSS variables defined in the template:

| Variable | Default | Usage |
|----------|---------|-------|
| `--primary` | `#1a7a6d` (teal) | Headers, badges, accents |
| `--accent` | `#e8713a` (orange) | Emphasis, secondary badges |
| `--bg` | `#f0ebe4` (warm gray) | Page background |
| `--black` | `#1a1a1a` | Dark panel, primary text |

You can request alternate color schemes when generating cards. The skill will maintain the same structural contrast ratios with your chosen colors.

### Typography

- **English display**: Playfair Display (serif)
- **Chinese body**: Noto Sans SC
- **Monospace/labels**: JetBrains Mono

All fonts are loaded via Google Fonts CDN.

## Contributing

Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details.

## License

This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.


path: scripts/html2png.py
content: #!/usr/bin/env python3
"""
html2png.py — 将视觉笔记卡片 HTML 渲染为高清 PNG 图片
依赖: pip install playwright && playwright install chromium

用法:
    python html2png.py <input.html> [output.png] [--scale=1.5]

示例:
    python html2png.py card.html                     # 输出 card.png (1.5x)
    python html2png.py card.html poster.png           # 指定输出文件名
    python html2png.py card.html --scale=2            # 2x 超清
    python html2png.py card.html poster.png --scale=1 # 1x 标准
"""

import sys
import argparse
from pathlib import Path


def html_to_png(input_html: str, output_png: str = None, scale: float = 1.5):
    """
    用 Playwright 无头 Chromium 将 HTML 渲染为 PNG。
    只截取 .poster 元素，不包含悬浮按钮等页面外元素。
    """
    from playwright.sync_api import sync_playwright

    input_path = Path(input_html).resolve()
    if not input_path.exists():
        print(f"File does not exist: {input_path}")
        return None

    if output_png is None:
        output_png = str(input_path.with_suffix(".png"))
    output_path = Path(output_png).resolve()

    print("Rendering...")
    print(f"   Input: {input_path}")
    print(f"   Scale: {scale}x")

    with sync_playwright() as p:
        browser = p.chromium.launch(args=["--no-sandbox", "--disable-setuid-sandbox"])
        page = browser.new_page(
            viewport={"width": 1280, "height": 900},
            device_scale_factor=scale,
        )

        # 加载 HTML
        page.goto(f"file://{input_path}", wait_until="networkidle")

        # 等待 Google Fonts 加载完成
        page.wait_for_function(
            """() => document.fonts.ready.then(() => document.fonts.check('16px "Noto Sans SC"'))""",
            timeout=15000,
        )
        # 额外等待确保渲染完成
        page.wait_for_timeout(500)

        # 隐藏悬浮按钮和 toast（如果存在）
        page.evaluate("""() => {
            const fab = document.querySelector('.fab-wrap');
            const toast = document.querySelector('.export-toast');
            if (fab) fab.style.display = 'none';
            if (toast) toast.style.display = 'none';
        }""")

        # 截取 .poster 元素
        poster = page.query_selector(".poster")
        if poster:
            poster.screenshot(path=str(output_path), type="png")
        else:
            # 回退：截全页
            page.screenshot(path=str(output_path), type="png", full_page=True)

        browser.close()

    file_size = output_path.stat().st_size
    size_str = (
        f"{file_size / 1024:.0f} KB"
        if file_size < 1024 * 1024
        else f"{file_size / 1024 / 1024:.1f} MB"
    )
    print(f"Export completed: {output_path} ({size_str})")
    return str(output_path)


def main():
    parser = argparse.ArgumentParser(description="将视觉笔记 HTML 渲染为 PNG")
    parser.add_argument("input", help="输入 HTML 文件路径")
    parser.add_argument("output", nargs="?", default=None, help="输出 PNG 文件路径 (默认同名.png)")
    parser.add_argument("--scale", type=float, default=1.5, help="缩放倍数 (默认 1.5)")
    args = parser.parse_args()

    result = html_to_png(args.input, args.output, args.scale)
    sys.exit(0 if result else 1)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)