mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 16:53:35 +08:00
🎨 style(theme): 提升主操作按钮醒目度、修复默认主题标签对比度并降级保存按钮
- 新增 --gn-accent-strong 系列(提饱和 1.55×)供主按钮填充:内置主题的强调色刻意低饱和 (Comfort Dark 仅 20%、Deep Ocean 28%),淡到接近灰蓝,填在按钮上读起来"没有颜色"。 只改按钮填充,侧边栏高亮与文字仍用原 accent,保持主题整体淡雅。 实测深色主题饱和度提升到 31%–72%,对工具栏对比度仍 ≥6.0、标签 ≥6.1 - 提饱和只对深色主题生效:浅色主题的强调色本身已约 46% 饱和,提了反而把白色标签从 4.5+ 压到 4.14 跌破 AA(该情况由新增测试抓出) - 默认 v2 主题补上此前从未定义的 --gn-on-accent:所有 var(--gn-on-accent, #fff) 都在回退 白字,深色下压 #22c55e 仅 2.28、浅色下压 #16a34a 仅 3.30,均不满足 WCAG AA。 改为 #0a2417 / #052e16,得 7.21 / 4.52 - 「保存」按钮降为描边次级样式:它与「执行」原本都是实心按钮,而两者填充色(accent 与 info) 在内置主题下区分度仅 1.05–1.25,相邻两个同色方块分不出谁是谁。 由于 Midnight Navy / Nord Slate 的 accent 本身即蓝青系,靠拉开色相无法解决, 改用层级区分:工具栏里只有「执行」是实心主按钮,主操作因此真正突出 - 补 2 项不变式测试:accent-strong 饱和度提升且标签 ≥4.5、其三态同样逐级加深 说明:原计划的「拉开执行与保存色相」经测量不可行(accent 自身即蓝青系,warn 已被手动事务 占用),故改为层级区分,与之前沟通的方案不同。
This commit is contained in:
@@ -150,6 +150,78 @@ const readableOnColor = (background: string): string => {
|
||||
* 白字只剩 2.65 的对比度;反之深色主题的浅琥珀底配深字,则必须往白色叠加。
|
||||
* 因此按前景色是深还是浅,决定徽标底往相反方向偏移。
|
||||
*/
|
||||
/**
|
||||
* 主操作按钮填充色的饱和度提升倍数。
|
||||
*
|
||||
* 内置主题的强调色刻意低饱和(Comfort Dark 仅 20%、Deep Ocean 28%),淡到接近灰蓝,
|
||||
* 填充在按钮上读起来"没有颜色"、不像可点的主操作。这里只提升**按钮填充**用的派生色,
|
||||
* 侧边栏高亮、文字、边框等仍用原 accent,保持主题整体的淡雅气质。
|
||||
*
|
||||
* 取 1.55:实测 4 个深色内置主题的饱和度提升到 31%–72%,同时对工具栏底的对比度仍 ≥6.0、
|
||||
* 标签可读性 ≥6.1;再往上(1.75)Midnight Navy 会到 82%,偏霓虹。
|
||||
*
|
||||
* 只对深色主题生效:浅色主题的强调色本身已有约 46% 饱和(如 warm-paper 的 #2d7864),
|
||||
* 不存在"淡到没颜色"的问题;对其提饱和反而会把白色标签的对比度从 4.5+ 压到 4.14,跌破 AA。
|
||||
*/
|
||||
const ACCENT_STRONG_SATURATION = 1.55;
|
||||
|
||||
const accentStrongSaturation = (palette: BuiltinThemePalette): number => (
|
||||
palette.mode === 'light' ? 1 : ACCENT_STRONG_SATURATION
|
||||
);
|
||||
|
||||
const rgbToHsl = ([r, g, b]: [number, number, number]): [number, number, number] => {
|
||||
const rn = r / 255;
|
||||
const gn = g / 255;
|
||||
const bn = b / 255;
|
||||
const max = Math.max(rn, gn, bn);
|
||||
const min = Math.min(rn, gn, bn);
|
||||
const lightness = (max + min) / 2;
|
||||
if (max === min) {
|
||||
return [0, 0, lightness];
|
||||
}
|
||||
const delta = max - min;
|
||||
const saturation = delta / (1 - Math.abs(2 * lightness - 1));
|
||||
let hue: number;
|
||||
if (max === rn) {
|
||||
hue = ((gn - bn) / delta + (gn < bn ? 6 : 0)) * 60;
|
||||
} else if (max === gn) {
|
||||
hue = ((bn - rn) / delta + 2) * 60;
|
||||
} else {
|
||||
hue = ((rn - gn) / delta + 4) * 60;
|
||||
}
|
||||
return [hue, saturation, lightness];
|
||||
};
|
||||
|
||||
const hslToHex = ([hue, saturation, lightness]: [number, number, number]): string => {
|
||||
const chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
|
||||
const secondary = chroma * (1 - Math.abs(((hue / 60) % 2) - 1));
|
||||
const offset = lightness - chroma / 2;
|
||||
const sector = Math.floor(hue / 60) % 6;
|
||||
const table: [number, number, number][] = [
|
||||
[chroma, secondary, 0],
|
||||
[secondary, chroma, 0],
|
||||
[0, chroma, secondary],
|
||||
[0, secondary, chroma],
|
||||
[secondary, 0, chroma],
|
||||
[chroma, 0, secondary],
|
||||
];
|
||||
const [r, g, b] = table[sector < 0 ? 0 : sector];
|
||||
const channel = (value: number): string => Math.max(0, Math.min(255, Math.round((value + offset) * 255)))
|
||||
.toString(16)
|
||||
.padStart(2, '0');
|
||||
return `#${channel(r)}${channel(g)}${channel(b)}`;
|
||||
};
|
||||
|
||||
/** saturate 保持色相与明度,仅提升饱和度。 */
|
||||
const saturate = (color: string, factor: number): string => {
|
||||
const rgb = parseHexColor(color);
|
||||
if (!rgb) {
|
||||
return color;
|
||||
}
|
||||
const [hue, saturation, lightness] = rgbToHsl(rgb);
|
||||
return hslToHex([hue, Math.min(1, saturation * factor), lightness]);
|
||||
};
|
||||
|
||||
const badgeBackground = (base: string, onColor: string): string => {
|
||||
const rgb = parseHexColor(onColor);
|
||||
// 前景偏深时把徽标底往白色偏移,前景偏浅(白字)时往黑色偏移。
|
||||
@@ -231,6 +303,11 @@ body[data-custom-theme][data-ui-version="v2"] {
|
||||
--gn-accent-hover: ${mixHex(palette.accent, accentStateAnchor(palette), ACCENT_HOVER_RATIO)};
|
||||
--gn-accent-active: ${mixHex(palette.accent, accentStateAnchor(palette), ACCENT_ACTIVE_RATIO)};
|
||||
|
||||
/* 只给主操作按钮填充用的提饱和派生色;侧边栏高亮、文字、边框仍用原 accent。 */
|
||||
--gn-accent-strong: ${saturate(palette.accent, accentStrongSaturation(palette))};
|
||||
--gn-accent-strong-hover: ${mixHex(saturate(palette.accent, accentStrongSaturation(palette)), accentStateAnchor(palette), ACCENT_HOVER_RATIO)};
|
||||
--gn-accent-strong-active: ${mixHex(saturate(palette.accent, accentStrongSaturation(palette)), accentStateAnchor(palette), ACCENT_ACTIVE_RATIO)};
|
||||
|
||||
/* 手动事务的「提交」按钮刻意不跟随主题强调色:待提交事务是需要用户显式决断的状态,
|
||||
若与普通主操作同色则无法区分(回滚已用 danger,提交用 warn 形成语义配对)。
|
||||
on-warn 按亮度择优,因为 warn 在各预设间跨度很大(#9f5f1d ~ #ebcb8b)。 */
|
||||
@@ -318,21 +395,28 @@ body[data-custom-theme][data-ui-version="v2"] .gn-v2-query-transaction-commit-bu
|
||||
color: var(--gn-accent-text, var(--gn-accent)) !important;
|
||||
}
|
||||
|
||||
/* 「保存」降为次级样式:与 v2-theme.css 保持一致。
|
||||
原先填 --gn-info,而 info 与 accent 在内置主题下的区分度仅 1.05–1.25,
|
||||
两个相邻实心按钮一眼分不清;且 Midnight Navy / Nord Slate 的 accent 本身即蓝青系,
|
||||
靠色相无法拉开,故改用层级区分,只让「执行」保持实心主按钮。 */
|
||||
body[data-custom-theme][data-ui-version="v2"] .gn-v2-query-toolbar-save-action.ant-btn-primary:not(:disabled) {
|
||||
background: var(--gn-info) !important;
|
||||
border-color: var(--gn-info) !important;
|
||||
color: var(--gn-on-info, #fff) !important;
|
||||
background: transparent !important;
|
||||
border-color: var(--gn-br-3) !important;
|
||||
color: var(--gn-fg-2) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
body[data-custom-theme][data-ui-version="v2"] .gn-v2-query-toolbar-save-action.ant-btn-primary:not(:disabled):hover,
|
||||
body[data-custom-theme][data-ui-version="v2"] .gn-v2-query-toolbar-save-action.ant-btn-primary:not(:disabled):focus-visible {
|
||||
background: color-mix(in srgb, var(--gn-info) 86%, var(--gn-bg-panel)) !important;
|
||||
border-color: color-mix(in srgb, var(--gn-info) 86%, var(--gn-bg-panel)) !important;
|
||||
background: var(--gn-bg-hover) !important;
|
||||
border-color: var(--gn-accent) !important;
|
||||
color: var(--gn-fg-1) !important;
|
||||
}
|
||||
|
||||
body[data-custom-theme][data-ui-version="v2"] .gn-v2-query-toolbar-save-action.ant-btn-primary:not(:disabled):active {
|
||||
background: color-mix(in srgb, var(--gn-info) 74%, var(--gn-bg-panel)) !important;
|
||||
border-color: color-mix(in srgb, var(--gn-info) 74%, var(--gn-bg-panel)) !important;
|
||||
background: var(--gn-bg-active) !important;
|
||||
border-color: var(--gn-accent) !important;
|
||||
color: var(--gn-fg-1) !important;
|
||||
}
|
||||
|
||||
body[data-custom-theme][data-ui-version="v2"] .gn-v2-ai-panel .ai-logo {
|
||||
|
||||
@@ -103,6 +103,60 @@ describe('内置主题的主要操作按钮三态', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const saturationOf = (value: string): number => {
|
||||
const [r, g, b] = parseHex(value).map((c) => c / 255);
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
const lightness = (max + min) / 2;
|
||||
return max === min ? 0 : (max - min) / (1 - Math.abs(2 * lightness - 1));
|
||||
};
|
||||
|
||||
/**
|
||||
* 主操作按钮填充用的提饱和派生色。
|
||||
* 回归背景:内置主题的强调色刻意低饱和(Comfort Dark 仅 20%、Deep Ocean 28%),
|
||||
* 直接填在按钮上读起来"没有颜色"、不像可点的主操作。
|
||||
*/
|
||||
describe('主操作按钮的提饱和填充色', () => {
|
||||
it('accent-strong 的饱和度高于原 accent,且标签仍可读', () => {
|
||||
for (const preset of BUILTIN_CUSTOM_THEME_PRESETS) {
|
||||
const accent = readToken(preset.css, '--gn-accent');
|
||||
const strong = readToken(preset.css, '--gn-accent-strong');
|
||||
const onAccent = readToken(preset.css, '--gn-on-accent');
|
||||
|
||||
// 浅色主题的强调色本身已足够饱和,不提升(提了会让白色标签跌破 AA)
|
||||
if (preset.baseMode === 'dark') {
|
||||
expect(
|
||||
saturationOf(strong),
|
||||
`${preset.id} 的 accent-strong 饱和度未提升`,
|
||||
).toBeGreaterThan(saturationOf(accent));
|
||||
} else {
|
||||
expect(saturationOf(strong)).toBeCloseTo(saturationOf(accent), 5);
|
||||
}
|
||||
expect(
|
||||
contrastRatio(onAccent, strong),
|
||||
`${preset.id} 的主按钮标签对比度不足`,
|
||||
).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
});
|
||||
|
||||
it('accent-strong 的三态同样逐级加深且达到可感知阈值', () => {
|
||||
for (const preset of BUILTIN_CUSTOM_THEME_PRESETS) {
|
||||
const strong = readToken(preset.css, '--gn-accent-strong');
|
||||
const hover = readToken(preset.css, '--gn-accent-strong-hover');
|
||||
const active = readToken(preset.css, '--gn-accent-strong-active');
|
||||
|
||||
expect(
|
||||
contrastRatio(strong, hover),
|
||||
`${preset.id} 的 strong base→hover 对比度不足`,
|
||||
).toBeGreaterThanOrEqual(MIN_STATE_DELTA);
|
||||
expect(
|
||||
contrastRatio(hover, active),
|
||||
`${preset.id} 的 strong hover→active 对比度不足`,
|
||||
).toBeGreaterThanOrEqual(MIN_STATE_DELTA);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 手动事务「提交」按钮用 warn 实心填充,刻意不跟随主题强调色。
|
||||
* 回归背景:原先用 accent-soft 底 + accent-2 文字,与普通主操作同色系且文字对比极低;
|
||||
|
||||
@@ -46,6 +46,12 @@ body[data-ui-version="v2"][data-theme="light"] {
|
||||
--gn-accent: #15803d;
|
||||
--gn-accent-2: #166534;
|
||||
--gn-accent-soft: #dcfce7;
|
||||
/* --gn-on-accent 此前未定义,所有 var(--gn-on-accent, #fff) 都回退成白字:
|
||||
白字压在 #16a34a 上仅 3.30,不满足 WCAG AA 的 4.5。深绿可得 4.52。 */
|
||||
--gn-on-accent: #052e16;
|
||||
--gn-accent-strong: #15803d;
|
||||
--gn-accent-strong-hover: #126634;
|
||||
--gn-accent-strong-active: #0d4d27;
|
||||
--gn-info: #0369a1;
|
||||
--gn-info-soft: #e0f2fe;
|
||||
--gn-warn: #d97706;
|
||||
@@ -91,6 +97,11 @@ body[data-ui-version="v2"][data-theme="dark"] {
|
||||
--gn-accent: #22c55e;
|
||||
--gn-accent-2: #16a34a;
|
||||
--gn-accent-soft: rgba(34, 197, 94, 0.16);
|
||||
/* 同上:白字压在 #22c55e 上仅 2.28,深绿可得 7.21。 */
|
||||
--gn-on-accent: #0a2417;
|
||||
--gn-accent-strong: #22c55e;
|
||||
--gn-accent-strong-hover: #1ca14d;
|
||||
--gn-accent-strong-active: #157a3b;
|
||||
--gn-info: #38bdf8;
|
||||
--gn-info-soft: rgba(56, 189, 248, 0.14);
|
||||
--gn-warn: #f59e0b;
|
||||
@@ -276,37 +287,54 @@ body[data-ui-version="v2"] .ant-btn-default:hover {
|
||||
color: var(--gn-fg-1) !important;
|
||||
}
|
||||
|
||||
/* 主操作按钮用提饱和的 --gn-accent-strong 填充:内置主题的 accent 刻意低饱和
|
||||
(Comfort Dark 仅 20%、Deep Ocean 28%),直接填在按钮上读起来"没有颜色"、不像主操作。
|
||||
只改按钮填充,侧边栏高亮与文字仍用原 accent,保持主题整体淡雅。
|
||||
前景色改用 --gn-on-accent:硬编码 #fff 在默认深色主题上压 #22c55e 仅 2.28,不满足 AA。 */
|
||||
body[data-ui-version="v2"] .ant-btn-primary {
|
||||
background: var(--gn-accent) !important;
|
||||
border-color: var(--gn-accent-2) !important;
|
||||
color: #fff !important;
|
||||
background: var(--gn-accent-strong, var(--gn-accent)) !important;
|
||||
border-color: var(--gn-accent-strong, var(--gn-accent-2)) !important;
|
||||
color: var(--gn-on-accent, #fff) !important;
|
||||
font-weight: 600 !important;
|
||||
box-shadow: var(--gn-shadow-sm), inset 0 0.5px 0 rgba(255,255,255,0.15) !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .ant-btn-primary:hover {
|
||||
background: var(--gn-accent-2) !important;
|
||||
border-color: var(--gn-accent-2) !important;
|
||||
background: var(--gn-accent-strong-hover, var(--gn-accent-2)) !important;
|
||||
border-color: var(--gn-accent-strong-hover, var(--gn-accent-2)) !important;
|
||||
color: var(--gn-on-accent, #fff) !important;
|
||||
}
|
||||
|
||||
/* 查询工具栏「保存」:与「运行」同款实心饱满,色值用蓝色 */
|
||||
body[data-ui-version="v2"] .ant-btn-primary:active {
|
||||
background: var(--gn-accent-strong-active, var(--gn-accent-2)) !important;
|
||||
border-color: var(--gn-accent-strong-active, var(--gn-accent-2)) !important;
|
||||
color: var(--gn-on-accent, #fff) !important;
|
||||
}
|
||||
|
||||
/* 查询工具栏「保存」降为次级样式:描边 + 中性底。
|
||||
原先它与「执行」一样是实心饱满按钮,而两者在内置主题下的填充色(accent 与 info)
|
||||
区分度仅 1.05–1.25 —— 相邻两个同色实心方块,一眼分不出哪个是执行、哪个是保存。
|
||||
由于 Midnight Navy / Nord Slate 的 accent 本身就是蓝青系,靠拉开色相无法解决,
|
||||
改用层级区分:工具栏里只有「执行」是实心主按钮,其余降级,主操作因此真正突出。 */
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar-save-action.ant-btn-primary {
|
||||
background: #2563eb !important;
|
||||
border-color: #1d4ed8 !important;
|
||||
color: #fff !important;
|
||||
background: transparent !important;
|
||||
border-color: var(--gn-br-3) !important;
|
||||
color: var(--gn-fg-2) !important;
|
||||
font-weight: 600 !important;
|
||||
box-shadow: var(--gn-shadow-sm), inset 0 0.5px 0 rgba(255, 255, 255, 0.18) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar-save-action.ant-btn-primary:hover {
|
||||
background: #1d4ed8 !important;
|
||||
border-color: #1e40af !important;
|
||||
color: #fff !important;
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar-save-action.ant-btn-primary:hover,
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar-save-action.ant-btn-primary:focus-visible {
|
||||
background: var(--gn-bg-hover) !important;
|
||||
border-color: var(--gn-accent) !important;
|
||||
color: var(--gn-fg-1) !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar-save-action.ant-btn-primary:active {
|
||||
background: #1e40af !important;
|
||||
border-color: #1e3a8a !important;
|
||||
background: var(--gn-bg-active) !important;
|
||||
border-color: var(--gn-accent) !important;
|
||||
color: var(--gn-fg-1) !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .ant-btn-primary:disabled {
|
||||
|
||||
Reference in New Issue
Block a user