This commit is contained in:
2026-09-20 23:35:03 +08:00
parent 941ce4baab
commit 388c700996
19 changed files with 393 additions and 71 deletions

View File

@@ -3,9 +3,13 @@
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" />
<title>剪贴板历史</title>
<link rel="stylesheet" href="themes/fonts.css" />
<title>Clipboard</title>
<!-- tokens 必须在 fonts 之前:两者都定义 --font-ui/--font-mono,同特异性
后加载的赢。fonts 在后才能让 Inter/JetBrains Mono 进字体栈(tokens
注释「fonts.css 会覆盖这两个」的前提);fonts 加载失败时 tokens 的
系统字体栈自然兜底。 -->
<link rel="stylesheet" href="themes/tokens.css" />
<link rel="stylesheet" href="themes/fonts.css" />
<link rel="stylesheet" href="themes/themes.css" />
<link rel="stylesheet" href="themes/accents.css" />
<link rel="stylesheet" href="styles.css" />
@@ -16,7 +20,7 @@
<!-- 拖动区 -->
<header id="drag-bar">
<img id="app-icon" alt="" />
<h1 id="title">剪贴板历史</h1>
<h1 id="title">Clipboard</h1>
<span id="ipc-status" class="ipc-dot" title="IPC 订阅状态" aria-label="IPC 状态"></span>
<span class="spacer"></span>
<button id="refresh-btn" class="icon-btn" title="刷新 (F5)" aria-label="刷新">
@@ -96,6 +100,11 @@
</footer>
</div>
<!-- 图片大图预览:点缩略图弹出,Esc / 点击任意处关闭 -->
<div id="lightbox" class="hidden" role="dialog" aria-modal="true" aria-label="图片大图预览" tabindex="-1">
<img id="lightbox-img" alt="图片大图预览" />
</div>
<!-- 单条卡片模板 -->
<template id="card-tpl">
<li class="card" tabindex="0">

View File

@@ -41,6 +41,8 @@ const $ipcDot = document.getElementById('ipc-status');
const $tabs = document.getElementById('tabs');
const $historyCount = document.getElementById('history-count');
const $favCount = document.getElementById('fav-count');
const $lightbox = document.getElementById('lightbox');
const $lightboxImg = document.getElementById('lightbox-img');
// ---------- 状态 ----------
@@ -131,6 +133,46 @@ async function loadImageInto(el) {
}
}
// ---------- 图片大图预览 ----------
// 数据复用懒加载的 FIFO 缓存:缩略图显示过就一定在缓存里,弹窗大多零 IPC;
// 没命中(缓存被挤出)才再走一次 getImage/favGetImage 拉取。
let lightboxReturnFocus = null; // 打开前的焦点元素,关闭后还回去
async function openLightbox(imgEl) {
const id = Number(imgEl.dataset.imgId);
const fromFav = imgEl.dataset.imgFav === '1';
const key = (fromFav ? 'f' : 'c') + id;
let b64 = fifoCache.get(key);
if (b64 === undefined) {
try {
b64 = await (fromFav ? favGetImage(id) : getImage(id));
if (b64) cacheImage(key, b64);
} catch (e) {
console.error('[renderer] lightbox getImage failed:', e);
return;
}
}
if (!b64) return; // 数据缺失就不弹一个空框
lightboxReturnFocus = document.activeElement;
$lightboxImg.src = `data:image/png;base64,${b64}`;
$lightbox.classList.remove('hidden');
$lightbox.focus(); // 焦点上遮罩:Esc 有着落,也防 Tab 掉进背后列表
}
function closeLightbox() {
$lightbox.classList.add('hidden');
// 清掉 src 释放解码后的大图内存;缓存里那份 base64 还在,重开零 IPC
$lightboxImg.removeAttribute('src');
if (lightboxReturnFocus && lightboxReturnFocus.isConnected) {
lightboxReturnFocus.focus();
}
lightboxReturnFocus = null;
}
// 点遮罩任意处(含大图本身)关闭
$lightbox.addEventListener('click', closeLightbox);
// ---------- 渲染 ----------
// 单张卡片:纯 DOM 构造,任何状态都不在这里改。
@@ -195,15 +237,36 @@ function buildCard(c, i) {
if (c.type === 'text') {
textEl.textContent = c.text || c.preview || '';
imgEl.classList.add('hidden');
} else {
} else if (c.hasImage) {
textEl.classList.add('hidden');
imgEl.classList.remove('hidden');
imgEl.removeAttribute('src');
if (c.hasImage) {
imgEl.dataset.imgId = String(c.id);
imgEl.dataset.imgFav = currentTab === 'favorites' ? '1' : '0';
imgObserver.observe(imgEl);
}
imgEl.dataset.imgId = String(c.id);
imgEl.dataset.imgFav = currentTab === 'favorites' ? '1' : '0';
imgObserver.observe(imgEl);
// 缩略图可点击/可聚焦:单击或 Enter/空格弹出大图预览。
// stopPropagation 防冒泡到卡片 onclick(选中)和全局 keydown(复制)。
imgEl.tabIndex = 0;
imgEl.setAttribute('role', 'button');
imgEl.title = '点击放大预览';
imgEl.alt = '图片预览(点击放大)';
imgEl.addEventListener('click', (e) => { e.stopPropagation(); openLightbox(imgEl); });
// dblclick 也会冒泡到卡片的 ondblclick(复制并写剪贴板)—— 双击缩略图
// 的用户意图是放大,不能顺手把剪贴板覆盖了。click 的 stopPropagation
// 只管 click 事件,拦不住 dblclick,得单独拦。
imgEl.addEventListener('dblclick', (e) => e.stopPropagation());
imgEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
e.stopPropagation();
openLightbox(imgEl);
}
});
} else {
// 图片条目但数据缺失(DB 里 image 列为 NULL 的极端情况)—— 退回文本
// 预览('[图片]' / '[图片] 800×600'),别让卡片渲染成一个空的破图框。
textEl.textContent = c.preview || '[图片]';
imgEl.classList.add('hidden');
}
// 点击选中按下标 —— 但必须点击时再从 DOM 现取:prependClip 增量插入
@@ -290,13 +353,20 @@ function prependClip(clip) {
const newCard = buildCard(clip, 0);
$list.insertBefore(newCard, $cards[0] || null); // null = 追加到末尾
$cards.unshift(newCard);
// Slot badge 重排:每次 prepend 之后,只有原来第 8 位(现在第 9 位)的卡需要
// 从「显示 9」变成「隐藏」,其它卡(0-8 已可见、9+ 已隐藏)都不动。
// 全表 1000 张卡都跑一遍是浪费。
const boundary = $cards[9]; // 第 10 位(下标 9):需要被隐藏
if (boundary) {
const slotEl = boundary.querySelector('.slot');
slotEl.classList.add('hidden');
// Slot badge 重排:新卡插到最顶后,原来第 1-9 位的卡整体后移一位,
// 徽标数字必须跟着 +1 —— 只藏第 10 位会留下两个「1」,且数字键 1-9
// 按实际下标复制(见 keydown 的 /^[1-9]$/ 分支),显示序号与按键错位。
// 受影响的只有前 10 张卡(下标 1-8 改数字、下标 9 隐藏),最多 9 次
// DOM 更新,不需要全表重扫。新卡(下标 0)的「1」由 buildCard(clip, 0) 写好。
const renumberEnd = Math.min($cards.length, 10);
for (let i = 1; i < renumberEnd; i++) {
const slotEl = $cards[i].querySelector('.slot');
if (i < 9) {
slotEl.textContent = String(i + 1);
slotEl.classList.remove('hidden');
} else {
slotEl.classList.add('hidden');
}
}
// 选中态:强制选中第一条(最新复制的条目),并滚到顶部让 Enter / 数字键 1 复制它。
// select() 内部会遍历 $cards 把旧 'selected' 清掉、把 $cards[0] 点亮,
@@ -462,6 +532,16 @@ document.addEventListener('keydown', (e) => {
reload();
return;
}
// 大图预览打开时模态拦截:Esc 关弹窗,其余键全部吞掉 —— 不然
// Enter/数字键会穿过弹窗触发列表复制,方向键会在背后移动选中项。
// (放在 F5 之后:刷新是全局的,弹窗开着也允许;Ctrl+L 则被拦下。)
if (!$lightbox.classList.contains('hidden')) {
if (e.key !== 'Escape') return; // 吞掉 Esc 之外的所有键
e.preventDefault();
closeLightbox();
return;
}
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'l') {
// Ctrl+L 聚焦搜索框。组合键完整匹配避免吃掉 Ctrl+Shift+L 之类。
e.preventDefault();

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" />
<title>设置</title>
<link rel="stylesheet" href="themes/fonts.css" />
<link rel="stylesheet" href="themes/tokens.css" />
<link rel="stylesheet" href="themes/fonts.css" />
<link rel="stylesheet" href="themes/themes.css" />
<link rel="stylesheet" href="themes/accents.css" />
<link rel="stylesheet" href="styles.css" />
@@ -61,7 +61,7 @@
<path d="M9 11h6M9 14.5h6M9 18h3"/>
</svg>
</div>
<div class="about-name">剪贴板</div>
<div class="about-name">Clipboard</div>
<div class="about-tagline">简洁的剪贴板管理器</div>
</div>
<div class="setting-row column">

View File

@@ -40,7 +40,10 @@
const { get, set, clearHistory, openDir, chooseDir, openExternal } = window.settingsApi;
const $auto=document.getElementById('opt-autolaunch'),$hotkey=document.getElementById('opt-hotkey'),$max=document.getElementById('opt-maxitems'),$display=document.getElementById('opt-displaylimit'),$poll=document.getElementById('opt-pollms'),$dir=document.getElementById('opt-datadir'),$change=document.getElementById('btn-change-dir'),$open=document.getElementById('btn-open-dir'),$clear=document.getElementById('btn-clear'),$close=document.getElementById('close-btn'),$status=document.getElementById('status-left');
function flash(msg){$status.textContent=msg;setTimeout(()=>$status.textContent='就绪',2000)}
// 连发两条提示时,第一条的 2 秒定时器会把第二条提前刷回「就绪」——
// 每次 flash 先清掉上一个定时器,重置时间以最后一条消息为准。
let flashTimer=null;
function flash(msg){$status.textContent=msg;clearTimeout(flashTimer);flashTimer=setTimeout(()=>$status.textContent='就绪',2000)}
// settings:set 在主进程先改运行时状态、后写盘;写盘失败(磁盘满/只读/杀软锁文件)
// 会带 saved:false 回来 —— 本次会话已生效但重启还原,必须明确提示,
// 不能让用户以为设置已经保存。
@@ -48,7 +51,19 @@ function flashIfNotSaved(res){
if(res&&res.saved===false){flash('⚠ 设置未保存到磁盘,重启后还原');return true}
return false
}
async function refresh(){const c=await get();$auto.checked=!!c.autoLaunch;$hotkey.value=c.hotkey||'';$max.value=String(c.maxItems);$display.value=String(c.displayLimit);$poll.value=String(c.pollMs);$dir.textContent=c.dataDir}
async function refresh(){
const c=await get();
$auto.checked=!!c.autoLaunch;$hotkey.value=c.hotkey||'';$max.value=String(c.maxItems);$display.value=String(c.displayLimit);$poll.value=String(c.pollMs);
// 显示实际在用的目录(dataDirEffective,含 session 兜底);与配置目录不
// 一致时(配置位置暂不可用)两个都给出来,并解释当前数据在哪。
if(c.dataDirEffective&&c.dataDirEffective!==c.dataDir){
$dir.textContent=c.dataDirEffective;
$dir.title='配置位置 '+c.dataDir+' 当前不可用,本会话暂用此目录(重启后若恢复会自动切回)';
}else{
$dir.textContent=c.dataDir;
$dir.title='';
}
}
$auto.addEventListener('change',async()=>{const res=await set('autoLaunch',$auto.checked);if(!flashIfNotSaved(res))flash('已'+($auto.checked?'开启':'关闭')+'开机自启动')});
$max.addEventListener('change',async()=>{const res=await set('maxItems',parseInt($max.value,10));if(!flashIfNotSaved(res))flash('最大条数已设为 '+$max.value)});
$display.addEventListener('change',async()=>{const res=await set('displayLimit',parseInt($display.value,10));if(!flashIfNotSaved(res))flash('界面显示条数已设为 '+$display.value)});
@@ -117,6 +132,10 @@ function renderThemes(currentTheme) {
const card = document.createElement('div');
card.className = 'theme-card' + (t.id === currentTheme ? ' active' : '');
card.dataset.theme = t.id;
// div + click 对键盘不可达:补 button 语义。Enter/Space 触发与鼠标一致的 click。
card.tabIndex = 0;
card.setAttribute('role', 'button');
card.setAttribute('aria-pressed', String(t.id === currentTheme));
card.innerHTML = `
<div class="theme-preview" style="background:${t.bg};color:${t.text}">
<span class="theme-preview-text">Aa</span>
@@ -127,7 +146,14 @@ function renderThemes(currentTheme) {
<div class="theme-desc">${t.desc}</div>
</div>
`;
card.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); card.click(); }
});
card.addEventListener('click', async () => {
// renderThemes 会销毁全部旧卡片:焦点卡没了焦点就掉到 body,键盘用户
// 迷失位置。重建后把焦点还到同名新卡(Chromium 会沿用 focus-visible 状态,
// 鼠标点击不会因此多出焦点框)。
const hadFocus = document.activeElement === card;
const res = await themeSet(t.id);
if (res) {
const eff = res.effectiveTheme || res.theme;
@@ -145,6 +171,10 @@ function renderThemes(currentTheme) {
} catch (_) {}
document.getElementById('opt-follow-system').checked = false;
renderThemes(eff);
if (hadFocus) {
const next = grid.querySelector(`[data-theme="${t.id}"]`);
if (next) next.focus();
}
flashIfNotSaved(res);
}
});

View File

@@ -117,14 +117,14 @@ html, body {
/* 置顶开启:图标上色 + 轻微背景,关闭时回到 muted */
#pin-btn[aria-pressed="true"] {
color: var(--accent);
color: var(--accent-fg);
background: var(--accent-soft);
}
#pin-btn[aria-pressed="true"]:hover {
/* 开启态下 hover 不要洗掉上色,只把背景加深一点 */
background: var(--accent-hover);
color: var(--accent);
color: var(--accent-fg);
}
#refresh-btn.spinning svg {
@@ -191,8 +191,8 @@ html, body {
}
#tabs .tab.active {
color: var(--accent);
border-bottom-color: var(--accent);
color: var(--accent-fg);
border-bottom-color: var(--accent-fg);
}
.tab-count {
@@ -213,7 +213,7 @@ html, body {
#tabs .tab.active .tab-count {
background: var(--accent-soft);
color: var(--accent);
color: var(--accent-fg);
}
/* ---------- 搜索 ---------- */
@@ -258,9 +258,11 @@ html, body {
box-shadow: 0 0 0 3px var(--accent-soft);
}
#search:focus + #search-clear,
/* 只有搜索图标随聚焦高亮。之前还有一条 #search:focus + #search-clear,
* 会让清空按钮在输入全程保持 accent 色 —— 一个次要的 × 按钮不该比输入框
* 本身更抢眼,且与 #search-clear 自身的 muted 设计矛盾,已移除。 */
#search-wrap:focus-within #search-icon {
color: var(--accent);
color: var(--accent-fg);
}
#search::placeholder {
@@ -360,6 +362,12 @@ html, body {
border-color: var(--accent);
}
/* .card 设过 outline:none,必须用等效焦点指示补回(卡片本身 tabindex=0) */
.card:focus-visible {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.card-head {
display: flex;
align-items: center;
@@ -378,7 +386,9 @@ html, body {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 10.5px;
/* 徽标含中文(文本/图片):tokens 自定标准 --fs-xs=11px 是 CJK 清晰下限,
* 之前 10.5px 与自己的标准矛盾,小字中文发糊。 */
font-size: var(--fs-xs);
color: var(--text-soft);
background: var(--bg-hover);
border-radius: 4px;
@@ -395,11 +405,11 @@ html, body {
}
.badge-image {
color: var(--accent);
color: var(--accent-fg);
}
.badge-image svg {
color: var(--accent);
color: var(--accent-fg);
opacity: 0.85;
}
@@ -433,8 +443,12 @@ html, body {
border-radius: 6px;
cursor: pointer;
opacity: 0;
/* 只设 opacity:0 的按钮「看不见但点得着」:点卡片头部想选中时会误触
* 透明的 ★/删除;键盘 Tab 也会聚焦到隐形按钮。visibility 进 transition
* 是离散动画:出现时立即可见 + opacity 淡入,消失时等淡出结束才隐藏。 */
visibility: hidden;
line-height: 1;
transition: opacity var(--transition-fast), background var(--transition-fast), color var(--transition-fast);
transition: opacity var(--transition-fast), background var(--transition-fast), color var(--transition-fast), visibility var(--transition-fast);
}
.action svg {
@@ -443,8 +457,15 @@ html, body {
}
.card:hover .action,
.card.selected .action {
.card.selected .action,
.action:focus-visible {
opacity: 1;
visibility: visible;
}
.action:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.action:hover {
@@ -453,12 +474,12 @@ html, body {
}
.action.copy {
color: var(--accent);
color: var(--accent-fg);
padding-right: 10px;
}
.action.copy:hover {
color: var(--accent);
color: var(--accent-fg);
background: var(--accent-hover);
}
@@ -507,7 +528,9 @@ html, body {
color: var(--text);
/* 中文正文行高 ≥1.55 才不会出现笔画"黏连",这里设 1.6 让 13px 中文清爽 */
line-height: 1.6;
word-break: break-all;
/* 超长无空格串(URL/路径)能断行,但不像 word-break:break-all 那样把
* 普通英文单词从中间生硬截断;中文本身任意位置可断,不受影响。 */
overflow-wrap: anywhere;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
@@ -529,6 +552,53 @@ html, body {
linear-gradient(-45deg, transparent 75%, var(--bg-soft) 75%);
background-size: 12px 12px;
background-position: 0 0, 0 6px, 6px -6px, -6px 0;
cursor: zoom-in;
}
/* 缩略图可聚焦(buildCard 里设了 role=button + tabindex),补键盘焦点指示 */
.image-preview:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
/* ---------- 图片大图预览 ---------- */
#lightbox {
position: fixed;
inset: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.72);
cursor: zoom-out;
}
/* 焦点放在遮罩上只为接 Esc,不需要焦点描边 */
#lightbox:focus { outline: none; }
#lightbox:not(.hidden) {
animation: lightbox-in var(--transition-fast);
}
@keyframes lightbox-in {
from { opacity: 0; }
}
#lightbox-img {
max-width: 92vw;
max-height: 88vh;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
/* 与 .image-preview 同款棋盘格底;深色遮罩上换成浅色格子,透明 PNG 才看得清 */
background:
linear-gradient(45deg, rgba(255, 255, 255, 0.14) 25%, transparent 25%),
linear-gradient(-45deg, rgba(255, 255, 255, 0.14) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, rgba(255, 255, 255, 0.14) 75%),
linear-gradient(-45deg, transparent 75%, rgba(255, 255, 255, 0.14) 75%);
background-size: 16px 16px;
background-position: 0 0, 0 8px, 8px -8px, -8px 0;
}
/* ---------- 空状态 ---------- */
@@ -652,14 +722,17 @@ kbd {
cursor: pointer;
line-height: 1;
margin-bottom: -1px;
transition: color 0.12s ease, border-color 0.12s ease;
/* 与全局动效 token 统一(--transition-fast = 100ms ease-out),不再散写 0.12s ease */
transition: color var(--transition-fast), border-color var(--transition-fast);
}
#tab-bar .tab:hover { color: var(--text); }
#tab-bar .tab[aria-selected="true"] {
color: var(--text);
border-bottom-color: var(--accent);
/* 与 #tabs .tab.active 一致:文字/下划线指示都走 --accent-fg,
* light 主题亮色系 accent 下划线才够清晰。 */
border-bottom-color: var(--accent-fg);
font-weight: var(--fw-semibold);
}
@@ -759,9 +832,11 @@ kbd {
.btn:hover { background: var(--bg-hover); border-color: var(--border-strong); }
.btn:active { background: var(--bg-soft); }
.btn-danger { color: var(--danger); border-color: var(--danger-bg); background: var(--danger-bg); }
.btn-danger:hover { background: var(--danger); color: var(--accent-text); border-color: var(--danger); }
/* hover 是「红底白字」:不能用 --accent-text(它随 accent 色系自适应,
* green/orange accent 下是深色字,红底深字对比度崩塌),这里恒为白。 */
.btn-danger:hover { background: var(--danger); color: #FFFFFF; border-color: var(--danger); }
.danger-zone { border-color: var(--danger-bg); }
.danger-zone:hover { border-color: rgba(239, 68, 68, 0.4); }
.danger-zone:hover { border-color: var(--danger-border); }
/* 开关 */
.switch { position: relative; display: inline-block; width: 36px; height: 20px; flex-shrink: 0; }
@@ -907,7 +982,11 @@ kbd {
width: 44px;
height: 44px;
border-radius: 10px;
background: linear-gradient(135deg, var(--accent), var(--accent-hover));
/* 立体渐变 = 实色 accent 上叠一层 白→黑 overlay(设计系统标准做法)。
* 之前渐变末端用 --accent-hover,那是个 20% 透明度的 hover 底色 token,
* 图标右下角几乎全透明、透出卡片底色,像褪了一块色。 */
background: var(--accent);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.16), rgba(0, 0, 0, 0.14)), var(--accent);
display: flex;
align-items: center;
justify-content: center;
@@ -943,10 +1022,28 @@ kbd {
.about-dev { font-size: var(--fs-base); color: var(--text); }
.ext-link {
font-size: var(--fs-sm);
color: var(--accent);
color: var(--accent-fg);
text-decoration: none;
word-break: break-all;
cursor: pointer;
transition: opacity var(--transition-fast);
}
.ext-link:hover { text-decoration: underline; opacity: 0.85; }
/* ---------- 键盘焦点可见性(:focus-visible) ----------
* 全局面控件统一的键盘焦点指示。:focus-visible 只在键盘导航时触发,
* 鼠标点击不出现描边,不影响现有观感。
* (.card / .action / #tab-bar .tab / .switch / select 已在各自区块定义。) */
.icon-btn:focus-visible,
#search-clear:focus-visible,
.btn:focus-visible,
#tabs .tab:focus-visible,
.theme-card:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
/* 圆形色板用双层 ring(与 .active 选中态同构,区别在 accent 色) */
.accent-swatch:focus-visible {
box-shadow: 0 0 0 2px var(--bg-base), 0 0 0 4px var(--accent);
}

View File

@@ -21,16 +21,30 @@
--accent: #10B981;
--accent-soft: rgba(16, 185, 129, 0.12);
--accent-hover: rgba(16, 185, 129, 0.20);
/* 亮绿底 + 白字只有 2.5:1(WCAG 需 4.5),slot 序号等 accent 底色文字改深色 → 5.8:1 */
--accent-text: #1F2937;
}
:root[data-accent="orange"] {
--accent: #F59E0B;
--accent-soft: rgba(245, 158, 11, 0.12);
--accent-hover: rgba(245, 158, 11, 0.20);
/* 同 green:亮橙底 + 白字 2.2:1 → 深色 6.8:1 */
--accent-text: #1F2937;
}
:root[data-accent="red"] {
--accent: #EF4444;
--accent-soft: rgba(239, 68, 68, 0.12);
--accent-hover: rgba(239, 68, 68, 0.20);
}
}
/* Light 主题下 accent 直接作文字色对比度不足(实测 orange 2.06 / green 2.43 /
* blue 3.52,WCAG 正文需 ≥4.5)—— 文字场景(--accent-fg)改用同色系深色变体,
* 全部 ≥4.6:1;色块填充 / 边框仍用 --accent 本身,品牌色观感不变。
* 深色主题各 accent 文字对比度 ≥3.2 且多为图标/强调短文本,维持原色。 */
:root[data-theme="light"][data-accent="purple"] { --accent-fg: #4F46E5; }
:root[data-theme="light"][data-accent="blue"] { --accent-fg: #2563EB; }
:root[data-theme="light"][data-accent="green"] { --accent-fg: #047857; }
:root[data-theme="light"][data-accent="orange"] { --accent-fg: #B45309; }
:root[data-theme="light"][data-accent="red"] { --accent-fg: #DC2626; }

View File

@@ -19,8 +19,8 @@
}
/* 字体变量赋值
* 中文优先:西文(Inter)排在中文兜底之后,App 里大部分文本是中文,
* 让 YaHei/PingFang 先匹配笔画渲染,CJK 之外再走 Inter 拿更好的西文观感。 */
* 西文优先:Inter 排最前拿更好的西文/数字观感;Inter 不含 CJK 字形,
* 中文会自动落到后面的 YaHei/PingFang 兜底,两段各取所长。 */
:root {
--font-ui: 'Inter', 'Microsoft YaHei UI', 'Microsoft YaHei', 'PingFang SC',
'Hiragino Sans GB', 'Segoe UI', system-ui, -apple-system,

View File

@@ -24,7 +24,13 @@
--accent: #5E6AD2;
--accent-soft: rgba(94, 106, 210, 0.12);
--accent-hover: rgba(94, 106, 210, 0.20);
/* accent 底色上的文字色:亮色系 accent(green/orange)白字对比度仅 ~2:1,
* accents.css 对这两个色系覆盖成深色文字。 */
--accent-text: #FFFFFF;
/* accent 作「文字色」用时的取值(Tab 激活、复制按钮、外链等)。
* 默认 = accent 本身;light 主题下亮色系 accent 对比度不足,
* accents.css 按 [data-theme="light"][data-accent="..."] 覆盖成深色变体。 */
--accent-fg: var(--accent);
/* 状态色 */
--success: #10B981;
@@ -32,6 +38,7 @@
--warn-bg: rgba(245, 158, 11, 0.12);
--danger: #EF4444;
--danger-bg: rgba(239, 68, 68, 0.12);
--danger-border: rgba(239, 68, 68, 0.4);
/* 阴影 */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);