# ian-xhs-carousel

Adopt this skill: https://proskills.md/skills/other/helloianneo-ian-xhs-carousel-ian-xhs-carousel

Page: https://proskills.md/skills/other/helloianneo-ian-xhs-carousel-ian-xhs-carousel
This document is the public skill bundle for that page. It contains SKILL.md and every helper file the catalog carries. No login is required.

## File: ian-xhs-carousel/SKILL.md
size: 9019
sha: 62ed4b7ae1f0d18ad9c0b5f573b097ff0244e0d9
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/SKILL.md

````text
---
name: ian-xhs-carousel
description: 把文章、案例拆解、素材、笔记或一个想法排成小红书 3:4 图文（1080×1440 轮播图），用一套产品界面风格：锌灰加一处橙，白页面上垫灰底盘，内容做成带分层阴影的卡片、窗口和对话框，有浅色和深色两版。先消化内容出分镜表，确认后从 20 个母版里排版、用代码画配图，渲染成每页 PNG 和总览图。只要用户提到小红书图文、小红书轮播、做成小红书、出图文、XHS carousel、排几页图、把这篇做成图，或者给了一篇内容想发小红书，都应该用这个 skill，哪怕没说"轮播"或"版式"。不负责小红书的帖子标题、正文和话题标签。
---

# 小红书图文（3:4）

把一份内容做成一组 1080×1440 的小红书图。风格已经定好：锌灰中性色加一处橙，白页面上垫一块灰底盘，内容做成一张张带分层阴影的卡片、窗口、对话框，像从一个精致软件里截出来的。有 20 个母版，浅色深色都能出。

母版是起点。内容需要的时候可以出血、错落、放大、增减行，只要底下那套颜色、字号、阴影和卡片样式还守着。

这个 skill 只管**图**。帖子标题、正文、话题标签不归它管，用户没要就不写。图上的字（每页标题、副标题、卡片里的字）归它管。

## 流程

一共六步，中间只停一次：分镜表出来以后，等用户确认。

### 1. 消化内容

先把材料读完，想清楚三件事再动手：
- 这组图要让看的人带走什么，用一句话说出来
- 材料里哪些是硬货（数据、步骤、对比、截图、代码、具体做法），哪些是铺垫，可以丢
- 用户手上已经有哪些图，还缺哪些

### 2. 分镜表（停下来等确认）

一页只讲一件事，用最少的页数讲完，一般 5 页左右：封面、三四页内容、结尾。图上只放看得见的东西：数字、对比、产品、做法。纯观点、来龙去脉、作者的心路，留给正文，最多留一页观点页（T15）。宁可少一页，让人划完还意犹未尽；长文里每个论点都做成一页，用户会觉得太复杂。

内容里的引子、铺垫，还有那些需要额外配图才讲得清的旁支，直接删，在分镜表下面说一句删了什么。

照这个格式给用户：

```
这组图想让人带走：<一句话>
主题：浅色 / 深色

| 页 | 这页讲什么 | 母版 | 图上标题 | 副标题 | 配图 |
|---|---|---|---|---|---|
| 01 | 封面 | T02 | 我做了个<br>记账小工具 | 拍一下小票，自动记好一笔 | 窗口里放首页截图，现成 |
| 02 | 原理 | T04 | 小票是怎么<br>变成一笔账的 | 先认字，再挑出金额和日期 | 代码画：三步各一张小图 |
| … |

要你给的图：
- 03 页：<具体要什么、为什么只能你给>
```

配图一栏写清楚来源，三种之一：
- **现成**：用户给的截图、照片、代码效果
- **代码画**：示意图、流程、数据图、图标、概念图，自己画（见第 3 步）
- **要你给**：只有用户能提供的，比如真实产品截图、本人照片、他自己的作品。这类不要自己画个假的冒充，单独列出来问

母版怎么选看 `references/layouts.md`。没有合适的就写"扩展"，用一句话说明打算怎么排。主题没说就用浅色。

用户改完或者说可以，再往下走。

### 3. 准备配图

母版只管把东西摆整齐，一组图好不好看，很大程度看配图。所以动手排版前，先把每张图的质量过一遍：
- 截图要清楚：用高分辨率的，裁掉和这页无关的部分（浏览器工具栏、桌面、别的窗口）；做深色版时尽量换成深色截图
- Logo、图标、品牌素材用官方的矢量文件（SVG），不要截一个糊的小图放大用
- 照片、效果图要挑最好看的那张，不是随手拿第一张
- 自己画的图要往精致里画，规则见下面

手上的图不够好，就在分镜表的"要你给的图"里直接说哪张不行、要什么样的。宁可等用户补一张好图，也不要拿一张糊的、随手截的图凑上去。

用户给的图直接用。缺的图用 canvas 或 SVG 代码画，只用灰和那一处橙，几何精确，从真实数据或真实逻辑生成。不用剪贴画、emoji、装饰渐变。动手前读 `references/figures.md`，那里有具体规则，还有可以直接用的 `assets/figure-kit.js`（颜色跟主题走）。

### 4. 排版填内容

在用户的工作目录里建一个文件夹，用这期内容起名，放进：
- `index.html`：从 `assets/starter.html` 开始，每页一个 `<section class="slide" data-layout="T04 步骤 · 文档">`
- `xhs.css`、`xhs.js`：从 `assets/` 复制，**不要改**。这一期特有的样式写在 `index.html` 的 `<style>` 里
- `figure-kit.js`（要画图的话）、配图的 JS、图片素材

母版的 HTML 结构从 `assets/templates.html` 里抄对应那段，把虚线占位 `.ph` 换成真内容（图片放进 `.fig`）。完整规则在 `references/design-system.md`，排版前读一遍。要点：
- 标题组下面垫底盘（`.stage`），卡片放在底盘上；观点页不要底盘
- 字号只用字号表里的几档，中文苹方，数字 Inter，代码 Geist Mono
- 橙一页只用一处（栏目标签前的小圆点不算），绿只表示"对、完成"
- 卡片描边 2、圆角 32，卡片里的分区用细线隔开，卡片之间留 24
- 截图放进窗口、手机或图槽，看起来像在真界面里显示
- 深色版：`<body data-theme="dark">`，或者渲染时加 `--theme dark`，版式不用动

**图上文案**：口语、平实，像熟人讲给熟人听。88 号标题每行最多 8 个字，64 号标题每行 9 个字左右，最多两行，用 `<br>` 手动断行，不留孤字。副标题一行，20 字以内；卡片里的说明 15 字以内。不写先否后肯（"不是…而是…"）、排比、冒号结论、金句结尾、反转。英文能不用就不用。用户有自己的文风规则的话，照那个来。

### 5. 渲染和自查

```bash
python3 <这个 skill 的目录>/scripts/render.py <文件夹>/index.html
python3 <这个 skill 的目录>/scripts/render.py <文件夹>/index.html <文件夹>/深色 --theme dark
```

脚本把 `01.png … NN.png`（1080×1440）和 `总览.png` 输出到同一个文件夹，第二行是深色版。它会顺带检查这些问题，有就打印出来：
- 文字溢出画布，或者贴到了底边
- 字号不在字号表里，字体不对
- 一页超过一处橙
- 还有没换掉的占位

要先装好 `playwright`（再跑一次 `playwright install chromium`）和 `Pillow`。Inter 和 Geist Mono 要联网从 Google Fonts 加载，没加载上时脚本会提示。中文用苹方，不在 Mac 上的话会退回思源黑体（Noto Sans SC），要自己装。

可选的页面属性：
- `data-hover=".tile:nth-child(2)"`：截图前把鼠标移到这个元素上，用来拍悬停状态
- `data-wait="3000"`：截图前多等一会儿，让动画跑完（默认 800 毫秒）
- 页内元素加 `data-poke`：截图前会收到一个 `poke` 事件，用来触发演示状态
- 元素加 `data-art`：这是图（卡片效果、截图、代码画的东西），里面的字号和颜色不按版式规则查
- 元素加 `data-accent`：里面的橙色算同一处，比如截图上的编号和下面列表的编号

脚本报的问题全部改掉，然后自己看一遍每页图，对着 `references/checklist.md` 过一遍。看得出来的问题自己改，不要留给用户挑。

### 6. 交付

把 `总览.png` 和各页发给用户，说一下：
- 每页用了哪个母版，哪页做了扩展，为什么
- 哪些图是代码画的，哪些还是占位，等他给
- 拿不准、想让他看一眼的地方，最多两三个

## 扩展版式

20 个母版覆盖不了的时候可以自己排，用 `xhs.css` 里现成的卡片、行、零件拼。新排法只能用同一套颜色、字号、阴影和圆角，`data-layout` 写成 `扩展 · 时间轴对比` 这样。同一种扩展用顺了两次以上，就建议用户把它收进版式库（`templates.html` 和 `layouts.md`）。

## 文件

- `references/design-system.md`：颜色、字号、底盘、卡片的固定规则，以及哪些地方可以放开。排版前读
- `references/layouts.md`：20 个母版各适合什么、怎么变，以及常见页序。选母版时读
- `references/figures.md`：代码画图的风格和做法。画图前读
- `references/checklist.md`：交付前自查
- `assets/xhs.css`、`assets/xhs.js`、`assets/templates.html`：版式库（样式、图标、20 个母版的 HTML）
- `assets/版式总览.png`、`assets/版式总览-深色.png`：20 个母版长什么样
- `assets/starter.html`：新一期的起始页
- `assets/figure-kit.js`：画图用的小工具
- `scripts/render.py`：渲染、检查、出总览
````

## File: ian-xhs-carousel/scripts/render.py
size: 7598
sha: a246a17cc1cbd4b273dd9fb44aaca56d09634a5d
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/scripts/render.py

```text
"""把 HTML 里的每个 .slide 渲染成 1080x1440 PNG，顺带查版式规则，最后出总览。

用法：python3 render.py <index.html> [输出文件夹] [--theme dark]
输出：01.png … NN.png、总览.png（默认和 HTML 同一个文件夹）
--theme dark：渲染前给 <body> 加 data-theme="dark"，出深色版
"""
import argparse
import glob
from pathlib import Path

from PIL import Image, ImageDraw, ImageFont
from playwright.sync_api import sync_playwright

SLIDE_W, SLIDE_H = 1080, 1440
VIEWPORT = {"width": SLIDE_W + 100, "height": SLIDE_H + 60}
DEFAULT_WAIT_MS = 800
SETTLE_MS = 1500
SHEET_BG = {"light": "#e4e4e7", "dark": "#3a3a40"}
SHEET_INK = {"light": "#18181b", "dark": "#f4f4f5"}

# 页面里的规则检查：返回这一页的问题列表
# [data-art] 和 .code 里面是图或代码，不查字号颜色；[data-accent] 里的橙色算同一处
LINT_JS = """
(slide) => {
  const SIZES = new Set([88, 64, 40, 32, 26, 24, 22]);
  const probe = document.createElement('i');
  probe.style.color = 'var(--orange)';
  slide.appendChild(probe);
  const ORANGE = getComputedStyle(probe).color;
  probe.remove();

  const issues = [];
  const box = slide.getBoundingClientRect();
  const skip = (el) => el.closest('[data-art], canvas, svg, .ph, .code');
  const textEls = [...slide.querySelectorAll('*')].filter((el) =>
    !skip(el) && [...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim()));
  for (const el of textEls) {
    const cs = getComputedStyle(el);
    const text = el.textContent.trim().slice(0, 12);
    const size = Math.round(parseFloat(cs.fontSize));
    if (!SIZES.has(size)) issues.push(`字号 ${size}px 不在字号表里：「${text}」`);
    if (!/PingFang|Inter|Geist Mono/.test(cs.fontFamily)) issues.push(`字体不对：「${text}」`);
    const r = el.getBoundingClientRect();
    if (r.width && (r.left < box.left - 1 || r.right > box.right + 1 || r.top < box.top - 1 || r.bottom > box.bottom + 1))
      issues.push(`文字超出画布：「${text}」`);
    else if (r.width && r.bottom > box.bottom - 40)
      issues.push(`文字贴到画布底边了：「${text}」`);
    if (el.scrollWidth > el.clientWidth + 1 && cs.overflow !== 'visible')
      issues.push(`文字被截断：「${text}」`);
  }

  const spots = new Set();
  slide.querySelectorAll('*').forEach((el) => {
    if (el.closest('[data-art], .code')) return;
    const cs = getComputedStyle(el);
    if (cs.color === ORANGE && [...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim()) ||
        cs.backgroundColor === ORANGE)
      spots.add(el.closest('[data-accent]') || el);
  });
  if (spots.size > 1) issues.push(`用了 ${spots.size} 处橙，一页最多一处（同一组标注外面包 data-accent）`);
  if (slide.querySelector('.ph')) issues.push('还有没换掉的占位 .ph');
  return issues;
}
"""

# 字体是按需加载的，没用到的不算；@import 整个失败时 document.fonts 里根本没有 Inter
FONT_CHECK_JS = """
() => {
  const faces = [...document.fonts];
  const bad = faces.filter((f) => f.status === 'error').map((f) => f.family);
  if (!faces.some((f) => f.family.replace(/"/g, '') === 'Inter')) bad.push('Inter、Geist Mono');
  return [...new Set(bad)];
}
"""

# 总览图标签用的中文字体：先找苹方，不在 Mac 上就找思源黑体
FONT_GLOBS = [
    "/System/Library/AssetsV2/com_apple_MobileAsset_Font*/*/AssetData/PingFang.ttc",
    "/System/Library/Fonts/PingFang.ttc",
    "/usr/share/fonts/**/NotoSansCJK*.tt[cf]",
    "/usr/share/fonts/**/NotoSansSC*.[ot]t[cf]",
    "C:/Windows/Fonts/msyh.tt[cf]",
]

def find_font():
    for pattern in FONT_GLOBS:
        hits = glob.glob(pattern, recursive=True)
        if hits:
            return hits[0]
    return None

def render(html: Path, out: Path, theme: str):
    out.mkdir(parents=True, exist_ok=True)
    pages = []
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page(viewport=VIEWPORT, device_scale_factor=2)
        page.on("console", lambda m: m.type in ("error", "warning") and print(f"  console {m.type}: {m.text}"))
        page.on("pageerror", lambda e: print(f"  页面报错: {e}"))
        page.goto(html.resolve().as_uri())
        if theme == "dark":
            page.evaluate("document.body.dataset.theme = 'dark'")
        page.evaluate("document.fonts.ready")
        page.wait_for_timeout(SETTLE_MS)
        missing = page.evaluate(FONT_CHECK_JS)
        if missing:
            print(f"  注意：{', '.join(missing)} 没加载上（要联网拉 Google Fonts），数字和代码会退回苹方 / SF Mono")
        for n, el in enumerate(page.query_selector_all(".slide"), start=1):
            name = f"{n:02d}"
            layout = el.get_attribute("data-layout") or ""
            el.scroll_into_view_if_needed()
            page.wait_for_timeout(300)
            el.evaluate("s => s.querySelectorAll('[data-poke]').forEach(e => e.dispatchEvent(new Event('poke')))")
            hover = el.get_attribute("data-hover")
            target = el.query_selector(hover) if hover else None
            if hover and not target:
                print(f"  {name}: data-hover 找不到 {hover}")
            if target:
                b = target.bounding_box()
                page.mouse.move(b["x"] + b["width"] * 0.3, b["y"] + b["height"] * 0.4)
                page.mouse.move(b["x"] + b["width"] * 0.55, b["y"] + b["height"] * 0.45, steps=20)
            else:
                page.mouse.move(2, 2)
            page.wait_for_timeout(int(el.get_attribute("data-wait") or DEFAULT_WAIT_MS))
            raw = out / f"_{name}@2x.png"
            el.screenshot(path=str(raw))
            Image.open(raw).resize((SLIDE_W, SLIDE_H), Image.LANCZOS).save(out / f"{name}.png", optimize=True)
            raw.unlink()
            issues = el.evaluate(LINT_JS)
            print(f"{name}  {layout or '(没写 data-layout)'}  {'OK' if not issues else ''}")
            for issue in issues:
                print(f"    ! {issue}")
            pages.append((name, layout))
        browser.close()
    return pages

def overview(out: Path, pages, theme: str):
    cols = 3 if len(pages) <= 9 else 4
    w, h, pad, lab = 360, 480, 40, 56
    rows = (len(pages) + cols - 1) // cols
    sheet = Image.new("RGB", (pad + cols * (w + pad), pad + rows * (h + lab + pad)), SHEET_BG[theme])
    draw = ImageDraw.Draw(sheet)
    path = find_font()
    font = ImageFont.truetype(path, 22, index=0) if path else ImageFont.load_default()
    for k, (name, layout) in enumerate(pages):
        r, c = divmod(k, cols)
        x, y = pad + c * (w + pad), pad + r * (h + lab + pad)
        sheet.paste(Image.open(out / f"{name}.png").resize((w, h), Image.LANCZOS), (x, y + lab))
        draw.text((x, y + 14), f"{name}  {layout}", fill=SHEET_INK[theme], font=font)
    sheet.save(out / "总览.png", optimize=True)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="把 .slide 渲染成小红书 3:4 PNG")
    parser.add_argument("html", type=Path)
    parser.add_argument("out", type=Path, nargs="?")
    parser.add_argument("--theme", choices=["light", "dark"], default="light")
    args = parser.parse_args()
    if not args.html.is_file():
        parser.exit(1, f"找不到 {args.html}\n")
    dest = args.out or args.html.parent
    result = render(args.html, dest, args.theme)
    if not result:
        parser.exit(1, "页面里没有 .slide\n")
    overview(dest, result, args.theme)
    print(f"已输出 {len(result)} 页和 总览.png 到 {dest}")
```

## File: ian-xhs-carousel/references/checklist.md
size: 2049
sha: 2e0a2b673421dfce9d5d4e13d1d3695389d635af
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/references/checklist.md

```text
# 交付前自查

先跑 `scripts/render.py`，它报的问题全部改掉。然后打开每页图和总览，逐条看。

## 整组
- [ ] 一页只讲一件事，删掉哪一页都会少点东西
- [ ] 总览里一眼看得出顺序和节奏，没有连续两页用同一个母版
- [ ] 封面缩到总览那么小，还看得清标题和主图
- [ ] 整组都是同一个主题（全浅或全深），没有混着来

## 每页
- [ ] 有底盘的页，底盘从标题组下面开始、铺出画布底边；T15 没有底盘，T19 的底盘从顶上垂下来
- [ ] 橙只有一处（栏目标签前的小圆点不算），用在这页最想让人看的东西上
- [ ] 标题断行自然，没有孤字，没有一行特别短
- [ ] 字号都在字号表里，没有为了塞下内容把字改小
- [ ] 卡片里的分区用细线隔开，卡片之间留 24
- [ ] 没有被裁到的字（窗口、手机、图可以出血，字不行）
- [ ] 内容区填得合适：不空得发虚，也不挤到底边
- [ ] 截图放在窗口、手机或者图槽里，线稿放在浅灰图槽里
- [ ] 没有留下虚线占位，用户还没给的图在交付时说清楚了

## 深色版（如果做了）
- [ ] 用 `--theme dark` 渲染，没有手写死的颜色在深色下变得刺眼或者看不见
- [ ] 代码画的图跟着换了颜色（用 `FigureKit.INK` 和 `onTheme`）
- [ ] 截图本身是浅色的话，考虑换成深色截图

## 配图
- [ ] 每张图单独拿出来也好看：截图清楚、裁得干净，Logo 和图标是矢量的，不糊不虚
- [ ] 不够好的图已经跟用户说了，没有拿凑数的图交差
- [ ] 从真数据、真路径、真逻辑画出来的
- [ ] 只用灰和那一处橙，线宽统一，标注 22 或 24 号
- [ ] 一组图共用一个母题

## 文案
- [ ] 口语，平实，像熟人讲给熟人听
- [ ] 没有先否后肯、排比、冒号结论、金句结尾、反转
- [ ] 英文能不用就没用（工具名、文件名除外）
- [ ] 副标题 20 字以内，卡片里的说明 15 字以内
```

## File: ian-xhs-carousel/references/design-system.md
size: 6527
sha: acae09d32e24e6037a0c20f1a55b278bc62e9b94
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/references/design-system.md

```text
# 设计规则

风格是一套产品界面的路子：锌灰中性色，只有一种橙色点题。白页面上垫一块灰底盘，内容做成一张张带分层阴影的卡片、窗口、对话框，像从一个真软件里截出来的。不用渐变，不用装饰图案，层次全靠底色深浅、阴影和字号来分。

所有值都写在 `assets/xhs.css` 的变量里。深色版只需要把 `<body>` 改成 `data-theme="dark"`，所有颜色和阴影都会跟着换，版式不用动。

规则分两层：固定层不动，守住这个风格；自由层可以放开，让每期长得不一样。

## 固定层（不动）

### 画布
- 1080×1440，内边距上 96、左右和下面各 80，内容宽 920
- 标题组和内容区之间的距离，默认 96（`.slide`），内容多时用 72（`.slide--tight`）

### 底盘
标题组下面那块灰色叫底盘（`.stage`）。它从内容上方 40 开始，左右各宽出 40，一直铺到画布底边外面，带一圈细边和大阴影。卡片都放在底盘上面。
- 底盘是这套风格最显眼的标志，大部分页都要有
- 观点页（T15）不要底盘，只留字
- 结尾对照页（T19）的底盘倒过来放：从画布顶边垂下来，托住上面那张图
- 一页只放一块底盘

### 字
中文用苹方，数字用 Inter，代码用 Geist Mono。Inter 和 Geist Mono 从 Google Fonts 加载，渲染时需要联网；断网时会退回苹方和 SF Mono，渲染脚本会提示。

| 类 | 规格 | 用在哪 |
|---|---|---|
| `.t-display` | 600 88/110，字距 −2% | 封面标题、观点页大字 |
| `.t-title` | 600 64/80，字距 −1% | 内页标题 |
| `.t-heading` | 500 40/52 | 卡片里的小标题（少用） |
| `.t-body` / `.sub` | 400 32/48 | 副标题、聊天正文 |
| `.t-strong` / `.row__head` | 500 32/48 | 条目标题、设置项名称 |
| `.t-caption` / `.row__note` | 400 26/40 | 条目说明、底栏说明 |
| `.t-label` / `.chip` / `.tag` | 500 24/32 | 栏目标签、标签、按钮 |
| `.t-small` | 400 22/36 | 时间戳、刻度、图里的标注 |
| `.t-num` | Inter 600 24/32 | 编号、计数 |
| `.t-metric` / `.t-metric-sm` | Inter 600 88 / 64 | 大数字 |
| `.t-code` / `.code` | Geist Mono 400 22/36 | 代码、提示词 |

字号只有 88 / 64 / 40 / 32 / 26 / 24 / 22 这几档，渲染脚本会查。一页只放一个 88 或 64 的标题。

### 颜色
文字分五级，由深到浅：`--text-strong`（标题、条目名）、`--text-medium`、`--text-body`（副标题）、`--text-muted`（说明）、`--text-faint`（最弱的提示）。

底色分几层：`--page` 是页面白，`--tray` 是底盘，`--surface` 是卡片，`--fill` 是卡片里的浅灰块（占位、标签、气泡），`--fill-strong` 比它再深一点。

**橙 `--orange` 一页只用一处。** 栏目标签前面那个小圆点每页都有，不算。橙色留给这一页最想让人看的那个东西，例如：
- 四宫格里推荐的那张卡（`.card--accent` 描边，加一个 `.tag--orange`）
- 柱状图里要比的那一根
- 观点页大字里的最后一行
- 时间线上"现在"那个点

截图上的一组编号标注（图上的编号和下面列表的编号）算一处，外层加 `data-accent`，脚本就会把它们当一处来数。

绿 `--green` 只用来表示"对、完成、上涨"（`.tag--green`、勾、开关），不当装饰色。

内容自带的颜色（品牌色、截图、照片）不算排版色，不受一页一处橙的限制。

### 卡片
- 卡片（`.card`）：2px 浅描边，圆角 32，阴影 `--shadow-card`
- 对话框和窗口（`.card--dialog`）：阴影更大更深，`--shadow-dialog`，看起来浮在底盘上面
- 卡片内部分区用 2px 细线隔开（`.split`），不要用间隙
- 卡片里的图圆角 20，小图标 20，手机屏 44
- 按钮、标签、编号这些小零件用 `--shadow-sm` 小阴影，或者不加阴影

### 间距
卡片之间 24，卡片里一行的内边距是上下 24、左右 36，宽松的行用 32/36。标题组内部：栏目标签到标题 32，标题到副标题 24。别的地方按 8 的倍数取。

### 文字
- 文字后面不垫色块（标签、气泡、按钮这类零件除外），不加描边和阴影
- 标题用 `<br>` 手动断行，不留孤字
- 一页的字，十秒内能读完

## 自由层（看内容放开）

母版是起点。下面这些可以按内容调：
- **出血**：窗口、手机、截图可以伸出画布底边被裁掉（T02、T11 本来就是这样），比硬塞进画布里好看
- **错落**：两台手机一高一低、几张卡片大小不一、通知卡下面再垫一张露个边
- **放大**：主角往大了放，配角缩小；截图可以只截局部再放大
- **换零件**：同一个母版里，行首可以是编号、勾、图标或者小图；行尾可以是标签、按钮、箭头，或者什么都不放
- **增减行数**：清单、时间线、列表一般三到五行，内容少就删几行，内容多就换成更紧凑的母版，不要把字号改小去硬塞

判断标准：把这页截出来，看着像不像某个精致软件的真实界面。只有排得整整齐齐的等分格子不是默认答案。

## 两种内容，两种放法

- **截图、照片、效果图**：放进窗口（`.chrome` 顶栏 + 图）、手机（`.phone`）或者卡片里的图槽（`.fig`），让它看起来是在一个真界面里显示
- **线稿、示意图、图标**：放进 `.fig` 浅灰图槽，和说明文字放在同一张卡里

## 常用类（`xhs.css`）

- 页面：`.slide`、`.slide--tight`、`.slide--center`、`.slide--end`
- 标题组：`.head`、`.head--center`、`.chip`、`.sub`
- 底盘：`.stage`、`.stage--fill`、`.stage--top`、`.stage--bare`
- 卡片：`.card`、`.card--dialog`、`.card--accent`、`.split`
- 卡片里的结构：`.bar`、`.foot`、`.chrome`、`.row`、`.row__text`、`.row__head`、`.row__note`
- 零件：`.btn(--primary/--round)`、`.tag(--green/--orange)`、`.badge(--orange/--soft/--green)`、`.pill`、`.ic`
- 图：`.fig`、`.fig--contain`、`.ph`（占位，换成真内容后删掉）
- 各母版自己的零件见 `xhs.css` 后半段，按 T01–T20 排

图标写成 `<i class="ic" data-i="check"></i>`，由 `xhs.js` 换成线框 SVG（32 格，线宽 2.5，圆头）。现有的图标有：close、check、cross、arrow-up、chevron-down、chevron-right、sparkle、info、copy、paperclip、code、search。要新增图标就在 `xhs.js` 的 `ICONS` 里加，画法照这套来。
```

## File: ian-xhs-carousel/references/figures.md
size: 4675
sha: f5d1ada03a152751e11c4bef835b70968ada957d
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/references/figures.md

```text
# 代码画图

缺图的时候自己画。画出来要像这套界面里本来就有的插图：只用灰，最多带一处橙，几何精确，每根线都有来处。

## 风格

- **颜色只从变量里取**：用 `FigureKit.INK`，它会现读 `xhs.css` 里的变量，浅色深色都对。
  - `strong`：主线、主体
  - `body`：次要的线
  - `muted`：标注、箭头
  - `faint` / `line`：辅助线、网格
  - `fill` / `fillStrong`：浅色块
  - `accent`：要点名的那一处，整页（包括排版）只能有这一处
  - `accentSoft`：橙色的浅底
- **从真东西生成**：用真实数据、真实的 Logo 路径、真实的算法去画，不要手摆一个"看起来像"的。一组四宫格的效果图，最好是从同一个源（同一个 Logo、同一张照片）采样出来，放在一起才像一套
- **几何精确**：坐标取整到半像素，线宽统一（细线 1.5、主线 2.5、强调 4），间距用倍数
- **少**：一张图只说一件事。三个元素能说清楚就不用五个
- **字**：图里的标注用 22 或 24 号苹方，颜色 `muted`。图里不写大标题，标题交给卡片
- **不要**：剪贴画、emoji、立体投影、装饰渐变、发光、手绘风、随手挑的颜色、为了好看加的纹理

## 放哪

图总是放在一个"容器"里，看起来像界面的一部分：
- **卡片里的图槽** `.fig`：浅灰底，圆角 20。线稿、示意图、效果图都放这里。T04 的行尾小图、T09 的宫格图、T08 的前后图都是这个
- **窗口** `.chrome` + 图：网页、软件截图，或者代码画出来的整屏效果
- **手机** `.phone`：竖屏截图
- **对话框正文** `.dialog-body` 里放一个 `.fig`：封面主图

图槽里的图默认铺满（`object-fit: cover`）；图标、Logo 这类不能裁的东西加 `.fig--contain`。

## 常见图怎么画

**流程**：三到四个阶段横排，每个阶段一个小图加一行 22 号标签，中间用细线箭头连。几个阶段的图用同一个源，一步步变化。

**放大局部**：从大图里挑最有代表性的一块放大，被放大的那一块用一次橙。旁边可以留一个小的原图位置示意。

**图例 / 档位**：横排几个样本，下面标数值，从小到大或者从暗到亮。

**数据**：柱、点、线用 `fillStrong` 或 `body`，只有要比的那一个用橙。不画坐标轴框，只留基线；刻度字 22 号 `muted`。T13 的柱状图就是这个画法，可以直接用 HTML 写。

**对比**：左右两张用同样的构图、同样的大小，只改那一个变量。

**图标**：照 `xhs.js` 里那套，32 格，线宽 2.5，圆头圆角，只用线框。能用圆、圆角矩形、直线拼出来就不要用复杂路径。

## 写法

`<canvas>` 和内联 `<svg>` 都可以。canvas 适合画数据和大量重复的元素，svg 适合画图标和简单示意。

`assets/figure-kit.js` 里有几个小工具，复制到这期文件夹里引用：
- `FigureKit.prepare(canvas)`：按元素实际大小和设备像素比设好画布，返回 `{ ctx, w, h }`
- `FigureKit.INK`：当前主题下的颜色
- `FigureKit.onTheme(draw)`：先画一次，切到深色时自动重画。画图函数都用它包一下，`--theme dark` 渲染时才会是深色
- `FigureKit.arrow(ctx, x0, y0, x1, y1)`：细线箭头
- `FigureKit.label(ctx, text, x, y, opts)`：图里的标注
- `FigureKit.sampleImage(img, cols, rows)`：把一张图采样成亮度格（0 到 1），用来做"从真东西生成"的图

画图的函数放在一个 IIFE 里，常量和查表写在前面，调用放在文件最后（不然会碰到"用在定义之前"的报错）。canvas 绝对定位、铺满一个有尺寸的 `.fig`，`prepare` 会读它的实际大小。

演示状态（悬停、展开）用 `data-poke` 加 `poke` 事件触发，渲染脚本截图前会发这个事件。

用户给的现成图片如果有浅色和深色两版，就按主题切换：`<img data-light="a.png" data-dark="a-dark.png">`，在 `onTheme` 里换 `src`。

**一套图共用一个母题**：几页的配图尽量是同一个东西换角度、换大小、换裁切。比如封面放全貌，内页拆局部，结尾放成品。看起来是一组，也不用每页重新想。

**第一版往大了画**：代码画的图第一遍几乎总是偏小、四周发空。先按图槽的大小铺满去画，宁可被裁掉一截。

## 画完自查

- 缩到手机屏幕大小（总览图里那么大），还看得出画的是什么吗
- 除了那一处橙，有没有混进别的颜色
- 线宽、字号是不是统一的
- 切到深色以后，线和字还看得清吗
```

## File: ian-xhs-carousel/references/layouts.md
size: 9259
sha: 7a3d752772f1052ae313113b53b971f4ff7389b6
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/references/layouts.md

```text
# 20 个母版

HTML 在 `assets/templates.html`，长相看 `assets/版式总览.png`（深色版看 `版式总览-深色.png`）。改设计的话三处一起改：`xhs.css`、`templates.html` 和这份说明。

母版按"这页在讲什么"分成七组。选的时候先想这页要讲的是步骤、对比、展示、数字还是一段话，再去对应的组里挑。

标题字数通用规则：88 号标题每行最多 8 个字，64 号标题每行 9 个字左右，都不超过两行（观点页除外）。副标题一行，20 字以内。

---

## 封面

### T01 封面 · 对话框
居中大标题，下面是一个对话框：顶栏（图标 + 标题 + 关闭）、一张大图、底栏（说明 + 按钮）。
- **适合**：讲一个工具、一个功能、一次操作；主图是一张截图或效果图
- **不适合**：主图是手机竖屏（用 T11 改成封面）
- **可以变**：按钮可以换成标签，或者去掉；顶栏标题写工具名
- **字数**：标题两行，按钮文字 4 字以内

### T02 封面 · 窗口
左对齐大标题，下面是一个浏览器窗口，窗口内容伸出画布底边。
- **适合**：讲一个网站、一个网页效果、一个产品官网
- **不适合**：要在图上标注细节（用 T10）
- **可以变**：地址栏写真实域名；窗口可以往右偏、露出右边一截
- **字数**：标题两行

### T03 封面 · 通知
居中大标题，下面是一叠通知卡，最后一张底下再垫一张露个边。
- **适合**：清单类、合集类封面（"5 个…"）；讲提醒、消息、自动化
- **不适合**：只有一个主角
- **可以变**：通知数量 2 到 4 条；第一条可以换成真实应用图标和真实文案
- **字数**：每条标题 12 字以内，说明 15 字以内

## 讲做法

### T04 步骤 · 文档
一张卡里三行编号步骤，每行右边带一张小图；下面再放两张小卡。
- **适合**：三步教程；一个流程加两个补充技巧
- **不适合**：步骤超过四步（用 T05 或分两页）
- **可以变**：底下两张小卡可以删掉，三行步骤的图放大；编号可以换成图标
- **字数**：步骤标题 10 字以内，说明 15 字以内

### T05 清单 · 待办
一张待办清单：表头写清单名和进度，下面五行，打勾的划掉。
- **适合**：检查清单、准备工作、避坑清单
- **不适合**：条目之间有先后顺序（用 T04 或 T06）
- **可以变**：全部不打勾也行；进度可以去掉；某一行加一个橙色标签点出重点
- **字数**：每条标题 12 字以内

### T06 时间线 · 动态
左边写时间，中间一根竖线串起圆点，右边写这天发生了什么，最后一个点是橙色的"现在"。
- **适合**：一段时间里的变化、项目历程、使用一个工具的几个阶段
- **不适合**：没有时间先后
- **可以变**：时间可以写日期、版本号、第几步；行数 3 到 5
- **字数**：时间 6 字以内，事件标题 10 字以内

## 对比

### T07 对比 · 对错
大标题下面一个对话框，里面两行，一行打勾一行打叉，各带一个标签。
- **适合**：对的做法和错的做法、推荐和不推荐
- **不适合**：两边都要配图（用 T08）
- **可以变**：可以加到三四行；底栏按钮可以去掉
- **字数**：每行标题 10 字以内，说明两行以内

### T08 前后 · 双栏
两张卡并排，每张上面一个标签（之前 / 之后），中间一张大图，下面标题说明。
- **适合**：改前改后、两种方案、两张效果图对比
- **不适合**：三个以上的东西
- **可以变**：两张卡可以一高一低错开；"之后"那张用橙标签
- **字数**：每栏标题 8 字以内

## 展示

### T09 宫格 · 卡片
两行两列四张卡，每张上图下字，推荐的那张加橙描边和橙标签。
- **适合**：四个例子、四种效果、四个工具
- **不适合**：只有两三个（用 T08 或 T04）
- **可以变**：四张卡的图共用一个母题，换角度换效果；也可以没有推荐那张
- **字数**：每张标题 8 字以内，说明 15 字以内

### T10 标注 · 截图
一个窗口里放截图，图上贴橙色编号；下面一张卡按编号逐条解释。
- **适合**：一张截图里有三四个地方要讲
- **不适合**：细节超过五处
- **可以变**：截图可以放大只留关键区域；编号也可以只贴在图上、不要下面的列表
- **字数**：每条标题 10 字以内，说明 15 字以内

### T11 手机 · 截图
两台手机，右边那台往下错一点，底部伸出画布，每台上面一个标签。
- **适合**：手机应用、小程序、竖屏截图；前后两个状态
- **不适合**：横屏截图（用 T02）
- **可以变**：只放一台手机，居中放大；两台手机也可以压在一起
- **字数**：标签 4 字以内

### T12 列表 · 应用
一个带搜索栏的列表窗口，五行应用，每行图标 + 名称 + 说明 + 标签，选中的那行有底色。
- **适合**：工具推荐、应用合集、资源列表
- **不适合**：每个条目都需要配大图（用 T09）
- **可以变**：行数 3 到 6；搜索栏可以写真实搜索词
- **字数**：名称 8 字以内，说明 15 字以内

## 数字

### T13 数据 · 指标
一张大卡放一个大数字和一组柱状图（最高那根是橙色），下面两张小卡各一个数字。
- **适合**：效果数据、增长、耗时、前后差距
- **不适合**：数字没有可比的东西
- **可以变**：柱状图可以换成折线或者进度条；底下两张小卡可以删掉
- **字数**：指标名 8 字以内

### T14 参数 · 设置
分组的设置面板，每行是名称 + 数值、色块、开关或者箭头。
- **适合**：参数、配色、配置项、工具设置
- **不适合**：参数要解释为什么（用 T04）
- **可以变**：分组 1 到 3 个；数值用真实参数
- **字数**：名称 6 字以内

## 文字

### T15 观点 · 大字
没有底盘。栏目标签 + 四行大字（最后一行橙色）+ 一张补充说明的小卡，整体垂直居中。
- **适合**：一句判断、一个结论；一组图里最多一页
- **不适合**：要讲具体做法
- **可以变**：大字两到四行；补充小卡可以去掉
- **字数**：每行 9 字以内，补充 60 字以内

### T16 对话 · 聊天
一个聊天窗口：顶栏写模型名，我问一句、它答一段，最多两轮，底下是输入框。
- **适合**：讲一个 AI 怎么回答、一段真实对话
- **不适合**：回答很长（截取最关键那段）
- **可以变**：一轮就够的话删掉第二轮
- **字数**：问 15 字以内，答 60 字以内

### T17 提示词 · 输入框
一个输入框：顶栏是模型选择，中间是完整提示词，底栏是复制和发送按钮。
- **适合**：分享一段可以直接复制的提示词
- **不适合**：提示词超过 12 行（截掉不重要的部分，或者拆成两页）
- **可以变**：顶栏标签写适用的工具；提示词分段写，段间留空
- **字数**：提示词正文用 22 号，整段 250 字以内

### T18 代码 · 编辑器
一个代码编辑器窗口：标签栏写文件名，中间代码，关键字橙色，底栏带复制按钮。
- **适合**：一段关键代码、一个配置文件
- **不适合**：代码超过 14 行
- **可以变**：只留最关键的十行，其余用 `…` 省略；可以开两个标签页
- **字数**：每行 50 个字符以内

## 结尾

### T19 结尾 · 对照
底盘从画布顶边垂下来，托着一张成品图；下面居中写结尾标题，最下面一张对照卡（名称 + 内容 + 标签）。
- **适合**：收尾时放成品，再把关键参数、用到的工具、出处列一下
- **不适合**：没有成品图
- **可以变**：对照卡 2 到 4 行；成品图可以换成几张错落的卡片
- **字数**：标题两行，名称 4 字以内

### T20 结尾 · 完成
居中大标题，下面一个完成对话框：大绿勾 + 一句话 + 三条收获，底栏一个按钮。
- **适合**：总结三条收获、教程做完的收尾
- **不适合**：收获超过三条
- **可以变**：按钮写下一步行动（"收藏备用"）；可以没有底栏
- **字数**：每条收获 12 字以内

---

## 页序的常见搭法

- **案例拆解**：T02 封面 → T10 拆细节 → T09 四种效果 → T18 关键代码 → T17 提示词 → T19 结尾
- **教程**：T01 封面 → T04 三步 → T10 关键细节 → T14 参数 → T20 收获
- **工具合集**：T03 封面 → T12 列表 → T09 效果 → T08 前后 → T19 结尾
- **观点**：T01 封面 → T13 数据 → T07 对错 → T15 观点 → T20 收获

封面和结尾大概率从封面组和结尾组里选。中间连续两页别用同一个母版。一组图里 T15 最多出现一次。

## 母版不够用的时候

可以照着这套零件拼一个新排法，`data-layout` 写成 `扩展 · 名字`。新排法只能用 `xhs.css` 里已有的颜色、字号、阴影和卡片样式；这期特有的样式写在 `index.html` 的 `<style>` 里。同一种扩展用顺了两次以上，就建议用户把它收进版式库。
```

## File: ian-xhs-carousel/assets/figure-kit.js
size: 3384
sha: 1a982de40f4fc4a82462ff61570f92d4f9c068ea
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/assets/figure-kit.js

```text
// 代码画图的小工具：颜色、画布准备、箭头、标注、图片采样
(function () {
  // 颜色每次现读 xhs.css 的变量，深色版（body[data-theme="dark"]）跟着换
  // 渲染脚本加 --theme dark 时，主题是页面加载完才切的，画图函数用 onTheme 包一下才会重画
  function readInk() {
    const cs = getComputedStyle(document.body);
    const v = (name) => cs.getPropertyValue(name).trim();
    return {
      strong: v('--text-strong'),
      body: v('--text-body'),
      muted: v('--text-muted'),
      faint: v('--text-faint'),
      line: v('--line'),
      hairline: v('--line-soft'),
      fill: v('--fill'),
      fillStrong: v('--fill-strong'),
      surface: v('--surface'),
      accent: v('--orange'),
      accentSoft: v('--orange-200'),
      green: v('--green'),
    };
  }
  const FONT = '"PingFang SC", sans-serif';
  const NOTE_SIZE = 24;
  const ARROW_HEAD = 8;

  // 按元素实际大小和设备像素比设好画布，之后都用 CSS 像素画
  function prepare(canvas) {
    const dpr = window.devicePixelRatio || 1;
    const rect = canvas.getBoundingClientRect();
    const w = Math.round(rect.width);
    const h = Math.round(rect.height);
    canvas.width = w * dpr;
    canvas.height = h * dpr;
    const ctx = canvas.getContext('2d');
    ctx.scale(dpr, dpr);
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    return { ctx, w, h };
  }

  function arrow(ctx, x0, y0, x1, y1, { color = readInk().muted, width = 2 } = {}) {
    const angle = Math.atan2(y1 - y0, x1 - x0);
    ctx.save();
    ctx.strokeStyle = color;
    ctx.lineWidth = width;
    ctx.beginPath();
    ctx.moveTo(x0, y0);
    ctx.lineTo(x1, y1);
    ctx.moveTo(x1 - ARROW_HEAD * Math.cos(angle - Math.PI / 6), y1 - ARROW_HEAD * Math.sin(angle - Math.PI / 6));
    ctx.lineTo(x1, y1);
    ctx.lineTo(x1 - ARROW_HEAD * Math.cos(angle + Math.PI / 6), y1 - ARROW_HEAD * Math.sin(angle + Math.PI / 6));
    ctx.stroke();
    ctx.restore();
  }

  function label(ctx, text, x, y, { color = readInk().muted, align = 'center', baseline = 'middle', weight = 400 } = {}) {
    ctx.save();
    ctx.font = `${weight} ${NOTE_SIZE}px ${FONT}`;
    ctx.fillStyle = color;
    ctx.textAlign = align;
    ctx.textBaseline = baseline;
    ctx.fillText(text, x, y);
    ctx.restore();
  }

  // 把一张已加载的图（img / canvas）采样成 rows×cols 的亮度格，值 0 到 1，透明当 0
  function sampleImage(img, cols, rows = cols) {
    const off = document.createElement('canvas');
    off.width = cols;
    off.height = rows;
    const c = off.getContext('2d', { willReadFrequently: true });
    c.drawImage(img, 0, 0, cols, rows);
    const { data } = c.getImageData(0, 0, cols, rows);
    return Array.from({ length: rows }, (_, r) =>
      Array.from({ length: cols }, (_, col) => {
        const i = (r * cols + col) * 4;
        const lum = (0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2]) / 255;
        return lum * (data[i + 3] / 255);
      }),
    );
  }

  // 主题切换后重画：FigureKit.onTheme(draw)，页面加载时先画一次
  function onTheme(draw) {
    draw();
    new MutationObserver(draw).observe(document.body, { attributes: true, attributeFilter: ['data-theme'] });
  }

  window.FigureKit = { get INK() { return readInk(); }, prepare, arrow, label, sampleImage, onTheme };
})();
```

## File: ian-xhs-carousel/assets/starter.html
size: 2002
sha: 333a65cfb12e5ee3b12d6da6180030422dc58823
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/assets/starter.html

```text
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>这期的名字</title>
<link rel="stylesheet" href="xhs.css">
<style>
/* 这一期特有的样式写这里，版式和颜色全部来自 xhs.css
 * 页序：T01 封面 → … → T19 结尾 */
</style>
</head>
<!-- 深色版：<body data-theme="dark">，或者渲染时加 --theme dark -->
<body>

<section class="slide" data-layout="T01 封面 · 对话框">
  <header class="head head--center">
    <p class="chip">栏目名</p>
    <h1 class="t-display">封面标题<br>两行最好看</h1>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage">
    <div class="card card--dialog split">
      <div class="bar"><i class="ic" data-i="code"></i><p class="bar__title">窗口标题</p><i class="ic" data-i="close"></i></div>
      <div class="dialog-body"><div class="ph" style="height:497px"></div></div>
      <div class="foot"><p class="foot__note">一句补充说明</p><span class="btn btn--primary">主要操作</span></div>
    </div>
  </div>
</section>

<!-- 中间各页：从 templates.html 抄对应母版，把 .ph 换成真内容 -->

<section class="slide slide--end" data-layout="T19 结尾 · 对照">
  <div class="stage stage--top"><div class="ph" style="width:760px;height:497px"></div></div>
  <header class="head head--center">
    <p class="chip">栏目名</p>
    <h2 class="t-title">结尾标题<br>两行最好看</h2>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="card split">
    <div class="row diff"><p class="diff__key">名称</p><p class="row__head grow">条目内容</p></div>
    <div class="row diff"><p class="diff__key">名称</p><p class="row__head grow">条目内容</p></div>
  </div>
</section>

<script src="xhs.js"></script>
<script src="figure-kit.js"></script>
<script>
  // 这期的配图写在这里或单独的 figures.js，画图函数用 FigureKit.onTheme(draw) 包一下
</script>
</body>
</html>
```

## File: ian-xhs-carousel/assets/templates.html
size: 23084
sha: e58f3072e6cc1fd92416a26725e592effc496019
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/assets/templates.html

```text
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>版式库 · 20 个母版</title>
<link rel="stylesheet" href="xhs.css">
</head>
<body>
<!-- 每个 section 是一个母版。用的时候整段抄进 index.html，把 .ph 占位换成真内容，字换成这期的字
     结构和类名都来自 xhs.css -->

<!-- ===================== 封面 ===================== -->

<section class="slide" data-layout="T01 封面 · 对话框">
  <header class="head head--center">
    <p class="chip">栏目名</p>
    <h1 class="t-display">封面标题放在这里<br>两行最好看</h1>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage">
    <div class="card card--dialog split">
      <div class="bar"><i class="ic" data-i="code"></i><p class="bar__title">窗口标题</p><i class="ic" data-i="close"></i></div>
      <div class="dialog-body"><div class="ph" style="height:497px"></div></div>
      <div class="foot"><p class="foot__note">一句补充说明</p><span class="btn">次要操作</span><span class="btn btn--primary">主要操作</span></div>
    </div>
  </div>
</section>

<section class="slide" data-layout="T02 封面 · 窗口">
  <header class="head">
    <p class="chip">栏目名</p>
    <h1 class="t-display">封面标题放在这里<br>两行最好看</h1>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage">
    <div class="card card--dialog split">
      <div class="chrome"><p class="chrome__address t-caption c-muted">example.com</p></div>
      <div class="ph ph--flat" style="height:760px"></div>
    </div>
  </div>
</section>

<section class="slide" data-layout="T03 封面 · 通知">
  <header class="head head--center">
    <p class="chip">栏目名</p>
    <h1 class="t-display">封面标题放在这里<br>两行最好看</h1>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage">
    <div class="notes">
      <div class="card note">
        <div class="ph note__icon"></div>
        <div class="note__text">
          <p class="note__meta"><span>应用名</span><span>现在</span></p>
          <p class="row__head">第一条，最想让人看到的</p>
          <p class="row__note">一句说明，十五字以内</p>
        </div>
      </div>
      <div class="card note">
        <div class="ph note__icon"></div>
        <div class="note__text">
          <p class="note__meta"><span>应用名</span><span>2 分钟前</span></p>
          <p class="row__head">第二条</p>
          <p class="row__note">一句说明，十五字以内</p>
        </div>
      </div>
      <div class="pile">
        <div class="card note">
          <div class="ph note__icon"></div>
          <div class="note__text">
            <p class="note__meta"><span>应用名</span><span>昨天</span></p>
            <p class="row__head">第三条</p>
            <p class="row__note">一句说明，十五字以内</p>
          </div>
        </div>
      </div>
    </div>
  </div>
</section>

<!-- ===================== 讲做法 ===================== -->

<section class="slide" data-layout="T04 步骤 · 文档">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-title">这页的标题<br>一行八九个字</h2>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage">
    <div class="card split">
      <div class="row"><span class="badge">1</span><div class="row__text"><p class="row__head">条目标题</p><p class="row__note">一句说明，十五字以内</p></div><div class="ph" style="width:232px;height:148px"></div></div>
      <div class="row"><span class="badge">2</span><div class="row__text"><p class="row__head">条目标题</p><p class="row__note">一句说明，十五字以内</p></div><div class="ph" style="width:232px;height:148px"></div></div>
      <div class="row"><span class="badge">3</span><div class="row__text"><p class="row__head">条目标题</p><p class="row__note">一句说明，十五字以内</p></div><div class="ph" style="width:232px;height:148px"></div></div>
    </div>
    <div class="hstack">
      <div class="card mini"><div class="ph" style="width:176px;height:176px"></div><div class="row__text"><p class="row__head">条目标题</p><p class="row__note">一句说明，<br>两行以内</p></div></div>
      <div class="card mini"><div class="ph" style="width:176px;height:176px"></div><div class="row__text"><p class="row__head">条目标题</p><p class="row__note">一句说明，<br>两行以内</p></div></div>
    </div>
  </div>
</section>

<section class="slide" data-layout="T05 清单 · 待办">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-title">这页的标题<br>一行八九个字</h2>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage">
    <div class="card split">
      <div class="row todo__head"><p class="row__head grow">清单名称</p><p class="t-num c-muted">2/5</p></div>
      <div class="row todo is-done"><span class="box"><i class="ic" data-i="check"></i></span><div class="row__text"><p class="row__head">已经做完的一项</p><p class="row__note">一句说明，十五字以内</p></div></div>
      <div class="row todo is-done"><span class="box"><i class="ic" data-i="check"></i></span><div class="row__text"><p class="row__head">已经做完的一项</p><p class="row__note">一句说明，十五字以内</p></div></div>
      <div class="row todo"><span class="box"></span><div class="row__text"><p class="row__head">还没做的一项</p><p class="row__note">一句说明，十五字以内</p></div><span class="tag tag--orange">标签</span></div>
      <div class="row todo"><span class="box"></span><div class="row__text"><p class="row__head">还没做的一项</p><p class="row__note">一句说明，十五字以内</p></div></div>
      <div class="row todo"><span class="box"></span><div class="row__text"><p class="row__head">还没做的一项</p><p class="row__note">一句说明，十五字以内</p></div></div>
    </div>
  </div>
</section>

<section class="slide" data-layout="T06 时间线 · 动态">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-title">这页的标题<br>一行八九个字</h2>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage">
    <ol class="card timeline">
      <li><p class="timeline__when">第 1 天</p><div class="row__text"><p class="row__head">第一件事</p><p class="row__note">一句说明，十五字以内</p></div></li>
      <li><p class="timeline__when">第 3 天</p><div class="row__text"><p class="row__head">第二件事</p><p class="row__note">一句说明，十五字以内</p></div></li>
      <li><p class="timeline__when">第 7 天</p><div class="row__text"><p class="row__head">第三件事</p><p class="row__note">一句说明，十五字以内</p></div></li>
      <li><p class="timeline__when">第 14 天</p><div class="row__text"><p class="row__head">第四件事</p><p class="row__note">一句说明，十五字以内</p></div></li>
      <li class="is-now"><p class="timeline__when">现在</p><div class="row__text"><p class="row__head">最后一件事</p><p class="row__note">一句说明，十五字以内</p></div></li>
    </ol>
  </div>
</section>

<!-- ===================== 对比 ===================== -->

<section class="slide" data-layout="T07 对比 · 对错">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-display">这页的标题<br>一行八九个字</h2>
  </header>
  <div class="stage">
    <div class="card card--dialog split">
      <div class="bar"><i class="ic" data-i="info"></i><p class="bar__title">窗口标题</p><i class="ic" data-i="close"></i></div>
      <div class="row row--loose"><span class="badge badge--green"><i class="ic" data-i="check"></i></span><div class="row__text"><p class="row__head">条目标题</p><p class="row__note">一句说明，<br>两行以内</p></div><span class="tag tag--green">标签</span></div>
      <div class="row row--loose"><span class="badge"><i class="ic" data-i="cross"></i></span><div class="row__text"><p class="row__head">条目标题</p><p class="row__note">一句说明，<br>两行以内</p></div><span class="tag">标签</span></div>
      <div class="foot"><p class="foot__note">一句补充说明</p><span class="btn btn--primary">主要操作</span></div>
    </div>
  </div>
</section>

<section class="slide" data-layout="T08 前后 · 双栏">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-title">这页的标题<br>一行八九个字</h2>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage">
    <div class="hstack hstack--wide">
      <div class="card versus"><span class="tag">之前</span><div class="ph" style="height:520px"></div><div class="row__text"><p class="row__head">条目标题</p><p class="row__note">一句说明，十五字以内</p></div></div>
      <div class="card versus"><span class="tag tag--orange">之后</span><div class="ph" style="height:520px"></div><div class="row__text"><p class="row__head">条目标题</p><p class="row__note">一句说明，十五字以内</p></div></div>
    </div>
  </div>
</section>

<!-- ===================== 展示 ===================== -->

<section class="slide" data-layout="T09 宫格 · 卡片">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-title">这页的标题<br>一行八九个字</h2>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage">
    <div class="grid2">
      <div class="card card--accent tile"><div class="ph" style="height:236px"></div><div class="row__text"><p class="row__head">例子名称 <span class="tag tag--orange">标签</span></p><p class="row__note">一句说明，十五字以内</p></div></div>
      <div class="card tile"><div class="ph" style="height:236px"></div><div class="row__text"><p class="row__head">例子名称</p><p class="row__note">一句说明，十五字以内</p></div></div>
      <div class="card tile"><div class="ph" style="height:236px"></div><div class="row__text"><p class="row__head">例子名称</p><p class="row__note">一句说明，十五字以内</p></div></div>
      <div class="card tile"><div class="ph" style="height:236px"></div><div class="row__text"><p class="row__head">例子名称</p><p class="row__note">一句说明，十五字以内</p></div></div>
    </div>
  </div>
</section>

<section class="slide slide--tight" data-layout="T10 标注 · 截图">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-title">这页的标题<br>一行八九个字</h2>
  </header>
  <div class="stage" data-accent>
    <div class="card card--dialog split">
      <div class="chrome"><p class="chrome__title">窗口标题</p></div>
      <div class="shot">
        <div class="ph ph--flat" style="height:400px"></div>
        <span class="badge badge--orange pin" style="left:120px;top:90px">1</span>
        <span class="badge badge--orange pin" style="left:640px;top:150px">2</span>
        <span class="badge badge--orange pin" style="left:300px;top:330px">3</span>
      </div>
    </div>
    <div class="card split">
      <div class="row"><span class="badge badge--soft">1</span><div class="row__text"><p class="row__head">条目标题</p><p class="row__note">一句说明，十五字以内</p></div></div>
      <div class="row"><span class="badge badge--soft">2</span><div class="row__text"><p class="row__head">条目标题</p><p class="row__note">一句说明，十五字以内</p></div></div>
      <div class="row"><span class="badge badge--soft">3</span><div class="row__text"><p class="row__head">条目标题</p><p class="row__note">一句说明，十五字以内</p></div></div>
    </div>
  </div>
</section>

<section class="slide" data-layout="T11 手机 · 截图">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-title">这页的标题<br>一行八九个字</h2>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage stage--fill">
    <div class="phones">
      <div class="phones__item"><span class="tag">标签</span><div class="phone"><div class="ph"></div></div></div>
      <div class="phones__item phones__item--low"><span class="tag tag--orange">标签</span><div class="phone"><div class="ph"></div></div></div>
    </div>
  </div>
</section>

<section class="slide" data-layout="T12 列表 · 应用">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-title">这页的标题<br>一行八九个字</h2>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage">
    <div class="card card--dialog split">
      <div class="search"><i class="ic" data-i="search"></i><p class="t-body c-muted grow">搜索</p><p class="t-caption c-faint">5 个</p></div>
      <ul class="apps">
        <li class="is-active"><div class="ph app-icon"></div><div class="row__text"><p class="row__head">应用名称</p><p class="row__note">一句说明，十五字以内</p></div><span class="tag tag--orange">标签</span></li>
        <li><div class="ph app-icon"></div><div class="row__text"><p class="row__head">应用名称</p><p class="row__note">一句说明，十五字以内</p></div><span class="tag">标签</span></li>
        <li><div class="ph app-icon"></div><div class="row__text"><p class="row__head">应用名称</p><p class="row__note">一句说明，十五字以内</p></div><span class="tag">标签</span></li>
        <li><div class="ph app-icon"></div><div class="row__text"><p class="row__head">应用名称</p><p class="row__note">一句说明，十五字以内</p></div><span class="tag">标签</span></li>
        <li><div class="ph app-icon"></div><div class="row__text"><p class="row__head">应用名称</p><p class="row__note">一句说明，十五字以内</p></div><span class="tag">标签</span></li>
      </ul>
    </div>
  </div>
</section>

<!-- ===================== 数字 ===================== -->

<section class="slide slide--tight" data-layout="T13 数据 · 指标">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-title">这页的标题<br>一行八九个字</h2>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage">
    <div class="card metric" data-accent>
      <p class="t-caption c-muted">指标名称</p>
      <p class="metric__value"><span class="t-metric">12,480</span><span class="t-body c-muted">单位</span></p>
      <div class="bars">
        <i style="height:70px"></i><i style="height:96px"></i><i style="height:84px"></i><i style="height:128px"></i><i style="height:112px"></i><i style="height:150px"></i><i class="is-hot" style="height:210px"></i>
      </div>
      <div class="bars__axis"><span>一</span><span>二</span><span>三</span><span>四</span><span>五</span><span>六</span><span class="c-orange">日</span></div>
    </div>
    <div class="hstack hstack--wide">
      <div class="card metric metric--sm"><p class="t-caption c-muted">指标名称</p><p class="metric__value"><span class="t-metric-sm">86%</span><span class="tag tag--green">+12%</span></p></div>
      <div class="card metric metric--sm"><p class="t-caption c-muted">指标名称</p><p class="metric__value"><span class="t-metric-sm">3.2 秒</span><span class="tag tag--green">−0.8</span></p></div>
    </div>
  </div>
</section>

<section class="slide slide--tight" data-layout="T14 参数 · 设置">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-title">这页的标题<br>一行八九个字</h2>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage settings">
    <div class="settings__group">
      <p class="settings__label">分组名称</p>
      <div class="card split">
        <div class="setting"><p class="setting__key">名称</p><p class="setting__value">数值</p></div>
        <div class="setting"><p class="setting__key">名称</p><span class="swatch" style="background:var(--orange)"></span><p class="setting__value">#EA580C</p></div>
        <div class="setting"><p class="setting__key">名称</p><p class="setting__value">24 px</p></div>
      </div>
    </div>
    <div class="settings__group">
      <p class="settings__label">分组名称</p>
      <div class="card split">
        <div class="setting"><p class="setting__key">名称</p><span class="toggle is-on"></span></div>
        <div class="setting"><p class="setting__key">名称</p><p class="setting__value">选项</p><i class="ic c-faint" data-i="chevron-right"></i></div>
        <div class="setting"><p class="setting__key">名称</p><p class="setting__value">选项</p><i class="ic c-faint" data-i="chevron-right"></i></div>
      </div>
    </div>
  </div>
</section>

<!-- ===================== 文字 ===================== -->

<section class="slide slide--center" data-layout="T15 观点 · 大字">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-display">这里放一句判断<br>大字最多四行<br>每行九个字左右<br><span class="c-orange">读完就记住了</span></h2>
  </header>
  <div class="card callout"><i class="ic" data-i="info"></i><p class="t-caption c-body">展开说一两句，六十字以内。讲清楚为什么这么想，或者什么情况下不成立，看的人能自己判断。</p></div>
</section>

<section class="slide slide--tight" data-layout="T16 对话 · 聊天">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-title">这页的标题<br>一行八九个字</h2>
  </header>
  <div class="stage">
    <div class="card card--dialog split">
      <div class="bar"><i class="ic" data-i="sparkle"></i><p class="row__head">模型名</p><i class="ic" data-i="chevron-down"></i></div>
      <div class="chat">
        <p class="chat__me">问的那句话，一行以内</p>
        <div class="chat__ai"><span class="avatar"><i class="ic" data-i="sparkle"></i></span><p>回答放在这里，两三行就够。重点那句可以单独成段，别把整段回答都贴上来。</p></div>
        <p class="chat__me">追问一句</p>
        <div class="chat__ai"><span class="avatar"><i class="ic" data-i="sparkle"></i></span><p>第二段回答，一两行。</p></div>
        <div class="input"><i class="ic" data-i="paperclip"></i><p class="grow">输入框提示文字</p><span class="btn btn--primary btn--round"><i class="ic" data-i="arrow-up"></i></span></div>
      </div>
    </div>
  </div>
</section>

<section class="slide" data-layout="T17 提示词 · 输入框">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-title">这页的标题放在这里</h2>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage">
    <div class="card card--dialog split">
      <div class="composer__top"><span class="model"><i class="ic" data-i="sparkle"></i>模型名<i class="ic" data-i="chevron-down"></i></span><span class="grow"></span><span class="tag">标签</span></div>
      <div class="prompt">
        <p>这里放完整的提示词。第一段交代要做什么、用什么做，看的人扫一眼就知道是干嘛的。</p>
        <p>1. 第一步要做的事，写清楚输入和输出。</p>
        <p>2. 第二步要做的事，写清楚判断标准。</p>
        <p>3. 第三步要做的事，写清楚交互和动效。</p>
        <p>4. 第四步要做的事，写清楚怎么检查。</p>
        <p>最后一句说明怎么交付。</p>
      </div>
      <div class="composer__bottom"><i class="ic" data-i="paperclip"></i><p class="foot__note">一句补充说明</p><span class="btn btn--round"><i class="ic" data-i="copy"></i></span><span class="btn btn--primary btn--round"><i class="ic" data-i="arrow-up"></i></span></div>
    </div>
  </div>
</section>

<section class="slide slide--tight" data-layout="T18 代码 · 编辑器">
  <header class="head">
    <p class="chip">栏目名</p>
    <h2 class="t-title">这页的标题<br>一行八九个字</h2>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage">
    <div class="card card--dialog split">
      <div class="tabs"><span class="tabs__item is-active"><i class="ic c-orange" data-i="code"></i>index.html</span><span class="tabs__item">style.css</span></div>
<pre class="code">&lt;span class=<b class="s">"tag"</b>&gt;标签&lt;/span&gt;

<b class="k">const</b> lines = <b class="n">48</b>;
<b class="k">const</b> gap = <b class="n">6</b>;

<b class="k">function</b> draw(ctx, img) {
  <b class="k">for</b> (<b class="k">let</b> i = <b class="n">0</b>; i &lt; lines; i++) {
    <b class="k">const</b> x = i * gap;
    <b class="k">const</b> h = sample(img, x);
    ctx.fillRect(x, <b class="n">0</b>, <b class="n">2</b>, h);
  }
}</pre>
      <div class="foot"><p class="foot__note">一句补充说明</p><span class="pill"><i class="ic" data-i="copy"></i>复制</span></div>
    </div>
  </div>
</section>

<!-- ===================== 结尾 ===================== -->

<section class="slide slide--end" data-layout="T19 结尾 · 对照">
  <div class="stage stage--top"><div class="ph" style="width:760px;height:497px"></div></div>
  <header class="head head--center">
    <p class="chip">栏目名</p>
    <h2 class="t-title">结尾标题放在这里<br>两行最好看</h2>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="card split">
    <div class="row diff"><p class="diff__key">名称</p><p class="row__head grow">条目内容</p><span class="tag tag--green">标签</span></div>
    <div class="row diff"><p class="diff__key">名称</p><p class="row__head grow">条目内容</p><span class="tag">标签</span></div>
    <div class="row diff"><p class="diff__key">名称</p><p class="row__head grow">条目内容</p></div>
  </div>
</section>

<section class="slide" data-layout="T20 结尾 · 完成">
  <header class="head head--center">
    <p class="chip">栏目名</p>
    <h2 class="t-display">结尾标题放这里<br>两行最好看</h2>
    <p class="sub">副标题一句话，二十字以内</p>
  </header>
  <div class="stage">
    <div class="card card--dialog split">
      <div class="done__top"><span class="done__mark"><i class="ic ic--lg" data-i="check"></i></span><p class="t-heading">三条收获</p></div>
      <ul class="done__list">
        <li><i class="ic c-green" data-i="check"></i>第一条，一行以内</li>
        <li><i class="ic c-green" data-i="check"></i>第二条，一行以内</li>
        <li><i class="ic c-green" data-i="check"></i>第三条，一行以内</li>
      </ul>
      <div class="foot foot--base"><p class="foot__note">一句补充说明</p><span class="btn btn--primary">主要操作</span></div>
    </div>
  </div>
</section>

<script src="xhs.js"></script>
</body>
</html>
```

## File: ian-xhs-carousel/assets/xhs.css
size: 18895
sha: dada07d4b7e6e42709dc70d9bc6b4f86e43a99a9
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/assets/xhs.css

```text
/* 小红书 3:4 图文的版式库。每期复制一份，不要改；这一期特有的样式写在 index.html 的 <style> 里
 * 风格：锌灰中性色 + 一处橙，白页面上垫一块灰底盘，内容是一张张带分层阴影的卡片
 * 深色：<body data-theme="dark">，所有颜色和阴影跟着变量换 */

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@600&family=Geist+Mono:wght@400&display=swap');

/* ---------- 颜色 ---------- */
:root {
  --page: #ffffff;
  --tray: #f4f4f5;
  --surface: #ffffff;
  --fill-soft: #f9f9fa;
  --fill: #f1f2f3;
  --fill-strong: #e9e9ec;

  --line-soft: #e9e9ec;
  --line: #d7d7db;
  --line-ring: rgb(183 184 189 / 0.3);

  --text-strong: #18181b;
  --text-medium: #3f4046;
  --text-body: #4f5159;
  --text-muted: #6b6f7a;
  --text-faint: #b7b8bd;

  --inverse: #232326;
  --on-inverse: #ffffff;

  --orange: #ea580c;
  --orange-200: #fed7aa;
  --orange-50: #fff7ed;
  --green: #16a329;
  --green-50: #f0fdf1;

  --shade: rgb(24 24 27 / 0.05);
  --shade-ring: rgb(183 184 189 / 0.15);
  --shade-highlight: rgb(255 255 255 / 0.12);
}

[data-theme="dark"] {
  --page: #0c0c0e;
  --tray: #18181b;
  --surface: #232326;
  --fill-soft: #1c1c1f;
  --fill: #2c2c30;
  --fill-strong: #3f4046;

  --line-soft: #2e2e33;
  --line: #3f4046;
  --line-ring: rgb(183 184 189 / 0.14);

  --text-strong: #f4f4f5;
  --text-medium: #d4d4d8;
  --text-body: #b4b4bb;
  --text-muted: #8e8e96;
  --text-faint: #5a5a62;

  --inverse: #f4f4f5;
  --on-inverse: #18181b;

  --orange: #fb923c;
  --orange-200: #7c3a12;
  --orange-50: #2b1709;
  --green: #4ade80;
  --green-50: #0f2918;

  --shade: rgb(0 0 0 / 0.35);
  --shade-ring: rgb(255 255 255 / 0.06);
  --shade-highlight: rgb(255 255 255 / 0.05);
}

/* ---------- 阴影 ---------- */
:root {
  --shadow-sm: 0 12px 12px -6px var(--shade), 0 6px 6px -3px var(--shade), 0 2px 2px -1px var(--shade);
  --shadow-card: 0 12px 30px -6px var(--shade), 0 6px 6px -3px var(--shade), 0 2px 2px -1px var(--shade);
  --shadow-dialog: 0 96px 96px -48px var(--shade), 0 48px 48px -24px var(--shade), 0 24px 24px -12px var(--shade), var(--shadow-sm);
  --shadow-tray: var(--shadow-dialog), 0 0 0 2px var(--shade-ring), inset 0 2px 0 var(--shade-highlight);
}

/* ---------- 字 ---------- */
:root {
  --font: "PingFang SC", "Noto Sans SC", sans-serif;
  --font-num: "Inter", "PingFang SC", sans-serif;
  --font-code: "Geist Mono", "SF Mono", Menlo, monospace;
}

.t-display { font: 600 88px/110px var(--font); letter-spacing: -0.02em; }
.t-title { font: 600 64px/80px var(--font); letter-spacing: -0.01em; }
.t-heading { font: 500 40px/52px var(--font); }
.t-body { font: 400 32px/48px var(--font); }
.t-strong { font: 500 32px/48px var(--font); }
.t-caption { font: 400 26px/40px var(--font); }
.t-label { font: 500 24px/32px var(--font); }
.t-small { font: 400 22px/36px var(--font); }
.t-num { font: 600 24px/32px var(--font-num); font-variant-numeric: tabular-nums; }
.t-code { font: 400 22px/36px var(--font-code); }

.c-strong { color: var(--text-strong); }
.c-body { color: var(--text-body); }
.c-muted { color: var(--text-muted); }
.c-faint { color: var(--text-faint); }
.c-orange { color: var(--orange); }
.c-green { color: var(--green); }

/* ---------- 画布 ---------- */
* { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-font-smoothing: antialiased; }
body { background: #d4d4d8; font-family: var(--font); color: var(--text-strong); }
[data-theme="dark"] body, body[data-theme="dark"] { background: #000; }
img { display: block; max-width: 100%; }

.slide {
  position: relative;
  width: 1080px;
  height: 1440px;
  margin: 40px auto;
  padding: 96px 80px 80px;
  display: flex;
  flex-direction: column;
  gap: 96px;
  overflow: hidden;
  background: var(--page);
}
.slide--tight { gap: 72px; }
.slide--center { justify-content: center; gap: 64px; }

/* 标题组：标签、标题、副标题 */
.head { display: flex; flex-direction: column; align-items: flex-start; }
.head--center { align-items: center; text-align: center; }
.head > .t-display, .head > .t-title { margin-top: 32px; color: var(--text-strong); }
.head > .sub { margin-top: 24px; font: 400 32px/48px var(--font); color: var(--text-body); }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 10px 24px 10px 20px;
  border: 2px solid var(--line-ring);
  border-radius: 999px;
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  font: 500 24px/32px var(--font);
  color: var(--text-medium);
}
.chip::before { content: ""; width: 12px; height: 12px; border-radius: 50%; background: var(--orange); }

/* 底盘：内容区后面那块灰，从内容上方 40 一直铺出画布底边 */
.stage { position: relative; isolation: isolate; display: flex; flex-direction: column; gap: 24px; }
.stage::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -40px -40px -2000px;
  border-radius: 24px;
  background: var(--tray);
  box-shadow: var(--shadow-tray);
}
.stage--bare::before { content: none; }
.stage--fill { flex: 1; min-height: 0; }

/* ---------- 卡片 ---------- */
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 2px solid var(--line-ring);
  border-radius: 32px;
  background: var(--surface);
  box-shadow: var(--shadow-card);
}
.card--dialog { box-shadow: var(--shadow-dialog); }
.card--accent { border-color: var(--orange-200); }
.split > * + * { border-top: 2px solid var(--line-soft); }

/* 窗口顶栏：图标 + 标题 + 关闭 */
.bar { display: flex; align-items: center; gap: 16px; padding: 28px 36px; }
.bar__title { flex: 1; font: 500 32px/48px var(--font); color: var(--text-strong); }
/* 窗口底栏：说明 + 按钮 */
.foot { display: flex; align-items: center; gap: 16px; padding: 24px 36px; }
.foot__note { flex: 1; font: 400 26px/40px var(--font); color: var(--text-muted); }

/* 浏览器 / 软件窗口的顶栏：三个点 + 地址或标题 */
.chrome { display: flex; align-items: center; gap: 24px; padding: 24px 32px; }
.chrome::before {
  content: "";
  flex: none;
  width: 18px;
  height: 18px;
  margin-right: 60px;
  border-radius: 50%;
  background: var(--line);
  box-shadow: 30px 0 var(--line), 60px 0 var(--line);
}
.chrome__title { flex: 1; text-align: center; font: 400 26px/40px var(--font); color: var(--text-muted); }
.chrome__address { flex: 1; padding: 10px 24px; border-radius: 999px; background: var(--fill); }
.chrome::after { content: ""; flex: none; width: 78px; }

/* 一行：行首（编号/勾/图）+ 标题说明 + 行尾 */
.row { display: flex; align-items: center; gap: 32px; padding: 24px 36px; }
.row--loose { padding: 32px 36px; gap: 28px; }
.row__text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.row__head { font: 500 32px/48px var(--font); color: var(--text-strong); }
.row__note { font: 400 26px/40px var(--font); color: var(--text-muted); }

/* ---------- 小零件 ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 16px 32px;
  border: 2px solid var(--line-ring);
  border-radius: 16px;
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  font: 500 24px/32px var(--font);
  color: var(--text-strong);
  white-space: nowrap;
}
.btn--primary { border: 0; background: var(--inverse); color: var(--on-inverse); }
.btn--round { padding: 0; width: 64px; height: 64px; justify-content: center; border-radius: 50%; }

.tag {
  flex: none;
  display: inline-block;
  padding: 4px 14px;
  border-radius: 10px;
  background: var(--fill);
  font: 500 24px/32px var(--font);
  color: var(--text-body);
  white-space: nowrap;
}
.tag--green { background: var(--green-50); color: var(--green); }
.tag--orange { background: var(--orange-50); color: var(--orange); }

/* 圆形编号 / 标记 */
.badge {
  flex: none;
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border: 2px solid var(--line-ring);
  border-radius: 50%;
  background: var(--fill);
  font: 600 24px/32px var(--font-num);
  color: var(--text-strong);
}
.badge--orange { border: 0; background: var(--orange); color: var(--on-inverse); }
.badge--soft { border: 0; background: var(--orange-50); color: var(--orange); }
.badge--green { border: 0; background: var(--green-50); color: var(--green); }

/* 图标：<i class="ic" data-i="check"></i>，xhs.js 换成 SVG，颜色跟 color 走 */
.ic { flex: none; display: inline-block; width: 32px; height: 32px; }
:where(.ic) { color: var(--text-body); }
.btn .ic, .badge .ic, .pill .ic, .model .ic, .tabs__item .ic { width: 28px; height: 28px; color: inherit; }
.ic svg { display: block; width: 100%; height: 100%; }
.ic--lg { width: 64px; height: 64px; }

/* 图槽：放截图、代码画的图。没内容时是虚线占位，xhs.js 会标上尺寸 */
.fig { position: relative; overflow: hidden; border-radius: 20px; background: var(--fill); }
.fig > img, .fig > canvas, .fig > svg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.fig--contain > img { object-fit: contain; }
.ph {
  display: grid;
  place-items: center;
  border: 2px dashed var(--line);
  border-radius: 16px;
  background: var(--fill);
  font: 400 22px/36px var(--font);
  color: var(--text-muted);
}
.ph::after { content: attr(data-size); }

.grow { flex: 1; min-height: 0; }
.hstack { display: flex; gap: 24px; }
.hstack > * { flex: 1; min-width: 0; }

/* ======================================================================
 * 下面是各个母版自己的零件，按 T01–T20 的顺序。名字和 templates.html 对得上
 * ====================================================================== */

.ph--flat { border: 0; border-radius: 0; }
.row__head .tag { margin-left: 8px; vertical-align: 4px; }
.hstack--wide { gap: 32px; }

/* T01 对话框正文 */
.dialog-body { padding: 24px; }

/* T03 通知：一叠通知卡，最后一张下面再垫一张露个边 */
.notes { display: flex; flex-direction: column; gap: 16px; }
.note { flex-direction: row; align-items: center; gap: 24px; padding: 28px 32px; }
.note__icon { flex: none; width: 80px; height: 80px; border-radius: 20px; }
.note__text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.note__meta { display: flex; justify-content: space-between; font: 400 22px/36px var(--font); color: var(--text-muted); }
.pile { position: relative; isolation: isolate; padding-bottom: 24px; }
.pile::after {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 24px 40px 0;
  border: 2px solid var(--line-ring);
  border-radius: 32px;
  background: var(--surface);
  box-shadow: var(--shadow-card);
}

/* T04 底下两张小卡 */
.mini { flex-direction: row; align-items: center; gap: 24px; padding: 20px; }
.mini > .ph, .mini > .fig { flex: none; border-radius: 20px; }

/* T05 待办 */
.todo__head { padding: 28px 36px; }
.box {
  flex: none;
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  border: 2px solid var(--line);
  border-radius: 14px;
}
.box .ic { width: 28px; height: 28px; color: var(--on-inverse); }
.is-done .box { border: 0; background: var(--inverse); }
.is-done .row__head { color: var(--text-muted); text-decoration: line-through; text-decoration-color: var(--text-faint); }

/* T06 时间线：左边时间，中间一根线串起圆点 */
.timeline { list-style: none; padding: 16px 36px; }
.timeline li { position: relative; display: flex; gap: 24px; padding: 20px 0; }
.timeline li::before {
  content: "";
  position: absolute;
  z-index: 1;
  left: 144px;
  top: 34px;
  width: 20px;
  height: 20px;
  border: 3px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
}
.timeline li::after { content: ""; position: absolute; left: 153px; top: 54px; bottom: -34px; width: 2px; background: var(--line-soft); }
.timeline li:last-child::after { content: none; }
.timeline li.is-now::before { border-color: var(--orange); background: var(--orange); }
.timeline .row__text { padding-left: 60px; }
.timeline__when { flex: none; width: 120px; padding-top: 8px; font: 500 24px/32px var(--font); color: var(--text-muted); }
.is-now .timeline__when { color: var(--text-strong); }

/* T08 前后两栏 */
.versus { gap: 20px; padding: 20px 20px 28px; }
.versus > .tag { align-self: flex-start; }
.versus > .row__text { padding: 0 12px; }

/* T09 四宫格 */
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
.tile { gap: 24px; padding: 20px 20px 28px; }
.tile > .row__text { padding: 0 12px; }

/* T10 截图上的编号 */
.shot { position: relative; }
.pin { position: absolute; box-shadow: 0 0 0 6px var(--orange-50), var(--shadow-sm); }

/* T11 两台手机，右边那台往下错一点，底部出血 */
.phones { flex: 1; display: flex; justify-content: center; align-items: flex-start; gap: 40px; }
.phones__item { display: flex; flex-direction: column; align-items: center; gap: 20px; }
.phones__item--low { margin-top: 96px; }
.phone {
  width: 380px;
  height: 800px;
  padding: 12px;
  border: 2px solid var(--line-ring);
  border-radius: 56px;
  background: var(--surface);
  box-shadow: var(--shadow-dialog);
}
.phone > .ph, .phone > .fig { width: 100%; height: 100%; border-radius: 44px; }

/* T12 应用列表 */
.search { display: flex; align-items: center; gap: 16px; padding: 24px 36px; }
.apps { list-style: none; padding: 12px; }
.apps li { display: flex; align-items: center; gap: 24px; padding: 16px 24px; border-radius: 20px; }
.apps li.is-active { background: var(--fill); }
.app-icon { flex: none; width: 80px; height: 80px; border-radius: 20px; }

/* T13 指标 */
.t-metric { font: 600 88px/110px var(--font-num); letter-spacing: -0.02em; font-variant-numeric: tabular-nums; color: var(--text-strong); }
.t-metric-sm { font: 600 64px/80px var(--font-num); letter-spacing: -0.01em; font-variant-numeric: tabular-nums; color: var(--text-strong); }
.metric { gap: 8px; padding: 36px; }
.metric--sm { padding: 28px 32px; }
.metric__value { display: flex; align-items: baseline; gap: 16px; }
.metric--sm .metric__value { align-items: center; justify-content: space-between; }
.bars { display: flex; align-items: flex-end; gap: 20px; height: 220px; margin-top: 32px; }
.bars i { flex: 1; border-radius: 12px 12px 4px 4px; background: var(--fill-strong); }
.bars i.is-hot { background: var(--orange); }
.bars__axis { display: flex; gap: 20px; font: 400 22px/36px var(--font); color: var(--text-muted); }
.bars__axis span { flex: 1; text-align: center; }

/* T14 设置 */
.settings { gap: 40px; }
.settings__group { display: flex; flex-direction: column; gap: 16px; }
.settings__label { padding: 0 8px; font: 500 24px/32px var(--font); color: var(--text-muted); }
.setting { display: flex; align-items: center; gap: 16px; padding: 24px 36px; }
.setting__key { flex: 1; font: 500 32px/48px var(--font); color: var(--text-strong); }
.setting__value { font: 400 32px/48px var(--font); color: var(--text-body); }
.swatch { flex: none; width: 32px; height: 32px; border-radius: 10px; box-shadow: inset 0 0 0 2px var(--line-ring); }
.toggle { position: relative; flex: none; width: 76px; height: 44px; border-radius: 999px; background: var(--fill-strong); }
.toggle::after {
  content: "";
  position: absolute;
  top: 4px;
  left: 4px;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: #fff;
  box-shadow: var(--shadow-sm);
}
.toggle.is-on { background: var(--green); }
.toggle.is-on::after { left: 36px; }

/* T15 大字页底下的补充 */
.callout { flex-direction: row; align-items: flex-start; gap: 20px; padding: 28px 32px; }
.callout > .ic { margin-top: 4px; }

/* T16 聊天 */
.chat { display: flex; flex-direction: column; gap: 24px; padding: 32px 36px 36px; }
.chat__me {
  align-self: flex-end;
  max-width: 75%;
  padding: 16px 28px;
  border-radius: 28px 28px 8px 28px;
  background: var(--fill);
  font: 400 32px/48px var(--font);
  color: var(--text-strong);
}
.chat__ai { display: flex; align-items: flex-start; gap: 20px; }
.chat__ai > p { flex: 1; font: 400 32px/48px var(--font); color: var(--text-medium); }
.avatar { flex: none; display: grid; place-items: center; width: 56px; height: 56px; border-radius: 50%; background: var(--inverse); }
.avatar .ic { width: 28px; height: 28px; color: var(--on-inverse); }
.input {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-top: 16px;
  padding: 12px 12px 12px 28px;
  border: 2px solid var(--line-soft);
  border-radius: 999px;
  background: var(--fill-soft);
  font: 400 32px/48px var(--font);
  color: var(--text-muted);
}

/* T17 提示词输入框 */
.composer__top, .composer__bottom { display: flex; align-items: center; gap: 16px; padding: 20px 28px; }
.model {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 8px 16px;
  border-radius: 14px;
  background: var(--fill);
  font: 500 24px/32px var(--font);
  color: var(--text-medium);
}
.prompt { display: flex; flex-direction: column; gap: 12px; padding: 28px 36px; font: 400 22px/36px var(--font); color: var(--text-medium); }

/* T18 代码编辑器 */
.tabs { display: flex; gap: 8px; padding: 16px 20px 0; background: var(--fill-soft); }
.tabs__item {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 14px 24px;
  border-radius: 16px 16px 0 0;
  font: 500 24px/32px var(--font-code);
  color: var(--text-muted);
}
.tabs__item.is-active { background: var(--surface); color: var(--text-strong); }
.code { padding: 32px 36px; font: 400 22px/36px var(--font-code); color: var(--text-medium); white-space: pre; tab-size: 2; overflow: hidden; }
.code b { font-weight: 400; }
.code .k { color: var(--orange); }
.code .s { color: var(--green); }
.code .n { color: var(--text-strong); }
.pill {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 10px 20px;
  border-radius: 999px;
  background: var(--fill);
  font: 500 24px/32px var(--font);
  color: var(--text-medium);
}

/* T19 结尾：图在顶上，底盘从画布顶边垂下来 */
.slide--end { gap: 56px; padding-top: 64px; }
.slide--end > .head { padding-top: 40px; }
.stage--top { align-items: center; }
.stage--top::before { inset: -2000px -40px -40px; }
.diff { gap: 24px; }
.diff__key { flex: none; width: 96px; font: 400 26px/40px var(--font); color: var(--text-muted); }

/* T20 完成 */
.done__top { display: flex; flex-direction: column; align-items: center; gap: 20px; padding: 48px 36px 36px; }
.done__mark { display: grid; place-items: center; width: 112px; height: 112px; border-radius: 50%; background: var(--green-50); }
.done__mark .ic { color: var(--green); }
.done__list { list-style: none; display: flex; flex-direction: column; gap: 20px; padding: 28px 48px; }
.done__list li { display: flex; align-items: center; gap: 16px; font: 500 32px/48px var(--font); color: var(--text-strong); }
.foot--base { background: var(--fill-soft); }
```

## File: ian-xhs-carousel/assets/xhs.js
size: 1845
sha: bdcf4ec39e3424bb59565c4843f7140bf6c49a90
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/assets/xhs.js

```text
// 版式库的小脚本：把 <i class="ic" data-i="名字"> 换成线框图标，给没填内容的 .ph 标上尺寸
(function () {
  // 32 格，线宽 2.5，圆头圆角，只用线框
  const ICONS = {
    close: '<path d="M8 8L24 24M24 8L8 24"/>',
    check: '<path d="M7 16.5L13 22.5L25 10"/>',
    cross: '<path d="M9 9L23 23M23 9L9 23"/>',
    'arrow-up': '<path d="M16 26V7M8 14L16 6L24 14"/>',
    'chevron-down': '<path d="M9 12.5L16 19.5L23 12.5"/>',
    'chevron-right': '<path d="M12.5 9L19.5 16L12.5 23"/>',
    sparkle: '<path d="M16 5C16.9 10.6 21.4 15.1 27 16C21.4 16.9 16.9 21.4 16 27C15.1 21.4 10.6 16.9 5 16C10.6 15.1 15.1 10.6 16 5Z"/>',
    info: '<circle cx="16" cy="16" r="11"/><path d="M16 15V22M16 10.5V11"/>',
    copy: '<rect x="11" y="11" width="15" height="15" rx="3"/><path d="M21 11V8a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h3"/>',
    paperclip: '<path d="M24 15L15.5 23.5a5 5 0 0 1-7-7L17 8a3.33 3.33 0 0 1 4.7 4.7L13.1 21.3a1.67 1.67 0 0 1-2.4-2.4L18 11.6"/>',
    code: '<path d="M11 10L5 16L11 22M21 10L27 16L21 22"/>',
    search: '<circle cx="14" cy="14" r="8"/><path d="M20 20L26 26"/>',
  };

  document.querySelectorAll('.ic[data-i]').forEach((el) => {
    const body = ICONS[el.dataset.i];
    if (!body) {
      console.warn(`没有这个图标：${el.dataset.i}`);
      return;
    }
    el.innerHTML = `<svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
  });

  // 还没换成真内容的占位，标出实际像素，备图时照着截
  document.querySelectorAll('.ph').forEach((el) => {
    const { width, height } = el.getBoundingClientRect();
    el.dataset.size = width < 120 ? '图' : `图 ${Math.round(width)}×${Math.round(height)}`;
  });

  window.XHS_ICONS = Object.keys(ICONS);
})();
```

## File: ian-xhs-carousel/assets/版式总览-深色.png
size: 728471
sha: 782809992aa0f37d4ea36d822830ff8120a8a844
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/assets/%E7%89%88%E5%BC%8F%E6%80%BB%E8%A7%88-%E6%B7%B1%E8%89%B2.png

Content is not inlined in the catalog. Read the upstream file: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/assets/%E7%89%88%E5%BC%8F%E6%80%BB%E8%A7%88-%E6%B7%B1%E8%89%B2.png

## File: ian-xhs-carousel/assets/版式总览.png
size: 696873
sha: c7e05d59b901ba4858583669bbc20031f8c342f2
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/assets/%E7%89%88%E5%BC%8F%E6%80%BB%E8%A7%88.png

Content is not inlined in the catalog. Read the upstream file: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/assets/%E7%89%88%E5%BC%8F%E6%80%BB%E8%A7%88.png

## File: ian-xhs-carousel/agents/openai.yaml
size: 307
sha: 7e84a58ca2fa9a49b27aba4a6c64a6793d2768f3
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/ian-xhs-carousel/agents/openai.yaml

```text
interface:
  display_name: "Ian 小红书图文"
  short_description: "把中文内容排成 3:4 小红书轮播图，20 个母版，浅色深色都能出"
  default_prompt: "Use $ian-xhs-carousel to 把这篇内容做成一组小红书图文，先出分镜表。"
policy:
  allow_implicit_invocation: true
```

## File: README.md
upstream: https://github.com/helloianneo/ian-xhs-carousel/blob/main/README.md

````text
# Ian XHS Carousel

**中文** | [English](README.en.md)

> 把文章、案例或笔记，排成一组 3:4 的小红书图文。
>
> 1080×1440 竖版 | 20 个母版 | 浅色 / 深色 | Claude Code Skill

---

## 这是什么

Ian XHS Carousel 是一个 AI Skill，把你的内容做成一组小红书轮播图，风格是简洁的产品界面风。

你把内容交给它，它先出一张分镜表给你确认，确认后再排版、出图。

**最好用 Claude 跑。**

---

## 适合谁用

- 写文章、做案例拆解，想顺手发一份小红书图文的人
- 做 AI、产品、设计、效率工具内容，图里常有截图、步骤、对比、数据、代码的人
- 想让每期图文风格统一、看着干净的人

想要手绘、插画、大色块海报这类风格的话，这个不太合适。

---

## 它会产出什么

- **分镜表**：每页讲什么、用哪个母版、标题怎么写，先给你确认
- **每页一张 PNG**：1080×1440，可以直接发
- **总览图**：所有页排在一起，一眼看整组节奏
- **HTML 源文件**：想自己改字、换图，改完重新渲染就行

浅色、深色两版都能出，版式一样。

小红书帖子的标题、正文和话题标签需要你另外写。

---

## 20 个母版

![20 个母版 · 浅色](examples/images/templates-light.png)

<details>
<summary>深色版</summary>

![20 个母版 · 深色](examples/images/templates-dark.png)

</details>

| 分组 | 母版 |
|---|---|
| 封面 | T01 对话框、T02 窗口、T03 通知 |
| 讲做法 | T04 步骤、T05 清单、T06 时间线 |
| 对比 | T07 对错、T08 前后双栏 |
| 展示 | T09 宫格、T10 截图标注、T11 手机截图、T12 应用列表 |
| 数字 | T13 数据指标、T14 参数设置 |
| 文字 | T15 观点大字、T16 聊天、T17 提示词、T18 代码 |
| 结尾 | T19 对照、T20 完成 |

20 个都不合适的时候，AI 会用同一套样式拼一个新排法。

---

## 安装

**1. 下载并复制到 Claude Code**

```bash
git clone https://github.com/helloianneo/ian-xhs-carousel.git
mkdir -p ~/.claude/skills
cp -R ./ian-xhs-carousel/ian-xhs-carousel ~/.claude/skills/
```

**2. 装渲染要用的工具**（第一次用时装一次）

```bash
pip install playwright pillow
playwright install chromium
```

渲染时会联网加载英文和代码字体，记得保持网络畅通。

---

## 素材要好看

一组图好不好看，很大程度看你给的素材。上传前花几分钟挑一挑：

- **截图**：用高清的，裁掉浏览器工具栏、桌面和别的窗口；做深色版就截深色模式的
- **Logo 和图标**：用官方的 SVG，别截一个小图再放大
- **照片和效果图**：一堆里挑最好看的那张
- **没有好图**：先空着，它会在分镜表里列出来问你要，别拿糊的凑

---

## 怎么用

在 Claude Code 里输入：

```text
/ian-xhs-carousel 把下面这篇文章做成一组小红书图文。

<粘贴文章>
```

不输命令也行，直接说"把这篇做成小红书"，它一般会自己用上。

**只要分镜表**

```text
/ian-xhs-carousel 先别排版，帮我看看下面这篇能做成几页，出分镜表就行。

<粘贴文章>
```

**出深色版**

```text
/ian-xhs-carousel 把这篇做成小红书图文，用深色主题。截图在 ./shots 里。

<粘贴文章>
```

**改某一页**

```text
第 3 页换成 T08 前后双栏，左边放旧版截图，右边放新版。
```

更多示例见 [examples/prompts.md](examples/prompts.md)。

---

## 工作流程

1. **读内容**：想清楚这组图要让人带走什么
2. **出分镜表**：一页讲一件事，一般 5 页左右，**停下来等你确认**
3. **准备配图**：用你给的素材，缺的会列出来问你
4. **排版**：从 20 个母版里挑合适的
5. **渲染和自查**：检查文字溢出、字号、配色，有问题自己改掉
6. **交付**：给你每页图和总览图

---

## 小贴士

- 分镜表那一步多花点时间，页数和每页讲什么定下来，后面基本不用返工
- 图上的字越少越好，说不完的留给正文
- 页数宁少勿多，5 页左右刚好

---

## 目录结构

```text
.
├── README.md
├── README.en.md
├── examples/              母版总览图、更多提示词
└── ian-xhs-carousel/      ← 安装的是这个文件夹
    ├── SKILL.md
    ├── assets/            样式、母版、图标
    ├── references/        设计规则、母版说明、自查清单
    └── scripts/           渲染脚本
```

---

## 相关项目

- [Ian Xiaohei Illustrations](https://github.com/helloianneo/ian-xiaohei-illustrations) — 中文小黑怪诞正文配图生成 Skill
- [Ian Handdrawn PPT](https://github.com/helloianneo/ian-handdrawn-ppt) — 中文手绘技术 PPT-style 页面图生成 Skill
- [Awesome Claude Code Skills](https://github.com/helloianneo/awesome-claude-code-skills) — Claude Code Skills / Agents / Plugins 精选合集
- [Obsidian + Claude AI Second Brain](https://github.com/helloianneo/obsidian-ai-second-brain) — Obsidian + Claude AI 个人知识库搭建指南

---

## 关于作者

**Ian (伊恩)** — 产品设计师 / AI Builder

GitHub 万星、Dribbble 万粉，用 AI 做好玩的东西，探索 AI + 自由。

- 网站：[ianneo.xyz](https://www.ianneo.xyz)
- X/Twitter：[@ianneo_ai](https://x.com/ianneo_ai)
- GitHub：[helloianneo](https://github.com/helloianneo)
- Indie Builders 社群：[ianneo.xyz/ib](https://ianneo.xyz/ib)

交流合作加我微信，备注来意：

<p>
  <img src="assets/ian-wechat-qr.jpg" alt="Ian 微信二维码" width="120">
</p>

扫不了就搜微信号 `ianneoxyz`。

---

## License

MIT License. See [LICENSE](LICENSE).
````
