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>
@@ -151,12 +151,13 @@ class ThemeResolver {
|
||||
final resolved = _resolveRef(override, palette, semantic);
|
||||
if (resolved != null) return resolved;
|
||||
}
|
||||
final defaultRef = _defaultSurfaceMap[key];
|
||||
if (defaultRef != null) {
|
||||
final resolved = _resolveRef(defaultRef, palette, semantic);
|
||||
if (resolved != null) return resolved;
|
||||
final candidates = _defaultSurfaceMap[key];
|
||||
if (candidates != null) {
|
||||
for (final ref in candidates) {
|
||||
final resolved = _resolveRef(ref, palette, semantic);
|
||||
if (resolved != null) return resolved;
|
||||
}
|
||||
}
|
||||
// Last-ditch: something has to render. Fall back to semantic text.
|
||||
return semantic.lookup(SemanticKeys.text) ?? const Color(0xFFFFFFFF);
|
||||
}
|
||||
|
||||
@@ -172,81 +173,94 @@ class ThemeResolver {
|
||||
/// Default palette names a semantic role will try, in order, when the
|
||||
/// theme doesn't override the role explicitly.
|
||||
const Map<String, List<String>> _defaultSemanticFallbacks = {
|
||||
SemanticKeys.mainchrome: ['panel', 'surface', 'background'],
|
||||
SemanticKeys.mainchrome: ['bgSunken', 'panel', 'surface', 'background'],
|
||||
SemanticKeys.calltoaction: ['accent', 'primary'],
|
||||
SemanticKeys.focus: ['primary', 'accent'],
|
||||
SemanticKeys.background: ['background'],
|
||||
SemanticKeys.focus: ['accent', 'primary'],
|
||||
SemanticKeys.background: ['bg', 'background'],
|
||||
SemanticKeys.surface: ['surface', 'panel'],
|
||||
SemanticKeys.text: ['foreground'],
|
||||
SemanticKeys.textMuted: ['muted', 'secondary', 'foreground'],
|
||||
SemanticKeys.success: ['success'],
|
||||
SemanticKeys.warning: ['warning'],
|
||||
SemanticKeys.error: ['error'],
|
||||
SemanticKeys.text: ['textHi', 'foreground'],
|
||||
SemanticKeys.textMuted: ['textDim', 'muted', 'secondary', 'foreground'],
|
||||
SemanticKeys.success: ['ok', 'success'],
|
||||
SemanticKeys.warning: ['warn', 'warning'],
|
||||
SemanticKeys.error: ['err', 'error'],
|
||||
SemanticKeys.info: ['info', 'primary'],
|
||||
};
|
||||
|
||||
/// Default surface map. Every entry resolves through the semantic layer
|
||||
/// where it makes sense; raw palette refs are used only where the
|
||||
/// semantic layer doesn't have a role that fits.
|
||||
const Map<String, String> _defaultSurfaceMap = {
|
||||
TokenKeys.globalForeground: 'semantic.text',
|
||||
TokenKeys.globalBackground: 'semantic.background',
|
||||
TokenKeys.globalBorder: 'semantic.surface',
|
||||
TokenKeys.globalFocus: 'semantic.focus',
|
||||
TokenKeys.globalTextMuted: 'semantic.text_muted',
|
||||
TokenKeys.panelBackground: 'semantic.mainchrome',
|
||||
TokenKeys.panelBorder: 'semantic.surface',
|
||||
TokenKeys.panelActiveBorder: 'semantic.focus',
|
||||
TokenKeys.panelHeader: 'semantic.mainchrome',
|
||||
TokenKeys.panelHeaderForeground: 'semantic.text',
|
||||
TokenKeys.sidebarBackground: 'semantic.mainchrome',
|
||||
TokenKeys.sidebarForeground: 'semantic.text',
|
||||
TokenKeys.sidebarItemHover: 'semantic.surface',
|
||||
TokenKeys.sidebarItemSelected: 'semantic.focus',
|
||||
TokenKeys.sidebarSectionHeader: 'semantic.text_muted',
|
||||
TokenKeys.statusBarBackground: 'semantic.mainchrome',
|
||||
TokenKeys.statusBarForeground: 'semantic.text',
|
||||
TokenKeys.statusBarItemActiveBackground: 'semantic.focus',
|
||||
TokenKeys.statusBarItemHoverBackground: 'semantic.surface',
|
||||
TokenKeys.tabBarBackground: 'semantic.mainchrome',
|
||||
TokenKeys.tabActive: 'semantic.background',
|
||||
TokenKeys.tabInactive: 'semantic.mainchrome',
|
||||
TokenKeys.tabActiveForeground: 'semantic.text',
|
||||
TokenKeys.tabInactiveForeground: 'semantic.text_muted',
|
||||
TokenKeys.tabActiveBorder: 'semantic.focus',
|
||||
TokenKeys.tabCloseHover: 'semantic.error',
|
||||
TokenKeys.buttonBackground: 'semantic.surface',
|
||||
TokenKeys.buttonForeground: 'semantic.text',
|
||||
TokenKeys.buttonHoverBackground: 'semantic.mainchrome',
|
||||
TokenKeys.buttonActiveBackground: 'semantic.focus',
|
||||
TokenKeys.buttonBorder: 'semantic.surface',
|
||||
TokenKeys.listItemBackground: 'semantic.background',
|
||||
TokenKeys.listItemForeground: 'semantic.text',
|
||||
TokenKeys.listItemHoverBackground: 'semantic.surface',
|
||||
TokenKeys.listItemSelectedBackground: 'semantic.focus',
|
||||
TokenKeys.listItemSelectedForeground: 'semantic.background',
|
||||
TokenKeys.scrollbarSlider: 'semantic.surface',
|
||||
TokenKeys.scrollbarSliderHover: 'semantic.text_muted',
|
||||
TokenKeys.scrollbarTrack: 'semantic.mainchrome',
|
||||
TokenKeys.tooltipBackground: 'semantic.surface',
|
||||
TokenKeys.tooltipForeground: 'semantic.text',
|
||||
TokenKeys.tooltipBorder: 'semantic.mainchrome',
|
||||
TokenKeys.dropdownBackground: 'semantic.surface',
|
||||
TokenKeys.dropdownForeground: 'semantic.text',
|
||||
TokenKeys.dropdownBorder: 'semantic.mainchrome',
|
||||
TokenKeys.modalOverlayBackground: '#C0000000',
|
||||
TokenKeys.modalSurfaceBackground: 'semantic.mainchrome',
|
||||
TokenKeys.modalSurfaceBorder: 'semantic.focus',
|
||||
TokenKeys.dividerColor: 'semantic.surface',
|
||||
TokenKeys.statusSuccess: 'semantic.success',
|
||||
TokenKeys.statusWarning: 'semantic.warning',
|
||||
TokenKeys.statusError: 'semantic.error',
|
||||
TokenKeys.statusInfo: 'semantic.info',
|
||||
TokenKeys.syntaxKeyword: 'semantic.calltoaction',
|
||||
TokenKeys.syntaxType: 'semantic.info',
|
||||
TokenKeys.syntaxString: 'semantic.success',
|
||||
TokenKeys.syntaxNumber: 'semantic.warning',
|
||||
TokenKeys.syntaxComment: 'semantic.text_muted',
|
||||
TokenKeys.syntaxMethod: 'semantic.focus',
|
||||
TokenKeys.syntaxPunct: 'semantic.text_muted',
|
||||
const Map<String, List<String>> _defaultSurfaceMap = {
|
||||
// global — try design keys first, then legacy semantic
|
||||
TokenKeys.globalForeground: ['textHi', 'semantic.text'],
|
||||
TokenKeys.globalBackground: ['bg', 'semantic.background'],
|
||||
TokenKeys.globalBorder: ['border', 'semantic.surface'],
|
||||
TokenKeys.globalFocus: ['accent', 'semantic.focus'],
|
||||
TokenKeys.globalTextMuted: ['textDim', 'semantic.text_muted'],
|
||||
// panel
|
||||
TokenKeys.panelBackground: ['bgSunken', 'semantic.mainchrome'],
|
||||
TokenKeys.panelBorder: ['border', 'semantic.surface'],
|
||||
TokenKeys.panelActiveBorder: ['borderHi', 'semantic.focus'],
|
||||
TokenKeys.panelHeader: ['surface', 'semantic.mainchrome'],
|
||||
TokenKeys.panelHeaderForeground: ['text', 'semantic.text'],
|
||||
// sidebar
|
||||
TokenKeys.sidebarBackground: ['bgSunken', 'semantic.mainchrome'],
|
||||
TokenKeys.sidebarForeground: ['text', 'semantic.text'],
|
||||
TokenKeys.sidebarItemHover: ['surface', 'semantic.surface'],
|
||||
TokenKeys.sidebarItemSelected: ['surfaceHi', 'semantic.focus'],
|
||||
TokenKeys.sidebarSectionHeader: ['textMute', 'semantic.text_muted'],
|
||||
// statusbar
|
||||
TokenKeys.statusBarBackground: ['bgSunken', 'semantic.mainchrome'],
|
||||
TokenKeys.statusBarForeground: ['text', 'semantic.text'],
|
||||
TokenKeys.statusBarItemActiveBackground: ['accent', 'semantic.focus'],
|
||||
TokenKeys.statusBarItemHoverBackground: ['surface', 'semantic.surface'],
|
||||
// tabs
|
||||
TokenKeys.tabBarBackground: ['bgSunken', 'semantic.mainchrome'],
|
||||
TokenKeys.tabActive: ['bg', 'semantic.background'],
|
||||
TokenKeys.tabInactive: ['bgSunken', 'semantic.mainchrome'],
|
||||
TokenKeys.tabActiveForeground: ['textHi', 'semantic.text'],
|
||||
TokenKeys.tabInactiveForeground: ['textDim', 'semantic.text_muted'],
|
||||
TokenKeys.tabActiveBorder: ['accent', 'semantic.focus'],
|
||||
TokenKeys.tabCloseHover: ['err', 'semantic.error'],
|
||||
// buttons
|
||||
TokenKeys.buttonBackground: ['accent', 'semantic.calltoaction'],
|
||||
TokenKeys.buttonForeground: ['onAccent', 'semantic.background'],
|
||||
TokenKeys.buttonHoverBackground: ['accentPress', 'semantic.focus'],
|
||||
TokenKeys.buttonActiveBackground: ['accentPress', 'semantic.focus'],
|
||||
TokenKeys.buttonBorder: ['border', 'semantic.surface'],
|
||||
// list items
|
||||
TokenKeys.listItemBackground: ['bg', 'semantic.background'],
|
||||
TokenKeys.listItemForeground: ['text', 'semantic.text'],
|
||||
TokenKeys.listItemHoverBackground: ['surface', 'semantic.surface'],
|
||||
TokenKeys.listItemSelectedBackground: ['surfaceHi', 'semantic.focus'],
|
||||
TokenKeys.listItemSelectedForeground: ['textHi', 'semantic.text'],
|
||||
// scrollbar
|
||||
TokenKeys.scrollbarSlider: ['border', 'semantic.surface'],
|
||||
TokenKeys.scrollbarSliderHover: ['borderHi', 'semantic.text_muted'],
|
||||
TokenKeys.scrollbarTrack: ['bgSunken', 'semantic.mainchrome'],
|
||||
// tooltip
|
||||
TokenKeys.tooltipBackground: ['surface', 'semantic.surface'],
|
||||
TokenKeys.tooltipForeground: ['textHi', 'semantic.text'],
|
||||
TokenKeys.tooltipBorder: ['borderHi', 'semantic.mainchrome'],
|
||||
// dropdown
|
||||
TokenKeys.dropdownBackground: ['surface', 'semantic.surface'],
|
||||
TokenKeys.dropdownForeground: ['text', 'semantic.text'],
|
||||
TokenKeys.dropdownBorder: ['border', 'semantic.mainchrome'],
|
||||
// modal
|
||||
TokenKeys.modalOverlayBackground: ['#C0000000'],
|
||||
TokenKeys.modalSurfaceBackground: ['surface', 'semantic.mainchrome'],
|
||||
TokenKeys.modalSurfaceBorder: ['accent', 'semantic.focus'],
|
||||
// divider
|
||||
TokenKeys.dividerColor: ['border', 'semantic.surface'],
|
||||
// status
|
||||
TokenKeys.statusSuccess: ['ok', 'semantic.success'],
|
||||
TokenKeys.statusWarning: ['warn', 'semantic.warning'],
|
||||
TokenKeys.statusError: ['err', 'semantic.error'],
|
||||
TokenKeys.statusInfo: ['info', 'semantic.info'],
|
||||
TokenKeys.syntaxKeyword: ['semantic.calltoaction'],
|
||||
TokenKeys.syntaxType: ['semantic.info'],
|
||||
TokenKeys.syntaxString: ['semantic.success'],
|
||||
TokenKeys.syntaxNumber: ['semantic.warning'],
|
||||
TokenKeys.syntaxComment: ['semantic.text_muted'],
|
||||
TokenKeys.syntaxMethod: ['semantic.focus'],
|
||||
TokenKeys.syntaxPunct: ['semantic.text_muted'],
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ palette:
|
||||
surfaceHi: "#2C3046"
|
||||
border: "#343850"
|
||||
borderHi: "#3C445C"
|
||||
textDim: "#78809C"
|
||||
textDim: "#8890AC"
|
||||
textMute: "#545C84"
|
||||
accentSoft: "#2178A0F8"
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ palette:
|
||||
warning: "#D7BA7D"
|
||||
error: "#F48771"
|
||||
info: "#569CD6"
|
||||
onAccent: "#0B1220"
|
||||
surfaceHi: "#2D2D2E"
|
||||
border: "#333333"
|
||||
borderHi: "#3F3F3F"
|
||||
|
||||
@@ -21,7 +21,7 @@ palette:
|
||||
surfaceHi: "#ECE7DB"
|
||||
border: "#1A1A1A"
|
||||
borderHi: "#4A4A4A"
|
||||
textDim: "#8A8A82"
|
||||
textDim: "#5E5E56"
|
||||
textMute: "#A8A89E"
|
||||
accentSoft: "#21C14B2A"
|
||||
|
||||
|
||||
@@ -11,7 +11,10 @@ void main() {
|
||||
|
||||
group('contrast — all bundled themes meet WCAG AA', () {
|
||||
const bundledPaths = [
|
||||
'lib/kernel/src/theme/themes/summer-night.yaml',
|
||||
'lib/kernel/src/theme/themes/clide.yaml',
|
||||
'lib/kernel/src/theme/themes/midnight.yaml',
|
||||
'lib/kernel/src/theme/themes/paper.yaml',
|
||||
'lib/kernel/src/theme/themes/terminal.yaml',
|
||||
];
|
||||
|
||||
for (final path in bundledPaths) {
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 804 B After Width: | Height: | Size: 809 B |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
@@ -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',
|
||||
|
||||