wire design palette through resolver with fallback chains

Surface map now tries palette keys (bgSunken, surfaceHi, textHi,
etc.) first, falling back to semantic refs for legacy themes.
Sidebar/panel bg differentiates from workspace bg. Buttons use
accent colour with onAccent text. Four-level text gradient flows
to widgets.

WCAG-AA contrast fixes: clide textDim lightened, midnight
onAccent darkened, paper textDim darkened. Contrast test updated
to cover all four themes. Resolver tests updated for the new
palette shape. Goldens regenerated.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-04-22 16:54:11 +02:00
co-authored by Claude
parent 861fa6652a
commit 8b5615426a
10 changed files with 157 additions and 121 deletions
+62 -44
View File
@@ -9,7 +9,41 @@ void main() {
Palette paletteOf(Map<String, Color> colors) => Palette(colors);
group('ThemeResolver default fallbacks', () {
test('palette-only summer-night shape resolves every token', () {
test('design-shape palette resolves every token', () {
final tokens = resolver.resolve(
palette: paletteOf(const {
'bg': Color(0xFF20202C),
'bgSunken': Color(0xFF1A1A24),
'surface': Color(0xFF242838),
'surfaceHi': Color(0xFF2C3046),
'border': Color(0xFF343850),
'borderHi': Color(0xFF3C445C),
'textHi': Color(0xFFE6E8F2),
'text': Color(0xFFB1BBE3),
'textDim': Color(0xFF78809C),
'textMute': Color(0xFF545C84),
'accent': Color(0xFF78A0F8),
'accentPress': Color(0xFF6C90DC),
'accentSoft': Color(0x2178A0F8),
'onAccent': Color(0xFF0D1020),
'ok': Color(0xFF7DD3A8),
'warn': Color(0xFFE6C370),
'err': Color(0xFFE87D7D),
'info': Color(0xFF78A0F8),
}),
);
expect(tokens.globalBackground, const Color(0xFF20202C));
expect(tokens.globalForeground, const Color(0xFFE6E8F2));
expect(tokens.panelBackground, const Color(0xFF1A1A24));
expect(tokens.sidebarItemSelected, const Color(0xFF2C3046));
expect(tokens.statusSuccess, const Color(0xFF7DD3A8));
expect(tokens.statusError, const Color(0xFFE87D7D));
expect(tokens.globalTextMuted, const Color(0xFF78809C));
expect(tokens.buttonBackground, const Color(0xFF78A0F8));
expect(tokens.buttonForeground, const Color(0xFF0D1020));
});
test('legacy palette keys still resolve via fallback chains', () {
final tokens = resolver.resolve(
palette: paletteOf(const {
'primary': Color(0xFF00A3D2),
@@ -28,27 +62,8 @@ void main() {
expect(tokens.globalBackground, const Color(0xFF21262F));
expect(tokens.globalForeground, const Color(0xFFE2E8F5));
expect(tokens.panelBackground, const Color(0xFF292E38));
expect(tokens.sidebarItemSelected, const Color(0xFF00A3D2));
expect(tokens.statusSuccess, const Color(0xFF00AB9A));
expect(tokens.statusError, const Color(0xFFF06C6F));
expect(tokens.globalTextMuted, const Color(0xFF6A7280));
});
test('missing "muted" falls back to foreground', () {
final tokens = resolver.resolve(
palette: paletteOf(const {
'primary': Color(0xFF111111),
'accent': Color(0xFF222222),
'background': Color(0xFF333333),
'surface': Color(0xFF444444),
'panel': Color(0xFF555555),
'foreground': Color(0xFFEEEEEE),
'success': Color(0xFF008800),
'warning': Color(0xFFFF8800),
'error': Color(0xFFFF0000),
}),
);
expect(tokens.globalTextMuted, const Color(0xFFEEEEEE));
});
});
@@ -56,15 +71,16 @@ void main() {
test('surface override wins over default', () {
final tokens = resolver.resolve(
palette: paletteOf(const {
'primary': Color(0xFF111111),
'accent': Color(0xFF222222),
'background': Color(0xFF333333),
'bg': Color(0xFF333333),
'bgSunken': Color(0xFF222222),
'surface': Color(0xFF444444),
'panel': Color(0xFF555555),
'foreground': Color(0xFFFFFFFF),
'success': Color(0xFF008800),
'warning': Color(0xFFFF8800),
'error': Color(0xFFFF0000),
'border': Color(0xFF555555),
'textHi': Color(0xFFFFFFFF),
'accent': Color(0xFF111111),
'ok': Color(0xFF008800),
'warn': Color(0xFFFF8800),
'err': Color(0xFFFF0000),
'info': Color(0xFF111111),
}),
surfaceOverride: const {
'panel.background': '#00FF00',
@@ -73,36 +89,38 @@ void main() {
expect(tokens.panelBackground, const Color(0xFF00FF00));
});
test('semantic override propagates to surface tokens using it', () {
test('semantic override propagates when palette key is absent', () {
final tokens = resolver.resolve(
palette: paletteOf(const {
'primary': Color(0xFF111111),
'accent': Color(0xFF222222),
'background': Color(0xFF333333),
'bg': Color(0xFF333333),
'bgSunken': Color(0xFF222222),
'surface': Color(0xFF444444),
'panel': Color(0xFF555555),
'foreground': Color(0xFFFFFFFF),
'success': Color(0xFF008800),
'warning': Color(0xFFFF8800),
'error': Color(0xFFFF0000),
'teal': Color(0xFF007777),
'surfaceHi': Color(0xFF555555),
'border': Color(0xFF666666),
'textHi': Color(0xFFFFFFFF),
'ok': Color(0xFF008800),
'warn': Color(0xFFFF8800),
'err': Color(0xFFFF0000),
'info': Color(0xFF111111),
}),
semanticOverride: const SemanticRoles({
'focus': Color(0xFF007777),
}),
);
// sidebarItemSelected defaults to semantic.focus — expect override.
expect(tokens.sidebarItemSelected, const Color(0xFF007777));
// No 'accent' in palette, so globalFocus falls through to
// semantic.focus which is overridden.
expect(tokens.globalFocus, const Color(0xFF007777));
});
test('extensionOverride populates extensionTokens map', () {
final tokens = resolver.resolve(
palette: paletteOf(const {
'primary': Color(0xFF111111),
'background': Color(0xFF333333),
'bg': Color(0xFF333333),
'bgSunken': Color(0xFF222222),
'surface': Color(0xFF444444),
'panel': Color(0xFF555555),
'foreground': Color(0xFFFFFFFF),
'border': Color(0xFF555555),
'textHi': Color(0xFFFFFFFF),
'accent': Color(0xFF111111),
}),
extensionOverride: const {
'ext.sqlite.table.background': '#ABCDEF',