mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Preserve activity stats contrast across desktop themes
The activity heatmap was reusing broad primary and inverse tokens whose meaning changes by theme. The usage tooltip became unreadable in dark mode, and nearby status badges referenced semantic container tokens that were not available in every theme. This introduces activity-specific theme tokens and locks the token surface with focused regression coverage. Constraint: Desktop supports light, white, and dark themes from globals.css Rejected: Change global primary or inverse tokens | broader blast radius across unrelated controls Confidence: high Scope-risk: narrow Directive: Keep activity heatmap colors behind activity-specific tokens before changing palette behavior Tested: cd desktop && bun run test ActivitySettings.test.tsx globals.test.ts Tested: bun run check:desktop Tested: Browser smoke on dark Token usage tooltip at http://127.0.0.1:5179 Not-tested: Native packaged app visual smoke
This commit is contained in:
parent
523db62b3b
commit
f1e3f4bd20
@ -112,6 +112,9 @@ describe('ActivitySettings', () => {
|
||||
expect(tooltip).toHaveTextContent('May 9, 2026')
|
||||
expect(tooltip).toHaveTextContent('4 sessions · 128K tokens')
|
||||
expect(tooltip).not.toHaveTextContent(/messages|tools/i)
|
||||
expect(tooltip.className).toContain('--color-activity-tooltip-surface')
|
||||
expect(tooltip.className).toContain('--color-activity-tooltip-border')
|
||||
expect(todayCell.className).toContain('--color-activity-cell-border')
|
||||
expect(screen.queryByText('Selected day')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -30,11 +30,11 @@ const HEAT_CELL_MIN = 6
|
||||
const HEAT_CELL_MAX = 22
|
||||
const TOOLTIP_WIDTH = 172
|
||||
const HEAT_COLORS = [
|
||||
'var(--color-surface-container)',
|
||||
'var(--color-primary-fixed)',
|
||||
'var(--color-primary-fixed-dim)',
|
||||
'var(--color-primary-container)',
|
||||
'var(--color-primary)',
|
||||
'var(--color-activity-heat-0)',
|
||||
'var(--color-activity-heat-1)',
|
||||
'var(--color-activity-heat-2)',
|
||||
'var(--color-activity-heat-3)',
|
||||
'var(--color-activity-heat-4)',
|
||||
]
|
||||
const DATE_LOCALES: Record<Locale, string> = {
|
||||
en: 'en-US',
|
||||
@ -380,8 +380,8 @@ export function ActivitySettings() {
|
||||
aria-describedby={activeTooltipDate === day.date ? tooltipId : undefined}
|
||||
className={`rounded-[3px] border transition-[border-color,transform] hover:scale-110 focus:outline-none focus:ring-2 focus:ring-[var(--color-brand)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface)] ${
|
||||
isSelected
|
||||
? 'border-[var(--color-text-primary)]'
|
||||
: 'border-[var(--color-border)]/30 hover:border-[var(--color-text-secondary)]'
|
||||
? 'border-[var(--color-activity-cell-border-active)]'
|
||||
: 'border-[var(--color-activity-cell-border)] hover:border-[var(--color-activity-cell-border-hover)]'
|
||||
}`}
|
||||
style={{
|
||||
width: heatCellSize,
|
||||
@ -401,11 +401,11 @@ export function ActivitySettings() {
|
||||
<div
|
||||
id={`activity-day-tooltip-${tooltipDay.date}`}
|
||||
role="tooltip"
|
||||
className="pointer-events-none absolute z-20 min-w-[172px] rounded-md border border-[var(--color-inverse-surface)] bg-[var(--color-inverse-surface)] px-3 py-2 text-xs shadow-xl"
|
||||
className="pointer-events-none absolute z-20 min-w-[172px] rounded-md border border-[var(--color-activity-tooltip-border)] bg-[var(--color-activity-tooltip-surface)] px-3 py-2 text-xs shadow-xl"
|
||||
style={tooltipStyle}
|
||||
>
|
||||
<div className="font-medium text-[var(--color-inverse-on-surface)]">{formatDateLabel(tooltipDay.date, locale)}</div>
|
||||
<div className="mt-1 text-[var(--color-primary-fixed)]">
|
||||
<div className="font-medium text-[var(--color-activity-tooltip-text)]">{formatDateLabel(tooltipDay.date, locale)}</div>
|
||||
<div className="mt-1 text-[var(--color-activity-tooltip-muted)]">
|
||||
{formatSessionCount(tooltipDay.sessionCount, t)} · {formatTokens(tooltipDay.tokens)} tokens
|
||||
</div>
|
||||
</div>
|
||||
@ -419,7 +419,7 @@ export function ActivitySettings() {
|
||||
<span
|
||||
key={color}
|
||||
aria-hidden="true"
|
||||
className="rounded-[3px] border border-[var(--color-border)]/30"
|
||||
className="rounded-[3px] border border-[var(--color-activity-cell-border)]"
|
||||
style={{ width: heatCellSize, height: heatCellSize, backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -148,7 +148,11 @@
|
||||
--color-inverse-primary: #FFB59D;
|
||||
|
||||
--color-success: #16A34A;
|
||||
--color-success-container: #E8F5E2;
|
||||
--color-info: #2D628F;
|
||||
--color-info-container: #E4F1FF;
|
||||
--color-warning: #CA8A04;
|
||||
--color-warning-container: #FFF3D6;
|
||||
|
||||
--font-headline: 'Manrope', sans-serif;
|
||||
--font-body: 'Inter', sans-serif;
|
||||
@ -175,6 +179,22 @@
|
||||
--color-surface-selected: var(--color-surface-container);
|
||||
--color-model-option-selected-bg: var(--color-primary-fixed);
|
||||
--color-model-option-selected-border: rgba(143, 72, 47, 0.2);
|
||||
--color-activity-heat-0: var(--color-surface-container);
|
||||
--color-activity-heat-1: #FFE6DE;
|
||||
--color-activity-heat-2: #FFBCA8;
|
||||
--color-activity-heat-3: #E57A58;
|
||||
--color-activity-heat-4: #8F482F;
|
||||
--color-activity-cell-border: rgba(135, 115, 109, 0.3);
|
||||
--color-activity-cell-border-hover: rgba(84, 67, 62, 0.72);
|
||||
--color-activity-cell-border-active: var(--color-text-primary);
|
||||
--color-activity-tooltip-surface: #2F312E;
|
||||
--color-activity-tooltip-border: rgba(255, 255, 255, 0.12);
|
||||
--color-activity-tooltip-text: #F2F1ED;
|
||||
--color-activity-tooltip-muted: #FFB59D;
|
||||
--color-success-container: #E8F5E2;
|
||||
--color-info: #2D628F;
|
||||
--color-info-container: #E4F1FF;
|
||||
--color-warning-container: #FFF3D6;
|
||||
--color-inspector-surface: #FBFAF6;
|
||||
--color-inspector-panel: #F4F2ED;
|
||||
--color-inspector-chip: #EBE7DF;
|
||||
@ -335,13 +355,29 @@
|
||||
--color-inverse-primary: #FFB59D;
|
||||
|
||||
--color-success: #087E4F;
|
||||
--color-success-container: #DCFAE6;
|
||||
--color-info: #0F5C8F;
|
||||
--color-info-container: #D9ECFF;
|
||||
--color-warning: #B54708;
|
||||
--color-warning-container: #FEF0C7;
|
||||
|
||||
--color-surface-sidebar: #F1F4F7;
|
||||
--color-surface-hover: #F2F5F8;
|
||||
--color-surface-selected: #E9EEF3;
|
||||
--color-model-option-selected-bg: #FFF0EA;
|
||||
--color-model-option-selected-border: rgba(143, 72, 47, 0.2);
|
||||
--color-activity-heat-0: #EEF2F6;
|
||||
--color-activity-heat-1: #FFE9E1;
|
||||
--color-activity-heat-2: #FFC4B1;
|
||||
--color-activity-heat-3: #E57A58;
|
||||
--color-activity-heat-4: #8F482F;
|
||||
--color-activity-cell-border: rgba(139, 152, 167, 0.32);
|
||||
--color-activity-cell-border-hover: rgba(75, 85, 99, 0.72);
|
||||
--color-activity-cell-border-active: var(--color-text-primary);
|
||||
--color-activity-tooltip-surface: #111827;
|
||||
--color-activity-tooltip-border: rgba(255, 255, 255, 0.14);
|
||||
--color-activity-tooltip-text: #F9FAFB;
|
||||
--color-activity-tooltip-muted: #FFC4B1;
|
||||
--color-inspector-surface: #FFFFFF;
|
||||
--color-inspector-panel: #F7F9FB;
|
||||
--color-inspector-chip: #EEF3F6;
|
||||
@ -483,13 +519,29 @@
|
||||
--color-inverse-primary: #8F482F;
|
||||
|
||||
--color-success: #7EDB8B;
|
||||
--color-success-container: rgba(126, 219, 139, 0.16);
|
||||
--color-info: #74E0F7;
|
||||
--color-info-container: rgba(116, 224, 247, 0.16);
|
||||
--color-warning: #F7C46C;
|
||||
--color-warning-container: rgba(247, 196, 108, 0.16);
|
||||
|
||||
--color-surface-sidebar: #171615;
|
||||
--color-surface-hover: var(--color-surface-container-highest);
|
||||
--color-surface-selected: var(--color-surface-container);
|
||||
--color-model-option-selected-bg: rgba(255, 181, 159, 0.13);
|
||||
--color-model-option-selected-border: rgba(255, 181, 159, 0.34);
|
||||
--color-activity-heat-0: #242322;
|
||||
--color-activity-heat-1: #4A2B24;
|
||||
--color-activity-heat-2: #85432F;
|
||||
--color-activity-heat-3: #C85F3C;
|
||||
--color-activity-heat-4: #FF8A5C;
|
||||
--color-activity-cell-border: rgba(229, 226, 225, 0.08);
|
||||
--color-activity-cell-border-hover: rgba(255, 181, 159, 0.48);
|
||||
--color-activity-cell-border-active: #FBE7E1;
|
||||
--color-activity-tooltip-surface: #F2F1ED;
|
||||
--color-activity-tooltip-border: rgba(255, 181, 159, 0.32);
|
||||
--color-activity-tooltip-text: #1B1C1A;
|
||||
--color-activity-tooltip-muted: #8F3217;
|
||||
--color-inspector-surface: #131211;
|
||||
--color-inspector-panel: #1C1B1B;
|
||||
--color-inspector-chip: #252120;
|
||||
|
||||
55
desktop/src/theme/globals.test.ts
Normal file
55
desktop/src/theme/globals.test.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import css from './globals.css?raw'
|
||||
|
||||
function getThemeBlock(selector: ':root,\n[data-theme="light"]' | '[data-theme="white"]' | '[data-theme="dark"]') {
|
||||
const start = css.indexOf(`${selector} {`)
|
||||
expect(start).toBeGreaterThanOrEqual(0)
|
||||
|
||||
const bodyStart = css.indexOf('{', start)
|
||||
let depth = 0
|
||||
for (let index = bodyStart; index < css.length; index += 1) {
|
||||
const char = css[index]
|
||||
if (char === '{') depth += 1
|
||||
if (char === '}') {
|
||||
depth -= 1
|
||||
if (depth === 0) {
|
||||
return css.slice(bodyStart + 1, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Theme block not closed: ${selector}`)
|
||||
}
|
||||
|
||||
describe('desktop theme tokens', () => {
|
||||
const themes = [':root,\n[data-theme="light"]', '[data-theme="white"]', '[data-theme="dark"]'] as const
|
||||
const requiredTokens = [
|
||||
'--color-activity-heat-0',
|
||||
'--color-activity-heat-1',
|
||||
'--color-activity-heat-2',
|
||||
'--color-activity-heat-3',
|
||||
'--color-activity-heat-4',
|
||||
'--color-activity-cell-border',
|
||||
'--color-activity-cell-border-hover',
|
||||
'--color-activity-cell-border-active',
|
||||
'--color-activity-tooltip-surface',
|
||||
'--color-activity-tooltip-border',
|
||||
'--color-activity-tooltip-text',
|
||||
'--color-activity-tooltip-muted',
|
||||
'--color-success-container',
|
||||
'--color-info',
|
||||
'--color-info-container',
|
||||
'--color-warning-container',
|
||||
]
|
||||
|
||||
it('defines activity and status tokens for every supported theme', () => {
|
||||
for (const theme of themes) {
|
||||
const block = getThemeBlock(theme)
|
||||
|
||||
for (const token of requiredTokens) {
|
||||
expect(block, `${theme} should define ${token}`).toContain(`${token}:`)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user