cdrop — 跨 OS 剪贴板与文件传输服务

Commilitia Drop:自托管的跨设备剪贴板同步与点对点文件传输。

- 后端 Go(chi / SQLite WAL / SSE Hub / WebRTC signaling + 状态机 / Relay ring buffer),编译进单个 distroless 镜像(前端 go:embed)。
- 前端 React + TanStack Router + Zustand,自实现 SSE + WebRTC P2P,NAT 受阻时回退服务端中继;聚珍(Juzhen)CJK 综合排版。
- 桌面端 Wails v2(macOS / Windows),瘦客户端复用 web。
- 鉴权 OIDC PKCE(自建 Casdoor 等),refresh_token 信封加密存系统密钥库;iOS Shortcut 用 HS256 scoped token。

架构文档与变更记录见 docs 分支(PROJECT_BRIEF / FRONTEND_DESIGN / CHANGELOG)。

本次为公开发布初始提交:完整开发历史(含部署细节)留存于私有归档,公开仓库自此干净起步。
This commit is contained in:
2026-06-15 21:38:28 +08:00
commit f21fa5b5e8
239 changed files with 29010 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
/*
* Base 层 —— html / body / 通用排印 / 滚动条基线。所有具体颜色与字体
* 通过语义令牌读取,不写硬编码值。
*/
*,
*::before,
*::after
{
box-sizing: border-box;
}
html,
body
{
margin: 0;
padding: 0;
background: var(--bg);
color: var(--text);
font-family: var(--font-sans);
font-size: 14px;
line-height: var(--lh-body);
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: "kern", "liga";
transition:
background-color var(--dur-slow) var(--ease-in-out),
color var(--dur-slow) var(--ease-in-out);
}
#root
{
min-height: 100vh;
background: var(--bg);
}
a
{
color: var(--accent);
text-decoration: none;
transition: color var(--dur-fast) var(--ease-out);
}
a:hover
{
color: color-mix(in srgb, var(--accent) 80%, var(--text));
text-decoration: underline;
text-underline-offset: 2px;
}
::selection
{
background: var(--accent-soft);
color: var(--text);
}
code,
kbd,
samp,
pre
{
font-family: var(--font-mono);
font-size: 0.95em;
}
/* 简单的滚动条美化(仅 webkit)—— 使用 surface-sunken 与 divider,跟随主题。 */
*::-webkit-scrollbar
{
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-track
{
background: transparent;
}
*::-webkit-scrollbar-thumb
{
background: var(--divider);
border-radius: var(--radius-pill);
border: 2px solid var(--bg);
}
*::-webkit-scrollbar-thumb:hover
{
background: color-mix(in srgb, var(--divider) 70%, var(--text));
}
+91
View File
@@ -0,0 +1,91 @@
/*
* CJK 排版精修 —— 参照 ~/Library/CloudStorage/Dropbox/工具设置/Claude/Base/
* templates/html/CJK-TYPOGRAPHY.md
*
* 仅在 <html lang="zh-*" | "ja" | "ko"> 时激活。:where(:lang(...)) 保持零
* 特异性,组件层可直接覆盖,无需 !important。i18n.ts 已经把 locale 切到
* documentElement.lang,这套规则自动跟随。
*
* 字重策略(项目约定,偏离上游 spec):
* - Body Sans(Orbit Gothic CJK)保持 font-weight 400 —— Sans 在 400 不存在
* 过细问题。上游 spec 把 body 提到 500 是针对 Noto Serif CJK 屏幕显示偏细
* 的特定问题,本项目 body 走 Sans,不适用。
* - .serif opt-in 类显式拉到 500 —— Noto Serif CJK 在 400 屏幕上确实偏细,
* 这是 Serif 路径独有的修正,留给将来 hero / marketing 长段落使用。
* - mono / code 锁 400,且 opt-out 所有 CJK 处理(autospace / trim / justify
* 都关掉),保 byte 对齐与 URL / token 可读性。
*/
/* ===== CJK lang root:挂行 / 换行 =====
中西间隙、标点挤压、原生 text-autospace 关闭均由聚珍(juzhen.css 的 .juzhen
normalize + JS pass)接管,此处不再设 text-spacing-trim / text-autospace
以免与聚珍托管挤压双重作用。仅保留两条段落级、与聚珍原子透明兼容的规则。 */
:where(:lang(zh), :lang(ja), :lang(ko))
{
/* 闭合标点在行尾溢出挂起,首字符开口标点挂在左边界,改善光学齐边 */
hanging-punctuation: first allow-end;
/* JIS X 4051 / clreq §3.1.2 禁则浏览器原生兜底(聚珍 jinze 为主) */
line-break: strict;
}
/* ===== 长英文段断词 ===== 聚珍(JuzhenlongWord pass 把 [A-Za-z]{6,} 包进
<span lang="en">,这里给浏览器一个原生英文断词字典 + 渲染连字符。
连字符仅由 CSS 渲染、不在 DOM,选取 / 复制返回原文无污染。原先 CJK
lang root 的 overflow-wrap: anywhere 已被本规则替代:不再硬斩单词,
也保留连字符提示。hyphenate-character 显式 ASCII "-" 避免 serif 字
型把 U+2010 画成全角(本项目 body 走 Sans 暂不踩此坑,但与上游模板
规范一致)。 */
[lang|="en"]
{
-webkit-hyphens: auto;
hyphens: auto;
hyphenate-character: "-";
hyphenate-limit-chars: 6 3 3; /* 词长 / 断点前 / 断点后最少字符 */
}
/* ===== 默认两端对齐:多行块容器(段落 / 列表项 / 引用 / 单元格 / 说明)
===== text-align-last: left 让段末与单行不被拉伸,只有真正的中间行受益。 */
:where(:lang(zh), :lang(ja), :lang(ko))
:is(p, li, blockquote, td, dd, summary, figcaption, caption,
.justify, .justify *)
{
text-align: justify;
text-align-last: left;
}
/* ===== 显式 opt-out:容器及其子树都不走 justify ===== */
:where(:lang(zh), :lang(ja), :lang(ko)) :is(.no-justify, .no-justify *)
{
text-align: start;
text-justify: auto;
text-align-last: auto;
}
/* ===== 显式对齐工具类 =====
.text-left / .text-center / .text-right 覆盖默认 justify + 传播到后代;
主要用例:<th> / <td> / <caption> 的数值列右对齐 / 居中。
规格 (0,1,0) 自身 + (0,1,0) 后代,压过默认规则 (0,0,1);
text-justify / text-align-last 显式 auto,清掉残留 inter-character 拉伸。 */
.text-left, .text-left * { text-align: left; text-justify: auto; text-align-last: auto; }
.text-center, .text-center * { text-align: center; text-justify: auto; text-align-last: auto; }
.text-right, .text-right * { text-align: right; text-justify: auto; text-align-last: auto; }
/* ===== Mono opt-out:byte 对齐,关掉所有 CJK 处理 ===== */
code, kbd, pre, samp,
.mono, .mono *
{
font-weight: 400;
text-autospace: no-autospace;
text-spacing-trim: space-all;
text-align: start;
text-justify: auto;
text-align-last: auto;
}
/* ===== Serif opt-in:Noto Serif CJK 在 400 屏幕偏细,锁定 500(Medium) ===== */
.serif
{
font-family: var(--font-serif);
font-weight: 500;
}
+169
View File
@@ -0,0 +1,169 @@
/*
* 本地优先字体层 —— 用 local() 覆盖 CDN 的纯 url() @font-face。
*
* 问题:三个字体 CDN 的 @font-face 都只有 src: url(...)、无 local()。浏览器解析到
* 这些 family 名(如 "Orbit Gothic CJK SC")时会被该 @font-face 捕获、直接走网络
* url,即便本机已安装同名字体也不会用 → 首屏闪烁、无谓流量。
*
* 修法:在 CDN <link>(见 index.html)之后,对项目实际用到的 family × 字重重新声明
* @font-facesrc 把 local() 放最前、CDN url() 兜底。同 family 同 weight/style 的
* @font-face 以「后定义者胜」,cdrop 打包 CSS 在 index.html 静态 <link> 之后加载,故
* 本层覆盖 CDN:本机命中即零网络瞬时渲染,未装则回退网络(swap)。
*
* 覆盖范围:仅项目用到的字重(400/500/600/700)× 实际激活的 region
* - Orbit Gothic CJK SC / TCSans 正文 Mono 的 CJK 落字)
* - Maple MonoMono 拉丁 / 符号)
* Serif 的 "Noto Serif CJK SC" 是本地专属名(gfonts 只定义 "Noto Serif SC"、不含该
* 名),字体栈里已排最前 → 本就本地优先,无需在此覆盖。日韩(JP/KR)region 暂未激活,
* 启用时按同样 4 字重补充即可。
*
* local() 同时给 family 名与 PostScript 名两种写法以提高命中率;font-display: swap
* 与 CDN 保持一致。
*/
/* ===== Orbit Gothic CJK SC(默认 / 简体 Sans + Mono CJK===== */
@font-face
{
font-family: "Orbit Gothic CJK SC";
font-style: normal;
font-weight: 400;
font-display: swap;
src:
local("Orbit Gothic CJK SC"),
local("OrbitGothicCJKsc-Regular"),
url("https://orbit-gothic.cdn.commilitia.net/WOFF2/CJKsc/OrbitGothicCJKsc-Regular.woff2") format("woff2");
}
@font-face
{
font-family: "Orbit Gothic CJK SC";
font-style: normal;
font-weight: 500;
font-display: swap;
src:
local("Orbit Gothic CJK SC"),
local("OrbitGothicCJKsc-Medium"),
url("https://orbit-gothic.cdn.commilitia.net/WOFF2/CJKsc/OrbitGothicCJKsc-Medium.woff2") format("woff2");
}
@font-face
{
font-family: "Orbit Gothic CJK SC";
font-style: normal;
font-weight: 600;
font-display: swap;
src:
local("Orbit Gothic CJK SC"),
local("OrbitGothicCJKsc-SemiBold"),
url("https://orbit-gothic.cdn.commilitia.net/WOFF2/CJKsc/OrbitGothicCJKsc-SemiBold.woff2") format("woff2");
}
@font-face
{
font-family: "Orbit Gothic CJK SC";
font-style: normal;
font-weight: 700;
font-display: swap;
src:
local("Orbit Gothic CJK SC"),
local("OrbitGothicCJKsc-Bold"),
url("https://orbit-gothic.cdn.commilitia.net/WOFF2/CJKsc/OrbitGothicCJKsc-Bold.woff2") format("woff2");
}
/* ===== Orbit Gothic CJK TC(繁体 Sans + Mono CJK===== */
@font-face
{
font-family: "Orbit Gothic CJK TC";
font-style: normal;
font-weight: 400;
font-display: swap;
src:
local("Orbit Gothic CJK TC"),
local("OrbitGothicCJKtc-Regular"),
url("https://orbit-gothic.cdn.commilitia.net/WOFF2/CJKtc/OrbitGothicCJKtc-Regular.woff2") format("woff2");
}
@font-face
{
font-family: "Orbit Gothic CJK TC";
font-style: normal;
font-weight: 500;
font-display: swap;
src:
local("Orbit Gothic CJK TC"),
local("OrbitGothicCJKtc-Medium"),
url("https://orbit-gothic.cdn.commilitia.net/WOFF2/CJKtc/OrbitGothicCJKtc-Medium.woff2") format("woff2");
}
@font-face
{
font-family: "Orbit Gothic CJK TC";
font-style: normal;
font-weight: 600;
font-display: swap;
src:
local("Orbit Gothic CJK TC"),
local("OrbitGothicCJKtc-SemiBold"),
url("https://orbit-gothic.cdn.commilitia.net/WOFF2/CJKtc/OrbitGothicCJKtc-SemiBold.woff2") format("woff2");
}
@font-face
{
font-family: "Orbit Gothic CJK TC";
font-style: normal;
font-weight: 700;
font-display: swap;
src:
local("Orbit Gothic CJK TC"),
local("OrbitGothicCJKtc-Bold"),
url("https://orbit-gothic.cdn.commilitia.net/WOFF2/CJKtc/OrbitGothicCJKtc-Bold.woff2") format("woff2");
}
/* ===== Maple MonoMono 拉丁 / 符号)===== */
@font-face
{
font-family: "Maple Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src:
local("Maple Mono"),
local("MapleMono-Regular"),
url("https://maple-mono.cdn.commilitia.net/MapleMono-Regular.ttf.woff2") format("woff2");
}
@font-face
{
font-family: "Maple Mono";
font-style: normal;
font-weight: 500;
font-display: swap;
src:
local("Maple Mono"),
local("MapleMono-Medium"),
url("https://maple-mono.cdn.commilitia.net/MapleMono-Medium.ttf.woff2") format("woff2");
}
@font-face
{
font-family: "Maple Mono";
font-style: normal;
font-weight: 600;
font-display: swap;
src:
local("Maple Mono"),
local("MapleMono-SemiBold"),
url("https://maple-mono.cdn.commilitia.net/MapleMono-SemiBold.ttf.woff2") format("woff2");
}
@font-face
{
font-family: "Maple Mono";
font-style: normal;
font-weight: 700;
font-display: swap;
src:
local("Maple Mono"),
local("MapleMono-Bold"),
url("https://maple-mono.cdn.commilitia.net/MapleMono-Bold.ttf.woff2") format("woff2");
}
+186
View File
@@ -0,0 +1,186 @@
/*
* 字体栈 —— 逐 region 明确化。
*
* 聚珍(Juzhen)2.0 不再内建逐语言字体级联,字体完全交消费端 CSS 决定。命名分两类,
* 回退策略不同:
*
* - 含「CJK」的合一字体("Orbit Gothic CJK <R>"、"Noto Serif CJK <R>" /
* Source Han):泛 CJK 全字覆盖,各区变体字符集相同、仅默认 glyph(locl)不同。
* 故每族只列「当前区一个」即可——它已覆盖全部 CJK 字,且 lang + 该区字体触发
* 正确 locl 地域字形,多列其他区无意义。这类多为用户机器上的本地字体,优先尝试。
* - 不含「CJK」的区域子集("Noto Serif <R>"Google Fonts 命名,适合 Web 分发):
* 每区只覆盖该区子集、字符集不全,故须跨区列全(当前区在前取 locl,余区补字),
* 以并集逼近全字覆盖。次于本地合一字体尝试。
*
* 三族(<R> SCTCJPKR:root 默认 SC,兼英文 / 无 lang 回退):
* - Sans "Orbit Gothic CJK <R>" → 系统 sans
* - Serif"Noto Serif CJK <R>"(本地合一)→ "Noto Serif <R>" 跨区(Web 子集)→ 系统 serif
* - Mono "Maple Mono" → "Orbit Gothic CJK <R>" → 系统 mono
*
* "Noto Serif CJK <R>"Adobe / fontconfig,同 Source Han Serif)与 "Noto Serif <R>"
* Google Fonts)命名不同:前者合一全覆盖、后者区域子集;拉丁正文回退 "Noto Serif"。
*
* 区域变体由 :lang() 切,<html lang="zh-CN|zh-TW|en-US"> 由 uiSlice.locale 同步
* 写入;项目 lang 取自 i18n.Locale,故繁体走 :lang(zh-TW) 而非 :lang(zh-Hant)
* 后续新增日韩沿 BCP47 区域 tag 添加即可。
*
* 字体文件 woff2 由远程 CDN 拉取(index.html 里 <link rel="stylesheet"> 引入
* @font-face 声明):
* - Orbit Gothic CJK → orbit-gothic.cdn.commilitia.net
* - Maple Mono → maple-mono.cdn.commilitia.net
* - Fredoka(字标) → gfonts.commilitia.net(同代理;--font-display 用)
* - Noto Serif(子集) → gfonts.commilitia.netGoogle Fonts 的 CF Worker 代理)
*
* 远程加载失败时按 fallback 链回退到系统字体,整体外观仍 OK。
*/
:root
{
--font-sans:
"Orbit Gothic CJK SC",
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
"PingFang SC",
"Microsoft YaHei",
"Hiragino Sans GB",
sans-serif;
--font-serif:
"Noto Serif CJK SC",
"Source Han Serif SC",
"Noto Serif SC",
"Noto Serif TC",
"Noto Serif JP",
"Noto Serif KR",
"Noto Serif",
"Songti SC",
Georgia,
Cambria,
"Times New Roman",
serif;
--font-mono:
"Maple Mono",
"Orbit Gothic CJK SC",
"JetBrains Mono",
ui-monospace,
SFMono-Regular,
Menlo,
Consolas,
monospace;
/* display 仅用于品牌字标("Commilitia Drop")与极少数大字号陈列。Fredoka 是
品牌资产 README 指定的字标字体(圆体 geometric sans),经 gfonts.commilitia.net
镜像加载;无 CJK 字形,CJK 字符按 var(--font-sans) 逐字回退(字标为纯拉丁,
不触发)。远程失败时整链回退到 sans,外观仍 OK。 */
--font-display:
"Fredoka",
var(--font-sans);
}
/* ===== 繁体(TC===== */
:lang(zh-TW),
:lang(zh-HK),
:lang(zh-MO)
{
--font-sans:
"Orbit Gothic CJK TC",
system-ui,
-apple-system,
"PingFang TC",
"Microsoft JhengHei",
"Hiragino Sans CNS",
sans-serif;
--font-serif:
"Noto Serif CJK TC",
"Source Han Serif TC",
"Noto Serif TC",
"Noto Serif SC",
"Noto Serif JP",
"Noto Serif KR",
"Noto Serif",
"Songti TC",
Georgia,
serif;
--font-mono:
"Maple Mono",
"Orbit Gothic CJK TC",
"JetBrains Mono",
ui-monospace,
SFMono-Regular,
Menlo,
Consolas,
monospace;
}
/* ===== 日文(JP===== */
:lang(ja)
{
--font-sans:
"Orbit Gothic CJK JP",
system-ui,
-apple-system,
"Hiragino Sans",
"Yu Gothic",
"Meiryo",
sans-serif;
--font-serif:
"Noto Serif CJK JP",
"Source Han Serif",
"Noto Serif JP",
"Noto Serif SC",
"Noto Serif TC",
"Noto Serif KR",
"Noto Serif",
"Yu Mincho",
Georgia,
serif;
--font-mono:
"Maple Mono",
"Orbit Gothic CJK JP",
"JetBrains Mono",
ui-monospace,
SFMono-Regular,
Menlo,
Consolas,
monospace;
}
/* ===== 韩文(KR===== */
:lang(ko)
{
--font-sans:
"Orbit Gothic CJK KR",
system-ui,
-apple-system,
"Apple SD Gothic Neo",
"Malgun Gothic",
sans-serif;
--font-serif:
"Noto Serif CJK KR",
"Source Han Serif KR",
"Noto Serif KR",
"Noto Serif SC",
"Noto Serif TC",
"Noto Serif JP",
"Noto Serif",
Georgia,
serif;
--font-mono:
"Maple Mono",
"Orbit Gothic CJK KR",
"JetBrains Mono",
ui-monospace,
SFMono-Regular,
Menlo,
Consolas,
monospace;
}
+27
View File
@@ -0,0 +1,27 @@
/*
* 全局样式入口。被 main.tsx 单点 import。
*
* 顺序约定:
* 1. tokens.primitives 原始常量(颜色 / 字号 / 间距 / 时长)
* 2. tokens.semantic 主题相关语义变量(--bg / --accent / ...
* 3. fonts 字体族变量与 :lang() 切换
* 4. fontface 本地优先 @font-face 覆盖层(local() shadow CDN 的纯 url
* 5. base html/body 与通用排印基线
* 6. juzhen 聚珍套件视觉尺寸(中西间隙 / 标点挤压 / justify 原子,必配)
* 7. cjk CJK 排版精修(项目层覆盖:justify 选择器 / mono opt-out
* 8. motion 动效 utility 与 reduced-motion
* 9. tailwind 最后,让 utilities 可覆盖 base
*/
@import "./tokens.primitives.css";
@import "./tokens.semantic.css";
@import "./fonts.css";
@import "./fontface.css";
@import "./base.css";
@import "../lib/cjk-autospace/dist/juzhen.css";
@import "./cjk.css";
@import "./motion.css";
@tailwind base;
@tailwind components;
@tailwind utilities;
+35
View File
@@ -0,0 +1,35 @@
/*
* Motion 层 —— 通用过渡 utility 与 reduced-motion 兜底。
*/
.transition-base
{
transition:
background-color var(--dur-base) var(--ease-out),
border-color var(--dur-base) var(--ease-out),
color var(--dur-base) var(--ease-out),
box-shadow var(--dur-base) var(--ease-out);
}
.transition-fast
{
transition:
background-color var(--dur-fast) var(--ease-out),
border-color var(--dur-fast) var(--ease-out),
color var(--dur-fast) var(--ease-out),
box-shadow var(--dur-fast) var(--ease-out),
transform var(--dur-fast) var(--ease-out);
}
@media (prefers-reduced-motion: reduce)
{
*,
*::before,
*::after
{
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
}
+121
View File
@@ -0,0 +1,121 @@
/*
* Primitives 层 —— 原始令牌。来自 Dracula Pro 官方调色板与设计稿基础尺度。
*
* 组件不直接消费这一层。主题切换不动这一层。新增主题 = 在 semantic 层把
* 这里的原始值映射给语义变量。
*/
:root
{
/* ---- Dracula Pro · Base 色板 ---- */
--c-purple-500: #9580FF; /* Base Purple — 大块面积/按钮背景适用 */
--c-purple-400: #AA99FF; /* Base BrightBlue — Purple 的官方亮版,适合
深色背景上的文字 / 链接,对比度 6.7:1 */
--c-purple-700: #644AC9; /* Alucard Purple — 亮色主题 accent */
--c-pink: #FF80BF; /* 扩展色 */
--c-pink-deep: #A3144D;
--c-red: #FF9580; /* dark · state-error */
--c-red-deep: #CB3A2A; /* light · state-error */
--c-orange: #FFCA80; /* dark · state-warn */
--c-orange-deep: #A34D14; /* light · state-warn */
--c-yellow: #FFFF80;
--c-yellow-deep: #846E15;
--c-green: #8AFF80; /* dark · state-online */
--c-green-deep: #14710A; /* light · state-online */
--c-cyan: #80FFEA;
--c-cyan-deep: #036A96;
/* ---- Dracula Pro · 文字 / Comment(色 token,非表面) ---- */
/* Dracula 官方深色 bg / surface 偏 250° 紫调,在紫色 accent 上对比度
塌缩到 ~4.2:1。本项目改走「中性灰表面 + Dracula 色作 accent / 角色色」,
表面色单独抽到 c-bg-dark-* 系列,这里只保留 foreground 与 comment。 */
--c-comment-dracula: #7970A9; /* official Comment —— 仅用于"非常 muted"
的场景(例如 hover-only 提示);作为
UI 次要文字色对比度只有 4.3:1,临界。 */
--c-comment-bright: #A89DD3; /* Comment 的官方派生亮版(同色相 250°,
饱和度 35% 明度 72%),用于 UI muted 文字,
对比度 ~7:1 (AAA Large + AA Normal)。 */
--c-foreground-dracula: #F8F8F2; /* official Foreground */
--c-foreground-alucard: #1F1F1F; /* official Foreground */
--c-comment-alucard: #635D97; /* official Comment */
/* ---- 中性灰深色 5 阶 surface(去紫调) ----
目标:让紫色 accent 在背景上对比度达 7:1+,避免 Dracula 官方紫底紫前景
塌缩。色相 H≈260°(略冷,与紫色 accent 同冷调系)、饱和度 8-12% 极低、
明度 9-22% 阶梯递进。 */
--c-bg-dark-neutral: #17171C; /* 页面底,最深 */
--c-sunken-dark-neutral: #1E1E24; /* 下沉(剪贴板 sunken / dropzone 底) */
--c-surface-dark-neutral: #26262E; /* 默认 panel */
--c-raised-dark-neutral: #2E2E37; /* hover / 主 panel */
--c-elev2-dark-neutral: #37373F; /* 双层 elev(对话框 / 弹层) */
--c-divider-dark-neutral: #3C3C45;
--c-divider-strong-dark: #54545F;
--c-text-muted-dark-neutral: #9C9CA9; /* 比 Comment 紫调更冷的中性 muted */
/* ---- 中性灰浅色 5 阶 surface(去紫调) ----
原 Alucard #F5F5F5/#FFFFFF/#EAEAEA 已基本中性,这里微调成更冷一档,
并显式拉出 5 阶,让 hover 的 surface→raised 有视觉差。
2026-05-16 调整:bg 与 sunken 各自再深一档,把卡片"浮"起来,
让默认白色 panel 从页面背景里明显分层(原 #F2F2F4 太接近白)。 */
--c-bg-light-neutral: #E9E8EE; /* 页面底,更冷更深一档,与白卡形成清晰 ~9% 亮度差 */
--c-sunken-light-neutral: #DDDCE3; /* 下沉表面,比 bg 再深一档 */
--c-surface-light-neutral: #FAFAFB; /* 默认 panel:几乎纯白 */
--c-raised-light-neutral: #FFFFFF; /* hover / 主 panel */
--c-elev2-light-neutral: #FFFFFF; /* 双层(primary panel) */
--c-divider-light-neutral: #D6D5DC;
--c-divider-strong-light: #B5B4BD;
--c-text-muted-light-neutral: #5B5A6E; /* 比 #635D97 略冷,饱和度更低 */
/* ---- 原 Dracula / Alucard 表面变量保留作 fallback,目前不消费 ---- */
--c-bg-dracula: var(--c-bg-dark-neutral);
--c-surface-dracula: var(--c-surface-dark-neutral);
--c-raised-dracula: var(--c-raised-dark-neutral);
--c-sunken-dracula: var(--c-sunken-dark-neutral);
--c-selection-dracula: #454158; /* official Selection,仅 ::selection 还要 */
--c-bg-alucard: var(--c-bg-light-neutral);
--c-surface-alucard: var(--c-surface-light-neutral);
--c-raised-alucard: var(--c-raised-light-neutral);
--c-sunken-alucard: var(--c-sunken-light-neutral);
--c-selection-alucard: #CFCFDE;
/* ---- 字号阶梯 ---- */
--fs-11: 11px;
--fs-12: 12px;
--fs-13: 13px;
--fs-15: 15px;
--fs-18: 18px;
--fs-22: 22px;
--fs-28: 28px;
/* ---- 行高 ---- */
--lh-body: 1.6; /* 正文 / 多行块 */
--lh-ui: 1.4; /* 按钮 / 单行 UI */
--lh-title: 1.2; /* 标题 */
/* ---- 字距 ---- */
--ls-brand: 0.06em;
--ls-label: 0.08em;
/* ---- 间距阶梯(基 4---- */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 24px;
--space-6: 32px;
--space-7: 48px;
--space-8: 64px;
/* ---- 圆角 ---- */
--radius-card: 10px;
--radius-control: 6px;
--radius-pill: 999px;
/* ---- 动效 ---- */
--dur-fast: 120ms;
--dur-base: 220ms;
--dur-slow: 320ms;
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
}
+191
View File
@@ -0,0 +1,191 @@
/*
* Semantic 层 —— 语义令牌。组件唯一应该消费的颜色 / 阴影变量。
*
* 主题切换 = 改一个 attribute:document.documentElement.dataset.theme = "dark"。
* 自定义主题 = 在 :root[data-theme="custom-x"] 下覆盖同一组变量。
*
* 系统主题:未设 data-theme 时,跟随 @media (prefers-color-scheme: dark)。
*
* 2026-05 重塑:
* - 表面切到中性灰 5 阶(去紫调),accent 仍是 Dracula Purple。
* - 引入 5 色角色:info / success / warn / error / emphasis + 各自 soft 派生。
* - 状态编码统一走角色色(Activity 左色条、Badge tone、Callout 等)。
*/
/* ===== Light · 中性灰 + Alucard accent / 角色色(默认) ===== */
:root,
:root[data-theme="light"]
{
--bg: var(--c-bg-light-neutral);
--surface: var(--c-surface-light-neutral);
--surface-raised: var(--c-raised-light-neutral);
--surface-sunken: var(--c-sunken-light-neutral);
--surface-elev2: var(--c-elev2-light-neutral);
--divider: var(--c-divider-light-neutral);
--divider-strong: var(--c-divider-strong-light);
--text: var(--c-foreground-alucard);
--text-muted: var(--c-text-muted-light-neutral);
--accent: var(--c-purple-700);
--accent-soft: color-mix(in srgb, var(--accent) 10%, transparent);
--accent-softer: color-mix(in srgb, var(--accent) 5%, transparent);
--accent-ring: color-mix(in srgb, var(--accent) 26%, transparent);
--accent-on: #FFFFFF;
/* 5 色角色 —— Alucard 调色板深版,作 status 编码主力 */
--info: var(--c-cyan-deep); /* #036A96 */
--success: var(--c-green-deep); /* #14710A */
--warn: var(--c-yellow-deep); /* #846E15 */
--error: var(--c-red-deep); /* #CB3A2A */
--emphasis: var(--c-pink-deep); /* #A3144D */
--orange: var(--c-orange-deep); /* #A34D14,备用 */
--info-soft: color-mix(in srgb, var(--info) 9%, transparent);
--success-soft: color-mix(in srgb, var(--success) 9%, transparent);
--warn-soft: color-mix(in srgb, var(--warn) 11%, transparent);
--error-soft: color-mix(in srgb, var(--error) 9%, transparent);
--emphasis-soft: color-mix(in srgb, var(--emphasis) 9%, transparent);
/* state-* 是历史 alias,保留兼容现有组件(StateDot 等) */
--state-online: var(--success);
--state-error: var(--error);
--state-warn: var(--warn);
--shadow-card: 0 1px 2px rgba(17, 17, 20, 0.05),
0 2px 6px rgba(17, 17, 20, 0.07);
--shadow-raised: 0 8px 20px rgba(17, 17, 20, 0.10),
0 2px 6px rgba(17, 17, 20, 0.06);
--shadow-overlay: 0 12px 32px rgba(17, 17, 20, 0.18);
--shadow-accent: 0 8px 22px rgba(100, 74, 201, 0.24);
/* 字母头像:浅色主题 白底黑字 */
--avatar-bg: #FFFFFF;
--avatar-fg: #1F1F1F;
/* 颜色阶 —— Mantine accent 色板用,10 阶手工对齐 Dracula Purple */
--accent-0: #F3F0FF;
--accent-1: #E4DEFF;
--accent-2: #C9BEFF;
--accent-3: #AC9BFE;
--accent-4: #9580FF;
--accent-5: #8369FB;
--accent-6: #7458EE;
--accent-7: #644AC9;
--accent-8: #523CA8;
--accent-9: #3F2D85;
color-scheme: light;
}
/* ===== Dark · 中性灰 + Dracula accent / 角色色 =====
*
* 对比度准则:
* - 表面用 c-*-dark-neutral 5 阶,色相 260° 但饱和度 8%,接近中性灰。
* 紫色 accent (#AA99FF) 在 #26262E 上对比度 ~8.5:1,胜过原 Dracula 紫底紫前景的 4.2:1。
* - --text-muted 用更冷的中性 #9C9CA9,而非 Comment 紫调,避免在中性灰上仍透紫感。
* - --accent 用 BrightBlue #AA99FF (Dracula PRO Base 官方"亮紫"),
* 按钮背景仍可用,accent-on 是 #1B1A23 强反差。
*/
:root[data-theme="dark"]
{
--bg: var(--c-bg-dark-neutral);
--surface: var(--c-surface-dark-neutral);
--surface-raised: var(--c-raised-dark-neutral);
--surface-sunken: var(--c-sunken-dark-neutral);
--surface-elev2: var(--c-elev2-dark-neutral);
--divider: var(--c-divider-dark-neutral);
--divider-strong: var(--c-divider-strong-dark);
--text: var(--c-foreground-dracula);
--text-muted: var(--c-text-muted-dark-neutral);
--accent: var(--c-purple-400);
--accent-soft: color-mix(in srgb, var(--accent) 20%, transparent);
--accent-softer: color-mix(in srgb, var(--accent) 10%, transparent);
--accent-ring: color-mix(in srgb, var(--accent) 36%, transparent);
--accent-on: #1B1A23;
/* 5 色角色 —— Dracula Pro neon,中性背景下饱和度足够 */
--info: var(--c-cyan); /* #80FFEA */
--success: var(--c-green); /* #8AFF80 */
--warn: var(--c-yellow); /* #FFFF80 */
--error: var(--c-red); /* #FF9580 */
--emphasis: var(--c-pink); /* #FF80BF */
--orange: var(--c-orange); /* #FFCA80 */
--info-soft: color-mix(in srgb, var(--info) 14%, transparent);
--success-soft: color-mix(in srgb, var(--success) 14%, transparent);
--warn-soft: color-mix(in srgb, var(--warn) 14%, transparent);
--error-soft: color-mix(in srgb, var(--error) 14%, transparent);
--emphasis-soft: color-mix(in srgb, var(--emphasis) 14%, transparent);
--state-online: var(--success);
--state-error: var(--error);
--state-warn: var(--warn);
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.45),
0 4px 12px rgba(0, 0, 0, 0.5);
--shadow-raised: 0 12px 28px rgba(0, 0, 0, 0.55),
0 2px 6px rgba(0, 0, 0, 0.4);
--shadow-overlay: 0 16px 40px rgba(0, 0, 0, 0.65);
--shadow-accent: 0 10px 28px rgba(170, 153, 255, 0.34);
--avatar-bg: #000000;
--avatar-fg: #FFFFFF;
color-scheme: dark;
}
/* ===== System:未指定 data-theme 时跟随系统 ===== */
@media (prefers-color-scheme: dark)
{
:root:not([data-theme])
{
--bg: var(--c-bg-dark-neutral);
--surface: var(--c-surface-dark-neutral);
--surface-raised: var(--c-raised-dark-neutral);
--surface-sunken: var(--c-sunken-dark-neutral);
--surface-elev2: var(--c-elev2-dark-neutral);
--divider: var(--c-divider-dark-neutral);
--divider-strong: var(--c-divider-strong-dark);
--text: var(--c-foreground-dracula);
--text-muted: var(--c-text-muted-dark-neutral);
--accent: var(--c-purple-400);
--accent-soft: color-mix(in srgb, var(--accent) 20%, transparent);
--accent-softer: color-mix(in srgb, var(--accent) 10%, transparent);
--accent-ring: color-mix(in srgb, var(--accent) 36%, transparent);
--accent-on: #1B1A23;
--info: var(--c-cyan);
--success: var(--c-green);
--warn: var(--c-yellow);
--error: var(--c-red);
--emphasis: var(--c-pink);
--orange: var(--c-orange);
--info-soft: color-mix(in srgb, var(--info) 14%, transparent);
--success-soft: color-mix(in srgb, var(--success) 14%, transparent);
--warn-soft: color-mix(in srgb, var(--warn) 14%, transparent);
--error-soft: color-mix(in srgb, var(--error) 14%, transparent);
--emphasis-soft: color-mix(in srgb, var(--emphasis) 14%, transparent);
--state-online: var(--success);
--state-error: var(--error);
--state-warn: var(--warn);
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.45),
0 4px 12px rgba(0, 0, 0, 0.5);
--shadow-raised: 0 12px 28px rgba(0, 0, 0, 0.55),
0 2px 6px rgba(0, 0, 0, 0.4);
--shadow-overlay: 0 16px 40px rgba(0, 0, 0, 0.65);
--shadow-accent: 0 10px 28px rgba(170, 153, 255, 0.34);
--avatar-bg: #000000;
--avatar-fg: #FFFFFF;
color-scheme: dark;
}
}