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:
@@ -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 / 卡片 lg–xl / 输入 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); }
|
||||
|
||||
/* 实心 accent(PRO 会员等强调标签) */
|
||||
.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 三处的图标集)
|
||||
|
||||
Reference in New Issue
Block a user