UI/细节调整
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 主题色派生单测(Node 内置 test runner)。
|
||||
*
|
||||
* 重点不是「算得对不对」,而是「任何预设色在两种模式下都可读」——
|
||||
* 对比度是这条链路上唯一会真正伤到用户的指标。
|
||||
*/
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {
|
||||
ACCENT_PRESETS,
|
||||
ACCENT_VAR_KEYS,
|
||||
contrastRatio,
|
||||
deriveAccent,
|
||||
hexToOklch,
|
||||
oklchToHex,
|
||||
resolveAccentHex,
|
||||
type ThemeMode
|
||||
} from './theme.ts'
|
||||
|
||||
const MODES: ThemeMode[] = ['light', 'dark']
|
||||
/** 深色模式的对比度基准(与实现里的 DARK_REFERENCE 一致) */
|
||||
const DARK_REFERENCE = '#1e1e22'
|
||||
|
||||
test('hex → OKLCH → hex 往返稳定', () => {
|
||||
for (const preset of ACCENT_PRESETS) {
|
||||
const back = oklchToHex(hexToOklch(preset.hex))
|
||||
const [r1, g1, b1] = [1, 3, 5].map(i => parseInt(preset.hex.slice(i, i + 2), 16))
|
||||
const [r2, g2, b2] = [1, 3, 5].map(i => parseInt(back.slice(i, i + 2), 16))
|
||||
// 允许 ±1/255 的取整误差
|
||||
assert.ok(Math.abs(r1 - r2) <= 1, `${preset.id} R: ${preset.hex} → ${back}`)
|
||||
assert.ok(Math.abs(g1 - g2) <= 1, `${preset.id} G: ${preset.hex} → ${back}`)
|
||||
assert.ok(Math.abs(b1 - b2) <= 1, `${preset.id} B: ${preset.hex} → ${back}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('默认蓝派生结果贴近现有手调值', () => {
|
||||
const light = deriveAccent('#0066cc', 'light')
|
||||
assert.equal(light['--primary'], '#0066cc')
|
||||
// 现有 --primary-soft / --primary-soft-text = #e8f1fc / #004f9e,只要求同一色族与量级
|
||||
assert.equal(hexToOklch(light['--primary-soft']).l > 0.9, true)
|
||||
assert.equal(contrastRatio(light['--primary-soft-text'], light['--primary-soft']) >= 4.5, true)
|
||||
})
|
||||
|
||||
test('所有预设 × 两种模式:强调色对底色、浅底文字对浅底、按钮文字对按钮底 均 ≥ 4.5:1', () => {
|
||||
for (const preset of ACCENT_PRESETS) {
|
||||
for (const mode of MODES) {
|
||||
const vars = deriveAccent(preset.hex, mode)
|
||||
const reference = mode === 'light' ? '#ffffff' : DARK_REFERENCE
|
||||
const label = `${preset.id}/${mode}`
|
||||
|
||||
assert.ok(
|
||||
contrastRatio(vars['--primary'], reference) >= 4.5,
|
||||
`${label} primary 对底对比度不足: ${contrastRatio(vars['--primary'], reference).toFixed(2)}`
|
||||
)
|
||||
assert.ok(
|
||||
contrastRatio(vars['--primary-soft-text'], vars['--primary-soft']) >= 4.5,
|
||||
`${label} soft-text 对 soft 对比度不足: ${contrastRatio(
|
||||
vars['--primary-soft-text'],
|
||||
vars['--primary-soft']
|
||||
).toFixed(2)}`
|
||||
)
|
||||
// 填充上的文字:浅色模式必然 ≥4.5:1;深色模式强调色偏亮,白字约 2.7–3:1
|
||||
//(与现有 #2997ff + 白字 的取舍一致),门槛按 UI 组件的 2.5:1 兜底
|
||||
const fillMin = mode === 'light' ? 4.5 : 2.5
|
||||
assert.ok(
|
||||
contrastRatio(vars['--primary-foreground'], vars['--primary']) >= fillMin,
|
||||
`${label} foreground 对 primary 对比度不足: ${contrastRatio(
|
||||
vars['--primary-foreground'],
|
||||
vars['--primary']
|
||||
).toFixed(2)}`
|
||||
)
|
||||
assert.equal(vars['--ring'], vars['--primary'], `${label} ring 应等于 primary`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('亮色(青柠)在浅色模式下被自动压暗', () => {
|
||||
const base = hexToOklch('#65a30d')
|
||||
const derived = hexToOklch(deriveAccent('#65a30d', 'light')['--primary'])
|
||||
// 青柠原色在白底上只有约 2.8:1,派生后必须达标且比原色暗
|
||||
assert.ok(derived.l < base.l, `未压暗: ${base.l} → ${derived.l}`)
|
||||
assert.ok(contrastRatio(oklchToHex(derived), '#ffffff') >= 4.5)
|
||||
})
|
||||
|
||||
test('色相保留(除被 sRGB 色域裁剪的极端彩度)', () => {
|
||||
for (const preset of ACCENT_PRESETS) {
|
||||
for (const mode of MODES) {
|
||||
const base = hexToOklch(preset.hex)
|
||||
const derived = hexToOklch(deriveAccent(preset.hex, mode)['--primary'])
|
||||
if (derived.c < 0.02) continue // 石墨这类近无彩色,色相无意义
|
||||
const diff = Math.abs(((derived.h - base.h + 540) % 360) - 180)
|
||||
assert.ok(diff < 12, `${preset.id}/${mode} 色相偏移过大: ${base.h} → ${derived.h}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('派生结果都是合法 hex,且每个变量都有值', () => {
|
||||
for (const preset of ACCENT_PRESETS) {
|
||||
for (const mode of MODES) {
|
||||
const vars = deriveAccent(preset.hex, mode)
|
||||
for (const key of ACCENT_VAR_KEYS) {
|
||||
assert.match(vars[key], /^#[0-9a-f]{6}$/, `${preset.id}/${mode} ${key} 非法: ${vars[key]}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('resolveAccentHex:默认与非法自定义都回落到内置默认', () => {
|
||||
assert.equal(resolveAccentHex('blue', null), null)
|
||||
assert.equal(resolveAccentHex('custom', null), null)
|
||||
assert.equal(resolveAccentHex('custom', 'not-a-color'), null)
|
||||
assert.equal(resolveAccentHex('custom', '#ffd60a'), '#ffd60a')
|
||||
assert.equal(resolveAccentHex('violet', null), '#7c3aed')
|
||||
})
|
||||
Reference in New Issue
Block a user