⏳ This skill is pending AI review.

Scores will appear once the review pipeline completes.

version unknown

ian-xhs-carousel

@helloianneo⭐ 255 stars

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

Choose how to use this skill

You do not need every option. Choose the path your AI client supports. The stable page stays the same; versioned files are immutable.

1. Native installer

This listing has no registered native installer command. Use the complete package or source fallback below, depending on what your client supports.

Do not guess an installer command or replace an existing version without reviewing the diff.

2. Complete package recommended

Download the ZIP when available. It includes SKILL.md plus the references, security notes and version metadata.

No complete ProSkills package is published for this listing yet.

3. Prompt-only

Copy the prompt above when the agent can read the stable page or when you want to adopt the workflow without installing a skill.

Need only the instruction file?

Download SKILL.md only if your client requires a single file. The complete ZIP is safer for a full installation because it preserves the references and release context.

No path installs or executes anything by itself. Your agent still needs access to the project files. Before updating, compare the installed version and review the diff.

—/10

// RATINGS

⭐GitHub Stars
⭐⭐⭐ 255 on GitHubGitHub ↗

Popular

🟢ProSkills Score
—
📍

Not yet listed on ClawHub or SkillsMP

// README

Ian XHS Carousel

中文 | English

把文章、案例或笔记,排成一组 3:4 的小红书图文。

1080×1440 竖版 | 20 个母版 | 浅色 / 深色 | Claude Code Skill


这是什么

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

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

最好用 Claude 跑。


适合谁用

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

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


它会产出什么

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

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

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


20 个母版

20 个母版 · 浅色

20 个母版 · 深色

分组母版
封面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

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. 装渲染要用的工具(第一次用时装一次)

pip install playwright pillow
playwright install chromium

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


素材要好看

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

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

怎么用

在 Claude Code 里输入:

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

<粘贴文章>

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

只要分镜表

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

<粘贴文章>

出深色版

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

<粘贴文章>

改某一页

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

更多示例见 examples/prompts.md。


工作流程

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

小贴士

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

目录结构

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

相关项目


关于作者

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

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

交流合作加我微信,备注来意:

扫不了就搜微信号 ianneoxyz。


License

MIT License. See LICENSE.

// HOW IT'S BUILT

// FILES

15 files shipped with this skill. Inline text is the catalog copy. The upstream link is the source repository.

  • ian-xhs-carousel/SKILL.md8.8 KB62ed4b7ae1f0upstream ↗
    ---
    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`:渲染、检查、出总览
    
  • ian-xhs-carousel/scripts/render.py7.4 KBa246a17cc1cbupstream ↗
    """把 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}")
    
  • ian-xhs-carousel/references/checklist.md2.0 KB2e0a2b673421upstream ↗
    # 交付前自查
    
    先跑 `scripts/render.py`,它报的问题全部改掉。然后打开每页图和总览,逐条看。
    
    ## 整组
    - [ ] 一页只讲一件事,删掉哪一页都会少点东西
    - [ ] 总览里一眼看得出顺序和节奏,没有连续两页用同一个母版
    - [ ] 封面缩到总览那么小,还看得清标题和主图
    - [ ] 整组都是同一个主题(全浅或全深),没有混着来
    
    ## 每页
    - [ ] 有底盘的页,底盘从标题组下面开始、铺出画布底边;T15 没有底盘,T19 的底盘从顶上垂下来
    - [ ] 橙只有一处(栏目标签前的小圆点不算),用在这页最想让人看的东西上
    - [ ] 标题断行自然,没有孤字,没有一行特别短
    - [ ] 字号都在字号表里,没有为了塞下内容把字改小
    - [ ] 卡片里的分区用细线隔开,卡片之间留 24
    - [ ] 没有被裁到的字(窗口、手机、图可以出血,字不行)
    - [ ] 内容区填得合适:不空得发虚,也不挤到底边
    - [ ] 截图放在窗口、手机或者图槽里,线稿放在浅灰图槽里
    - [ ] 没有留下虚线占位,用户还没给的图在交付时说清楚了
    
    ## 深色版(如果做了)
    - [ ] 用 `--theme dark` 渲染,没有手写死的颜色在深色下变得刺眼或者看不见
    - [ ] 代码画的图跟着换了颜色(用 `FigureKit.INK` 和 `onTheme`)
    - [ ] 截图本身是浅色的话,考虑换成深色截图
    
    ## 配图
    - [ ] 每张图单独拿出来也好看:截图清楚、裁得干净,Logo 和图标是矢量的,不糊不虚
    - [ ] 不够好的图已经跟用户说了,没有拿凑数的图交差
    - [ ] 从真数据、真路径、真逻辑画出来的
    - [ ] 只用灰和那一处橙,线宽统一,标注 22 或 24 号
    - [ ] 一组图共用一个母题
    
    ## 文案
    - [ ] 口语,平实,像熟人讲给熟人听
    - [ ] 没有先否后肯、排比、冒号结论、金句结尾、反转
    - [ ] 英文能不用就没用(工具名、文件名除外)
    - [ ] 副标题 20 字以内,卡片里的说明 15 字以内
    
  • ian-xhs-carousel/references/design-system.md6.4 KBacae09d32e24upstream ↗
    # 设计规则
    
    风格是一套产品界面的路子:锌灰中性色,只有一种橙色点题。白页面上垫一块灰底盘,内容做成一张张带分层阴影的卡片、窗口、对话框,像从一个真软件里截出来的。不用渐变,不用装饰图案,层次全靠底色深浅、阴影和字号来分。
    
    所有值都写在 `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` 里加,画法照这套来。
    
  • ian-xhs-carousel/references/figures.md4.6 KBf5d1ada03a15upstream ↗
    # 代码画图
    
    缺图的时候自己画。画出来要像这套界面里本来就有的插图:只用灰,最多带一处橙,几何精确,每根线都有来处。
    
    ## 风格
    
    - **颜色只从变量里取**:用 `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`。
    
    **一套图共用一个母题**:几页的配图尽量是同一个东西换角度、换大小、换裁切。比如封面放全貌,内页拆局部,结尾放成品。看起来是一组,也不用每页重新想。
    
    **第一版往大了画**:代码画的图第一遍几乎总是偏小、四周发空。先按图槽的大小铺满去画,宁可被裁掉一截。
    
    ## 画完自查
    
    - 缩到手机屏幕大小(总览图里那么大),还看得出画的是什么吗
    - 除了那一处橙,有没有混进别的颜色
    - 线宽、字号是不是统一的
    - 切到深色以后,线和字还看得清吗
    
  • ian-xhs-carousel/references/layouts.md9.0 KB7a3d752772f1upstream ↗
    # 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>` 里。同一种扩展用顺了两次以上,就建议用户把它收进版式库。
    
  • ian-xhs-carousel/assets/figure-kit.js3.3 KB1a982de40f4fupstream ↗
    // 代码画图的小工具:颜色、画布准备、箭头、标注、图片采样
    (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 };
    })();
    
  • ian-xhs-carousel/assets/starter.html2.0 KB333a65cfb12eupstream ↗
    <!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>
    
  • ian-xhs-carousel/assets/templates.html23 KBe58f3072e6ccupstream ↗
    <!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>
    
  • ian-xhs-carousel/assets/xhs.css18 KBdada07d4b7e6upstream ↗
    /* 小红书 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); }
    
  • ian-xhs-carousel/assets/xhs.js1.8 KBbdcf4ec39e34upstream ↗
    // 版式库的小脚本:把 <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);
    })();
    
  • ian-xhs-carousel/assets/版式总览-深色.png711 KB782809992aa0upstream ↗

    Contents are not inlined in the catalog. Open the upstream file.

  • ian-xhs-carousel/assets/版式总览.png681 KBc7e05d59b901upstream ↗

    Contents are not inlined in the catalog. Open the upstream file.

  • ian-xhs-carousel/agents/openai.yaml307 B7e84a58ca2faupstream ↗
    interface:
      display_name: "Ian 小红书图文"
      short_description: "把中文内容排成 3:4 小红书轮播图,20 个母版,浅色深色都能出"
      default_prompt: "Use $ian-xhs-carousel to 把这篇内容做成一组小红书图文,先出分镜表。"
    policy:
      allow_implicit_invocation: true
    
  • README.mdupstream ↗

    Contents are not inlined in the catalog. Open the upstream file.

Plain-text copy, no login: /skill-bundles/helloianneo-ian-xhs-carousel-ian-xhs-carousel.md

// REPO STATS

255 stars