feat(ds-flow): Phase 1.3 — 原型公用组件原子层 atoms.css

design/prototype/atoms.css:把分散在 preview 规格 / usercenter shared.tsx /
website css / Flutter widgets 的组件样式沉淀成 canonical 原子类,只引
var(--token)。含 .btn(primary/ghost/subtle/danger + 尺寸)、.card、.input
/.field、.pill/.badge(success/warning/danger/neutral 状态 + accent/outline)、
.langsel/.menu(对齐刚统一的自控语言下拉)。

明暗双主题靠语义 token 自动适配,文件内无 [data-theme] 分支。
验证:零硬编码色;45 个 token 引用全部在 prototype/tokens.css 有定义。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-08 00:04:52 +08:00
parent 805bf8e2ca
commit 56f673791e
2 changed files with 273 additions and 1 deletions
+272
View File
@@ -0,0 +1,272 @@
/* =============================================================
穿山甲 VPN · Pangolin VPN — Prototype Atoms
design/prototype/atoms.css
-------------------------------------------------------------
本文件是「原型公用组件原子层」的唯一真相源(single source)。
canonical CSS 原子类,沉淀自 design/preview/*.html 组件规格、
web/usercenter/components/shared.tsx、web/website/src/styles/website.css
与 client/lib/widgets/*.dart 的交互态语义。
铁律(写/改本文件必守,对齐 design/CLAUDE.md §1):
1. 颜色 / 圆角 / 间距 / 字号 / 阴影 / 字体 一律走 var(--token)
token 定义在 tokens.css)。禁任何硬编码 hex / rgb / 魔法数。
2. 明暗双主题「自动」适配:原子类只引语义 token--surface / --fg1 …),
主题切换由 tokens.css 的 [data-theme="dark"] 负责——
本文件里不写任何 [data-theme] 分支。
3. 暖大地色 · 大圆角(按钮全胶囊 radius-full / 卡片 lgxl / 输入 md)·
柔和暖阴影 · 状态用「色点 + 文字胶囊」非 emoji。
4. 交互态语义:hover 主色加深一档 · press 缩放 .97 · focus clay 光环
shadow-focus)· disabled sand-200 底 + sand-400 字。
5. 每新增一个原子,须在 design/prototype/index.html 登记(登记簿单源)。
使用前先在页面 <head> 引入:tokens.css(令牌)+ 本文件(原子)。
============================================================= */
/* =============================================================
BUTTONS · .btn + variants
全胶囊圆角 · hover 加深 · press 收缩 .97 · focus clay 光环
变体:.btn-primary(实心 clay) · .btn-ghost(描边/幽灵) ·
.btn-subtle(次要描边) · .btn-danger · 尺寸 .btn-lg · .btn-icon
============================================================= */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
font-family: var(--font-sans);
font-weight: 600;
font-size: var(--text-sm);
line-height: 1;
border: none;
border-radius: var(--radius-full);
padding: var(--space-3) var(--space-5);
cursor: pointer;
white-space: nowrap;
transition: background-color var(--dur-fast) var(--ease-out),
color var(--dur-fast) var(--ease-out),
border-color var(--dur-fast) var(--ease-out),
transform var(--dur-fast) var(--ease-out),
box-shadow var(--dur-fast) var(--ease-out);
}
.btn > svg { width: 16px; height: 16px; flex-shrink: 0; }
.btn:focus-visible {
outline: none;
box-shadow: var(--shadow-focus);
}
.btn:active { transform: scale(0.97); }
.btn:disabled,
.btn.is-disabled {
background: var(--sand-200);
color: var(--sand-400);
cursor: not-allowed;
transform: none;
box-shadow: none;
border-color: transparent;
}
/* Primary — 实心黏土铜 */
.btn-primary { background: var(--accent); color: var(--fg-on-accent); }
.btn-primary:hover { background: var(--accent-hover); }
.btn-primary:active { background: var(--accent-press); }
/* Ghost — 透明底、clay 字,hover 上 accent-subtle */
.btn-ghost { background: transparent; color: var(--accent); }
.btn-ghost:hover { background: var(--accent-subtle); }
/* Subtle — surface 底 + 强描边(次要动作) */
.btn-subtle {
background: var(--surface);
color: var(--fg1);
border: 1.5px solid var(--border-strong);
}
.btn-subtle:hover { border-color: var(--accent); color: var(--accent); }
/* Danger — 危险动作(断开 / 移除设备) */
.btn-danger { background: var(--danger-subtle); color: var(--red-600); }
.btn-danger:hover { background: var(--danger); color: var(--fg-on-accent); }
/* 尺寸 / 形态修饰 */
.btn-lg { padding: var(--space-4) var(--space-6); font-size: var(--text-body); }
.btn-icon { padding: var(--space-3); } /* 正方形图标按钮(配 radius-full 成圆) */
.btn-block { width: 100%; }
/* =============================================================
CARD · .card
surface 底 + border + radius-xl + shadow-sm
============================================================= */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-sm);
padding: var(--space-5);
}
.card-flush { padding: 0; } /* 列表容器(server-row 组)用,内容自带 padding */
/* =============================================================
INPUT · .input (+ .field / .flabel)
强描边 + radius-md + focus clay 光环
============================================================= */
.field { display: block; }
.flabel {
display: block;
font-family: var(--font-sans);
font-size: var(--text-caption);
font-weight: 600;
color: var(--fg2);
margin-bottom: var(--space-2);
}
.input {
width: 100%;
box-sizing: border-box;
font-family: var(--font-sans);
font-size: var(--text-sm);
color: var(--fg1);
background: var(--surface);
border: 1.5px solid var(--border-strong);
border-radius: var(--radius-md);
padding: var(--space-3) var(--space-4);
transition: border-color var(--dur-fast) var(--ease-out),
box-shadow var(--dur-fast) var(--ease-out);
}
.input::placeholder { color: var(--fg3); }
.input:focus,
.input.is-focused {
outline: none;
border-color: var(--accent);
box-shadow: var(--shadow-focus);
}
.input:disabled {
background: var(--bg-subtle);
color: var(--fg3);
cursor: not-allowed;
}
.input.is-error { border-color: var(--danger); }
.input.is-error:focus { box-shadow: 0 0 0 4px var(--danger-subtle); }
/* =============================================================
BADGE / PILL · .pill (+ status modifiers)
状态胶囊:色点 + 文字(非 emoji)。全胶囊圆角。
状态:.is-success / .is-warning / .is-danger / .is-neutral
变体:.pill-accent(实心 clay,如 PRO) · .pill-outline(accent 描边,如节点属性)
============================================================= */
.pill {
display: inline-flex;
align-items: center;
gap: var(--space-2);
font-family: var(--font-sans);
font-size: var(--text-caption);
font-weight: 600;
line-height: 1;
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-full);
background: var(--sand-100);
color: var(--fg2);
}
.pill .dot {
width: 7px;
height: 7px;
border-radius: var(--radius-full);
background: var(--sand-400);
flex-shrink: 0;
}
/* alias.badge === .pill(语义等价,命名习惯不同) */
.badge { }
.pill.is-success { background: var(--success-subtle); color: var(--green-600); }
.pill.is-success .dot { background: var(--success); }
.pill.is-warning { background: var(--warning-subtle); color: var(--amber-600); }
.pill.is-warning .dot { background: var(--warning); }
.pill.is-danger { background: var(--danger-subtle); color: var(--red-600); }
.pill.is-danger .dot { background: var(--danger); }
.pill.is-neutral { background: var(--sand-100); color: var(--fg2); }
.pill.is-neutral .dot { background: var(--sand-400); }
/* 实心 accentPRO 会员等强调标签) */
.pill-accent { background: var(--accent); color: var(--fg-on-accent); }
/* accent 描边(节点属性:流媒体 / P2P 等) */
.pill-outline {
background: var(--accent-subtle);
color: var(--accent);
border: 1px solid var(--accent-border);
}
/* =============================================================
LANGUAGE SELECT · .langsel (触发药丸) + .menu (绝对定位菜单)
对齐 usercenter shared.tsx::LangSeg / website 的自定义下拉:
不依赖原生 <select>(会漂移),按钮 + 自控绝对定位菜单。
============================================================= */
.langsel {
position: relative;
display: inline-block;
}
/* 触发按钮:药丸形,bg-subtle 底 + 细描边 */
.langsel-trigger {
display: inline-flex;
align-items: center;
gap: var(--space-2);
font-family: var(--font-sans);
font-size: var(--text-caption);
font-weight: 700;
color: var(--fg2);
background: var(--bg-subtle);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-3);
cursor: pointer;
box-shadow: var(--shadow-sm);
transition: border-color var(--dur-fast) var(--ease-out),
color var(--dur-fast) var(--ease-out);
}
.langsel-trigger:hover { border-color: var(--border-strong); color: var(--fg1); }
.langsel-trigger:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
.langsel-trigger > svg { width: 11px; height: 11px; }
/* 展开态:caret 翻转(配合 .is-open 或 [aria-expanded="true"] */
.langsel-trigger[aria-expanded="true"] > svg,
.langsel.is-open .langsel-trigger > svg { transform: rotate(180deg); }
/* 绝对定位菜单(下拉列表容器) */
.menu {
position: absolute;
top: calc(100% + var(--space-2));
right: 0;
min-width: 132px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
padding: var(--space-1);
display: flex;
flex-direction: column;
gap: 1px;
z-index: 60;
}
.menu-item {
display: block;
width: 100%;
text-align: left;
border: none;
background: transparent;
cursor: pointer;
font-family: var(--font-sans);
font-size: var(--text-sm);
font-weight: 500;
color: var(--fg1);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-sm);
white-space: nowrap;
transition: background-color var(--dur-fast) var(--ease-out);
}
.menu-item:hover { background: var(--bg-subtle); }
/* 选中项:accent-subtle 底 + clay 字 */
.menu-item.is-selected,
.menu-item[aria-selected="true"] {
background: var(--accent-subtle);
color: var(--accent);
font-weight: 700;
}
@@ -41,7 +41,7 @@
- [x] 1.2 `design/prototype/tokens.css`:从现有 `colors_and_type.css` 迁移/规整为
「基础 `:root`(主题无关标量:间距/圆角/字号/字体/阴影/动效)+ `[data-theme=dark]` 颜色覆盖块」结构。
**保持数值不变**,只重排为 ds-flow 结构;`colors_and_type.css` 作为兼容别名或迁移为薄封装(不破坏现有 codegen)
- [ ] 1.3 `design/prototype/atoms.css`:把按钮/卡片/输入/**语言下拉**/徽章/状态药丸等公用原子类沉淀为
- [x] 1.3 `design/prototype/atoms.css`:把按钮/卡片/输入/**语言下拉**/徽章/状态药丸等公用原子类沉淀为
只引 `var(--token)` 的 CSS(镜像 `design/preview/` 现有规格 + client widgets 实现语义)
- [ ] 1.4 `design/prototype/icons.js`SVG sprite 单源(`<symbol id="i-*">`),
收敛现有分散图标(website Icon.astro / usercenter icons.tsx / Flutter pangolin_icons.dart 三处的图标集)