# Sakuno UI

> 平面 · 克制 · 二次元 —— 纯 CSS + JS 的静态页面 WebUI 框架
> 名字取自瓜生樱乃：安静、柔软，却让人记得住。

零依赖、无构建步骤。**两个文件**（样式 + 脚本，146 个图标已内置）。v2.5 新增折线/面积图、漏斗图、代码 Diff、终端窗口、多选下拉与虚拟滚动列表，`<link>` + 一个 `<script>` 引入，任何 HTML 静态页面都能直接用。

> 让 Agent 写页面时，先读 [`SKILL.md`](./SKILL.md)：里面有组件选择表、颜色矩阵、常用配方和 29 条避坑清单。

```
sakuno/
├── sakuno.css            # 全部样式（约 7900 行，含注释）
├── sakuno.js             # 交互层 + 图标 sprite，零依赖
├── sakuno.min.css        # 生产版（26.1 KB gzip）
├── sakuno.min.js         # 生产版（54.0 KB gzip，含 146 个图标）
├── index.html            # 文档站 + 图标库 + 全组件演示
├── standalone/index.html # 同一份文档站，CSS / JS / 图标 / 图片全部内联
├── SKILL.md              # 给 Agent 的接入指南
├── llms.txt              # 站点级索引（Agent 抓一个文件就够）
├── docs/sakuno.md        # 整页文档的 Markdown 版（构建产物）
├── favicon.svg
├── assets/sakuno.webp
├── tools/                # 构建脚本（图标提取 / 站点打包）
└── dist/                 # 可直接部署到 Cloudflare Pages 的目录
```

---

## 单文件版

`standalone/index.html` 是自包含版本：`sakuno.min.css`、`sakuno.min.js` 与 `assets/sakuno.webp`（base64）全部写在同一个 HTML 里，**不产生任何外部请求**。双击即可打开，也可以直接发邮件、丢进没有静态服务器的沙箱预览、贴进 Confluence 之类的富文本容器。

- 体积：原始约 650 KB，gzip 后约 165 KB（含全部图标）
- 与多文件版共用同一份 CSS/JS 产物，功能完全一致（模态、下拉、命令面板、主题切换均已在 `file://` 协议下验证通过）

改完源码后不要手工拼装，跑构建脚本即可（会顺带压缩、内联、导出 Markdown、生成 `dist/`）：

```bash
python3 tools/build_site.py
```

脚本做的事：esbuild 压缩 → 把 `SKILL.md` / `llms.txt` 内联进文档站 → 生成 `standalone/index.html` → Playwright 导出 `docs/sakuno.md` → 组装 `dist/`。


---

## 快速开始

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link rel="stylesheet" href="sakuno.css" />
</head>
<body class="sk-scroll">

  <div class="sk-container sk-section">
    <h1 class="sk-h2">你好，<span class="sk-em">樱乃</span></h1>
    <button class="sk-btn sk-btn--primary sk-mt-3">开始使用</button>
  </div>

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

不引 `sakuno.js` 也能用：按钮、卡片、表单、徽章、表格、进度条等纯 CSS 组件照常工作，只有模态、下拉、Toast、命令面板等交互组件需要脚本。

---

## 设计原则

视觉基线直接取自 kungal（KunUI）：**纯色、1px 描边、无渐变、卡片零阴影**。层级不靠投影表达，靠背景明度差与描边完成——页面底色 `#f4f4f7`，卡片纯白，描边 `#dadae5`，三者差值很小，但足够分辨。

| 维度 | 取值 |
| --- | --- |
| 主色 | `#1271ea` 亮蓝（纯色填充，`background-image: none`） |
| 点缀 | `#ff94da` 樱粉，高明度，只做情绪强调 |
| 色板 | 7 组语义色 × 11 档色阶（50–950），由 OKLCH 感知均匀生成 |
| 中性 | 带紫调的灰阶：`#f4f4f7` → `#ececf7` → `#dadae5` |
| 描边 | `1px solid #dadae5`，与背景的差值控制在极小区间 |
| 圆角 | 控件 12 / 卡片 16 / 胶囊全圆，比常见框架大一档 |
| 阴影 | **卡片、按钮无阴影**；只有模态 / 菜单 / 下拉用 `--sk-shadow-3` |
| 动效 | `cubic-bezier(.2,0,0,1)`，150 / 250 / 350ms，只跑 transform 与 opacity |
| 字体 | 系统字体栈，不加载外部字体 |

二次元的柔软感来自三处：略大的圆角、樱粉色的克制使用、以及排版里的留白，而不是光效。

---

## 图标

框架内**不使用任何 emoji 作为图标**。所有图标来自 Font Awesome Pro，构建时从字体文件里提取轮廓，
把轮廓编译进 `sakuno.js` 的 `SK_PATHS`，运行时注入内联 SVG sprite 到 `#sk-sprite`，页面里只写一行：

```html
<i data-sk-icon="bell"></i>
<svg class="sk-i"><use href="#i-bell"></use></svg>  <!-- 等价写法 -->
```

- 尺寸由 `font-size` 决定（1em 高），颜色继承 `currentColor`
- 常用尺寸：`.sk-i--sm` / `.sk-i--lg` / `.sk-i--xl`
- 146 个图标覆盖常见 UI 场景；增删改在 `tools/icon-list.json` 里写图标名，跑 `python3 tools/gen_icons.py`
- 图标全清单由 `Sakuno.icons`（数组）暴露，JS 可直接枚举；文档站的图标库网格也是从它动态渲染的，
  自建图标选择器可参考：`Sakuno.icons.forEach(n => …)` + MutationObserver 自动替换新增图标
- 动态插入的 `<i data-sk-icon>` 会被 MutationObserver 自动替换成 SVG，无需手动重挂

组件内部那些「画出来的」图标（复选勾、下拉箭头、排序箭头、面包屑分隔）用的是同一套轮廓，
以 CSS `mask-image` 内联 data URI 的形式存在，所以纯 CSS 组件不依赖 JS 也有正确图标。

---

## 构建与部署

```bash
python3 tools/gen_icons.py     # 从字体提取轮廓 → sakuno.js 内置数据 + 图标元数据
python3 tools/build_site.py    # 压缩 + 生成单文件版 + 导出 Markdown + 组装 dist/
```

`dist/` 就是部署目录：直接拖到 Cloudflare Pages，构建命令留空，输出目录选根目录。
`sakuno-cf-pages.zip` 是 dist 的**完整镜像**（根目录结构，无 `dist/` 前缀），两者内容一致，任选其一上传。
里面除了 `index.html` + 两个 min 文件，还带 `SKILL.md`、`llms.txt`、`README.md`、
`docs/sakuno.md`、`standalone/index.html`、源码、`assets/sakuno.webp`、`favicon.svg`，
以及 `_headers`（长缓存 / 安全头 / 文档 MIME + CORS）、`404.html`、`robots.txt`、`sitemap.xml`。

> 部署后建议逐条验证 `sitemap.xml` 里的 URL 均为 200。构建脚本已内置硬校验：
> 缺少 Agent 接入文件、或 sitemap 声明了未打包的路径，会直接构建失败。

---

## 颜色 × 变体矩阵

每个组件只需要两个属性就能组合出完整形态，不必记几十个类名：

```html
<button class="sk-btn" data-color="success" data-variant="flat">已通过</button>
<span class="sk-chip" data-color="secondary">樱乃</span>
<div class="sk-tabs sk-tabs--solid" data-sk-tabs data-color="danger">…</div>
```

- `data-color`：`primary` / `secondary` / `success` / `warning` / `danger` / `info` / `default`
- `data-variant`：`solid` 实心 · `flat` 淡底（主色 20% 透明）· `bordered` 描边 · `light` 仅文字 · `ghost` 幽灵

淡色底统一用 `color-mix(in oklab, var(--色) 20%, transparent)` 生成，所以**深色模式不需要另写一套色板**，同一行 CSS 自动适配。

旧的单类写法（`sk-btn--primary`、`sk-badge--success`、`sk-tag--accent`…）全部保留，互不冲突。

---

## 主题定制

```css
:root {
  --sk-primary: #1271ea;   /* 主色：改这一个，按钮/链接/焦点环全跟着变 */
  --sk-accent:  #ff94da;   /* 点缀：樱粉 */
  --sk-primary-text: #004da9; /* 淡色底上的深档文字 */
  --sk-bg: #f4f4f7;        /* 页面底色 */
}
```

常用变量：

| 变量 | 说明 |
| --- | --- |
| `--sk-primary` / `--sk-primary-hover` | 主色与其 hover 值（纯色，请勿改成渐变） |
| `--sk-primary-soft` / `--sk-primary-line` | 主色的淡底与浅描边 |
| `--sk-accent` / `--sk-accent-soft` / `--sk-accent-ink` | 点缀色的三级用法 |
| `--sk-bg` `--sk-surface` `--sk-surface-2` `--sk-surface-3` | 四级背景 |
| `--sk-border` / `--sk-border-strong` | 描边（默认值刻意接近背景） |
| `--sk-text` / `--sk-text-soft` / `--sk-muted` | 三级文字 |
| `--sk-success` `--sk-warning` `--sk-danger` `--sk-info` | 语义色，各带 `-soft` 淡底 |
| `--sk-r-xs` … `--sk-r-full` | 圆角梯度 |
| `--sk-shadow-1/2/3` | 阴影（仅浮层使用） |
| `--sk-ease` / `--sk-dur-1..3` | 动效曲线与时长 |

### 深色模式

```html
<html data-theme="dark">   <!-- 强制深色 -->
<html data-theme="light">  <!-- 强制浅色 -->
<html>                     <!-- 跟随系统 -->
```

```html
<button data-sk-theme-toggle aria-label="切换主题">…</button>

<script>
  Sakuno.theme.toggle();
  Sakuno.theme.set("dark");   // 写入 localStorage
  Sakuno.theme.set("auto");   // 跟随系统
</script>
```

**避免首屏闪烁**：在 `<head>` 中、样式表之前插入

```html
<script>
  try {
    var t = localStorage.getItem("sakuno-theme");
    if (t) document.documentElement.setAttribute("data-theme", t);
  } catch (e) {}
</script>
```

### 与已有样式共存

组件全部位于 `@layer sk.components` 内且大量使用 `:where()`，特异性极低。你的选择器天然优先：

```css
.sk-btn { border-radius: 4px; }  /* 一定生效，无需 !important */
```

---

## 组件清单（110+）

| 分类 | 组件 |
| --- | --- |
| 基础 | 按钮（5 变体 × 7 色 × 6 尺寸 / loading / 图标）、按钮组、分段控制器、开关组、**胶囊 Chip**（5 变体 × 7 色 × 3 尺寸 / 可关闭 / 带头像）、徽章（8 变体）、计数角标（包裹任意元素 / 圆点）、可关闭标签、头像（尺寸 / 色调 / 状态点 / 头像组）、人员信息条、用户胶囊、文字链接 |
| 表单 | 输入框、文本域、输入组、自定义下拉 Select、**可搜索组合框 Combobox**、**级联选择 Cascader**、**双端区间滑块 Range**、**密码强度 Password**、复选框、单选框、卡片式选择、开关、滑块、评分、数字步进器、验证码 OTP、标签输入、上传区（点击 / 拖拽）、字段容器（标签 / 提示 / 错误）、**颜色选择 Color**（色板 / 取色器 / HEX）、**时间选择 Time**（时分两列 / 步进）、**树选择 TreeSelect**（单选 / 多选 / 搜索）、**穿梭框 Transfer**（双列检索 / 全选）、**滑块验证 Verify**（拖拽 / 成功态） |
| 展示 | 卡片（基础 / 柔和 / 强调 / 横向 / 封面 / 7 色淡色变体）、统计卡、迷你柱状图、表格（排序 / 全选 / 批量条）、**数据表工具条与迷你分页**、**热力图**、**文件列表**、列表、描述列表、进度条（线性 / 分段 / 不定长）、环形进度、骨架屏、加载器（圆环 / 樱花环 / 三点）、空状态、结果页、代码块、键帽、引用、**仪表盘 Gauge**（单值 / 阈值变色）、**环形图 Donut**、**迷你折线 Sparkline**、**评论区 Comments**、**对话流 Chat**（左右分边 / 打字指示）、**JSON 查看器**（折叠 / 类型配色 / 工具栏）、**代码高亮**（HTML/CSS/JS/JSON/Shell 自动嗅探，亮暗主题适配）、**价格卡 Plan**（推荐档）、**倒计时 Countdown**、**看板 Board**（原生拖拽）、**图片灯箱 Lightbox**、**水印 Watermark**、**柱状图 BarChart**（data 驱动 / 高亮）、**雷达图 Radar**、**甘特图 Gantt**（纯 CSS 变量）、**评分分布 RateBars**、**数字滚动 CountUp**、**图片对比 Compare**、**瀑布流 Masonry**、**签名板 Signature**（canvas / 导出 PNG） |
| 导航 | 标签页（下划线 / 实心 / 浅色 / 描边 / 胶囊 五变体 × 7 色）、折叠面板、面包屑、分页、步骤条、时间线、树形、轮播（圆点 / 箭头 / 自动播放 / 键盘）、侧边导航、底部标签栏、吸顶导航、回到顶部、滚动进度条、**可拖拽分栏** |
| 浮层 | 模态框、抽屉（左 / 右）、下拉菜单、气泡卡片、CSS 提示气泡、右键菜单、Toast、命令面板、Promise 确认框、日历、**通知中心**、**新手引导 Tour**、**气泡确认 Popconfirm**、**底部动作面板 Sheet**（拖拽下滑关闭） |
| 反馈 | 提示条（5 语气 / 描边变体）、横幅 Banner |
| 其他 | 拖拽排序列表、三态主题开关、点阵背景、网格背景、樱花形状、滚动进场、**跑马灯 Marquee**（无缝循环）、**内联编辑 Editable**、**复制字段 CopyField**、**可拖拽分栏 Splitter** |

所有组件的可交互示例在 `index.html`。

---

## JS API

```js
/* 提示 */
Sakuno.toast("已保存");
Sakuno.toast.success("保存成功", { title: "已完成", duration: 3000 });
Sakuno.toast.info("常驻提示", { duration: 0 });

/* 确认框 → Promise<boolean> */
const ok = await Sakuno.confirm({
  title: "删除这条记录？",
  message: "删除后无法恢复。",
  confirmText: "删除",
  danger: true,
});

/* 浮层 */
Sakuno.modal("#myModal");
Sakuno.close("#myModal");

/* 新手引导 */
Sakuno.tour([
  { el: "#search", title: "搜索", text: "支持 ⌘K 唤起。" },
  { el: "#table", title: "数据表", text: "表头可排序。" },
], { onEnd: (done) => done && Sakuno.toast.success("完成") });

/* 主题三态 */
Sakuno.theme.set("auto");   // light | dark | auto

/* 命令面板（自动绑定 ⌘/Ctrl + K） */
Sakuno.command({
  placeholder: "搜索…",
  items: [{ group: "动作", label: "新建文件", hint: "⌘N", action: () => {} }],
}).open();

/* 主题 */
Sakuno.theme.toggle();

/* 动态插入 DOM 后重新绑定 */
Sakuno.init(document.getElementById("app"));

/* 图标 */
Sakuno.icon.check;                    // 常用图标的 SVG 字符串
Sakuno.iconEl("house", "sk-i--lg");   // 拿一个 SVG 节点
Sakuno.icons;                         // 146 个可用图标名

/* 评分 */
Sakuno.setRating(document.querySelector(".sk-rating"), 5);

/* 灯箱 */
Sakuno.lightbox("assets/sakuno.webp", "说明文字");
```

### 事件

| 事件 | 触发时机 |
| --- | --- |
| `sakuno:select` | 自定义下拉选中，`detail.value` |
| `sakuno:date` | 日历选择日期，`detail.value`（`YYYY-MM-DD`） |
| `sakuno:complete` | 验证码填满，`detail.value` |
| `sakuno:rate` | 评分变化，`detail.value` |
| `sakuno:tab` | 标签页切换 |
| `sakuno:theme` / `sakuno:ready` | 主题变化 / 初始化完成 |
| `sakuno:open` / `sakuno:close` | 浮层打开 / 关闭 |
| `sakuno:bulk` | 表格批量选择，`detail.count` |
| `sakuno:sort` | 表格排序 / 列表拖拽排序，`detail.index/dir` 或 `detail.order` |
| `sakuno:tagadd` / `sakuno:tagremove` | 标签增删，`detail.count` |
| `sakuno:range` | 区间滑块，`detail.min/max` |
| `sakuno:cascader` | 级联选择，`detail.value/path` |
| `sakuno:slide` | 轮播切换，`detail.index/total` |
| `sakuno:split` | 分栏拖拽，`detail.value` |
| `sakuno:page` | 分页切换，`detail.page` |
| `sakuno:verify` | 滑块验证通过 / 失败，`detail.ok` |
| `sakuno:board` | 看板卡片换列，`detail.card/column/count` |
| `sakuno:countdown` | 倒计时归零，`detail.done` |
| `sakuno:lightbox` | 灯箱打开，`detail.src` |

### 数据属性（不写 JS 也能用）

| 属性 | 作用 |
| --- | --- |
| `data-sk-open="#id"` | 打开模态框 / 抽屉 |
| `data-sk-dismiss` | 关闭所在浮层 |
| `data-sk-toggle` | 切换同容器内的 `.sk-menu` / `.sk-popover` |
| `data-sk-theme-toggle` | 切换深浅色 |
| `data-sk-tooltip="文案"` + `data-sk-tip="bottom\|left\|right"` | 纯 CSS 提示 |
| `data-sk-copy` / `data-sk-copy="#el"` | 复制文本（空值则复制同容器 `pre`） |
| `data-sk-json` | 把 JSON 渲染成可折叠树，自动注入工具栏（复制 / 全部展开 / 全部折叠） |
| `data-sk-highlight="html\|css\|js\|json\|shell\|auto"` | 代码块自动语法高亮（零依赖 tokenizer，语言可自动嗅探） |
| `data-sk-verify` | 滑块验证 |
| `data-sk-countdown` + `data-deadline` | 倒计时 |
| `data-sk-lightbox="组名"` | 图片灯箱 |
| `data-sk-rating` 不适用，用 `sk-rating` + `data-value` | 评分 |
| `data-sk-close` | `data-sk-dismiss` 的等价别名 |
| `data-sk-tabs` + `data-sk-tab` / `data-sk-panel` | 标签页 |
| `data-sk-accordion`（可加 `data-sk-multiple`） | 折叠面板 |
| `data-sk-select` + `.sk-select__option` | 自定义下拉 |
| `data-sk-calendar` + `data-sk-cal-prev/next` | 日历 |
| `data-sk-stepper` + `data-sk-step="1\|-1"` | 步进器 |
| `data-sk-dropzone` | 上传区（点击 / 拖拽） |
| `data-sk-context="#tpl"` | 绑定右键菜单模板 |
| `data-sk-sortable` + `th[data-sort]` | 表格排序 |
| `data-sk-check-all` / `data-sk-row-check` + `[data-sk-bulk]` | 表格批量选择 |
| `data-sk-search-live="#table"` | 输入即过滤行（行加 `data-sk-filter`） |
| `data-sk-combo` / `data-sk-cascader` / `data-sk-range` / `data-sk-password` | 进阶表单组件 |
| `data-sk-sortable`（列表） / `data-sk-split` / `data-sk-notify` / `data-sk-heatmap` | 拖拽排序 / 分栏 / 通知 / 热力图 |
| `data-sk-cal-today` | 日历回到今天 |
| `data-sk-theme-set="light\|dark\|auto"` | 三态主题按钮 |
| `data-sk-reveal` | 滚动进场 |
| `data-sk-loading` | 按钮载入态 |

---

## 无障碍

- 键盘可达：`:focus-visible` 统一焦点环，Tab / Shift+Tab 正常穿行
- 浮层：锁滚动、焦点移入、Tab 内循环、Esc 关闭、关闭后焦点归位
- 语义：`role="dialog"` `aria-modal` `aria-selected` `aria-expanded` `aria-current` `aria-live`
- 标签页 ← → 切换，菜单 ↑ ↓ 导航，验证码自动跳格与退格回退
- `prefers-reduced-motion` 下动画归零，`prefers-contrast: more` 下加深描边与次要文字

---

## 性能

- 零依赖、零构建；**gzip 后 CSS 26.1 KB + JS 54.0 KB ≈ 80.1 KB**，其中 25 KB 是 146 个图标
- 事件全部委托到 `document`，全局只有一个滚动监听 + `rAF` 节流
- 动画只作用于 `transform` / `opacity`，不触发布局
- 进场动画用 `IntersectionObserver`，触发即 `unobserve`
- 图标是内联 SVG 字符串，运行时不发任何网络请求
- 没有渐变 → 没有大面积重绘；没有 `backdrop-filter` → 滚动不会被合成层拖慢

## 浏览器支持

Chrome / Edge 111+、Safari 16.4+、Firefox 113+（依赖 `@layer`、`color-mix` 与现代选择器）。

## License

MIT
