56f673791e
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>
273 lines
9.7 KiB
CSS
273 lines
9.7 KiB
CSS
/* =============================================================
|
||
穿山甲 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;
|
||
}
|