# 赵子民设计系统 · zzm v4 · 全文 > 赵子民全部产品共用的界面设计系统:纸 · 墨 · 朱三色、8pt 网格、五层光照的微拟物按键。每个页面都有 Markdown 版本,样张的 HTML 是构建时由 React 源码渲染出的真实标记,可原样粘贴。同步自 edu.zhaozimin v23.5.1(086937d)。 站点 v0.1.1 · 2026-09-23 · 共 38 页。页与页之间以一行 ======== 分隔。 --- title: 一块材料切出来的界面 url: https://ui.zhaozimin.com/ markdown: https://ui.zhaozimin.com/index.md group: 开始 origin: 规范文档 synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 一块材料切出来的界面 > 赵子民全部产品共用的设计系统。三种颜色、一把 8pt 梯子、一个「光从正上方来」的光照模型——每个令牌和组件都同时有人读版和机读版。 ## 给 Agent 的一行 在 Cursor、Claude Code 或任何能联网的工具里,把下面这个地址交给它,说「按这个设计系统做」。它会从目录读到每一页的 Markdown 版本,包括令牌、铁律和可以直接粘贴的代码。 Agent 入口: ```text https://ui.zhaozimin.com/llms.txt ``` 写静态页时,一行引入全部质感,然后在根节点加上作用域类名: 静态页: ```html ``` ## 从这里开始 ## 这套系统相信什么 - **一屏一枚朱印。** 朱砂红只出现一次,永远标「你在这里 / 下一步」。红是导航信号,不是装饰。 - **圆是徽章,方角是控件。** 文字按钮一律圆角矩形;999 胶囊只留给不可点的徽章。看形状就知道能不能点。 - **一个光源。** 光从正上方来,每个控件都是一枚真实的键。阴影只从质感类取,组件里不许各写各的光。 - **一把梯子。** 间距、高度、行高、圆角都从 4 的倍数里取,不发明数字。整站看起来才像一块材料切出来的。 - **留白即上手。** 能收进次级入口的,绝不摆在主动线上。 完整说明见[设计原则](/foundations/principles)。 ## 如果你是 Agent 1. 先读 [llms.txt](https://ui.zhaozimin.com/llms.txt) 拿目录,需要全文时读 [llms-full.txt](https://ui.zhaozimin.com/llms-full.txt)。 2. 引入 [zzm.css](https://ui.zhaozimin.com/zzm.css),根节点加 `class="zzm-v4"`。不加这个类,样式一点都不生效。 3. 按钮用 `.zzm-btn` 家族,卡片用 `.zzm-card` / `.zzm-cardlink`,输入框用 `.zzm-input`;颜色只用 `var(--zzm-*)`。 4. 不要自己写 `box-shadow`,不要把按钮做成胶囊,不要引入网络字体。 5. 每个组件页都有 `.md` 版本,里面的 HTML 是构建时渲染出来的真实标记,可以原样粘贴。 本站同步自 edu.zhaozimin v23.5.1(086937d),那是赵子民在线课程站的生产代码。 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* base.css */ .zzm-v4 input.zzm-input:focus, .zzm-v4 textarea.zzm-input:focus { border-color: #161616 !important; } .zzm-v4 .zzm-skel { position: relative; overflow: hidden; background: #EFEFEC; border-radius: 8px; } .zzm-v4 .zzm-skel::after { content: ''; position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .65), transparent); animation: zzmShimmer 1.4s ease infinite; } /* controls.css */ .zzm-v4 .zzm-btn { -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 16px; font-family: inherit; font-size: 13px; font-weight: 600; letter-spacing: .2px; white-space: nowrap; cursor: pointer; user-select: none; border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */ border: 1px solid rgba(0, 0, 0, .14); color: #3F3F3B; background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */ 0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */ 0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */ inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */ inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */ transition: box-shadow .16s ease, transform .16s ease, background .16s ease, border-color .16s ease, filter .16s ease, opacity .16s ease; } .zzm-v4 .zzm-btn:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .18); box-shadow: 0 0 0 4px rgba(255, 255, 255, .8), 0 2px 4px rgba(0, 0, 0, .10), 0 4px 12px rgba(0, 0, 0, .09), inset 0 1px 0 rgba(255, 255, 255, .95), inset 0 -1px 0 rgba(0, 0, 0, .05); } .zzm-v4 .zzm-btn:active { transform: translateY(0); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */ box-shadow: 0 0 0 3px rgba(255, 255, 255, .45), 0 1px 0 rgba(255, 255, 255, .65), inset 0 2px 4px rgba(0, 0, 0, .13), inset 0 1px 2px rgba(0, 0, 0, .09); } .zzm-v4 .zzm-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */ 0 1px 2px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-btn:disabled { cursor: not-allowed; transform: none; } .zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) { color: #A2A29C; background: #F2F2EF; border-color: rgba(0, 0, 0, .08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04); } .zzm-v4 .zzm-btn--ondark:disabled:not(.zzm-btn--busy) { color: rgba(255, 255, 255, .45); background: rgba(255, 255, 255, .08); border-color: rgba(255, 255, 255, .16); box-shadow: none; } .zzm-v4 .zzm-btn--busy { position: relative; overflow: hidden; pointer-events: none; opacity: .92; filter: saturate(.72); transform: none; } .zzm-v4 .zzm-btn--busy::before { content: ""; position: relative; z-index: 1; /* 旋转环压在反光层之上 */ width: 12px; height: 12px; flex: none; border-radius: 50%; border: 2px solid currentColor; border-top-color: transparent; opacity: .7; animation: zzmBtnSpin .7s linear infinite; } .zzm-v4 .zzm-btn--busy::after { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; transform: translateX(-180%); background: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, .45) 50%, transparent 60%); animation: zzmPillShine 4.4s ease-in-out infinite; } .zzm-v4 .zzm-btn--primary { color: #FFFFFF; border-color: rgba(0, 0, 0, .85); background: linear-gradient(180deg, #3A3A36 0%, #1D1D1B 58%, #141412 100%); box-shadow: 0 0 0 3px rgba(22, 22, 22, .06), 0 1px 2px rgba(0, 0, 0, .18), 0 3px 10px rgba(22, 22, 22, .24), inset 0 1px 0 rgba(255, 255, 255, .20), inset 0 -1px 0 rgba(0, 0, 0, .38); } .zzm-v4 .zzm-btn--primary:hover { filter: brightness(1.12); border-color: rgba(0, 0, 0, .85); box-shadow: 0 0 0 4px rgba(22, 22, 22, .09), 0 2px 4px rgba(0, 0, 0, .20), 0 5px 14px rgba(22, 22, 22, .28), inset 0 1px 0 rgba(255, 255, 255, .24), inset 0 -1px 0 rgba(0, 0, 0, .38); } .zzm-v4 .zzm-btn--primary:active { filter: none; background: linear-gradient(180deg, #101010 0%, #22221F 100%); box-shadow: 0 0 0 3px rgba(22, 22, 22, .06), inset 0 2px 5px rgba(0, 0, 0, .55), inset 0 1px 2px rgba(0, 0, 0, .4); } .zzm-v4 .zzm-btn--accent { color: #FFFFFF; border-color: rgba(122, 42, 29, .9); background: linear-gradient(180deg, #C25743 0%, #A6402F 56%, #99392A 100%); box-shadow: 0 0 0 3px rgba(166, 64, 47, .10), /* 朱色反光光晕 */ 0 1px 2px rgba(0, 0, 0, .14), 0 3px 10px rgba(166, 64, 47, .28), inset 0 1px 0 rgba(255, 255, 255, .32), inset 0 -1px 0 rgba(0, 0, 0, .20); } .zzm-v4 .zzm-btn--accent:hover { filter: brightness(1.06); border-color: rgba(122, 42, 29, .9); box-shadow: 0 0 0 4px rgba(166, 64, 47, .16), 0 2px 4px rgba(0, 0, 0, .16), 0 5px 14px rgba(166, 64, 47, .34), inset 0 1px 0 rgba(255, 255, 255, .36), inset 0 -1px 0 rgba(0, 0, 0, .20); } .zzm-v4 .zzm-btn--accent:active { filter: none; background: linear-gradient(180deg, #8E3425 0%, #A03D2C 100%); box-shadow: 0 0 0 3px rgba(166, 64, 47, .10), inset 0 2px 5px rgba(0, 0, 0, .34), inset 0 1px 2px rgba(0, 0, 0, .24); } .zzm-v4 .zzm-btn--ghost { border-color: transparent; background: transparent; box-shadow: none; color: #4A4A46; } .zzm-v4 .zzm-btn--ghost:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .14); background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), 0 1px 2px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-btn--ghost:active { transform: translateY(0); border-color: rgba(0, 0, 0, .14); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); box-shadow: inset 0 2px 4px rgba(0, 0, 0, .12); } .zzm-v4 .zzm-video-play { display: grid; place-items: center; width: 72px; height: 72px; padding: 0; border: 1px solid rgba(0, 0, 0, .16); border-radius: 50%; cursor: pointer; color: #1D1D1B; background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 4px rgba(255, 255, 255, .24), 0 2px 4px rgba(0, 0, 0, .16), 0 6px 16px rgba(0, 0, 0, .22), inset 0 1px 0 rgba(255, 255, 255, .96), inset 0 -1px 0 rgba(0, 0, 0, .08); transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; } .zzm-v4 .zzm-video-play:hover { border-color: rgba(0, 0, 0, .3); box-shadow: 0 0 0 5px rgba(255, 255, 255, .3), 0 3px 6px rgba(0, 0, 0, .18), 0 9px 22px rgba(0, 0, 0, .28), inset 0 1px 0 rgba(255, 255, 255, .98), inset 0 -1px 0 rgba(0, 0, 0, .08); } .zzm-v4 .zzm-video-play:active { transform: scale(.98); background: linear-gradient(180deg, #E9E9E6 0%, #F4F4F1 100%); box-shadow: 0 0 0 4px rgba(255, 255, 255, .24), inset 0 2px 5px rgba(0, 0, 0, .16), inset 0 1px 2px rgba(0, 0, 0, .10); } .zzm-v4 .zzm-video-play:focus-visible { outline: none; box-shadow: 0 0 0 4px rgba(166, 64, 47, .28), 0 2px 4px rgba(0, 0, 0, .16), 0 6px 16px rgba(0, 0, 0, .22), inset 0 1px 0 rgba(255, 255, 255, .96); } .zzm-v4 .zzm-video-play__icon { width: 24px; height: 24px; margin-left: 4px; fill: currentColor; } .zzm-v4 .zzm-btn--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; } .zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; } .zzm-v4 .zzm-cardlink { background: #FFFFFF; border: 1px solid rgba(0, 0, 0, .09); border-radius: 16px; padding: 12px; cursor: pointer; box-shadow: 0 1px 2px rgba(0, 0, 0, .04); transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; } .zzm-v4 .zzm-cardlink:hover { transform: translateY(-3px); border-color: rgba(0, 0, 0, .3); box-shadow: 0 10px 30px rgba(0, 0, 0, .09), 0 1px 2px rgba(0, 0, 0, .04); } .zzm-v4 .zzm-card { background: #FFFFFF; border: 1px solid rgba(0, 0, 0, .09); border-radius: 16px; box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 6px 18px rgba(0, 0, 0, .05), inset 0 1px 0 rgba(255, 255, 255, .85); } .zzm-v4 input.zzm-input, .zzm-v4 textarea.zzm-input { background: #FDFDFC; box-shadow: inset 0 1.5px 3px rgba(0, 0, 0, .06), inset 0 1px 1px rgba(0, 0, 0, .04); } .zzm-v4 input.zzm-input:focus, .zzm-v4 textarea.zzm-input:focus { box-shadow: inset 0 1.5px 3px rgba(0, 0, 0, .06), 0 0 0 3px rgba(22, 22, 22, .07); /* 聚焦光晕 */ } .zzm-v4 .zzm-pill-shine { position: relative; overflow: hidden; } .zzm-v4 .zzm-pill-shine::after { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; transform: translateX(-180%); background: linear-gradient(105deg, transparent 34%, rgba(255, 255, 255, .85) 50%, transparent 66%); animation: zzmPillShine 4.6s ease-in-out infinite; animation-delay: var(--zzm-shine-delay, 0s); } /* keyframes */ @keyframes zzmShimmer { 100% { transform: translateX(100%) } } @keyframes zzmBtnSpin { to { transform: rotate(360deg) } } @keyframes zzmPillShine { 0% { transform: translateX(-180%) } 14% { transform: translateX(180%) } 100% { transform: translateX(180%) } } ``` --- 上一页:无 · 下一页:[快速接入](https://ui.zhaozimin.com/start/quickstart.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 快速接入 Quickstart url: https://ui.zhaozimin.com/start/quickstart/ markdown: https://ui.zhaozimin.com/start/quickstart.md group: 开始 origin: 规范文档 synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 快速接入 Quickstart > 三种接法:只要样式(任何技术栈)、直接用 React 组件、或者把地址交给 Agent。先读完这页的四条前提,能省掉九成的对不上。 ## 先读:四条前提 - ✓ 要:根节点加 `class="zzm-v4"`——全部样式只在这个作用域里生效 - ✓ 要:用系统字体,不引入网络字体;中文靠 PingFang SC,Windows 落到微软雅黑 - ✓ 要:只有浅色:v4 已废除暗色分支,界面永远是纸白底 - ✓ 要:不需要 Tailwind:zzm.css 自带组件依赖的那部分基线(box-sizing、零外距等) ## 一、只要样式(任何技术栈) 引入一个 CSS 文件,用类名写 HTML。Vue、Svelte、纯静态页、服务端模板都一样。 index.html: ```html ``` 不方便给根节点加类名时,改用 `https://ui.zhaozimin.com/zzm.global.css`:同样的规则,去掉了 `.zzm-v4` 前缀,对整页生效。 ### 样张 · 一张用类名拼出来的表单卡 看「HTML」页签——那就是你要写的全部 React: ```jsx export function StaticPage() { return (
订阅更新
新课上线时发一封邮件,不发别的。
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
订阅更新
新课上线时发一封邮件,不发别的。
``` ## 二、用 React 组件 组件源码按 edu 的目录结构原样放在本站 `/src/system/` 下,从提炼层来的组件在 `/src/extracted/`。把需要的文件复制进项目,保持相对路径不变即可 import。 - 运行时:React 19。 - 只有 `MarkdownNotes`(笔记标注块)额外需要 `lucide-react` 和 `framer-motion`。 - `LessonCard` 会 import 一个播放预取函数,在设计系统里用空实现代替(见课节卡页)。 - 样式仍然要引入 zzm.css,并在外层加 `zzm-v4`。 App.jsx: ```jsx import './zzm.css' import { Btn } from './components/v4/Button.jsx' export default function App() { return (
墨键 · 主操作
) } ``` | 要什么 | 去哪拿 | |---|---| | 全部样式 | `/zzm.css`(带作用域)或 `/zzm.global.css`(不带) | | 令牌 | `/tokens.json`(W3C 设计令牌格式),或直接用 CSS 变量 `var(--zzm-*)` | | 一份完整规范 | `/DESIGN.md` | | 组件源码 | `/src/system/components/v4/*.jsx`、`/src/extracted/*.jsx` | | 某个组件的规则 + 代码 | 组件页右上「复制给 Agent」,或直接读 `/components/<名字>.md` | ## 三、交给 Agent 把下面这段话贴给你的编程助手。它会自己去读目录和需要的页面。 给 Agent 的提示词: ```text 请按「赵子民设计系统 v4」实现这个界面: - 规范与目录:https://ui.zhaozimin.com/llms.txt(先读),完整规范 https://ui.zhaozimin.com/DESIGN.md - 样式:引入 https://ui.zhaozimin.com/zzm.css,根节点加 class="zzm-v4" - 颜色只用 var(--zzm-*);间距、高度、圆角只取 4 的倍数 - 按钮用 .zzm-btn 家族,圆角矩形,禁胶囊;阴影只从质感类取,不要自己写 box-shadow - 一屏只用一次朱砂红(--zzm-accent),标「下一步」 - 需要某个组件时,读 https://ui.zhaozimin.com/components/<名字>.md,里面的 HTML 可以原样粘贴 ``` ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* base.css */ .zzm-v4 input.zzm-input:focus, .zzm-v4 textarea.zzm-input:focus { border-color: #161616 !important; } /* controls.css */ .zzm-v4 .zzm-btn { -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 16px; font-family: inherit; font-size: 13px; font-weight: 600; letter-spacing: .2px; white-space: nowrap; cursor: pointer; user-select: none; border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */ border: 1px solid rgba(0, 0, 0, .14); color: #3F3F3B; background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */ 0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */ 0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */ inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */ inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */ transition: box-shadow .16s ease, transform .16s ease, background .16s ease, border-color .16s ease, filter .16s ease, opacity .16s ease; } .zzm-v4 .zzm-btn:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .18); box-shadow: 0 0 0 4px rgba(255, 255, 255, .8), 0 2px 4px rgba(0, 0, 0, .10), 0 4px 12px rgba(0, 0, 0, .09), inset 0 1px 0 rgba(255, 255, 255, .95), inset 0 -1px 0 rgba(0, 0, 0, .05); } .zzm-v4 .zzm-btn:active { transform: translateY(0); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */ box-shadow: 0 0 0 3px rgba(255, 255, 255, .45), 0 1px 0 rgba(255, 255, 255, .65), inset 0 2px 4px rgba(0, 0, 0, .13), inset 0 1px 2px rgba(0, 0, 0, .09); } .zzm-v4 .zzm-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */ 0 1px 2px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-btn:disabled { cursor: not-allowed; transform: none; } .zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) { color: #A2A29C; background: #F2F2EF; border-color: rgba(0, 0, 0, .08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04); } .zzm-v4 .zzm-btn--primary { color: #FFFFFF; border-color: rgba(0, 0, 0, .85); background: linear-gradient(180deg, #3A3A36 0%, #1D1D1B 58%, #141412 100%); box-shadow: 0 0 0 3px rgba(22, 22, 22, .06), 0 1px 2px rgba(0, 0, 0, .18), 0 3px 10px rgba(22, 22, 22, .24), inset 0 1px 0 rgba(255, 255, 255, .20), inset 0 -1px 0 rgba(0, 0, 0, .38); } .zzm-v4 .zzm-btn--primary:hover { filter: brightness(1.12); border-color: rgba(0, 0, 0, .85); box-shadow: 0 0 0 4px rgba(22, 22, 22, .09), 0 2px 4px rgba(0, 0, 0, .20), 0 5px 14px rgba(22, 22, 22, .28), inset 0 1px 0 rgba(255, 255, 255, .24), inset 0 -1px 0 rgba(0, 0, 0, .38); } .zzm-v4 .zzm-btn--primary:active { filter: none; background: linear-gradient(180deg, #101010 0%, #22221F 100%); box-shadow: 0 0 0 3px rgba(22, 22, 22, .06), inset 0 2px 5px rgba(0, 0, 0, .55), inset 0 1px 2px rgba(0, 0, 0, .4); } .zzm-v4 .zzm-btn--ghost { border-color: transparent; background: transparent; box-shadow: none; color: #4A4A46; } .zzm-v4 .zzm-btn--ghost:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .14); background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), 0 1px 2px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-btn--ghost:active { transform: translateY(0); border-color: rgba(0, 0, 0, .14); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); box-shadow: inset 0 2px 4px rgba(0, 0, 0, .12); } .zzm-v4 .zzm-card { background: #FFFFFF; border: 1px solid rgba(0, 0, 0, .09); border-radius: 16px; box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 6px 18px rgba(0, 0, 0, .05), inset 0 1px 0 rgba(255, 255, 255, .85); } .zzm-v4 input.zzm-input, .zzm-v4 textarea.zzm-input { background: #FDFDFC; box-shadow: inset 0 1.5px 3px rgba(0, 0, 0, .06), inset 0 1px 1px rgba(0, 0, 0, .04); } .zzm-v4 input.zzm-input:focus, .zzm-v4 textarea.zzm-input:focus { box-shadow: inset 0 1.5px 3px rgba(0, 0, 0, .06), 0 0 0 3px rgba(22, 22, 22, .07); /* 聚焦光晕 */ } ``` --- 上一页:[一块材料切出来的界面](https://ui.zhaozimin.com/index.md) · 下一页:[给 Agent 用](https://ui.zhaozimin.com/start/for-agents.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 给 Agent 用 For Agents url: https://ui.zhaozimin.com/start/for-agents/ markdown: https://ui.zhaozimin.com/start/for-agents.md group: 开始 origin: 规范文档 synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 给 Agent 用 For Agents > 这个站的每一页都有机读版本。本页说明入口在哪、按什么顺序读、Markdown 头里的字段是什么意思,以及复刻完怎么自检。 ## 入口 | 地址 | 是什么 | 什么时候用 | |---|---|---| | `/llms.txt` | 全站目录,按 llms.txt 约定书写,每条链接指向对应页面的 .md | 第一站 | | `/llms-full.txt` | 全站所有页面的 Markdown 拼成一个文件 | 想一次读完 | | `/DESIGN.md` | 单文件规范:YAML 头放令牌与组件参数,正文放原则与铁律 | 只能读一个文件时 | | `/tokens.json` | W3C 设计令牌(DTCG)格式,数值直接读自 tokens.css | 导入设计工具或生成主题 | | `/zzm.css` | 全部样式合成一个文件(.zzm-v4 作用域) | 静态页、任何技术栈 | | `/zzm.global.css` | 同上,去掉作用域前缀 | 不能给根节点加类名时 | | `/<页面>.md` | 任一页面的 Markdown 版本 | 只要某个组件 | | `/src/...` | 同步来的源码原文,URL 路径就是仓库路径 | 要 React 源码时 | 每个 HTML 页面的 `` 里都有 `` 指向自己的 .md。页面内容在构建时就写进了 HTML,不执行 JS 也能读全。`robots.txt` 对所有爬虫放行。 ## 推荐阅读顺序 1. `/llms.txt`:知道有哪些页面。 2. `/foundations/color.md`、`/foundations/spacing.md`、`/foundations/elevation.md`:颜色、尺寸、光影三件事决定八成观感。 3. 需要哪个组件读哪个组件的 .md。样张的 HTML 是构建时由 React 渲染出来的真实标记,粘贴即可。 4. 每页末尾「本页用到的 CSS」是按类名从源文件原样裁出的规则;引入完整的 zzm.css 就不必逐页拼。 ## Markdown 头部字段 /components/button.md 的头部: ```yaml --- title: 按钮 Btn url: https://ui.zhaozimin.com/components/button markdown: https://ui.zhaozimin.com/components/button.md group: 组件 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/components/v4/Button.jsx # 源码原文地址 synced: edu v23.5.1 (086937d) · 同步日期 scope: .zzm-v4 # 根节点必须带这个类 css: https://ui.zhaozimin.com/zzm.css tokens: [--zzm-accent] --- ``` | 字段 | 含义 | |---|---| | `origin` | 同步(edu 是源头,逐字复制)/ 提炼(从 edu 后台样张整理成组件,本站是源头)/ 复刻(剥离业务状态的展示版)/ 规范 | | `source` | 实现这一页的源文件,可直接 GET | | `synced` | edu 的版本号与提交;数值以它为准 | | `scope` | 样式作用域,永远是 .zzm-v4 | | `tokens` | 这一页直接依赖的 CSS 变量 | ## 铁律速查 - ✓ 要:根节点 `class="zzm-v4"`,引入 zzm.css - ✓ 要:颜色只用 `var(--zzm-*)`;间距/高度/行高/圆角只取 4 的倍数 - ✓ 要:按钮圆角矩形:sm/md 8、lg 12;高度 28/36/44 - ✗ 不要:把可点的按钮做成 999 胶囊 - ✓ 要:阴影只从 `.zzm-btn` `.zzm-card` `.zzm-input` 这些质感类取 - ✗ 不要:自己写 `box-shadow`、`backdrop-filter`、渐变 - ✓ 要:一屏只出现一次朱色 `--zzm-accent`,标「下一步」 - ✗ 不要:用鲜艳纯红、奶油色暖底、网络字体、暗色主题 - ✓ 要:禁用=灰面灰字;等待= `busy` - ✗ 不要:用 opacity 淡化表示禁用 ## 复刻完怎么自检 把你的实现和本站样张在同一台 Mac、同一缩放下截图,逐像素比对。常见偏差按出现频率排序: 1. **没加 `.zzm-v4`**:按钮变成浏览器默认样式。 2. **字体不对**:出现了 Inter 或网络字体。删掉,用系统字体栈 `-apple-system, "PingFang SC", …`。 3. **尺寸差 1–2 像素**:缺了 `box-sizing: border-box`。zzm.css 的基线段已包含;自己拆文件时别漏掉。 4. **阴影方向不对**:自己写了 box-shadow。删掉,交给质感类。 5. **红太多**:同屏出现了两处以上朱色。 --- 上一页:[快速接入](https://ui.zhaozimin.com/start/quickstart.md) · 下一页:[设计原则](https://ui.zhaozimin.com/foundations/principles.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 设计原则 Principles url: https://ui.zhaozimin.com/foundations/principles/ markdown: https://ui.zhaozimin.com/foundations/principles.md group: 基础 origin: 规范文档 source: - src/system/PRODUCT.md # https://ui.zhaozimin.com/src/system/PRODUCT.md - src/system/styles/v4/SPACING.md # https://ui.zhaozimin.com/src/system/styles/v4/SPACING.md synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 设计原则 Principles > 高级、克制、好上手。三个字:墨 · 纸 · 朱。进来就安静下来,觉得「这很简单,我马上能开始」。 ## 纲领:一体感,不是数字苛政 目标只有一个:**整个网站看起来是一块材料切出来的**。每个板块共用同一套视觉词汇,而不是东一块西一块各说各话。 数字(4 和 8 的倍数)只是手段。当所有间距、高度、圆角都取自同一把梯子,画面自然产生统一的呼吸节奏,拼凑感就消失了。所以规则很简单:**需要一个尺寸时,不要发明数字,从梯子上取一档。** 拿不准取哪档,就看同类组件用了哪档,跟它一致——一致性比精确性重要。 ## 五条原则 | 原则 | 意思 | 落到界面上 | |---|---|---| | 一屏一枚朱印 | 红只出现一次,永远标「你在这里 / 下一步」 | 朱色是导航信号,不是装饰;同屏第二处红就是错 | | 留白即上手 | 首页至少八成是留白 | 能收进次级入口的,绝不摆在主动线上 | | 设计语言自解释 | 不需要教程 | 看到红就知道点哪,看到卡片就知道能进;圆=徽章不可点,方角=控件可点 | | 剧场只在首次 | 开屏动画只播一次、可跳过 | 产品内动效 150–250ms,只传递状态;尊重减少动效设置 | | 地图即导航 | 阶段顺序=解锁顺序=侧栏顺序 | 三者同构,用户不用在脑子里做换算 | ## 两条铁律 - ✓ 要:**光影铁律**:阴影只从质感类(`.zzm-btn` `.zzm-card` `.zzm-input`)取,组件里禁止散写 `box-shadow`。一体感来自全站共用同一个光照模型 - ✓ 要:**按钮禁胶囊**:文字按钮一律圆角矩形(sm/md 8 · lg 12),999 胶囊只留给不可点的徽章 ## 不要做的事 - ✗ 不要:鲜艳纯红。红必须是哑光的朱砂 / 印泥色,而且用量极少 - ✗ 不要:营销优先的首屏:大 Hero、多个 CTA、社交证明横幅。第一眼必须是学习内容本身 - ✗ 不要:奶油色、米色暖纸底。白就是中性白,不偏暖 - ✗ 不要:打卡中断、红点焦虑这类胁迫式活跃机制。地图只照亮位置,不制造焦虑 - ✗ 不要:模糊背景(backdrop-filter)。性能代价大,全域禁用 - ✗ 不要:网络字体。系统字体就是这套系统的一部分 ## 无障碍 - 正文对比度达到 WCAG AA(≥ 4.5:1)。 - 全站尊重 `prefers-reduced-motion`:开屏降级为静帧,动画与过渡时长压到 0.01ms。 - 键盘可达:焦点可见,统一为朱色光晕 `0 0 0 3px rgba(166,64,47,.2)`。 - 红色只做冗余信号:同时有图标或文字,不单靠颜色传达意思。 ## 跨项目复用:移植骨,不移植皮 这套系统诞生在「纸·墨·朱」网页课程站里。带到别的产品时: - **可以直接搬**:间距梯、控件高度梯、圆角梯、细线三档、五层光照模型的结构。 - **要重新标定**:品牌朱 `#A6402F` 和纸面 `#F6F6F4` 属于课程站的身份;微拟物凸键的强度在网页上成立,搬进 macOS 原生窗口会显旧,保留五层结构、把幅度降一个数量级。 - **深色界面**:顶部反光要从 `rgba(255,255,255,.88)` 左右降到 `.055` 左右——深底上任何高于 .1 的白线都会变成刺眼亮边;投影要加深。 --- 上一页:[给 Agent 用](https://ui.zhaozimin.com/start/for-agents.md) · 下一页:[色彩](https://ui.zhaozimin.com/foundations/color.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 色彩 Color url: https://ui.zhaozimin.com/foundations/color/ markdown: https://ui.zhaozimin.com/foundations/color.md group: 基础 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/styles/v4/tokens.css # https://ui.zhaozimin.com/src/system/styles/v4/tokens.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css tokens: [--zzm-paper, --zzm-surface, --zzm-ink, --zzm-accent] --- # 色彩 Color > 黑、白、朱砂红。红只出现一次,永远标「你在这里 / 下一步」。白就是中性白,不偏暖。 ## 品牌与底面 | 令牌 | 值 | 用途 | |---|---|---| | `--zzm-accent` | `#A6402F` | | | `--zzm-paper` | `#F6F6F4` | 应用画布 / 侧栏底 | | `--zzm-surface` | `#FFFFFF` | 主内容面 / 卡片 | | `--zzm-ink` | `#161616` | 主文字墨色 | | `--zzm-ink-strong` | `#1D1D1B` | 深墨(黑胶囊 CTA) | 品牌朱有三个可选值 `#A6402F`(默认)/ `#8C3A31` / `#B5543F`,由外壳在运行时注入 `--zzm-accent`。任何颜色引用都写 `var(--zzm-accent, #A6402F)`,脱离外壳也有正确兜底。文字选区用朱色 16% 透明:`rgba(166,64,47,.16)`。 ## 文字灰阶 | 令牌 | 值 | 用途 | |---|---|---| | `--zzm-text-1` | `#26261F` | | | `--zzm-text-2` | `#3F3F3B` | | | `--zzm-text-3` | `#4A4A46` | | | `--zzm-text-4` | `#6E6E6A` | | | `--zzm-text-5` | `#72726E` | | | `--zzm-muted` | `#8B8B85` | | | `--zzm-muted-2` | `#A2A29C` | | | `--zzm-muted-3` | `#B0B0AA` | | | `--zzm-faint` | `#C6C6C0` | | 正文用 `text-2`,说明文字用 `text-5`,元数据与标签用 `muted-2`,分隔符用 `faint`。灰都带一点点黄绿偏(例如 `#8B8B85` 而不是 `#8B8B8B`),和纸面同一个色温。 ## 暗底 只用在封面、播放器、付款页这类「放内容」的深色面上;界面本身没有暗色主题。 | 令牌 | 值 | 用途 | |---|---|---| | `--zzm-dark-card` | `#131316` | | | `--zzm-dark-poster` | `#0E0E10` | | | `--zzm-dark-pay` | `#0B0B0C` | | ## 细线三档 黑色微透明只允许三档,同一用途同一档。深于 .18 的深色遮罩和滚动条不算细线。 | 令牌 | 值 | 用途 | |---|---|---| | `--zzm-hairline` | `rgba(0, 0, 0, 0.07)` | | | `--zzm-hairline-2` | `rgba(0, 0, 0, 0.09)` | | | `--zzm-hairline-strong` | `rgba(0, 0, 0, 0.14)` | | ## 课时档位信号色 黑白系统里唯一被允许的功能色,只用在档位胶囊上(见[徽章与档位](/components/badge))。每档三件套:实色、浅底、深字。 | 令牌 | 值 | 用途 | |---|---|---| | `--zzm-tier-free` | `#16A34A` | 免费 · 绿 | | `--zzm-tier-free-bg` | `rgba(22, 163, 74, 0.12)` | | | `--zzm-tier-free-ink` | `#157F3C` | | | `--zzm-tier-member` | `#E0920A` | 会员 · 黄金 | | `--zzm-tier-member-bg` | `rgba(224, 146, 10, 0.18)` | | | `--zzm-tier-member-ink` | `#8A5606` | | | `--zzm-tier-ext` | `#A6402F` | 拓展 · 红(品牌朱同族) | | `--zzm-tier-ext-bg` | `rgba(166, 64, 47, 0.11)` | | | `--zzm-tier-ext-ink` | `#A6402F` | | ## 组件里的字面量对照 v4 组件大量直接写色值而不是 CSS 变量。复刻时遇到下面这些字面量,它们对应的就是这些令牌: | 字面量 | 令牌 | 常见位置 | |---|---|---| | `#161616` | `--zzm-ink` | 标题、当前项文字 | | `#1D1D1B` | `--zzm-ink-strong` | 墨键底、圆徽、开关开启、选中复选框 | | `#3F3F3B` | `--zzm-text-2` | 正文、纸键文字 | | `#72726E` | `--zzm-text-5` | 卡片简介 | | `#8B8B85` | `--zzm-muted` | 面包屑、说明、元数据 | | `#A2A29C` | `--zzm-muted-2` | mono 小标签、表头 | | `#C6C6C0` | `--zzm-faint` | 面包屑分隔符 | | `#131316` | `--zzm-dark-card` | 封面与缩略图暗底 | | `rgba(0,0,0,.09)` | `--zzm-hairline-2` | 卡片描边 | | `#EFEFEC` | — | 骨架块、中性标签底(无对应令牌) | ## 铁律 - ✓ 要:同一屏只出现一处朱色,标当前位置或下一步 - ✗ 不要:鲜艳纯红,或把朱色当装饰 - ✓ 要:颜色写 `var(--zzm-*)`,带字面量兜底 - ✗ 不要:奶油色、米色暖底 - ✓ 要:朱色只做冗余信号:旁边总有图标或文字 - ✗ 不要:档位色(绿 / 黄)出现在档位胶囊以外的地方 --- 上一页:[设计原则](https://ui.zhaozimin.com/foundations/principles.md) · 下一页:[字体与字阶](https://ui.zhaozimin.com/foundations/typography.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 字体与字阶 Typography url: https://ui.zhaozimin.com/foundations/typography/ markdown: https://ui.zhaozimin.com/foundations/typography.md group: 基础 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/styles/v4/tokens.css # https://ui.zhaozimin.com/src/system/styles/v4/tokens.css - src/system/styles/v4/SPACING.md # https://ui.zhaozimin.com/src/system/styles/v4/SPACING.md synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css tokens: [--zzm-font, --zzm-mono] --- # 字体与字阶 Typography > 系统字体,没有网络字体。字号沿用 0.5 步进的字阶,不再发明新字号;行高一律写 px,且是 4 的倍数。 ## 字体栈 | 令牌 | 值 | 用途 | |---|---|---| | `--zzm-font` | `-apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif` | | | `--zzm-mono` | `ui-monospace, "SF Mono", Menlo, monospace` | | 中文依次落到 PingFang SC(Mac / iOS)→ Hiragino Sans GB → Microsoft YaHei(Windows)。所以「1:1」严格成立于苹果设备。不要为了统一而引入网络字体——系统字体本身就是这套设计的一部分。 ## 角色 ## 字号阶 只用这些字号。半像素不会破坏节奏,因为行高已经落在 4pt 基线上。 | 字号 | 行高 | 用在哪 | |---|---|---| | 9.5 | —(单行) | 角标、编号 chip、会员标签 | | 10 / 10.5 | 16 | mono 标签、元数据、档位胶囊 | | 11.5 | 16 | 小说明、卡片「适合 · …」 | | 12 / 12.5 | 20 | 面包屑、说明文字、按钮 sm、输入框 | | 13 / 13.5 | 20 | 按钮 md、侧栏导航、表格正文 | | 14 / 14.5 | 20–24 | 正文、按钮 lg、课节卡标题 | | 16 | 24 | 卡片标题 | | 22 / 26 | 32 | 页面标题、节标题 | | 38–48 | 48 / 56 | 首屏大标题 | ## 字重 常用 400(正文)/ 450(未选中的导航与页签)/ 600(按钮、当前项、强调)/ 650(卡片标题)/ 700(页面标题)/ 750(首屏大标题)。系统字体支持可变字重,450、650、750 都能正确渲染。 ## 铁律 - ✓ 要:显式行高写 px,并且是 4 的倍数:13px 正文配 20px,16px 标题配 24px - ✗ 不要:发明字阶以外的新字号 - ✓ 要:无单位行高只允许 `1`(单行字形复位) - ✗ 不要:引入 Inter、Nunito 等网络字体 - ✓ 要:元数据、编号、代码、节标签用 mono - ✗ 不要:正文用 mono --- 上一页:[色彩](https://ui.zhaozimin.com/foundations/color.md) · 下一页:[间距与基线](https://ui.zhaozimin.com/foundations/spacing.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 间距与基线 Spacing url: https://ui.zhaozimin.com/foundations/spacing/ markdown: https://ui.zhaozimin.com/foundations/spacing.md group: 基础 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/styles/v4/SPACING.md # https://ui.zhaozimin.com/src/system/styles/v4/SPACING.md - src/system/styles/v4/tokens.css # https://ui.zhaozimin.com/src/system/styles/v4/tokens.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 间距与基线 Spacing > 一把 8pt 梯子加 4pt 微调。padding、margin、gap、定位偏移、组件高度、行高全部从梯上取值——不发明数字。 ## 间距梯 | 档位 | 语义 | |---|---| | 4 | 微调 | | 8 | 元素内 | | 12 | 紧凑 | | 16 | 标准 | | 20 | 舒适 | | 24 | 区块内 | | 32 | 区块间 | | 40 | 大区块 | | 48 | 节内距 | | 56–88 | 节与节 | | 令牌 | 值 | 用途 | |---|---|---| | `--zzm-s-1` | `4px` | 微调 | | `--zzm-s-2` | `8px` | 元素内 | | `--zzm-s-3` | `12px` | 紧凑 | | `--zzm-s-4` | `16px` | 标准 | | `--zzm-s-5` | `20px` | 舒适 | | `--zzm-s-6` | `24px` | 区块内 | | `--zzm-s-8` | `32px` | 区块间 | | `--zzm-s-10` | `40px` | 大区块 | | `--zzm-s-12` | `48px` | 节内距 | | `--zzm-s-16` | `64px` | 节间距 | JSX 内联样式允许写字面量,但值必须在梯上。拿不准取哪档,就看同类组件用了哪档,跟它一致。 ## 控件高度三档 **28** 工具条键 · **36** 表单键与输入框 · **44** 主操作键。容器与头部导航取 32 / 40 / 48 / 56 / 64(60 是头部导航的历史值,允许)。可点击目标不小于 28px 高。 ## 4pt 基线 - 显式行高一律写 **px 且为 4 的倍数**:13px 正文 → 20px;12.5px 副文 → 20px;16px 卡片标题 → 24px;22–26px 节标题 → 32px;40–48px 首屏 → 48 / 56px。 - 无单位行高只允许 `1`(单行字形复位,盒高由容器管)。 - 段落间距从梯上取:正文段间 8 / 12,区块间 16 / 24。 ## 例外(审计器放行) - `0`、边框宽 `1 / 1.5 / 2px`(细线不是间距)、`999 / 50%` 圆角、百分比、`calc()`、`var()`、`auto`。 - SVG 内部坐标、`transform`、动画关键帧数值——图形语言,不归网格管。 - 宽度暂不强制(侧栏 266 等历史值牵连布局公式);新代码的固定宽度请自觉取梯。 ## 执法器 纪律:自己改过的文件不新增偏离: ```bash npm run audit:spacing # 报告态 npm run audit:spacing -- --strict # 有偏离即失败 ``` ## 铁律 - ✓ 要:需要尺寸时从梯上取一档 - ✗ 不要:写 13px、22px 这类不在梯上的间距 - ✓ 要:优先 8 的倍数,4 只做微调 - ✗ 不要:为「看起来差一点」发明新值——一致性比精确性重要 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* controls.css */ .zzm-v4 .zzm-btn { -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 16px; font-family: inherit; font-size: 13px; font-weight: 600; letter-spacing: .2px; white-space: nowrap; cursor: pointer; user-select: none; border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */ border: 1px solid rgba(0, 0, 0, .14); color: #3F3F3B; background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */ 0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */ 0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */ inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */ inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */ transition: box-shadow .16s ease, transform .16s ease, background .16s ease, border-color .16s ease, filter .16s ease, opacity .16s ease; } .zzm-v4 .zzm-btn:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .18); box-shadow: 0 0 0 4px rgba(255, 255, 255, .8), 0 2px 4px rgba(0, 0, 0, .10), 0 4px 12px rgba(0, 0, 0, .09), inset 0 1px 0 rgba(255, 255, 255, .95), inset 0 -1px 0 rgba(0, 0, 0, .05); } .zzm-v4 .zzm-btn:active { transform: translateY(0); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */ box-shadow: 0 0 0 3px rgba(255, 255, 255, .45), 0 1px 0 rgba(255, 255, 255, .65), inset 0 2px 4px rgba(0, 0, 0, .13), inset 0 1px 2px rgba(0, 0, 0, .09); } .zzm-v4 .zzm-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */ 0 1px 2px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-btn:disabled { cursor: not-allowed; transform: none; } .zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) { color: #A2A29C; background: #F2F2EF; border-color: rgba(0, 0, 0, .08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04); } .zzm-v4 .zzm-btn--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; } .zzm-v4 .zzm-btn--lg { height: 44px; padding: 0 24px; font-size: 14px; border-radius: 12px; gap: 8px; } .zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; } .zzm-v4 .zzm-btn--icon.zzm-btn--lg { width: 44px; } ``` --- 上一页:[字体与字阶](https://ui.zhaozimin.com/foundations/typography.md) · 下一页:[圆角与细线](https://ui.zhaozimin.com/foundations/radius.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 圆角与细线 Radius & Hairline url: https://ui.zhaozimin.com/foundations/radius/ markdown: https://ui.zhaozimin.com/foundations/radius.md group: 基础 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/styles/v4/tokens.css # https://ui.zhaozimin.com/src/system/styles/v4/tokens.css - src/system/styles/v4/SPACING.md # https://ui.zhaozimin.com/src/system/styles/v4/SPACING.md synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 圆角与细线 Radius & Hairline > 圆角五档,同一层级用同一档:外层卡片 16,卡内嵌套 12。细线三档,同一用途用同一档。 ## 圆角梯 | 令牌 | 值 | 用途 | |---|---|---| | `--zzm-r-1` | `4px` | | | `--zzm-r-2` | `8px` | | | `--zzm-r-3` | `12px` | | | `--zzm-r-4` | `16px` | | | `--zzm-r-pill` | `999px` | | 另有 `20 / 24` 给大容器,`50%` 给圆形。高度 ≤ 8px 的进度条、微点允许 1–2px 端帽圆角。此前散布的 3/5/7/9/10/11/14/18 已按 ≤5→4、6–9→8、10–13→12、14–17→16、18–22→20 归一。 ## 嵌套律 ### 样张 · 卡 16 · 内嵌 12 React: ```jsx export function Nesting() { return (
外层 16,内嵌 12
内距 16 − 边框 1 ≈ 圆角差 4
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
外层 16,内嵌 12
内距 16 − 边框 1 ≈ 圆角差 4
``` ## 细线三档 | 令牌 | 值 | 用途 | |---|---|---| | `--zzm-hairline` | `rgba(0, 0, 0, 0.07)` | | | `--zzm-hairline-2` | `rgba(0, 0, 0, 0.09)` | | | `--zzm-hairline-strong` | `rgba(0, 0, 0, 0.14)` | | 此前 .05/.06/.08/.1/.12/.13/.15/.16 散布 12 档,已归并到这三档。深于 .18 的深色遮罩与滚动条不属细线。分隔主要靠 1px 细线和留白分层,抬升感交给质感类的统一光影。 ## 铁律 - ✓ 要:按钮圆角矩形:sm/md 8、lg 12 - ✗ 不要:按钮用 999 胶囊——胶囊只给不可点的徽章 - ✓ 要:外层卡片 16、卡内嵌套 12 - ✗ 不要:同一层级混用不同圆角 - ✓ 要:细线只取 .07 / .09 / .14 - ✗ 不要:.1、.12 这类中间值 --- 上一页:[间距与基线](https://ui.zhaozimin.com/foundations/spacing.md) · 下一页:[光照模型](https://ui.zhaozimin.com/foundations/elevation.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 光照模型 Elevation url: https://ui.zhaozimin.com/foundations/elevation/ markdown: https://ui.zhaozimin.com/foundations/elevation.md group: 基础 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/styles/v4/controls.css # https://ui.zhaozimin.com/src/system/styles/v4/controls.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 光照模型 Elevation > 光从正上方来。每个可交互控件都是一枚真实的键:凸面、顶部反光、贴地投影、反光光晕、按压内陷,五层缺一不可。 ## 五层,逐层叠上去 ## 四种表面 凸的是键,凹的是输入槽,浮起的是卡片。四种表面共用同一个光源,所以放在一起不打架。把鼠标移到最右边的卡链上看悬停。 ## 源码 五层光影在 `controls.css` 里是这样写的(原样裁出): controls.css · 基键 / 悬停 / 按压: ```css .zzm-v4 .zzm-btn { -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 16px; font-family: inherit; font-size: 13px; font-weight: 600; letter-spacing: .2px; white-space: nowrap; cursor: pointer; user-select: none; border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */ border: 1px solid rgba(0, 0, 0, .14); color: #3F3F3B; background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */ 0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */ 0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */ inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */ inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */ transition: box-shadow .16s ease, transform .16s ease, background .16s ease, border-color .16s ease, filter .16s ease, opacity .16s ease; } .zzm-v4 .zzm-btn:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .18); box-shadow: 0 0 0 4px rgba(255, 255, 255, .8), 0 2px 4px rgba(0, 0, 0, .10), 0 4px 12px rgba(0, 0, 0, .09), inset 0 1px 0 rgba(255, 255, 255, .95), inset 0 -1px 0 rgba(0, 0, 0, .05); } .zzm-v4 .zzm-btn:active { transform: translateY(0); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */ box-shadow: 0 0 0 3px rgba(255, 255, 255, .45), 0 1px 0 rgba(255, 255, 255, .65), inset 0 2px 4px rgba(0, 0, 0, .13), inset 0 1px 2px rgba(0, 0, 0, .09); } ``` ## 深色面上要重新标定 同一套五层模型搬到深色面(付款页、播放器锁罩)时,数值必须重标: - 顶部反光从 `.88` 左右降到 `.055` 左右——深底上任何高于 .1 的白线都会变成刺眼亮边。 - 投影要加深(`.4` 左右)。 - `--ondark` 暗面键就是重标后的结果,直接用它,不要自己调。 ## 铁律 - ✓ 要:阴影只从 `.zzm-btn` `.zzm-card` `.zzm-cardlink` `.zzm-input` 取 - ✗ 不要:在组件里散写 `box-shadow`——各写各的光源方向必然穿帮 - ✓ 要:光源永远在正上方:投影只向下 - ✗ 不要:`backdrop-filter` 模糊(性能原因全域禁用) - ✓ 要:按下去真的「凹」:`:active` 翻转渐变 + inset 阴影 - ✗ 不要:把微拟物强度原样搬进原生桌面窗口——保留结构,幅度降一个数量级 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* base.css */ .zzm-v4 input.zzm-input:focus, .zzm-v4 textarea.zzm-input:focus { border-color: #161616 !important; } /* controls.css */ .zzm-v4 .zzm-btn { -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 16px; font-family: inherit; font-size: 13px; font-weight: 600; letter-spacing: .2px; white-space: nowrap; cursor: pointer; user-select: none; border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */ border: 1px solid rgba(0, 0, 0, .14); color: #3F3F3B; background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */ 0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */ 0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */ inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */ inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */ transition: box-shadow .16s ease, transform .16s ease, background .16s ease, border-color .16s ease, filter .16s ease, opacity .16s ease; } .zzm-v4 .zzm-btn:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .18); box-shadow: 0 0 0 4px rgba(255, 255, 255, .8), 0 2px 4px rgba(0, 0, 0, .10), 0 4px 12px rgba(0, 0, 0, .09), inset 0 1px 0 rgba(255, 255, 255, .95), inset 0 -1px 0 rgba(0, 0, 0, .05); } .zzm-v4 .zzm-btn:active { transform: translateY(0); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */ box-shadow: 0 0 0 3px rgba(255, 255, 255, .45), 0 1px 0 rgba(255, 255, 255, .65), inset 0 2px 4px rgba(0, 0, 0, .13), inset 0 1px 2px rgba(0, 0, 0, .09); } .zzm-v4 .zzm-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */ 0 1px 2px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-btn:disabled { cursor: not-allowed; transform: none; } .zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) { color: #A2A29C; background: #F2F2EF; border-color: rgba(0, 0, 0, .08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04); } .zzm-v4 .zzm-cardlink { background: #FFFFFF; border: 1px solid rgba(0, 0, 0, .09); border-radius: 16px; padding: 12px; cursor: pointer; box-shadow: 0 1px 2px rgba(0, 0, 0, .04); transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; } .zzm-v4 .zzm-cardlink:hover { transform: translateY(-3px); border-color: rgba(0, 0, 0, .3); box-shadow: 0 10px 30px rgba(0, 0, 0, .09), 0 1px 2px rgba(0, 0, 0, .04); } .zzm-v4 .zzm-card { background: #FFFFFF; border: 1px solid rgba(0, 0, 0, .09); border-radius: 16px; box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 6px 18px rgba(0, 0, 0, .05), inset 0 1px 0 rgba(255, 255, 255, .85); } .zzm-v4 input.zzm-input, .zzm-v4 textarea.zzm-input { background: #FDFDFC; box-shadow: inset 0 1.5px 3px rgba(0, 0, 0, .06), inset 0 1px 1px rgba(0, 0, 0, .04); } .zzm-v4 input.zzm-input:focus, .zzm-v4 textarea.zzm-input:focus { box-shadow: inset 0 1.5px 3px rgba(0, 0, 0, .06), 0 0 0 3px rgba(22, 22, 22, .07); /* 聚焦光晕 */ } ``` --- 上一页:[圆角与细线](https://ui.zhaozimin.com/foundations/radius.md) · 下一页:[动效](https://ui.zhaozimin.com/foundations/motion.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 动效 Motion url: https://ui.zhaozimin.com/foundations/motion/ markdown: https://ui.zhaozimin.com/foundations/motion.md group: 基础 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/styles/v4/keyframes.css # https://ui.zhaozimin.com/src/system/styles/v4/keyframes.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 动效 Motion > 产品内动效 150–250ms,只传递状态。标准缓动 cubic-bezier(.2,.7,.2,1)。戏剧化只留给首次进站的开屏,之后产品永远安静。 ## 重播 ### 样张 · MotionReplay React: ```jsx import { useState } from 'react' import { Btn } from './components/v4/Button.jsx' const ACCENT = 'var(--zzm-accent, #A6402F)' const EASE = 'cubic-bezier(.2,.7,.2,1)' const MONO = "ui-monospace,'SF Mono',Menlo,monospace" const ROWS = [ ['zzmSpring', '1.1s', '弹性落定', { width: 20, height: 20, borderRadius: '50%', background: ACCENT, animation: `zzmSpring 1.1s ${EASE} both` }], ['zzmIn', '.35s', '进场上浮', { width: 96, height: 20, borderRadius: 4, background: '#1D1D1B', animation: 'zzmIn .35s ease both' }], ['zzmSlide', '.28s', '侧栏滑入', { width: 96, height: 20, borderRadius: 4, background: '#8B8B85', animation: 'zzmSlide .28s ease both' }], ['zzmGrow', '.55s', '展开生长', { width: 160, height: 20, borderRadius: 4, background: '#D6D6D1', overflow: 'hidden', animation: `zzmGrow .55s ${EASE} both` }], ['zzmPulse', '2.6s ∞', '当前节点呼吸', { width: 20, height: 20, borderRadius: '50%', border: `1.5px solid ${ACCENT}`, animation: 'zzmPulse 2.6s ease-in-out infinite' }], ] export default function MotionReplay() { const [n, setN] = useState(0) return (
{ROWS.map(([name, dur, use, style]) => (
{name} {dur}
{use}
))}
setN(n + 1)}>重播
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
zzmSpring 1.1s
弹性落定
zzmIn .35s
进场上浮
zzmSlide .28s
侧栏滑入
zzmGrow .55s
展开生长
zzmPulse 2.6s ∞
当前节点呼吸
``` ## 缓动与时长 | 用途 | 值 | |---|---| | 标准缓动 | `cubic-bezier(.2,.7,.2,1)`——快起慢落 | | 悬停 / 按压过渡 | `.16s ease`(按钮、卡片、链接) | | 状态切换 | 150–250ms | | 进场 | `zzmIn .35s`、`zzmUp .25s` | | 展开 | `zzmGrow .55s` | | 弹性强调 | `zzmSpring 1.1s` | ## 关键帧清单 | 名称 | 效果 | 用在哪 | |---|---|---| | `zzmPulse` | 透明度 .15 ↔ .55 呼吸 | 学习地图当前节点光晕 | | `zzmIn` / `zzmIn2` | 上移 12px 淡入 | 屏幕进场 | | `zzmUp` / `zzmUp2` | 上移 10px 淡入 | 卡片、提示进场 | | `zzmSlide` | 左移 14px 淡入 | 侧栏滑入 | | `zzmGrow` | max-height 0 → 960 展开 | 折叠面板 | | `zzmSpring` | 横移过冲再回弹 | 弹性演示 | | `zzmFade` | 纯淡入 | 遮罩 | | `zzmPayIn` | 上移 20px + 缩放 .985 淡入 | 付款页 | | `zzmShimmer` | 一道微光横扫 | 骨架屏 | | `zzmBtnSpin` | 旋转一周 | busy 键旋转环 | | `zzmPillShine` | 快扫一次后长歇 | 档位胶囊反光、busy 键反光 | 全部带 `zzm` 前缀,全局注册无副作用;组件里用内联 `animation:` 引用。`@keyframes zzmDialogIn` 是提炼层为确认窗新增的一条。 ## 减少动效 `prefers-reduced-motion: reduce` 时,所有动画与过渡时长压到 0.01ms、只播一次。这段规则在 zzm.css 的基线段里,逐字取自 edu。 ```css @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } ``` ## 铁律 - ✓ 要:动效只传递状态:出现、展开、按下、完成 - ✗ 不要:装饰性循环动画(呼吸光晕只给「当前位置」) - ✓ 要:动 transform 和 opacity - ✗ 不要:动 width / height / padding 引起重排 - ✓ 要:尊重减少动效设置 - ✗ 不要:开屏式大动画出现在第二次访问 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* tokens.css · 用到的令牌 */ :root { --zzm-accent: #A6402F; } /* controls.css */ .zzm-v4 .zzm-btn { -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 16px; font-family: inherit; font-size: 13px; font-weight: 600; letter-spacing: .2px; white-space: nowrap; cursor: pointer; user-select: none; border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */ border: 1px solid rgba(0, 0, 0, .14); color: #3F3F3B; background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */ 0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */ 0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */ inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */ inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */ transition: box-shadow .16s ease, transform .16s ease, background .16s ease, border-color .16s ease, filter .16s ease, opacity .16s ease; } .zzm-v4 .zzm-btn:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .18); box-shadow: 0 0 0 4px rgba(255, 255, 255, .8), 0 2px 4px rgba(0, 0, 0, .10), 0 4px 12px rgba(0, 0, 0, .09), inset 0 1px 0 rgba(255, 255, 255, .95), inset 0 -1px 0 rgba(0, 0, 0, .05); } .zzm-v4 .zzm-btn:active { transform: translateY(0); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */ box-shadow: 0 0 0 3px rgba(255, 255, 255, .45), 0 1px 0 rgba(255, 255, 255, .65), inset 0 2px 4px rgba(0, 0, 0, .13), inset 0 1px 2px rgba(0, 0, 0, .09); } .zzm-v4 .zzm-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */ 0 1px 2px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-btn:disabled { cursor: not-allowed; transform: none; } .zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) { color: #A2A29C; background: #F2F2EF; border-color: rgba(0, 0, 0, .08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04); } .zzm-v4 .zzm-btn--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; } .zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; } ``` --- 上一页:[光照模型](https://ui.zhaozimin.com/foundations/elevation.md) · 下一页:[版式](https://ui.zhaozimin.com/foundations/layout.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 版式 Layout url: https://ui.zhaozimin.com/foundations/layout/ markdown: https://ui.zhaozimin.com/foundations/layout.md group: 基础 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/styles/v4/SPACING.md # https://ui.zhaozimin.com/src/system/styles/v4/SPACING.md synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 版式 Layout > 主内容区最宽 1080、左右内距 48;节标题共用一个骨架;卡片共用一套语法。板块之间的「联系感」来自这三件事。 ## 容器 | 区域 | 尺寸 | |---|---| | 主内容区 | `max-width: 1080px` + 左右内距 `48px` | | 窄文档流 | `max-width: 820px` | | 侧栏 | `266px`(edu 历史值,牵连布局公式,暂不改) | | 面包屑顶栏 | 钉顶,高 48,底部 .07 细线 | | 收起钮 | 恒位:左 14、上 12,28×28 幽灵图标键;展开/收起两态同坐标 | ## 节标题 长页一律用同一个骨架:mono 编号 + 标题 + 右侧注记,底部一条 .09 细线。不另起炉灶。 ### 样张 · SectionHead 骨架 React: ```jsx export function SectionHead() { return (
02

课程总览

6 系列 · 67 节
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
02

课程总览

6 系列 · 67 节
``` ## 卡片语法 所有内容卡共用:白卡 + 1px 细线 + 圆角 16 + 内距 16–24 + `.zzm-card` 浮起光影。可点的卡用 `.zzm-cardlink`(见[卡片与卡链](/components/card))。 ### 样张 · 标准内容卡 React: ```jsx export function CardGrammar() { return (
学习进度
本周完成 6 节
白卡 · 1px 细线 · 圆角 16 · 内距 16–24 · 浮起光影。
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
学习进度
本周完成 6 节
白卡 · 1px 细线 · 圆角 16 · 内距 16–24 · 浮起光影。
``` ## 节奏 落地页、长页的节与节之间统一 60px 顶距;节内区块间 24 / 32。分隔靠 1px 细线与留白分层,不靠色块。 ## 铁律 - ✓ 要:内容区最宽 1080,节与节共用这一个宽度 - ✗ 不要:每个板块自己定宽度 - ✓ 要:节标题用 SectionHead 骨架 - ✗ 不要:用色带或粗线分隔板块 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* tokens.css · 用到的令牌 */ :root { --zzm-mono: ui-monospace, "SF Mono", Menlo, monospace; } /* base.css */ .zzm-v4 .zzm-inklink { color: inherit; transition: color .16s ease; } .zzm-v4 .zzm-inklink:hover { color: #161616; } .zzm-v4 .zzm-inklink:focus-visible { outline: none; border-radius: 4px; box-shadow: 0 0 0 3px rgba(166, 64, 47, .2); } /* controls.css */ .zzm-v4 .zzm-card { background: #FFFFFF; border: 1px solid rgba(0, 0, 0, .09); border-radius: 16px; box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 6px 18px rgba(0, 0, 0, .05), inset 0 1px 0 rgba(255, 255, 255, .85); } ``` --- 上一页:[动效](https://ui.zhaozimin.com/foundations/motion.md) · 下一页:[图标](https://ui.zhaozimin.com/foundations/icons.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 图标 Icons url: https://ui.zhaozimin.com/foundations/icons/ markdown: https://ui.zhaozimin.com/foundations/icons.md group: 基础 origin: 复刻自 edu 同名界面的展示版,剥离了业务状态 source: - src/extracted/icons.jsx # https://ui.zhaozimin.com/src/extracted/icons.jsx - src/system/lib/siteIcons.js # https://ui.zhaozimin.com/src/system/lib/siteIcons.js synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 图标 Icons > 产品界面用自绘的 16 网格线性图标:1.3–1.5 描边、圆头圆角、墨灰色;内容区(首页作品、笔记标注块)用 lucide-react。 ## 品牌标 星座标:四颗墨点连成一条上行折线,第三颗是朱色——「你在这里」。它就是整个设计系统的缩影:黑白里只有一处红。favicon 是它的反白版(墨底方块)。 ## 自绘图标(产品界面) 规格:16 网格(导航图标 15px 显示)、描边 1.3–1.5、`stroke-linecap: round`、颜色 `#3A3A36`(收起图标 `#6E6E6A`)。朱色只出现在「当前位置」那个点上(地图图标、锁头的钥匙孔)。源码在 `/src/extracted/icons.jsx`,逐笔复刻自 edu 各处的内联 SVG。 ## lucide 注册表(内容图标) 首页作品与荣誉区的图标存在数据库里,存的是名字,渲染时映射成 lucide 组件;未知名字兜底为 Sparkles。 ## 标注块图标 笔记标注块的 13 个规范型各有一个 lucide 图标(颜色见[笔记标注块](/patterns/callouts))。 ## 铁律 - ✓ 要:界面图标线性、圆头、墨灰色 - ✗ 不要:填充图标、彩色图标、emoji 当图标 - ✓ 要:图标旁边有文字,或有 aria-label - ✗ 不要:一个图标里出现两处朱色 --- 上一页:[版式](https://ui.zhaozimin.com/foundations/layout.md) · 下一页:[按钮](https://ui.zhaozimin.com/components/button.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 按钮 Btn url: https://ui.zhaozimin.com/components/button/ markdown: https://ui.zhaozimin.com/components/button.md group: 组件 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/components/v4/Button.jsx # https://ui.zhaozimin.com/src/system/components/v4/Button.jsx - src/system/styles/v4/controls.css # https://ui.zhaozimin.com/src/system/styles/v4/controls.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css tokens: [--zzm-accent] --- # 按钮 Btn > 光从正上方来——每个按钮都是一枚真实的键:凸面渐变、顶部反光、贴地投影、反光光晕、按压内陷。文字按钮一律圆角矩形,禁胶囊。 ## 试验台 三条轴自由组合:五种键面 × 三档高度 × 三种状态。下方同时给出对应的 JSX 和类名。 ### 样张 · ButtonPlayground (交互试验台,仅在网页上可操作;各组合的代码见下方样张) ## 五种键面 **纸键**是默认;**墨键**给一屏里最主要的操作;**朱键**只给品牌强调、播放或破坏性确认;**幽灵键**静默时是文字,悬停才「长成键」,用于工具条;**暗面键**只在深色面上用。 ### 样张 · 纸 · 墨 · 朱 · 幽灵 React: ```jsx import { Btn, btnClass } from './components/v4/Button.jsx' export function Variants() { return ( <> 纸键(默认) 墨键 · 主操作 朱键 · 强调 幽灵键 ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html ``` ### 样张 · 暗面键 付款页 / 播放器锁罩 / 海报态 React: ```jsx import { Btn, btnClass } from './components/v4/Button.jsx' export function OnDark() { return ( <> 暗面键 朱键在暗面同样成立 禁用 ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html ``` ## 三档高度 高度落在 4pt 网格上:`sm` 28(工具条)、`md` 36(表单与一般操作)、`lg` 44(主操作,圆角升到 12)。`icon` 把键变成正方形。 ### 样张 · 28 · 36 · 44 · 图标键 React: ```jsx import { Btn, btnClass } from './components/v4/Button.jsx' export function Sizes() { return ( <> 小 · 28 中 · 36 大 · 44 · 圆角 12 ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html ``` ## 等待与禁用:两种语义两张脸 `busy` 是「正在做」:出旋转环、一道反光扫过、饱和度略降、不可点,但**保留变体本色**。`disabled` 是「不能做」:灰面灰字。禁用**永远不用 opacity 淡化**——白字 × 半透明 × 灰面会让字直接消失。任何点击后要等的按钮都用 `busy`。 ### 样张 · busy 与 disabled React: ```jsx import { Btn, btnClass } from './components/v4/Button.jsx' export function States() { return ( <> Saving… 保存中… 提交中… 禁用 禁用 · 灰面灰字 ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html ``` ## 链接长成键 ``、`查看定价 阅读文档 ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html 查看定价 阅读文档 ``` ## 属性 | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `variant` | paper · primary · accent · ghost · ondark | paper | 五种键面 | | `size` | sm · md · lg | md | 高 28 / 36 / 44;lg 圆角 12 | | `icon` | boolean | false | 正方形图标键,宽=高 | | `block` | boolean | false | 撑满容器宽度 | | `busy` | boolean | false | 等待态:旋转环 + 反光 + 本色 + 自动禁用 | | `disabled` | boolean | false | 不能做:灰面灰字 | | `className` | string | '' | 追加类名;style 只放布局,不放背景和阴影 | ## 铁律 - ✓ 要:圆角矩形:`sm`/`md` 圆角 8,`lg` 圆角 12 - ✗ 不要:把文字按钮做成 999 胶囊——胶囊只留给不可点的徽章 - ✓ 要:光影只从 `.zzm-btn` 取,调用方 `style` 只写 margin / flex / width - ✗ 不要:在 `style` 里散写 `box-shadow`、渐变或 `border-radius` 盖掉质感 - ✓ 要:点击后要等待的操作用 `busy` - ✗ 不要:用 `opacity` 淡化来表示禁用 - ✓ 要:一屏只有一个墨键或朱键做主操作 - ✗ 不要:朱键当装饰色——它只标「下一步」或危险动作 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* controls.css */ .zzm-v4 .zzm-btn { -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 16px; font-family: inherit; font-size: 13px; font-weight: 600; letter-spacing: .2px; white-space: nowrap; cursor: pointer; user-select: none; border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */ border: 1px solid rgba(0, 0, 0, .14); color: #3F3F3B; background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */ 0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */ 0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */ inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */ inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */ transition: box-shadow .16s ease, transform .16s ease, background .16s ease, border-color .16s ease, filter .16s ease, opacity .16s ease; } .zzm-v4 .zzm-btn:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .18); box-shadow: 0 0 0 4px rgba(255, 255, 255, .8), 0 2px 4px rgba(0, 0, 0, .10), 0 4px 12px rgba(0, 0, 0, .09), inset 0 1px 0 rgba(255, 255, 255, .95), inset 0 -1px 0 rgba(0, 0, 0, .05); } .zzm-v4 .zzm-btn:active { transform: translateY(0); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */ box-shadow: 0 0 0 3px rgba(255, 255, 255, .45), 0 1px 0 rgba(255, 255, 255, .65), inset 0 2px 4px rgba(0, 0, 0, .13), inset 0 1px 2px rgba(0, 0, 0, .09); } .zzm-v4 .zzm-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */ 0 1px 2px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-btn:disabled { cursor: not-allowed; transform: none; } .zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) { color: #A2A29C; background: #F2F2EF; border-color: rgba(0, 0, 0, .08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04); } .zzm-v4 .zzm-btn--ondark:disabled:not(.zzm-btn--busy) { color: rgba(255, 255, 255, .45); background: rgba(255, 255, 255, .08); border-color: rgba(255, 255, 255, .16); box-shadow: none; } .zzm-v4 .zzm-btn--busy { position: relative; overflow: hidden; pointer-events: none; opacity: .92; filter: saturate(.72); transform: none; } .zzm-v4 .zzm-btn--busy::before { content: ""; position: relative; z-index: 1; /* 旋转环压在反光层之上 */ width: 12px; height: 12px; flex: none; border-radius: 50%; border: 2px solid currentColor; border-top-color: transparent; opacity: .7; animation: zzmBtnSpin .7s linear infinite; } .zzm-v4 .zzm-btn--busy::after { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; transform: translateX(-180%); background: linear-gradient(105deg, transparent 40%, rgba(255, 255, 255, .45) 50%, transparent 60%); animation: zzmPillShine 4.4s ease-in-out infinite; } .zzm-v4 .zzm-btn--primary { color: #FFFFFF; border-color: rgba(0, 0, 0, .85); background: linear-gradient(180deg, #3A3A36 0%, #1D1D1B 58%, #141412 100%); box-shadow: 0 0 0 3px rgba(22, 22, 22, .06), 0 1px 2px rgba(0, 0, 0, .18), 0 3px 10px rgba(22, 22, 22, .24), inset 0 1px 0 rgba(255, 255, 255, .20), inset 0 -1px 0 rgba(0, 0, 0, .38); } .zzm-v4 .zzm-btn--primary:hover { filter: brightness(1.12); border-color: rgba(0, 0, 0, .85); box-shadow: 0 0 0 4px rgba(22, 22, 22, .09), 0 2px 4px rgba(0, 0, 0, .20), 0 5px 14px rgba(22, 22, 22, .28), inset 0 1px 0 rgba(255, 255, 255, .24), inset 0 -1px 0 rgba(0, 0, 0, .38); } .zzm-v4 .zzm-btn--primary:active { filter: none; background: linear-gradient(180deg, #101010 0%, #22221F 100%); box-shadow: 0 0 0 3px rgba(22, 22, 22, .06), inset 0 2px 5px rgba(0, 0, 0, .55), inset 0 1px 2px rgba(0, 0, 0, .4); } .zzm-v4 .zzm-btn--accent { color: #FFFFFF; border-color: rgba(122, 42, 29, .9); background: linear-gradient(180deg, #C25743 0%, #A6402F 56%, #99392A 100%); box-shadow: 0 0 0 3px rgba(166, 64, 47, .10), /* 朱色反光光晕 */ 0 1px 2px rgba(0, 0, 0, .14), 0 3px 10px rgba(166, 64, 47, .28), inset 0 1px 0 rgba(255, 255, 255, .32), inset 0 -1px 0 rgba(0, 0, 0, .20); } .zzm-v4 .zzm-btn--accent:hover { filter: brightness(1.06); border-color: rgba(122, 42, 29, .9); box-shadow: 0 0 0 4px rgba(166, 64, 47, .16), 0 2px 4px rgba(0, 0, 0, .16), 0 5px 14px rgba(166, 64, 47, .34), inset 0 1px 0 rgba(255, 255, 255, .36), inset 0 -1px 0 rgba(0, 0, 0, .20); } .zzm-v4 .zzm-btn--accent:active { filter: none; background: linear-gradient(180deg, #8E3425 0%, #A03D2C 100%); box-shadow: 0 0 0 3px rgba(166, 64, 47, .10), inset 0 2px 5px rgba(0, 0, 0, .34), inset 0 1px 2px rgba(0, 0, 0, .24); } .zzm-v4 .zzm-btn--ghost { border-color: transparent; background: transparent; box-shadow: none; color: #4A4A46; } .zzm-v4 .zzm-btn--ghost:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .14); background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), 0 1px 2px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-btn--ghost:active { transform: translateY(0); border-color: rgba(0, 0, 0, .14); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); box-shadow: inset 0 2px 4px rgba(0, 0, 0, .12); } .zzm-v4 .zzm-btn--ondark { color: #FFFFFF; border-color: rgba(255, 255, 255, .28); background: linear-gradient(180deg, rgba(255, 255, 255, .16) 0%, rgba(255, 255, 255, .06) 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .05), 0 2px 8px rgba(0, 0, 0, .4), inset 0 1px 0 rgba(255, 255, 255, .28), inset 0 -1px 0 rgba(0, 0, 0, .3); } .zzm-v4 .zzm-btn--ondark:hover { border-color: rgba(255, 255, 255, .4); background: linear-gradient(180deg, rgba(255, 255, 255, .22) 0%, rgba(255, 255, 255, .10) 100%); box-shadow: 0 0 0 4px rgba(255, 255, 255, .08), 0 3px 10px rgba(0, 0, 0, .45), inset 0 1px 0 rgba(255, 255, 255, .34), inset 0 -1px 0 rgba(0, 0, 0, .3); } .zzm-v4 .zzm-btn--ondark:active { background: linear-gradient(180deg, rgba(0, 0, 0, .28) 0%, rgba(255, 255, 255, .04) 100%); box-shadow: inset 0 2px 5px rgba(0, 0, 0, .5); } .zzm-v4 .zzm-btn--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; } .zzm-v4 .zzm-btn--lg { height: 44px; padding: 0 24px; font-size: 14px; border-radius: 12px; gap: 8px; } .zzm-v4 .zzm-btn--icon { width: 36px; padding: 0; } .zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; } .zzm-v4 .zzm-btn--icon.zzm-btn--lg { width: 44px; } /* keyframes */ @keyframes zzmBtnSpin { to { transform: rotate(360deg) } } @keyframes zzmPillShine { 0% { transform: translateX(-180%) } 14% { transform: translateX(180%) } 100% { transform: translateX(180%) } } ``` --- 上一页:[图标](https://ui.zhaozimin.com/foundations/icons.md) · 下一页:[视频播放键](https://ui.zhaozimin.com/components/video-play.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 视频播放键 VideoPlay url: https://ui.zhaozimin.com/components/video-play/ markdown: https://ui.zhaozimin.com/components/video-play.md group: 组件 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/styles/v4/controls.css # https://ui.zhaozimin.com/src/system/styles/v4/controls.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 视频播放键 VideoPlay > 全站唯一允许为圆形的交互键。圆形不是装饰例外,而是「点按即开始播放」这个媒体语义的通用符号——导学封面与所有课时海报共用这一个原件。 ### 样张 · 72px 纸面凸键 悬停外圈变亮、居中版放大 1.06;按下内陷 React: ```jsx export function PlayButton() { return ( ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html ``` ## 在海报上居中 加 `--center` 修饰:绝对定位到父容器正中,悬停 / 按压的缩放也由它接管。父容器需要 `position: relative`。 ### 样张 · 课时海报 16:9 React: ```jsx export function OnPoster() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ### 样张 · 导学封面 4:3 导学封面位固定 4:3,封面图 contain 完整展示 React: ```jsx export function GuideCover() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ## 类名 | 类 | 作用 | |---|---| | `.zzm-video-play` | 72×72 圆形纸面凸键,墨色三角,键盘焦点为朱色光晕 | | `.zzm-video-play--center` | 绝对居中于父容器(translate -50% -50%) | | `.zzm-video-play__icon` | 24px 三角图标,左移 4px 做视觉居中 | ## 铁律 - ✓ 要:只用于「点按即开始播放」 - ✗ 不要:借它的圆形去做别的按钮——文字按钮仍然是圆角矩形 - ✓ 要:导学封面与课时海报用同一个原件 - ✗ 不要:在不同页面给同一个动作画不同的播放键 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* controls.css */ .zzm-v4 .zzm-video-play { display: grid; place-items: center; width: 72px; height: 72px; padding: 0; border: 1px solid rgba(0, 0, 0, .16); border-radius: 50%; cursor: pointer; color: #1D1D1B; background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 4px rgba(255, 255, 255, .24), 0 2px 4px rgba(0, 0, 0, .16), 0 6px 16px rgba(0, 0, 0, .22), inset 0 1px 0 rgba(255, 255, 255, .96), inset 0 -1px 0 rgba(0, 0, 0, .08); transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; } .zzm-v4 .zzm-video-play--center { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); } .zzm-v4 .zzm-video-play:hover { border-color: rgba(0, 0, 0, .3); box-shadow: 0 0 0 5px rgba(255, 255, 255, .3), 0 3px 6px rgba(0, 0, 0, .18), 0 9px 22px rgba(0, 0, 0, .28), inset 0 1px 0 rgba(255, 255, 255, .98), inset 0 -1px 0 rgba(0, 0, 0, .08); } .zzm-v4 .zzm-video-play--center:hover { transform: translate(-50%, -50%) scale(1.06); } .zzm-v4 .zzm-video-play:active { transform: scale(.98); background: linear-gradient(180deg, #E9E9E6 0%, #F4F4F1 100%); box-shadow: 0 0 0 4px rgba(255, 255, 255, .24), inset 0 2px 5px rgba(0, 0, 0, .16), inset 0 1px 2px rgba(0, 0, 0, .10); } .zzm-v4 .zzm-video-play--center:active { transform: translate(-50%, -50%) scale(.98); } .zzm-v4 .zzm-video-play:focus-visible { outline: none; box-shadow: 0 0 0 4px rgba(166, 64, 47, .28), 0 2px 4px rgba(0, 0, 0, .16), 0 6px 16px rgba(0, 0, 0, .22), inset 0 1px 0 rgba(255, 255, 255, .96); } .zzm-v4 .zzm-video-play__icon { width: 24px; height: 24px; margin-left: 4px; fill: currentColor; } ``` --- 上一页:[按钮](https://ui.zhaozimin.com/components/button.md) · 下一页:[输入槽](https://ui.zhaozimin.com/components/input.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 输入槽 Input url: https://ui.zhaozimin.com/components/input/ markdown: https://ui.zhaozimin.com/components/input.md group: 组件 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/styles/v4/controls.css # https://ui.zhaozimin.com/src/system/styles/v4/controls.css - src/system/styles/v4/base.css # https://ui.zhaozimin.com/src/system/styles/v4/base.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 输入槽 Input > 键是凸的,槽是凹的。.zzm-input 给输入框一层内凹阴影和近白底色;几何(高 36、圆角 8、.14 描边、内距 12)由调用方写在 style 里。 ### 样张 · 默认 聚焦:描边变墨 + .07 墨色光晕 React: ```jsx export function Basic() { return ( ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html ``` ### 样张 · 朱色聚焦 .zzm-input-accent——只给「下一步就靠它」的那一个输入框 React: ```jsx export function AccentFocus() { return ( ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html ``` ### 样张 · 错误态 朱色描边 + 3% 朱底 + 下方一行说明 React: ```jsx export function Invalid() { return (
邮箱格式不对,少了 @
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
邮箱格式不对,少了 @
``` ### 样张 · 多行 React: ```jsx export function Area() { return ( ``` ## 质感从哪来 `.zzm-input` 只负责两件事:底色 `#FDFDFC`、内凹阴影 `inset 0 1.5px 3px rgba(0,0,0,.06)`。聚焦时 `base.css` 把描边改成墨色(带 `!important`,所以内联描边也会被覆盖)。占位符颜色 `#B4B4AF` 在 `.zzm-v4` 作用域里统一设定。 ## 铁律 - ✓ 要:高 36(与 md 按钮同高,放一行能对齐) - ✗ 不要:输入框做成胶囊 - ✓ 要:错误用朱色描边 + 文字说明怎么改 - ✗ 不要:只变红不说原因 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* tokens.css · 用到的令牌 */ :root { --zzm-accent: #A6402F; } /* base.css */ .zzm-v4 input.zzm-input:focus, .zzm-v4 textarea.zzm-input:focus { border-color: #161616 !important; } .zzm-v4 input.zzm-input-accent:focus { border-color: var(--zzm-accent, #A6402F) !important; } /* controls.css */ .zzm-v4 input.zzm-input, .zzm-v4 textarea.zzm-input { background: #FDFDFC; box-shadow: inset 0 1.5px 3px rgba(0, 0, 0, .06), inset 0 1px 1px rgba(0, 0, 0, .04); } .zzm-v4 input.zzm-input:focus, .zzm-v4 textarea.zzm-input:focus { box-shadow: inset 0 1.5px 3px rgba(0, 0, 0, .06), 0 0 0 3px rgba(22, 22, 22, .07); /* 聚焦光晕 */ } .zzm-v4 input.zzm-input-accent:focus { box-shadow: inset 0 1.5px 3px rgba(0, 0, 0, .06), 0 0 0 3px rgba(166, 64, 47, .14); } ``` --- 上一页:[视频播放键](https://ui.zhaozimin.com/components/video-play.md) · 下一页:[开关](https://ui.zhaozimin.com/components/switch.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 开关 Switch url: https://ui.zhaozimin.com/components/switch/ markdown: https://ui.zhaozimin.com/components/switch.md group: 组件 origin: 提炼自 edu AdmDesign 样张,ui.zhaozimin 是源头 source: - src/extracted/Switch.jsx # https://ui.zhaozimin.com/src/extracted/Switch.jsx - src/extracted/extracted.css # https://ui.zhaozimin.com/src/extracted/extracted.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 开关 Switch > 墨色开关:关闭是浅灰轨道,开启是墨色轨道,白色滑块沿标准缓动滑过去。 ### 样张 · 点一下试试 React: ```jsx import { useState } from 'react' import { Switch } from './extracted/Switch.jsx' export default function SwitchDemo() { const [on, setOn] = useState(true) return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ### 样张 · 三种状态 React: ```jsx import { Switch } from './extracted/Switch.jsx' export function States() { return ( <> ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html ``` ## 属性 | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `checked` | boolean | false | 是否开启(受控) | | `onChange` | (next: boolean) => void | — | 切换回调 | | `disabled` | boolean | false | 禁用:更浅的轨道 + 60% 透明 | | `label` | string | — | 右侧文字;缺省时读屏名为「开关」 | | `id` | string | — | 关联 label 的 for | ## 说明 尺寸 42×25、滑块偏移 2.5 是 edu 样张的原值,为了 1:1 保留,不在 4pt 梯上。它在 edu 里只以后台样张存在,这里提炼成组件,本站是它的源头。 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* extracted.css */ .zzm-v4 .zzm-switch__knob { box-shadow: 0 1px 3px rgba(0, 0, 0, .25); } .zzm-v4 .zzm-switch:disabled .zzm-switch__knob { box-shadow: 0 1px 3px rgba(0, 0, 0, .14); } .zzm-v4 .zzm-switch:focus-visible, .zzm-v4 .zzm-check:focus-visible, .zzm-v4 .zzm-tab:focus-visible, .zzm-v4 .zzm-page:focus-visible, .zzm-v4 .zzm-sort:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .2); } ``` --- 上一页:[输入槽](https://ui.zhaozimin.com/components/input.md) · 下一页:[复选框](https://ui.zhaozimin.com/components/checkbox.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 复选框 Checkbox url: https://ui.zhaozimin.com/components/checkbox/ markdown: https://ui.zhaozimin.com/components/checkbox.md group: 组件 origin: 提炼自 edu AdmDesign 样张,ui.zhaozimin 是源头 source: - src/extracted/Checkbox.jsx # https://ui.zhaozimin.com/src/extracted/Checkbox.jsx - src/extracted/extracted.css # https://ui.zhaozimin.com/src/extracted/extracted.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 复选框 Checkbox > 16px 方框、圆角 4。选中是墨底白勾,未选是 1.5px 描边;文字随状态从灰变深。 ### 样张 · 点一下试试 React: ```jsx import { useState } from 'react' import { Checkbox } from './extracted/Checkbox.jsx' export default function CheckboxDemo() { const [a, setA] = useState(true) const [b, setB] = useState(false) return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ### 样张 · 两种状态 React: ```jsx import { Checkbox } from './extracted/Checkbox.jsx' export function States() { return ( <> ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html ``` ## 属性 | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `checked` | boolean | false | 是否选中(受控) | | `onChange` | (next: boolean) => void | — | 切换回调 | | `label` | string | — | 右侧文字;整行都是点击目标 | | `disabled` | boolean | false | 禁用 | ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* extracted.css */ .zzm-v4 .zzm-switch:focus-visible, .zzm-v4 .zzm-check:focus-visible, .zzm-v4 .zzm-tab:focus-visible, .zzm-v4 .zzm-page:focus-visible, .zzm-v4 .zzm-sort:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .2); } ``` --- 上一页:[开关](https://ui.zhaozimin.com/components/switch.md) · 下一页:[徽章与档位](https://ui.zhaozimin.com/components/badge.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 徽章与档位 Badge url: https://ui.zhaozimin.com/components/badge/ markdown: https://ui.zhaozimin.com/components/badge.md group: 组件 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/screens/v4/course/TierPill.jsx # https://ui.zhaozimin.com/src/system/screens/v4/course/TierPill.jsx - src/system/lib/courseTiers.js # https://ui.zhaozimin.com/src/system/lib/courseTiers.js - src/extracted/Tag.jsx # https://ui.zhaozimin.com/src/extracted/Tag.jsx synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css tokens: [--zzm-tier-free, --zzm-tier-member, --zzm-tier-ext] --- # 徽章与档位 Badge > 一切不可点的标识。圆=徽章不可点,方角=控件可点——这条信号让用户看形状就知道能不能点。 ## 课时档位 免费绿 / 会员黄 / 拓展红——黑白系统里唯一被允许的功能信号色,配色全部走 `--zzm-tier-*` 令牌。`level` 就是课节的 `required_level`:0、1 归免费,2 会员,3 拓展。 ### 样张 · TierPill React: ```jsx import { TierPill } from './screens/v4/course/TierPill.jsx' export function Tiers() { return ( <> ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html 免费 会员 拓展 ``` ### 样张 · 反光扫过 shine:一道反光时不时扫过;delay 逐个错开,避免同屏齐刷 React: ```jsx import { TierPill } from './screens/v4/course/TierPill.jsx' export function TiersShine() { return ( <> ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html 免费 会员 拓展 ``` | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `level` | 0 · 1 · 2 · 3 | 0 | 课节档位(required_level) | | `shine` | boolean | false | 挂反光(.zzm-pill-shine) | | `delay` | number(秒) | 0 | 反光错峰延时,经 --zzm-shine-delay 传入 | | `style` | object | — | 只放 margin 等布局 | ## 小标签与圆徽 ### 样张 · Tag · RoundBadge 9.5px · 600 · 字距 1px · 圆角 4 React: ```jsx import { Tag, StatusPill, RoundBadge } from './extracted/Tag.jsx' export function Tags() { return ( <> 试看 会员 ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html 试看 会员 ``` ## 状态胶囊 ### 样张 · StatusPill 通过 / 中性 / 拦截 React: ```jsx import { Tag, StatusPill, RoundBadge } from './extracted/Tag.jsx' export function Pills() { return ( <> 验证通过 · 可发送 冷却中 · 60s 请求过多 · 暂时拦截 ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html 验证通过 · 可发送 冷却中 · 60s 请求过多 · 暂时拦截 ``` ## 铁律 - ✓ 要:徽章、标签、状态不可点 - ✗ 不要:给徽章加点击事件——要能点就改成按钮 - ✓ 要:档位色只出现在档位胶囊上 - ✗ 不要:用档位绿、黄去表达别的意思 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* tokens.css · 用到的令牌 */ :root { --zzm-tier-free-bg: rgba(22, 163, 74, 0.12); --zzm-tier-free-ink: #157F3C; --zzm-tier-member-bg: rgba(224, 146, 10, 0.18); --zzm-tier-member-ink: #8A5606; --zzm-tier-ext-bg: rgba(166, 64, 47, 0.11); --zzm-tier-ext-ink: #A6402F; --zzm-accent: #A6402F; } /* controls.css */ .zzm-v4 .zzm-pill-shine { position: relative; overflow: hidden; } .zzm-v4 .zzm-pill-shine::after { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; transform: translateX(-180%); background: linear-gradient(105deg, transparent 34%, rgba(255, 255, 255, .85) 50%, transparent 66%); animation: zzmPillShine 4.6s ease-in-out infinite; animation-delay: var(--zzm-shine-delay, 0s); } /* keyframes */ @keyframes zzmPillShine { 0% { transform: translateX(-180%) } 14% { transform: translateX(180%) } 100% { transform: translateX(180%) } } ``` --- 上一页:[复选框](https://ui.zhaozimin.com/components/checkbox.md) · 下一页:[面包屑](https://ui.zhaozimin.com/components/breadcrumb.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 面包屑 Breadcrumb url: https://ui.zhaozimin.com/components/breadcrumb/ markdown: https://ui.zhaozimin.com/components/breadcrumb.md group: 组件 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/components/v4/Breadcrumb.jsx # https://ui.zhaozimin.com/src/system/components/v4/Breadcrumb.jsx - src/system/styles/v4/base.css # https://ui.zhaozimin.com/src/system/styles/v4/base.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 面包屑 Breadcrumb > 层级轨迹:灰字可点、悬停变墨,末项是墨色加粗的当前页,斜杠分隔。全站课程页把它钉在顶栏。 ### 样张 · 三级轨迹 React: ```jsx import { Breadcrumb } from './components/v4/Breadcrumb.jsx' export function Basic() { return ( {} }, { label: 'Obsidian 知识管理', onClick: () => {} }, { label: '学习地图' }, ]} /> ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html ``` ### 样张 · 窄处不换行 调用方传 flexWrap: nowrap + overflow: hidden,每项按剩余空间省略 React: ```jsx import { Breadcrumb } from './components/v4/Breadcrumb.jsx' export function Narrow() { return (
{} }, { label: 'Obsidian 知识管理', onClick: () => {} }, { label: '双链与反向链接:让想法第一次连起来' }, ]} />
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ## 属性 | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `items` | [{ label, onClick? }] | [] | 有 onClick 渲染为可点按钮;没有的是当前页(aria-current);null 项自动过滤 | | `style` | object | — | 并入 nav 容器;窄处传 flexWrap / overflow | ## 规格 12px · 项色 `#8B8B85`(由容器继承)· 分隔符 `#C6C6C0` · 当前项 `#161616` / 600 · 间距 8 · 行高 20。可点项用 `.zzm-inklink`:基色 `inherit`,悬停变墨,键盘焦点朱色光晕。 ## 铁律 - ✓ 要:颜色交给容器继承 - ✗ 不要:在项上写内联 `color`——它会压死 :hover - ✓ 要:轨迹由页面自己组,组件只管长相 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* base.css */ .zzm-v4 .zzm-inklink { color: inherit; transition: color .16s ease; } .zzm-v4 .zzm-inklink:hover { color: #161616; } .zzm-v4 .zzm-inklink:focus-visible { outline: none; border-radius: 4px; box-shadow: 0 0 0 3px rgba(166, 64, 47, .2); } ``` --- 上一页:[徽章与档位](https://ui.zhaozimin.com/components/badge.md) · 下一页:[标签页](https://ui.zhaozimin.com/components/tabs.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 标签页 Tabs url: https://ui.zhaozimin.com/components/tabs/ markdown: https://ui.zhaozimin.com/components/tabs.md group: 组件 origin: 提炼自 edu AdmDesign 样张,ui.zhaozimin 是源头 source: - src/extracted/Tabs.jsx # https://ui.zhaozimin.com/src/extracted/Tabs.jsx - src/extracted/extracted.css # https://ui.zhaozimin.com/src/extracted/extracted.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 标签页 Tabs > 下划线标签页。当前项墨色 600 加 2px 朱色下划线,其余灰字 450。那条朱线就是这一屏的「你在这里」。 ### 样张 · 点一下试试 React: ```jsx import { useState } from 'react' import { Tabs } from './extracted/Tabs.jsx' const ITEMS = ['概览', '课程', '设置'] const BODY = ['学习进度、最近观看与收藏。', '全部课程系列与课节。', '账户、通知与偏好。'] export default function TabsDemo() { const [tab, setTab] = useState(0) return (

{BODY[tab]}

) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html

学习进度、最近观看与收藏。

``` ### 样张 · 静态标记 React: ```jsx import { Tabs } from './extracted/Tabs.jsx' export function Static() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ## 属性 | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `items` | string[] | [] | 标签文字 | | `value` | number | 0 | 当前下标(受控) | | `onChange` | (index) => void | — | 切换回调 | | `label` | string | '标签页' | tablist 的读屏名 | ## 铁律 - ✓ 要:同屏用了朱色下划线,就不再用第二处朱色做装饰 - ✗ 不要:做成胶囊分段控件——需要分段时用 sm 按钮组(当前项墨键) ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* tokens.css · 用到的令牌 */ :root { --zzm-accent: #A6402F; } /* extracted.css */ .zzm-v4 .zzm-tab { transition: color .16s ease; } .zzm-v4 .zzm-tab:hover { color: #161616 !important; } .zzm-v4 .zzm-switch:focus-visible, .zzm-v4 .zzm-check:focus-visible, .zzm-v4 .zzm-tab:focus-visible, .zzm-v4 .zzm-page:focus-visible, .zzm-v4 .zzm-sort:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .2); } ``` --- 上一页:[面包屑](https://ui.zhaozimin.com/components/breadcrumb.md) · 下一页:[分页](https://ui.zhaozimin.com/components/pagination.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 分页 Pagination url: https://ui.zhaozimin.com/components/pagination/ markdown: https://ui.zhaozimin.com/components/pagination.md group: 组件 origin: 提炼自 edu AdmDesign 样张,ui.zhaozimin 是源头 source: - src/extracted/Pagination.jsx # https://ui.zhaozimin.com/src/extracted/Pagination.jsx - src/extracted/extracted.css # https://ui.zhaozimin.com/src/extracted/extracted.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 分页 Pagination > 26px 方格:当前页墨底白字,其余 .14 描边,省略号不加框。页数多时只留首页、末页和当前页前后各一页。 ### 样张 · 点一下试试 React: ```jsx import { useState } from 'react' import { Pagination } from './extracted/Pagination.jsx' export default function PaginationDemo() { const [page, setPage] = useState(1) return } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html ``` ### 样张 · 第 6 页 / 共 12 页 React: ```jsx import { Pagination } from './extracted/Pagination.jsx' export function Static() { return } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html ``` ## 属性 | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `page` | number | 1 | 当前页(从 1 开始,受控) | | `total` | number | 1 | 总页数;≤ 7 全部显示 | | `onChange` | (page) => void | — | 翻页回调,已夹在 1…total 之间 | ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* extracted.css */ .zzm-v4 .zzm-page { transition: border-color .16s ease; } .zzm-v4 .zzm-page:not(:disabled):hover { border-color: rgba(0, 0, 0, .3) !important; } .zzm-v4 .zzm-page:disabled { cursor: not-allowed; } .zzm-v4 .zzm-switch:focus-visible, .zzm-v4 .zzm-check:focus-visible, .zzm-v4 .zzm-tab:focus-visible, .zzm-v4 .zzm-page:focus-visible, .zzm-v4 .zzm-sort:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .2); } ``` --- 上一页:[标签页](https://ui.zhaozimin.com/components/tabs.md) · 下一页:[确认窗](https://ui.zhaozimin.com/components/dialog.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 确认窗 Dialog url: https://ui.zhaozimin.com/components/dialog/ markdown: https://ui.zhaozimin.com/components/dialog.md group: 组件 origin: 提炼自 edu AdmDesign 样张,ui.zhaozimin 是源头 source: - src/extracted/ConfirmDialog.jsx # https://ui.zhaozimin.com/src/extracted/ConfirmDialog.jsx - src/system/styles/v4/controls.css # https://ui.zhaozimin.com/src/system/styles/v4/controls.css - src/extracted/extracted.css # https://ui.zhaozimin.com/src/extracted/extracted.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 确认窗 Dialog > 纸面卡片、宽 440、圆角 16,遮罩只降亮不模糊——背景仍能认出正在操作什么。朱键只用于破坏性确认。 ### 样张 · 真实弹出 Esc 或点遮罩=取消;打开时焦点落在确认键,关闭后归还 React: ```jsx import { useState } from 'react' import { ConfirmProvider, useConfirm } from './extracted/ConfirmDialog.jsx' import { Btn } from './components/v4/Button.jsx' function Actions() { const confirm = useConfirm() const [result, setResult] = useState('还没有操作') const remove = async () => { const ok = await confirm({ title: '确认删除这条笔记?', description: '此操作不可撤销。', tone: 'destructive', confirmText: '删除', }) setResult(ok ? '已删除' : '已取消') } const publish = async () => { const ok = await confirm({ title: '发布这节课?', description: '发布后学员立即可见。', confirmText: '发布' }) setResult(ok ? '已发布' : '已取消') } return (
删除笔记 发布课节 {result}
) } export default function DialogDemo() { return ( ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
还没有操作
``` ### 样张 · 破坏性确认 面板本身(inline 陈列) React: ```jsx import { ConfirmPanel } from './extracted/ConfirmDialog.jsx' export function Destructive() { return ( ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html

确认删除这条笔记?

此操作不可撤销。确认窗保留操作上下文,不让浏览器原生灰框打断视觉语言。

``` ### 样张 · 普通确认 React: ```jsx import { ConfirmPanel } from './extracted/ConfirmDialog.jsx' export function Normal() { return } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html

发布这节课?

发布后学员立即可见。

``` ## useConfirm() 与 edu 的 `useDialog().confirm` 同一个形状:在根部挂一次 `ConfirmProvider`,任何地方 `await confirm({...})` 拿到 `true / false`。 | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `title` | string | — | 标题(17px / 700) | | `description` | string | — | 说明(13px / 20,灰) | | `tone` | 'default' · 'primary' · 'destructive' | 'default' | 确认键:墨键,破坏性用朱键 | | `confirmText` | string | '确认' | 确认键文字,写动作本身(「删除」而不是「确定」) | | `cancelText` | string | '取消' | 取消键文字 | ## 从 edu 到这里改了什么 edu 的确认窗建在 shadcn `dialog.jsx` 与 Tailwind 工具类上。这里去掉了这两层依赖:皮肤仍是 `controls.css` 的 `.zzm-dialog-*`,定位与入场由 `extracted.css` 用纯 CSS 补齐,所以不装 Tailwind 也能 1:1 成立。 ## 铁律 - ✓ 要:朱键只给「删除、作废、注销」这类破坏性确认 - ✗ 不要:用 `window.confirm()` 原生灰框 - ✓ 要:遮罩 `rgba(22,22,22,.46)`,只降亮 - ✗ 不要:给遮罩加模糊 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* controls.css */ .zzm-v4 .zzm-btn { -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 16px; font-family: inherit; font-size: 13px; font-weight: 600; letter-spacing: .2px; white-space: nowrap; cursor: pointer; user-select: none; border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */ border: 1px solid rgba(0, 0, 0, .14); color: #3F3F3B; background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */ 0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */ 0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */ inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */ inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */ transition: box-shadow .16s ease, transform .16s ease, background .16s ease, border-color .16s ease, filter .16s ease, opacity .16s ease; } .zzm-v4 .zzm-btn:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .18); box-shadow: 0 0 0 4px rgba(255, 255, 255, .8), 0 2px 4px rgba(0, 0, 0, .10), 0 4px 12px rgba(0, 0, 0, .09), inset 0 1px 0 rgba(255, 255, 255, .95), inset 0 -1px 0 rgba(0, 0, 0, .05); } .zzm-v4 .zzm-btn:active { transform: translateY(0); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */ box-shadow: 0 0 0 3px rgba(255, 255, 255, .45), 0 1px 0 rgba(255, 255, 255, .65), inset 0 2px 4px rgba(0, 0, 0, .13), inset 0 1px 2px rgba(0, 0, 0, .09); } .zzm-v4 .zzm-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */ 0 1px 2px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-btn:disabled { cursor: not-allowed; transform: none; } .zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) { color: #A2A29C; background: #F2F2EF; border-color: rgba(0, 0, 0, .08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04); } .zzm-v4 .zzm-btn--primary { color: #FFFFFF; border-color: rgba(0, 0, 0, .85); background: linear-gradient(180deg, #3A3A36 0%, #1D1D1B 58%, #141412 100%); box-shadow: 0 0 0 3px rgba(22, 22, 22, .06), 0 1px 2px rgba(0, 0, 0, .18), 0 3px 10px rgba(22, 22, 22, .24), inset 0 1px 0 rgba(255, 255, 255, .20), inset 0 -1px 0 rgba(0, 0, 0, .38); } .zzm-v4 .zzm-btn--primary:hover { filter: brightness(1.12); border-color: rgba(0, 0, 0, .85); box-shadow: 0 0 0 4px rgba(22, 22, 22, .09), 0 2px 4px rgba(0, 0, 0, .20), 0 5px 14px rgba(22, 22, 22, .28), inset 0 1px 0 rgba(255, 255, 255, .24), inset 0 -1px 0 rgba(0, 0, 0, .38); } .zzm-v4 .zzm-btn--primary:active { filter: none; background: linear-gradient(180deg, #101010 0%, #22221F 100%); box-shadow: 0 0 0 3px rgba(22, 22, 22, .06), inset 0 2px 5px rgba(0, 0, 0, .55), inset 0 1px 2px rgba(0, 0, 0, .4); } .zzm-v4 .zzm-btn--accent { color: #FFFFFF; border-color: rgba(122, 42, 29, .9); background: linear-gradient(180deg, #C25743 0%, #A6402F 56%, #99392A 100%); box-shadow: 0 0 0 3px rgba(166, 64, 47, .10), /* 朱色反光光晕 */ 0 1px 2px rgba(0, 0, 0, .14), 0 3px 10px rgba(166, 64, 47, .28), inset 0 1px 0 rgba(255, 255, 255, .32), inset 0 -1px 0 rgba(0, 0, 0, .20); } .zzm-v4 .zzm-btn--accent:hover { filter: brightness(1.06); border-color: rgba(122, 42, 29, .9); box-shadow: 0 0 0 4px rgba(166, 64, 47, .16), 0 2px 4px rgba(0, 0, 0, .16), 0 5px 14px rgba(166, 64, 47, .34), inset 0 1px 0 rgba(255, 255, 255, .36), inset 0 -1px 0 rgba(0, 0, 0, .20); } .zzm-v4 .zzm-btn--accent:active { filter: none; background: linear-gradient(180deg, #8E3425 0%, #A03D2C 100%); box-shadow: 0 0 0 3px rgba(166, 64, 47, .10), inset 0 2px 5px rgba(0, 0, 0, .34), inset 0 1px 2px rgba(0, 0, 0, .24); } .zzm-v4 .zzm-btn--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; } .zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; } .zzm-v4 .zzm-dialog-panel { width: min(calc(100% - 32px), 440px); max-width: 440px; gap: 16px; padding: 24px; border: 1px solid rgba(0, 0, 0, .14); border-radius: 16px; background: #FFFFFF; box-shadow: 0 1px 2px rgba(0, 0, 0, .10), 0 18px 48px rgba(0, 0, 0, .22), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-dialog-title { color: #1D1D1B; font-size: 17px; font-weight: 700; line-height: 24px; } .zzm-v4 .zzm-dialog-description { color: #6E6E68; font-size: 13px; line-height: 20px; } .zzm-v4 .zzm-dialog-actions { margin-top: 4px; gap: 8px; } /* extracted.css */ .zzm-v4 .zzm-dialog-panel { position: fixed; top: 50%; left: 50%; z-index: 51; display: grid; transform: translate(-50%, -50%); animation: zzmDialogIn .2s cubic-bezier(.2, .7, .2, 1) both; } .zzm-v4 .zzm-dialog-panel--inline { position: relative; top: auto; left: auto; z-index: auto; transform: none; animation: none; } .zzm-v4 .zzm-dialog-header { display: grid; gap: 8px; } .zzm-v4 .zzm-dialog-title, .zzm-v4 .zzm-dialog-description { margin: 0; } .zzm-v4 .zzm-dialog-actions { display: flex; justify-content: flex-end; } /* keyframes */ @keyframes zzmDialogIn { from { opacity: 0; transform: translate(-50%, -50%) scale(.96) } to { opacity: 1; transform: translate(-50%, -50%) } } ``` --- 上一页:[分页](https://ui.zhaozimin.com/components/pagination.md) · 下一页:[轻提示](https://ui.zhaozimin.com/components/toast.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 轻提示 Toast url: https://ui.zhaozimin.com/components/toast/ markdown: https://ui.zhaozimin.com/components/toast.md group: 组件 origin: 提炼自 edu AdmDesign 样张,ui.zhaozimin 是源头 source: - src/extracted/Feedback.jsx # https://ui.zhaozimin.com/src/extracted/Feedback.jsx - src/extracted/extracted.css # https://ui.zhaozimin.com/src/extracted/extracted.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 轻提示 Toast > 深墨胶囊,从下方浮起,1.8 秒后消失。它不可点,所以可以是胶囊。文案说结果,不说过程:「已保存到你的笔记库」。 ### 样张 · 点一下试试 React: ```jsx import { useRef, useState } from 'react' import { Toast } from './extracted/Feedback.jsx' import { Btn } from './components/v4/Button.jsx' export default function ToastDemo() { const [show, setShow] = useState(false) const timer = useRef(null) const fire = () => { clearTimeout(timer.current) setShow(false) requestAnimationFrame(() => setShow(true)) timer.current = setTimeout(() => setShow(false), 1800) } return (
保存笔记
{show ? ✓ 已保存到你的笔记库 : null}
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ### 样张 · 静态样式 React: ```jsx import { Toast } from './extracted/Feedback.jsx' export function Static() { return ✓ 已保存到你的笔记库 } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
✓ 已保存到你的笔记库
``` ## 定位的坑 `.zzm-toast` 带 `zzmUp` 进场动画,动画结束帧是 `transform: none`——如果你用 `left 50%` 加 `translateX(-50%)` 居中,动画会把 transform 覆盖掉,提示就偏到右边去了。**用外层 flex 居中**: 正确的居中方式: ```html
✓ 已保存到你的笔记库
``` ## 铁律 - ✓ 要:说结果:「已保存」「已复制」「已删除」 - ✗ 不要:说过程或道歉:「操作成功!」「抱歉,出错了」 - ✓ 要:1.8 秒自动消失;出错要让人处理时用确认窗或内联错误 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* controls.css */ .zzm-v4 .zzm-btn { -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 16px; font-family: inherit; font-size: 13px; font-weight: 600; letter-spacing: .2px; white-space: nowrap; cursor: pointer; user-select: none; border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */ border: 1px solid rgba(0, 0, 0, .14); color: #3F3F3B; background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */ 0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */ 0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */ inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */ inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */ transition: box-shadow .16s ease, transform .16s ease, background .16s ease, border-color .16s ease, filter .16s ease, opacity .16s ease; } .zzm-v4 .zzm-btn:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .18); box-shadow: 0 0 0 4px rgba(255, 255, 255, .8), 0 2px 4px rgba(0, 0, 0, .10), 0 4px 12px rgba(0, 0, 0, .09), inset 0 1px 0 rgba(255, 255, 255, .95), inset 0 -1px 0 rgba(0, 0, 0, .05); } .zzm-v4 .zzm-btn:active { transform: translateY(0); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */ box-shadow: 0 0 0 3px rgba(255, 255, 255, .45), 0 1px 0 rgba(255, 255, 255, .65), inset 0 2px 4px rgba(0, 0, 0, .13), inset 0 1px 2px rgba(0, 0, 0, .09); } .zzm-v4 .zzm-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */ 0 1px 2px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-btn:disabled { cursor: not-allowed; transform: none; } .zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) { color: #A2A29C; background: #F2F2EF; border-color: rgba(0, 0, 0, .08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04); } /* extracted.css */ .zzm-v4 .zzm-toast { display: inline-block; background: #1D1D1B; color: #FFFFFF; font-size: 12px; padding: 9px 18px; border-radius: 999px; white-space: nowrap; box-shadow: 0 8px 24px rgba(0, 0, 0, .3); animation: zzmUp .25s ease both; } /* keyframes */ @keyframes zzmUp { from { opacity: 0; transform: translateY(10px) } to { opacity: 1; transform: none } } ``` --- 上一页:[确认窗](https://ui.zhaozimin.com/components/dialog.md) · 下一页:[进度条](https://ui.zhaozimin.com/components/progress.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 进度条 Progress url: https://ui.zhaozimin.com/components/progress/ markdown: https://ui.zhaozimin.com/components/progress.md group: 组件 origin: 提炼自 edu AdmDesign 样张,ui.zhaozimin 是源头 source: - src/extracted/Feedback.jsx # https://ui.zhaozimin.com/src/extracted/Feedback.jsx synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 进度条 Progress > 4px 细轨,.09 灰底,朱色填充。高度 ≤ 8px 的微件允许 2px 端帽圆角。 ### 样张 · 24% · 64% · 100% React: ```jsx import { Progress } from './extracted/Feedback.jsx' export function Values() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `value` | 0–100 | 0 | 百分比,自动夹紧 | | `label` | string | '进度' | progressbar 的读屏名 | ## 铁律 - ✓ 要:朱色填充表示「你走到了这里」 - ✗ 不要:同屏多条朱色进度条——只有当前关注的那条用朱色,其余用墨色 --- 上一页:[轻提示](https://ui.zhaozimin.com/components/toast.md) · 下一页:[骨架屏](https://ui.zhaozimin.com/components/skeleton.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 骨架屏 Skeleton url: https://ui.zhaozimin.com/components/skeleton/ markdown: https://ui.zhaozimin.com/components/skeleton.md group: 组件 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/components/v4/Skeleton.jsx # https://ui.zhaozimin.com/src/system/components/v4/Skeleton.jsx - src/system/styles/v4/base.css # https://ui.zhaozimin.com/src/system/styles/v4/base.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 骨架屏 Skeleton > 纸灰底块,一道微光 1.4 秒扫过。取代「LOADING…」文字。卡片不另写骨架——同一个组件传 loading,骨架与真实态共用同一段布局。 ### 样张 · 基础骨架块 React: ```jsx import { Skel } from './components/v4/Skeleton.jsx' export function Lines() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `w` | number · string | '100%' | 宽:数字为 px,也可写百分比 | | `h` | number · string | 12 | 高 | | `r` | number | 8 | 圆角(取圆角梯) | | `style` | object | — | 并入,常用于 marginTop | ## 卡片自带骨架 课程卡、系列卡、课节卡都接受 `loading`:在同一段 JSX 里把数据位换成 `Skel`。改真卡的内距或字号,骨架自动跟着变——能消失的分支比能写对的分支好。 ### 样张 · CourseCard · SeriesCard · LessonCard React: ```jsx import { CourseCard } from './components/v4/CourseCard.jsx' import { SeriesCard } from './screens/v4/course/SeriesCard.jsx' import { LessonCard } from './screens/v4/course/LessonCard.jsx' export function CardSkeletons() { return ( <> ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ## 铁律 - ✓ 要:骨架与真实态同源(loading prop) - ✗ 不要:单独手写一份「看起来差不多」的骨架组件 - ✗ 不要:用「加载中…」文字或转圈占位整块内容 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* base.css */ .zzm-v4 .zzm-skel { position: relative; overflow: hidden; background: #EFEFEC; border-radius: 8px; } .zzm-v4 .zzm-skel::after { content: ''; position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .65), transparent); animation: zzmShimmer 1.4s ease infinite; } /* controls.css */ .zzm-v4 .zzm-cardlink { background: #FFFFFF; border: 1px solid rgba(0, 0, 0, .09); border-radius: 16px; padding: 12px; cursor: pointer; box-shadow: 0 1px 2px rgba(0, 0, 0, .04); transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; } .zzm-v4 .zzm-cardlink:hover { transform: translateY(-3px); border-color: rgba(0, 0, 0, .3); box-shadow: 0 10px 30px rgba(0, 0, 0, .09), 0 1px 2px rgba(0, 0, 0, .04); } /* keyframes */ @keyframes zzmShimmer { 100% { transform: translateX(100%) } } ``` --- 上一页:[进度条](https://ui.zhaozimin.com/components/progress.md) · 下一页:[卡片与卡链](https://ui.zhaozimin.com/components/card.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 卡片与卡链 Card url: https://ui.zhaozimin.com/components/card/ markdown: https://ui.zhaozimin.com/components/card.md group: 组件 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/styles/v4/controls.css # https://ui.zhaozimin.com/src/system/styles/v4/controls.css - src/system/styles/v4/base.css # https://ui.zhaozimin.com/src/system/styles/v4/base.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 卡片与卡链 Card > 两种容器:.zzm-card 是浮在纸上的白面板;.zzm-cardlink 是可点的内容卡——悬停时描边加深、上浮 3px、落下柔影。 ## 浮起面板 ### 样张 · .zzm-card 白面 · .09 细线 · 圆角 16 · 两层落影 + 顶部反光 React: ```jsx export function Panel() { return (
本周
完成 6 节 · 共 92 分钟
比上周多 2 节。
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
本周
完成 6 节 · 共 92 分钟
比上周多 2 节。
``` ## 可点卡链 课节、历史、收藏这类「点进去」的卡统一用 `.zzm-cardlink`:白卡承托、内距 12、内嵌图圆角 12。把鼠标移上去。 ### 样张 · .zzm-cardlink React: ```jsx export function CardLink() { return (
双链与反向链接
让想法第一次连起来。
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
双链与反向链接
让想法第一次连起来。
``` ### 样张 · 完成态 --done 整卡朱色 4% 底 + 35% 描边;必须走类,内联 borderColor 会压死悬停描边 React: ```jsx export function Done() { return (
为什么是 Obsidian
整卡朱染:远看就知道这节看完了。
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
为什么是 Obsidian
整卡朱染:远看就知道这节看完了。
``` ## 类名 | 类 | 用途 | |---|---| | `.zzm-card` | 静态内容面板:统计、表单卡、说明卡 | | `.zzm-cardlink` | 可点内容卡:悬停描边 .3 + 上浮 3px + 柔影 | | `.zzm-cardlink--done` | 已完成:整卡朱染 | | `.zzm-hovercard` | 课程卡 / 系列卡的悬停手感(与 cardlink 相同,不带白卡承托) | ## 铁律 - ✓ 要:卡 16、卡内嵌图 12 - ✗ 不要:在卡上内联写 `box-shadow` 或 `borderColor` - ✓ 要:完成态用容器级朱染 - ✗ 不要:在角落放一个 18px 小勾让人去找 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* tokens.css · 用到的令牌 */ :root { --zzm-mono: ui-monospace, "SF Mono", Menlo, monospace; } /* controls.css */ .zzm-v4 .zzm-cardlink { background: #FFFFFF; border: 1px solid rgba(0, 0, 0, .09); border-radius: 16px; padding: 12px; cursor: pointer; box-shadow: 0 1px 2px rgba(0, 0, 0, .04); transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; } .zzm-v4 .zzm-cardlink:hover { transform: translateY(-3px); border-color: rgba(0, 0, 0, .3); box-shadow: 0 10px 30px rgba(0, 0, 0, .09), 0 1px 2px rgba(0, 0, 0, .04); } .zzm-v4 .zzm-cardlink--done { background: rgba(166, 64, 47, .04); border-color: rgba(166, 64, 47, .35); } .zzm-v4 .zzm-cardlink--done:hover { border-color: rgba(166, 64, 47, .55); } .zzm-v4 .zzm-card { background: #FFFFFF; border: 1px solid rgba(0, 0, 0, .09); border-radius: 16px; box-shadow: 0 1px 2px rgba(0, 0, 0, .04), 0 6px 18px rgba(0, 0, 0, .05), inset 0 1px 0 rgba(255, 255, 255, .85); } ``` --- 上一页:[骨架屏](https://ui.zhaozimin.com/components/skeleton.md) · 下一页:[课程卡](https://ui.zhaozimin.com/components/course-card.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 课程卡 CourseCard url: https://ui.zhaozimin.com/components/course-card/ markdown: https://ui.zhaozimin.com/components/course-card.md group: 组件 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/components/v4/CourseCard.jsx # https://ui.zhaozimin.com/src/system/components/v4/CourseCard.jsx synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 课程卡 CourseCard > 综合首页与落地页共用的课程卡:16:9 暗底头图(封面或星座线稿)+ 名称、简介、适合人群、元数据与脚注动作。 登录后的综合首页卡就是基准形态;落地页不再维护变体,只换脚注文案与颜色。 ### 样张 · 综合首页 脚注「进入课程 →」墨色;悬停上浮 React: ```jsx import { CourseCard } from './components/v4/CourseCard.jsx' const DEMO = { num: '01', name: 'Obsidian 知识管理', intro: '从零搭建一套以你为中心、能用十年的知识管理系统。', aud: '刚开始整理数字笔记的知识工作者', cover: null, art: { pl: [{ pts: '28,78 84,30 132,62 186,22 214,52', c: 'rgba(255,255,255,.28)', w: '1.2' }], dt: [ { x: 28, y: 78, r: 2.4, f: '#FFFFFF' }, { x: 84, y: 30, r: 2.4, f: '#FFFFFF' }, { x: 132, y: 62, r: 3.2, f: '@' }, { x: 186, y: 22, r: 2.4, f: '#FFFFFF' }, { x: 214, y: 52, r: 2.4, f: '#FFFFFF' }, ], }, } export function Hub() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
01
Obsidian 知识管理
从零搭建一套以你为中心、能用十年的知识管理系统。
适合 · 刚开始整理数字笔记的知识工作者
6 系列 · 67 节 进入课程 →
``` ### 样张 · 落地页 脚注换成朱色的下一步 React: ```jsx import { CourseCard } from './components/v4/CourseCard.jsx' const DEMO = { num: '01', name: 'Obsidian 知识管理', intro: '从零搭建一套以你为中心、能用十年的知识管理系统。', aud: '刚开始整理数字笔记的知识工作者', cover: null, art: { pl: [{ pts: '28,78 84,30 132,62 186,22 214,52', c: 'rgba(255,255,255,.28)', w: '1.2' }], dt: [ { x: 28, y: 78, r: 2.4, f: '#FFFFFF' }, { x: 84, y: 30, r: 2.4, f: '#FFFFFF' }, { x: 132, y: 62, r: 3.2, f: '@' }, { x: 186, y: 22, r: 2.4, f: '#FFFFFF' }, { x: 214, y: 52, r: 2.4, f: '#FFFFFF' }, ], }, } export function Landing() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
01
Obsidian 知识管理
从零搭建一套以你为中心、能用十年的知识管理系统。
适合 · 刚开始整理数字笔记的知识工作者
6 系列 · 67 节 免费试 3 节 →
``` ### 样张 · 骨架态 React: ```jsx import { CourseCard } from './components/v4/CourseCard.jsx' export function Loading() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ## 属性 | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `course` | { num, name, intro, aud, cover?, art: { pl, dt } } | — | art 是星座线稿:pl 折线、dt 圆点;颜色 "@" 表示品牌朱 | | `meta` | string | — | 左下 mono 元数据,如「6 系列 · 67 节」 | | `cta` | string | '进入课程 →' | 右下动作文案 | | `ctaColor` | string | '#161616' | 动作文案颜色 | | `onClick` | () => void | — | 整卡点击 | | `loading` | boolean | false | 骨架态(同一段布局) | ## 铁律 - ✓ 要:封面按上传原图呈现,不叠蒙层 - ✗ 不要:为了一行小字压暗整幅封面 - ✓ 要:编号用深色小底自持可读性 - ✗ 不要:用 textShadow 救浅字——白底海报上一样糊 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* tokens.css · 用到的令牌 */ :root { --zzm-accent: #A6402F; --zzm-mono: ui-monospace, "SF Mono", Menlo, monospace; } /* base.css */ .zzm-v4 .zzm-hovercard { transition: all .16s ease; } .zzm-v4 .zzm-hovercard:hover { transform: translateY(-3px); border-color: rgba(0, 0, 0, .3) !important; box-shadow: 0 10px 30px rgba(0, 0, 0, .07); } .zzm-v4 .zzm-skel { position: relative; overflow: hidden; background: #EFEFEC; border-radius: 8px; } .zzm-v4 .zzm-skel::after { content: ''; position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .65), transparent); animation: zzmShimmer 1.4s ease infinite; } /* keyframes */ @keyframes zzmShimmer { 100% { transform: translateX(100%) } } ``` --- 上一页:[卡片与卡链](https://ui.zhaozimin.com/components/card.md) · 下一页:[系列卡](https://ui.zhaozimin.com/components/series-card.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 系列卡 SeriesCard url: https://ui.zhaozimin.com/components/series-card/ markdown: https://ui.zhaozimin.com/components/series-card.md group: 组件 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/screens/v4/course/SeriesCard.jsx # https://ui.zhaozimin.com/src/system/screens/v4/course/SeriesCard.jsx synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 系列卡 SeriesCard > 课程总览里的一个系列:头图、名称、两行简介、节数与总时长,再加一排每课一格的进度方块——完成的格子是朱色打勾。 ### 样张 · 完成 3 / 6 总时长由各课 duration_minutes 求和 React: ```jsx import { SeriesCard } from './screens/v4/course/SeriesCard.jsx' export function Progress() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
笔记系统
从收集到输出的流水线:卡片盒方法论、标签与检索、从收集到内化、让笔记长成文章。
6 节课 总时长 1 小时 24 分
已完成 3/6
``` ### 样张 · 选中态 朱色 1px 描边 + 1px 外环 React: ```jsx import { SeriesCard } from './screens/v4/course/SeriesCard.jsx' export function Selected() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
笔记系统
从收集到输出的流水线:卡片盒方法论、标签与检索、从收集到内化、让笔记长成文章。
6 节课 总时长 1 小时 24 分
已完成 3/6
``` ### 样张 · 骨架态 热力格下方「已完成 N/M」行也有等高占位,数据到达时不跳 React: ```jsx import { SeriesCard } from './screens/v4/course/SeriesCard.jsx' export function Loading() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ## 属性 | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `cat` | { name, sub, cover?, ls: 课节数组[] } | — | 系列数据;头图优先 cover,再回退首个有封面的课节,再回退线稿 | | `hist` | { doneIds: Set } | — | 完成的课节 ID 集合 | | `selected` | boolean | false | 选中描边 | | `onClick` | () => void | — | 整卡点击 | | `loading` | boolean | false | 骨架态 | ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* tokens.css · 用到的令牌 */ :root { --zzm-mono: ui-monospace, "SF Mono", Menlo, monospace; --zzm-accent: #A6402F; } /* base.css */ .zzm-v4 .zzm-hovercard { transition: all .16s ease; } .zzm-v4 .zzm-hovercard:hover { transform: translateY(-3px); border-color: rgba(0, 0, 0, .3) !important; box-shadow: 0 10px 30px rgba(0, 0, 0, .07); } .zzm-v4 .zzm-skel { position: relative; overflow: hidden; background: #EFEFEC; border-radius: 8px; } .zzm-v4 .zzm-skel::after { content: ''; position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .65), transparent); animation: zzmShimmer 1.4s ease infinite; } /* keyframes */ @keyframes zzmShimmer { 100% { transform: translateX(100%) } } ``` --- 上一页:[课程卡](https://ui.zhaozimin.com/components/course-card.md) · 下一页:[课节卡](https://ui.zhaozimin.com/components/lesson-card.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 课节卡 LessonCard url: https://ui.zhaozimin.com/components/lesson-card/ markdown: https://ui.zhaozimin.com/components/lesson-card.md group: 组件 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/screens/v4/course/LessonCard.jsx # https://ui.zhaozimin.com/src/system/screens/v4/course/LessonCard.jsx - src/system/screens/v4/course/TierPill.jsx # https://ui.zhaozimin.com/src/system/screens/v4/course/TierPill.jsx synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 课节卡 LessonCard > 一节课:16:9 暗底缩略图,四角是编号、档位角标、时长;未解锁时整卡压暗并居中一把锁;看完后整卡朱染。 ### 样张 · 已完成 整卡朱染 + 标题行「✓ 已完成」 React: ```jsx import { LessonCard } from './screens/v4/course/LessonCard.jsx' // 课节位置数组:[标题, 时长, 简介, md, 免费, 源, uid, 课节ID, 行数据] const lesson = (id, title, level) => [title, '15:32', '让想法第一次连起来。', '', level < 2 ? 1 : 0, '', '', id, { cover_url: null, required_level: level }] const OPEN = { canWatchCourse: () => true, canWatchLesson: () => true } const GUEST = { canWatchCourse: () => false, canWatchLesson: (a) => (a[8]?.required_level ?? 0) < 2 } export function Done() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ### 样张 · 试看 免费课节对未购者显朱色「试看」角标 React: ```jsx import { LessonCard } from './screens/v4/course/LessonCard.jsx' // 课节位置数组:[标题, 时长, 简介, md, 免费, 源, uid, 课节ID, 行数据] const lesson = (id, title, level) => [title, '15:32', '让想法第一次连起来。', '', level < 2 ? 1 : 0, '', '', id, { cover_url: null, required_level: level }] const OPEN = { canWatchCourse: () => true, canWatchLesson: () => true } const GUEST = { canWatchCourse: () => false, canWatchLesson: (a) => (a[8]?.required_level ?? 0) < 2 } export function Trial() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ### 样张 · 会员 · 未解锁 暗底锁罩;锁孔是这张卡唯一的朱色 React: ```jsx import { LessonCard } from './screens/v4/course/LessonCard.jsx' // 课节位置数组:[标题, 时长, 简介, md, 免费, 源, uid, 课节ID, 行数据] const lesson = (id, title, level) => [title, '15:32', '让想法第一次连起来。', '', level < 2 ? 1 : 0, '', '', id, { cover_url: null, required_level: level }] const OPEN = { canWatchCourse: () => true, canWatchLesson: () => true } const GUEST = { canWatchCourse: () => false, canWatchLesson: (a) => (a[8]?.required_level ?? 0) < 2 } export function Locked() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ### 样张 · 拓展 · 未解锁 传 catName 时脚注带系列名 React: ```jsx import { LessonCard } from './screens/v4/course/LessonCard.jsx' // 课节位置数组:[标题, 时长, 简介, md, 免费, 源, uid, 课节ID, 行数据] const lesson = (id, title, level) => [title, '15:32', '让想法第一次连起来。', '', level < 2 ? 1 : 0, '', '', id, { cover_url: null, required_level: level }] const OPEN = { canWatchCourse: () => true, canWatchLesson: () => true } const GUEST = { canWatchCourse: () => false, canWatchLesson: (a) => (a[8]?.required_level ?? 0) < 2 } export function Extension() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ### 样张 · 骨架态 React: ```jsx import { LessonCard } from './screens/v4/course/LessonCard.jsx' export function Loading() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
``` ## 属性 | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `si / li` | number | — | 系列下标 / 课节下标,生成左上 S1 · E04 编号 | | `a` | 课节位置数组 | — | 见下表 | | `plan` | { canWatchCourse(ci), canWatchLesson(a) } | — | 权限视图:决定角标显隐与是否上锁 | | `done` | boolean | — | 已完成 → 整卡朱染 | | `tierTag` | boolean | false | 标题前显档位胶囊(带反光) | | `catName` | string | — | 脚注前缀系列名 | | `ci` | number | 0 | 课程下标(传给 canWatchCourse) | | `loading` | boolean | false | 骨架态 | ## 课节位置数组 a | 下标 | 含义 | |---|---| | 0 | 标题;无封面时截首段(「:」前,≤ 12 字)作卡面短标题 | | 1 | 时长文字,右下角显示 | | 2 | 简介,作脚注 | | 3 | Markdown 笔记(卡片不用) | | 4 | 是否免费 | | 5 / 6 | 视频源标签 / 播放器 uid(卡片不用) | | 7 | 课节 ID | | 8 | 行数据:`cover_url`、`required_level`、`source`、`duration_minutes` | 在 edu 里,悬停可看的课节会预取播放凭证(`hoverPrefetchProps`)。设计系统站用 `src/stubs/playUrlCache.js` 的空实现顶替,源文件一字未改。 ## 铁律 - ✓ 要:四角元素(编号、时长、角标)各自带深色小底 - ✗ 不要:给整张封面加暗色蒙层 - ✓ 要:有封面就不叠短标题——封面图自己写着课名 - ✗ 不要:锁定用 backdrop 模糊(同屏几十张卡,GPU 吃不消) ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* tokens.css · 用到的令牌 */ :root { --zzm-accent: #A6402F; --zzm-mono: ui-monospace, "SF Mono", Menlo, monospace; --zzm-tier-free-bg: rgba(22, 163, 74, 0.12); --zzm-tier-free-ink: #157F3C; --zzm-tier-member-bg: rgba(224, 146, 10, 0.18); --zzm-tier-member-ink: #8A5606; } /* base.css */ .zzm-v4 .zzm-skel { position: relative; overflow: hidden; background: #EFEFEC; border-radius: 8px; } .zzm-v4 .zzm-skel::after { content: ''; position: absolute; inset: 0; transform: translateX(-100%); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .65), transparent); animation: zzmShimmer 1.4s ease infinite; } /* controls.css */ .zzm-v4 .zzm-cardlink { background: #FFFFFF; border: 1px solid rgba(0, 0, 0, .09); border-radius: 16px; padding: 12px; cursor: pointer; box-shadow: 0 1px 2px rgba(0, 0, 0, .04); transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; } .zzm-v4 .zzm-cardlink:hover { transform: translateY(-3px); border-color: rgba(0, 0, 0, .3); box-shadow: 0 10px 30px rgba(0, 0, 0, .09), 0 1px 2px rgba(0, 0, 0, .04); } .zzm-v4 .zzm-cardlink--done { background: rgba(166, 64, 47, .04); border-color: rgba(166, 64, 47, .35); } .zzm-v4 .zzm-cardlink--done:hover { border-color: rgba(166, 64, 47, .55); } .zzm-v4 .zzm-pill-shine { position: relative; overflow: hidden; } .zzm-v4 .zzm-pill-shine::after { content: ""; position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; transform: translateX(-180%); background: linear-gradient(105deg, transparent 34%, rgba(255, 255, 255, .85) 50%, transparent 66%); animation: zzmPillShine 4.6s ease-in-out infinite; animation-delay: var(--zzm-shine-delay, 0s); } /* keyframes */ @keyframes zzmShimmer { 100% { transform: translateX(100%) } } @keyframes zzmPillShine { 0% { transform: translateX(-180%) } 14% { transform: translateX(180%) } 100% { transform: translateX(180%) } } ``` --- 上一页:[系列卡](https://ui.zhaozimin.com/components/series-card.md) · 下一页:[封面图](https://ui.zhaozimin.com/components/cover-image.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 封面图 CoverImage url: https://ui.zhaozimin.com/components/cover-image/ markdown: https://ui.zhaozimin.com/components/cover-image.md group: 组件 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/components/v4/CoverImage.jsx # https://ui.zhaozimin.com/src/system/components/v4/CoverImage.jsx synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 封面图 CoverImage > 封面三态:加载中只露父容器的暗底,就绪后 350ms 淡入,失败回退到线稿。杜绝「先画线稿、封面到了再跳变盖上」的闪跳。 ### 样张 · 三种结局 React: ```jsx import { CoverImage } from './components/v4/CoverImage.jsx' const ACCENT = 'var(--zzm-accent, #A6402F)' const COVER = 'data:image/svg+xml;utf8,' + encodeURIComponent( '' + '上传的课程封面' + '', ) const Fallback = () => ( ) const Frame = ({ label, children }) => (
{children}
{label}
) export default function CoverImageStates() { return (
} /> } /> } />
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
无封面 → 直接画线稿
正常 → 暗底上淡入 350ms
失效 → 回退线稿
``` ## 属性 | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `src` | string · null | — | 封面地址;为空直接渲染 fallback | | `fallback` | ReactNode | null | 无封面或加载失败时的兜底(通常是星座线稿) | | `fit` | 'cover' · 'contain' | 'cover' | 导学封面用 contain 保证完整展示 | | `fadeMs` | number | 350 | 淡入时长 | 父容器负责暗底、比例和圆角(`aspect-ratio: 16/9; background: #131316; position: relative; overflow: hidden`);图片绝对定位铺满。 ## 一个真实踩过的坑 图片的 `load` 事件不冒泡,React 的 `onLoad` 要等 commit 后才绑定。磁盘缓存命中时,字节比监听先到,`load` 已经触发过了,组件会永远卡在「加载中」——图片明明就绪却 `opacity: 0`,回访用户只看见一片黑。所以这个组件挂载后先检查 `el.complete`,没就绪才补挂**原生**的 load / error 监听。复刻时别换回 `onLoad`。 ## 铁律 - ✓ 要:封面按上传原图呈现 - ✗ 不要:提供「蒙层」参数——叠字可读性由那颗字自己解决 --- 上一页:[课节卡](https://ui.zhaozimin.com/components/lesson-card.md) · 下一页:[神秘卡](https://ui.zhaozimin.com/components/mystery-card.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 神秘卡 MysteryCard url: https://ui.zhaozimin.com/components/mystery-card/ markdown: https://ui.zhaozimin.com/components/mystery-card.md group: 组件 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/components/v4/MysteryCard.jsx # https://ui.zhaozimin.com/src/system/components/v4/MysteryCard.jsx synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 神秘卡 MysteryCard > 尚未揭晓的课:3D 绸缎遮罩、朱印封蜡、一道流光,悬停时微微掀起。不显示名称和价格,也不能购买。 ### 样张 · 把鼠标移上去 React: ```jsx import { MysteryCard } from './components/v4/MysteryCard.jsx' export default function MysteryCardDemo() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
即将揭晓
敬请期待
``` ## 刻意的例外 3D 绸缎是这套系统里**有意为之的例外**,专为「神秘感」破例;配色仍守黑白灰加一枚朱色封蜡。它自带全部样式(包括注入自己的 `@keyframes zzmMysterySheen`),尺寸随网格走(宽高比 266 : 316)。 只有一种用法:课程目录里 `comingSoon` 的课渲染成它。不要把这种质感用到别的地方。 ## 铁律 - ✓ 要:只用于尚未上线的课程 - ✗ 不要:把 3D、流光这类效果搬到普通卡片上 --- 上一页:[封面图](https://ui.zhaozimin.com/components/cover-image.md) · 下一页:[星图](https://ui.zhaozimin.com/components/star-map.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 星图 StarMap url: https://ui.zhaozimin.com/components/star-map/ markdown: https://ui.zhaozimin.com/components/star-map.md group: 组件 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/components/v4/StarMapSvg.jsx # https://ui.zhaozimin.com/src/system/components/v4/StarMapSvg.jsx - src/system/domain/v4/mapGeometry.js # https://ui.zhaozimin.com/src/system/domain/v4/mapGeometry.js synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 星图 StarMap > 学习地图:一条折线穿过五个站点。走过的站墨色实心、实线相连;当前站朱色,外圈一道脉冲光晕;还没到的站白底灰边、虚线相连。 ### 样张 · 5 站 · 已完成 2 站 React: ```jsx import { StarMapSvg } from './components/v4/StarMapSvg.jsx' import { buildStarMap } from './domain/v4/mapGeometry.js' const CATS = [{ ls: [['为什么是 Obsidian'], ['建库与初始设置'], ['界面与核心操作'], ['双链与反向链接'], ['图谱视图'], ['插件安装与安全'], ['Dataview'], ['Templater'], ['卡片盒方法论'], ['从收集到内化']] }] const MAP = { nodes: [ { t: '认识与建库', ref: [[0, 0], [0, 1]] }, { t: '连接想法', ref: [[0, 2], [0, 3]] }, { t: '图谱与插件', ref: [[0, 4], [0, 5], [0, 6], [0, 7]] }, { t: '笔记方法', ref: [[0, 8]] }, { t: '内化输出', ref: [[0, 9]] }, ], pts: [[86, 292], [212, 148], [336, 262], [478, 128], [614, 268]], } const DONE = new Set(['0-0-0', '0-0-1', '0-0-2', '0-0-3', '0-0-4']) export function Roadmap() { const geo = buildStarMap(MAP, CATS, DONE, 0) return (
{}} />
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
第 1 站 认识与建库 ✓ 为什么是 Obsid… ✓ 建库与初始设置 第 2 站 连接想法 ✓ 界面与核心操作 ✓ 双链与反向链接 第 3 站 3 图谱与插件 ✓ 图谱视图 插件安装与安全 Dataview … 共 4 节 第 4 站 4 笔记方法 卡片盒方法论 第 5 站 5 内化输出 从收集到内化
``` ## 几何从哪来 `buildStarMap(map, cats, doneSet, courseIdx)` 把地图数据变成绘图几何:22 颗装饰背景星、站间连线、每站前三节课的预览。`map.pts` 是 740 × 400 画布上的坐标,与 `map.nodes` 一一对应;`doneSet` 里的键是 `课程-系列-课节`。 | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `geo` | buildStarMap(...) 的返回值 | — | { stars, edges, nodes, … } | | `onNodeClick` | (index) => void | — | 点击站点 | ## 铁律 - ✓ 要:朱色只给当前站:它就是「你在这里」 - ✗ 不要:给已完成的站也上朱色 - ✓ 要:脉冲光晕只挂在当前站(zzmPulse 2.6s) ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* tokens.css · 用到的令牌 */ :root { --zzm-mono: ui-monospace, "SF Mono", Menlo, monospace; --zzm-accent: #A6402F; } /* base.css */ .zzm-v4 .zzm-node { transition: opacity .15s ease; } .zzm-v4 .zzm-node:hover { opacity: .75; } ``` --- 上一页:[神秘卡](https://ui.zhaozimin.com/components/mystery-card.md) · 下一页:[侧栏导航](https://ui.zhaozimin.com/patterns/sidebar.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 侧栏导航 Sidebar url: https://ui.zhaozimin.com/patterns/sidebar/ markdown: https://ui.zhaozimin.com/patterns/sidebar.md group: 模式 origin: 复刻自 edu 同名界面的展示版,剥离了业务状态 source: - src/extracted/SidebarNav.jsx # https://ui.zhaozimin.com/src/extracted/SidebarNav.jsx - src/extracted/icons.jsx # https://ui.zhaozimin.com/src/extracted/icons.jsx synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 侧栏导航 Sidebar > 纸色侧栏,宽 266。导航行是扁平的,不是键——当前行只加一层 .07 灰底和 600 字重;真正的操作(收起、退出)才用微拟物键。 ### 样张 · 综合页侧栏 悬停导航行、名片、退出键试试 React: ```jsx import { SidebarNav } from './extracted/SidebarNav.jsx' import { ClockIcon, HomeIcon, MapIcon, PlayRectIcon, StarIcon } from './extracted/icons.jsx' export function Hub() { return ( }] }, { items: [ { id: 'fav', label: '我的收藏', icon: , count: 12 }, { id: 'hist', label: '历史记录', icon: , count: 38 }, ] }, ]} user={{ name: '学员示例', email: 'learner@example.com' }} /> ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html ``` ### 样张 · 课程页侧栏 分组标签:mono 10 · 字距 1.5 · #A2A29C React: ```jsx import { SidebarNav } from './extracted/SidebarNav.jsx' import { ClockIcon, HomeIcon, MapIcon, PlayRectIcon, StarIcon } from './extracted/icons.jsx' export function Course() { return ( }, { id: 'ovw', label: '课程总览', icon: }, ] }, { label: '主线课程', items: [ { id: 's1', label: '软件基础', count: 5 }, { id: 's2', label: '插件教学', count: 4 }, { id: 's3', label: '笔记系统', count: 4 }, ] }, { label: '增值课程', items: [{ id: 's4', label: '人脉管理', count: 3 }] }, ]} /> ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html ``` ## 规格 | 部位 | 规格 | |---|---| | 容器 | 宽 266 · 底 `#F6F6F4` · 右侧 .07 细线 · 进场 `zzmSlide .28s` | | 头部 | 收起键(28 幽灵图标键,恒位左 14 / 上 12)+ 星座标 22 + 品牌名 15 / 650 | | 导航行 | 内距 9 × 10 · 圆角 8 · 13.5px · 当前行 `.07` 灰底 + 600 · 悬停 `.zzm-navbtn` | | 计数 | mono 9.5 · `#B0B0AA` · 两位补零 | | 名片 | 36 圆形头像 · 名字 13 / 600 · 邮箱 10.5 灰 · 悬停 `.zzm-userfoot` | | 退出 | `.zzm-btn --block` 纸键 | ## 铁律 - ✓ 要:导航行扁平,当前行灰底 - ✗ 不要:把导航行做成一排凸键 - ✓ 要:收起键展开态与收起态同一坐标,零位移 - ✗ 不要:用朱色标当前导航项——朱色留给内容里的「下一步」 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* tokens.css · 用到的令牌 */ :root { --zzm-accent: #A6402F; } /* base.css */ .zzm-v4 .zzm-navbtn:hover { background: rgba(0, 0, 0, .07) !important; } .zzm-v4 .zzm-userfoot:hover { background: rgba(0, 0, 0, .045); border-color: rgba(0, 0, 0, .07) !important; } /* controls.css */ .zzm-v4 .zzm-btn { -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 16px; font-family: inherit; font-size: 13px; font-weight: 600; letter-spacing: .2px; white-space: nowrap; cursor: pointer; user-select: none; border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */ border: 1px solid rgba(0, 0, 0, .14); color: #3F3F3B; background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */ 0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */ 0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */ inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */ inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */ transition: box-shadow .16s ease, transform .16s ease, background .16s ease, border-color .16s ease, filter .16s ease, opacity .16s ease; } .zzm-v4 .zzm-btn:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .18); box-shadow: 0 0 0 4px rgba(255, 255, 255, .8), 0 2px 4px rgba(0, 0, 0, .10), 0 4px 12px rgba(0, 0, 0, .09), inset 0 1px 0 rgba(255, 255, 255, .95), inset 0 -1px 0 rgba(0, 0, 0, .05); } .zzm-v4 .zzm-btn:active { transform: translateY(0); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */ box-shadow: 0 0 0 3px rgba(255, 255, 255, .45), 0 1px 0 rgba(255, 255, 255, .65), inset 0 2px 4px rgba(0, 0, 0, .13), inset 0 1px 2px rgba(0, 0, 0, .09); } .zzm-v4 .zzm-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */ 0 1px 2px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-btn:disabled { cursor: not-allowed; transform: none; } .zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) { color: #A2A29C; background: #F2F2EF; border-color: rgba(0, 0, 0, .08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04); } .zzm-v4 .zzm-btn--ghost { border-color: transparent; background: transparent; box-shadow: none; color: #4A4A46; } .zzm-v4 .zzm-btn--ghost:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .14); background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), 0 1px 2px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-btn--ghost:active { transform: translateY(0); border-color: rgba(0, 0, 0, .14); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); box-shadow: inset 0 2px 4px rgba(0, 0, 0, .12); } .zzm-v4 .zzm-btn--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; } .zzm-v4 .zzm-btn--icon { width: 36px; padding: 0; } .zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; } .zzm-v4 .zzm-btn--icon.zzm-btn--lg { width: 44px; } .zzm-v4 .zzm-btn--block { width: 100%; } ``` --- 上一页:[星图](https://ui.zhaozimin.com/components/star-map.md) · 下一页:[数据表格](https://ui.zhaozimin.com/patterns/data-table.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 数据表格 DataTable url: https://ui.zhaozimin.com/patterns/data-table/ markdown: https://ui.zhaozimin.com/patterns/data-table.md group: 模式 origin: 提炼自 edu AdmDesign 样张,ui.zhaozimin 是源头 source: - src/extracted/DataTable.jsx # https://ui.zhaozimin.com/src/extracted/DataTable.jsx - src/extracted/extracted.css # https://ui.zhaozimin.com/src/extracted/extracted.css synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 数据表格 DataTable > 同名但用途不同的数据先用一排小键分区,再进表格。浅灰 mono 表头、可排序列、细线分行;窄屏只横向滚动,不折行。 ### 样张 · 分区 + 排序 点表头排序;再点一次反向 React: ```jsx import { useMemo, useState } from 'react' import { DataTable } from './extracted/DataTable.jsx' import { StatusPill } from './extracted/Tag.jsx' import { Btn } from './components/v4/Button.jsx' const ROWS = [ { code: 'G7KM4Q2HP9RX', from: '邀请人甲', to: '新学员 A', used: true, reward: 100 }, { code: 'T2WQ8LN5VJ3C', from: '邀请人乙', to: '新学员 B', used: false, reward: 0 }, { code: 'M9PD6RZ4KX1B', from: '邀请人丙', to: '新学员 C', used: true, reward: 200 }, ] const COLUMNS = [ { key: 'code', label: '邀请码', fr: 1.2, mono: true }, { key: 'from', label: '邀请人', fr: 1.1 }, { key: 'to', label: '被邀请人', fr: 1.1 }, { key: 'used', label: '状态', fr: 0.7, render: (r) => {r.used ? '已使用' : '可用'} }, { key: 'reward', label: '奖励', fr: 0.6, mono: true, accent: true, render: (r) => `¥${r.reward}` }, ] export default function DataTableDemo() { const [scope, setScope] = useState('register') const [sort, setSort] = useState({ key: 'from', dir: 'asc' }) const rows = useMemo(() => { const out = [...ROWS].sort((a, b) => (a[sort.key] > b[sort.key] ? 1 : a[sort.key] < b[sort.key] ? -1 : 0)) return sort.dir === 'desc' ? out.reverse() : out }, [sort]) const onSort = (key) => setSort((s) => ({ key, dir: s.key === key && s.dir === 'asc' ? 'desc' : 'asc' })) return (
setScope('register')}>注册激活码 setScope('course')}>课程 / 会员激活码
r.code} />
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
M9PD6RZ4KX1B 邀请人丙 新学员 C 已使用 ¥200
T2WQ8LN5VJ3C 邀请人乙 新学员 B 可用 ¥0
G7KM4Q2HP9RX 邀请人甲 新学员 A 已使用 ¥100
``` ### 样张 · 静态标记 React: ```jsx import { DataTable } from './extracted/DataTable.jsx' import { StatusPill } from './extracted/Tag.jsx' export function Static() { return ( {r.state} }, { key: 'reward', label: '奖励', fr: 0.6, mono: true, accent: true }, ]} rows={[{ code: 'G7KM4Q2HP9RX', from: '邀请人甲', state: '已使用', reward: '¥100' }]} minWidth={560} /> ) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
邀请码 邀请人 状态 奖励
G7KM4Q2HP9RX 邀请人甲 已使用 ¥100
``` ## 属性 | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `columns` | [{ key, label, fr?, mono?, accent?, render?, sortable? }] | [] | fr 为列宽份额;mono 用等宽字;accent 用朱色(金额) | | `rows` | object[] | [] | 数据行 | | `sort` | { key, dir: 'asc' · 'desc' } | — | 当前排序(受控) | | `onSort` | (key) => void | — | 传了才显示排序键 | | `minWidth` | number | 680 | 内容最小宽度,窄屏靠横向滚动 | | `rowKey` | (row, i) => key | 下标 | 行的 key | ## 规格 表头:内距 10 × 18 · 底 `#FAFAF8` · mono 10 · 字距 1 · `#A2A29C` · 下方 .07 细线。数据行:内距 11 × 18 · 12px · 行间 .07 细线。容器:.09 描边 · 圆角 16。分区键:当前用 sm 墨键,其余 sm 纸键。 ## 本页用到的 CSS 按样张里出现的类名,从源文件原样裁出(完整版见 zzm.css): ```css /* tokens.css · 用到的令牌 */ :root { --zzm-accent: #A6402F; } /* controls.css */ .zzm-v4 .zzm-btn { -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 16px; font-family: inherit; font-size: 13px; font-weight: 600; letter-spacing: .2px; white-space: nowrap; cursor: pointer; user-select: none; border-radius: 8px; /* 圆角矩形——按钮禁胶囊 */ border: 1px solid rgba(0, 0, 0, .14); color: #3F3F3B; background: linear-gradient(180deg, #FFFFFF 0%, #F2F2EF 100%); box-shadow: 0 0 0 3px rgba(255, 255, 255, .55), /* ④ 反光光晕环 */ 0 1px 2px rgba(0, 0, 0, .09), /* ③ 近影 */ 0 2px 6px rgba(0, 0, 0, .06), /* ③ 远影 */ inset 0 1px 0 rgba(255, 255, 255, .92), /* ② 顶部反光 */ inset 0 -1px 0 rgba(0, 0, 0, .05); /* ① 底缘收沉 */ transition: box-shadow .16s ease, transform .16s ease, background .16s ease, border-color .16s ease, filter .16s ease, opacity .16s ease; } .zzm-v4 .zzm-btn:hover { transform: translateY(-1px); border-color: rgba(0, 0, 0, .18); box-shadow: 0 0 0 4px rgba(255, 255, 255, .8), 0 2px 4px rgba(0, 0, 0, .10), 0 4px 12px rgba(0, 0, 0, .09), inset 0 1px 0 rgba(255, 255, 255, .95), inset 0 -1px 0 rgba(0, 0, 0, .05); } .zzm-v4 .zzm-btn:active { transform: translateY(0); background: linear-gradient(180deg, #ECECE9 0%, #F4F4F1 100%); /* ⑤ 凹面翻转 */ box-shadow: 0 0 0 3px rgba(255, 255, 255, .45), 0 1px 0 rgba(255, 255, 255, .65), inset 0 2px 4px rgba(0, 0, 0, .13), inset 0 1px 2px rgba(0, 0, 0, .09); } .zzm-v4 .zzm-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .20), /* 键盘焦点=朱色光晕 */ 0 1px 2px rgba(0, 0, 0, .09), 0 2px 6px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .92); } .zzm-v4 .zzm-btn:disabled { cursor: not-allowed; transform: none; } .zzm-v4 .zzm-btn:disabled:not(.zzm-btn--busy) { color: #A2A29C; background: #F2F2EF; border-color: rgba(0, 0, 0, .08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, .7), 0 1px 2px rgba(0, 0, 0, .04); } .zzm-v4 .zzm-btn--primary { color: #FFFFFF; border-color: rgba(0, 0, 0, .85); background: linear-gradient(180deg, #3A3A36 0%, #1D1D1B 58%, #141412 100%); box-shadow: 0 0 0 3px rgba(22, 22, 22, .06), 0 1px 2px rgba(0, 0, 0, .18), 0 3px 10px rgba(22, 22, 22, .24), inset 0 1px 0 rgba(255, 255, 255, .20), inset 0 -1px 0 rgba(0, 0, 0, .38); } .zzm-v4 .zzm-btn--primary:hover { filter: brightness(1.12); border-color: rgba(0, 0, 0, .85); box-shadow: 0 0 0 4px rgba(22, 22, 22, .09), 0 2px 4px rgba(0, 0, 0, .20), 0 5px 14px rgba(22, 22, 22, .28), inset 0 1px 0 rgba(255, 255, 255, .24), inset 0 -1px 0 rgba(0, 0, 0, .38); } .zzm-v4 .zzm-btn--primary:active { filter: none; background: linear-gradient(180deg, #101010 0%, #22221F 100%); box-shadow: 0 0 0 3px rgba(22, 22, 22, .06), inset 0 2px 5px rgba(0, 0, 0, .55), inset 0 1px 2px rgba(0, 0, 0, .4); } .zzm-v4 .zzm-btn--sm { height: 28px; padding: 0 12px; font-size: 12px; gap: 4px; } .zzm-v4 .zzm-btn--icon.zzm-btn--sm { width: 28px; } /* extracted.css */ .zzm-v4 .zzm-sort { transition: color .16s ease; } .zzm-v4 .zzm-sort:hover { color: #161616 !important; } .zzm-v4 .zzm-switch:focus-visible, .zzm-v4 .zzm-check:focus-visible, .zzm-v4 .zzm-tab:focus-visible, .zzm-v4 .zzm-page:focus-visible, .zzm-v4 .zzm-sort:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(166, 64, 47, .2); } ``` --- 上一页:[侧栏导航](https://ui.zhaozimin.com/patterns/sidebar.md) · 下一页:[安全验证外壳](https://ui.zhaozimin.com/patterns/verify-shell.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 安全验证外壳 VerifyShell url: https://ui.zhaozimin.com/patterns/verify-shell/ markdown: https://ui.zhaozimin.com/patterns/verify-shell.md group: 模式 origin: 提炼自 edu AdmDesign 样张,ui.zhaozimin 是源头 source: - src/extracted/VerifyShell.jsx # https://ui.zhaozimin.com/src/extracted/VerifyShell.jsx - src/extracted/Tag.jsx # https://ui.zhaozimin.com/src/extracted/Tag.jsx synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 安全验证外壳 VerifyShell > 第三方人机验证(Cloudflare Turnstile、阿里云验证码)的外围壳:始终可见、占位固定,底下三种反馈态——通过、冷却、限流。 ### 样张 · 外壳 + 三种反馈 React: ```jsx import { VerifyShell } from './extracted/VerifyShell.jsx' export function Shell() { return (
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html
第三方安全验证 · 始终可见外围壳
正在进行人机验证… CLOUDFLARE TURNSTILE
验证通过 · 可发送 冷却中 · 60s 请求过多 · 暂时拦截
``` ## 为什么要一个外壳 第三方验证控件的 iframe 加载慢、时有时无。直接放进表单,它出现和消失会把表单顶得上下跳。外壳先占住位置(最小高 54),真实 iframe 渲染在里面(`children`);外壳下方用状态胶囊统一给出反馈。 | 属性 | 取值 | 默认 | 说明 | |---|---|---|---| | `title` | string | '第三方安全验证 · 始终可见外围壳' | 外壳标题 | | `provider` | string | 'CLOUDFLARE\nTURNSTILE' | 右侧服务商标识(未传 children 时显示) | | `states` | ('ok' · 'cooldown' · 'limited')[] | 三种都显示 | 要显示的反馈态 | | `children` | ReactNode | — | 真实验证控件挂载点 | ## 规则 - ✓ 要:单次 token:每次发送验证码前重新验证 - ✓ 要:发送后 60 秒冷却,冷却中显示倒计时 - ✓ 要:被限流时明确说「请求过多 · 暂时拦截」 - ✗ 不要:验证失败时只让按钮变灰、不说原因 --- 上一页:[数据表格](https://ui.zhaozimin.com/patterns/data-table.md) · 下一页:[笔记标注块](https://ui.zhaozimin.com/patterns/callouts.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 笔记标注块 Callouts url: https://ui.zhaozimin.com/patterns/callouts/ markdown: https://ui.zhaozimin.com/patterns/callouts.md group: 模式 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/components/v4/MarkdownNotes.jsx # https://ui.zhaozimin.com/src/system/components/v4/MarkdownNotes.jsx - src/system/lib/markdown.js # https://ui.zhaozimin.com/src/system/lib/markdown.js synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 笔记标注块 Callouts > 课程笔记用 Obsidian 的语法和官方色板:老用户零学习成本,一眼分清提示、警告和完成。这是刻意的边界——笔记区是内容,不是产品界面。 ### 样张 · 一段课程笔记 React: ```jsx import { MarkdownNotes } from './components/v4/MarkdownNotes.jsx' const MD = `## 本节要点 把收集、整理、输出三件事拆开做,每一步只解决一个问题。 - 收集:先存,不分类 - 整理:每周一次,按项目归档 - 输出:一篇笔记只回答一个问题 > [!note] 笔记 > 标注块语法与 Obsidian 完全一致,老用户零学习成本。 > [!tip] 技巧 > 在笔记里写 \`[跳到第 3 节](#jump:demo-3)\` 会生成朱色跳转胶囊。 > [!warning]- 注意(点我展开) > 折叠标注块默认收起,标题后加 \`-\` 即可。 > [!success] 完成 > 看完这节,你应该能搭出自己的收件箱。 | 阶段 | 频率 | 产出 | |---|---|---| | 收集 | 随时 | 收件箱 | | 整理 | 每周 | 项目笔记 | \`\`\`js // 围栏代码块:缩进逐字保真 function inbox(note) { return { ...note, tags: [] } } \`\`\` ` export default function CalloutsDemo() { return (
{}} />
) } ``` HTML(构建时由上面的 React 渲染得到,可直接粘贴): ```html

本节要点

把收集、整理、输出三件事拆开做,每一步只解决一个问题。

  • 收集:先存,不分类
  • 整理:每周一次,按项目归档
  • 输出:一篇笔记只回答一个问题
笔记

标注块语法与 Obsidian 完全一致,老用户零学习成本。

技巧

在笔记里写 [跳到第 3 节](#jump:demo-3) 会生成朱色跳转胶囊。

注意(点我展开)
完成

看完这节,你应该能搭出自己的收件箱。

阶段 频率 产出
收集 随时 收件箱
整理 每周 项目笔记
// 围栏代码块:缩进逐字保真
function inbox(note) {
  return { ...note, tags: [] }
}
``` ## 十三种标注块 | 类型 | 默认标题 | 色族 | RGB | 图标 | |---|---|---|---|---| | `note` | Note | blue | `8, 109, 221` | Pencil | | `abstract` | Abstract | cyan | `0, 191, 188` | ClipboardList | | `info` | Info | blue | `8, 109, 221` | Info | | `todo` | Todo | blue | `8, 109, 221` | CheckCircle2 | | `tip` | Tip | cyan | `0, 191, 188` | Flame | | `success` | Success | green | `8, 185, 78` | Check | | `question` | Question | yellow | `224, 172, 0` | HelpCircle | | `warning` | Warning | orange | `236, 117, 0` | AlertTriangle | | `failure` | Failure | red | `233, 49, 71` | X | | `danger` | Danger | red | `233, 49, 71` | Zap | | `bug` | Bug | red | `233, 49, 71` | Bug | | `example` | Example | purple | `120, 82, 238` | List | | `quote` | Quote | mono | `90, 90, 86` | Quote | 类型大小写不敏感;未知类型回落到 `note`(与 Obsidian 一致)。别名:`summary`/`tldr`→abstract,`hint`/`important`→tip,`check`/`done`→success,`help`/`faq`→question,`caution`/`attention`→warning,`fail`/`missing`→failure,`error`→danger,`cite`→quote。标题后加 `-` 默认折叠、加 `+` 默认展开。 ## 还支持 标题、段落、列表(含中文顿号 `1、`)、可横向滚动的表格、**缩进逐字保真**的围栏代码块、引用、裸链接自动识别、点击放大的图片,以及 `[文字](#jump:课节ID)` 朱色跳转胶囊——点它跳到对应课节。 ## 铁律 - ✓ 要:笔记区(内容)用 Obsidian 官方色 - ✗ 不要:把这些颜色带进按钮、卡片、徽章等产品界面 - ✓ 要:链接没通过白名单(safeHref)时降级为纯文本 --- 上一页:[安全验证外壳](https://ui.zhaozimin.com/patterns/verify-shell.md) · 下一页:[勋章与下载](https://ui.zhaozimin.com/resources/assets.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 勋章与下载 Assets url: https://ui.zhaozimin.com/resources/assets/ markdown: https://ui.zhaozimin.com/resources/assets.md group: 资源 origin: 逐字同步自 edu.zhaozimin v23.5.1(086937d),edu 是源头 source: - src/system/manifest.json # https://ui.zhaozimin.com/src/system/manifest.json synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 勋章与下载 Assets > 会员勋章、全部机读下载件,以及本站每一个源文件的出处与指纹。 ## 会员勋章 ## 下载件 | 文件 | 内容 | |---|---| | /zzm.css | 全部样式(.zzm-v4 作用域) | | /zzm.global.css | 全部样式(无作用域) | | /tokens.json | W3C 设计令牌 | | /DESIGN.md | 单文件规范 | | /llms.txt | 全站目录 | | /llms-full.txt | 全站全文 | | /favicon.svg | 星座标(墨底反白) | ## 同步自 edu v23.5.1(086937d · 2026-09-23) 以下文件逐字复制自 edu.zhaozimin,`src/system/` 只读。sha256 与 `/src/system/manifest.json` 一致;构建前 `npm run check:drift` 会逐个核对。 | 文件 | 角色 | sha256 | 字节 | |---|---|---|---| | src/system/styles/v4/tokens.css | style | `9012c08bcbbc` | 3729 | | src/system/styles/v4/keyframes.css | style | `43cef3e5325c` | 2164 | | src/system/styles/v4/base.css | style | `dc7c5fe86bfd` | 4610 | | src/system/styles/v4/controls.css | style | `3790a93a67e6` | 16912 | | src/system/styles/v4/SPACING.md | spec | `ccaa64a37854` | 7354 | | src/system/components/v4/Button.jsx | component | `414b31e1a2ef` | 1996 | | src/system/components/v4/Breadcrumb.jsx | component | `4af9a08bb456` | 2372 | | src/system/components/v4/Skeleton.jsx | component | `a5a10b30b729` | 1069 | | src/system/components/v4/CoverImage.jsx | component | `f684b534fe36` | 2910 | | src/system/components/v4/CourseCard.jsx | component | `7d4811d83d26` | 4868 | | src/system/components/v4/MysteryCard.jsx | component | `43324542a505` | 4343 | | src/system/components/v4/StarMapSvg.jsx | component | `87081c12856f` | 2122 | | src/system/components/v4/MarkdownNotes.jsx | component | `c4c2face72f1` | 12651 | | src/system/components/course/ImageLightbox.jsx | dependency | `be2db36a21ae` | 3708 | | src/system/screens/v4/course/LessonCard.jsx | component | `bde44ea3b2e2` | 10862 | | src/system/screens/v4/course/SeriesCard.jsx | component | `15d19bca55f3` | 6661 | | src/system/screens/v4/course/TierPill.jsx | component | `51e2ea1045e6` | 1295 | | src/system/lib/courseTiers.js | dependency | `20f029d930c4` | 2002 | | src/system/lib/markdown.js | dependency | `198e538bdd95` | 28934 | | src/system/lib/motion.js | dependency | `03e85c605500` | 5553 | | src/system/lib/siteIcons.js | dependency | `6e4ce239df38` | 2104 | | src/system/domain/v4/mapGeometry.js | dependency | `e04543df05b6` | 3502 | | public/v4/badge-0.png | asset | `e532e7d00255` | 189335 | | public/v4/badge-1.png | asset | `975943c5bdd6` | 214276 | | public/v4/badge-2.png | asset | `1b546c46a273` | 215027 | | public/v4/badge-all.png | asset | `7ad4a48f0f7a` | 225098 | | public/beian-gongan.png | asset | `974bbcc14bfa` | 2330 | | public/favicon.svg | asset | `b77a12077e99` | 451 | | public/favicon.png | asset | `5bb4941163fa` | 8916 | | public/apple-touch-icon.png | asset | `47325746c00e` | 5684 | | src/system/PRODUCT.md | spec | `4689282ab40e` | 2248 | ## 提炼层(本站为源头) | 文件 | 地址 | |---|---| | src/extracted/CLAUDE.md | /src/extracted/CLAUDE.md | | src/extracted/Checkbox.jsx | /src/extracted/Checkbox.jsx | | src/extracted/ConfirmDialog.jsx | /src/extracted/ConfirmDialog.jsx | | src/extracted/DataTable.jsx | /src/extracted/DataTable.jsx | | src/extracted/Feedback.jsx | /src/extracted/Feedback.jsx | | src/extracted/Pagination.jsx | /src/extracted/Pagination.jsx | | src/extracted/SidebarNav.jsx | /src/extracted/SidebarNav.jsx | | src/extracted/Switch.jsx | /src/extracted/Switch.jsx | | src/extracted/Tabs.jsx | /src/extracted/Tabs.jsx | | src/extracted/Tag.jsx | /src/extracted/Tag.jsx | | src/extracted/VerifyShell.jsx | /src/extracted/VerifyShell.jsx | | src/extracted/baseline.css | /src/extracted/baseline.css | | src/extracted/extracted.css | /src/extracted/extracted.css | | src/extracted/icons.jsx | /src/extracted/icons.jsx | --- 上一页:[笔记标注块](https://ui.zhaozimin.com/patterns/callouts.md) · 下一页:[更新记录](https://ui.zhaozimin.com/resources/changelog.md) · 全站目录:https://ui.zhaozimin.com/llms.txt ======== --- title: 更新记录 Changelog url: https://ui.zhaozimin.com/resources/changelog/ markdown: https://ui.zhaozimin.com/resources/changelog.md group: 资源 origin: 规范文档 synced: edu v23.5.1 (086937d) · 2026-09-23 scope: .zzm-v4 # 根节点必须带这个类,否则样式不生效 css: https://ui.zhaozimin.com/zzm.css --- # 更新记录 Changelog > 当前站点 v0.1.1,同步自 edu.zhaozimin v23.5.1(086937d)。 ## v0.1.1 · 2026-09-23 双站上线。 - **国际站** `ui.zhaozimin.com`:Cloudflare Pages,推送 main 自动构建发布。 - **大陆站** `ui.zhaozimin.cn`:阿里云 1Panel 静态站,GitHub Actions 推送自动发布,约 40 秒到位;页脚挂 ICP 与公安备案(备案按域名,国际站不挂)。 - **产物改为目录式输出**:`/components/button/` 而不是 `/components/button` → `button.html`。文件式输出依赖「访问 /x 自动找 x.html」这一平台特性,Cloudflare 支持而 nginx 不支持;改完之后同一份产物放在任何静态服务器上都成立。 - **同一份代码发两套站**:构建期 `SITE_ORIGIN` 决定站内绝对地址(canonical、llms.txt、sitemap、每页 .md 头),`SITE_REGION=cn` 决定是否挂备案。 ## v0.1.0 · 2026-09-23 首次发布。 - **同步**:从 edu.zhaozimin v23.5.1(086937d)逐字同步 30 个文件——四份样式、SPACING.md、12 个组件、5 个逻辑依赖、4 枚勋章、3 个站点图标、PRODUCT.md。 - **提炼**:把 edu 后台设计系统页(AdmDesign)里只以 inline 样张存在的开关、复选框、标签页、分页、轻提示、进度条、数据表格、验证外壳整理成正式组件;本站是它们的源头。 - **改造**:确认窗在 edu 依赖 shadcn 与 Tailwind,这里保留同一 API 与 `.zzm-dialog-*` 皮肤,用纯 CSS 补齐定位,去掉两层依赖。 - **新增**:宿主基线(`baseline.css`)——把 v4 组件依赖的 Tailwind preflight 子集与减少动效规则单独拿出来,使组件脱离 edu 也能 1:1 成立。 - **复刻**:侧栏导航与自绘图标集,剥离路由状态后的展示版。 - **机读层**:每页 .md、llms.txt、llms-full.txt、DESIGN.md、tokens.json、zzm.css、zzm.global.css、源码直读、robots.txt 全面放行。 ## 怎么更新 edu 更新后: ```bash npm run sync # 从 ../edu.zhaozimin 重新同步,写 manifest npm run check:drift # 核对快照与源头 npm run build ``` --- 上一页:[勋章与下载](https://ui.zhaozimin.com/resources/assets.md) · 下一页:无 · 全站目录:https://ui.zhaozimin.com/llms.txt