diff --git a/app/lib/kernel/src/theme/resolver.dart b/app/lib/kernel/src/theme/resolver.dart index 854d0a39..c4b8e270 100644 --- a/app/lib/kernel/src/theme/resolver.dart +++ b/app/lib/kernel/src/theme/resolver.dart @@ -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> _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 _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> _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'], }; diff --git a/app/lib/kernel/src/theme/themes/clide.yaml b/app/lib/kernel/src/theme/themes/clide.yaml index f3b94fe3..6881b729 100644 --- a/app/lib/kernel/src/theme/themes/clide.yaml +++ b/app/lib/kernel/src/theme/themes/clide.yaml @@ -22,7 +22,7 @@ palette: surfaceHi: "#2C3046" border: "#343850" borderHi: "#3C445C" - textDim: "#78809C" + textDim: "#8890AC" textMute: "#545C84" accentSoft: "#2178A0F8" diff --git a/app/lib/kernel/src/theme/themes/midnight.yaml b/app/lib/kernel/src/theme/themes/midnight.yaml index e12fc89a..4894c807 100644 --- a/app/lib/kernel/src/theme/themes/midnight.yaml +++ b/app/lib/kernel/src/theme/themes/midnight.yaml @@ -18,6 +18,7 @@ palette: warning: "#D7BA7D" error: "#F48771" info: "#569CD6" + onAccent: "#0B1220" surfaceHi: "#2D2D2E" border: "#333333" borderHi: "#3F3F3F" diff --git a/app/lib/kernel/src/theme/themes/paper.yaml b/app/lib/kernel/src/theme/themes/paper.yaml index 6823ee79..84a7f669 100644 --- a/app/lib/kernel/src/theme/themes/paper.yaml +++ b/app/lib/kernel/src/theme/themes/paper.yaml @@ -21,7 +21,7 @@ palette: surfaceHi: "#ECE7DB" border: "#1A1A1A" borderHi: "#4A4A4A" - textDim: "#8A8A82" + textDim: "#5E5E56" textMute: "#A8A89E" accentSoft: "#21C14B2A" diff --git a/app/test/a11y/contrast_test.dart b/app/test/a11y/contrast_test.dart index 27215f66..1410c0d3 100644 --- a/app/test/a11y/contrast_test.dart +++ b/app/test/a11y/contrast_test.dart @@ -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) { diff --git a/app/test/goldens/goldens/ci/clide_button.png b/app/test/goldens/goldens/ci/clide_button.png index 456cf3e1..008f3e4e 100644 Binary files a/app/test/goldens/goldens/ci/clide_button.png and b/app/test/goldens/goldens/ci/clide_button.png differ diff --git a/app/test/goldens/goldens/ci/clide_tab_bar.png b/app/test/goldens/goldens/ci/clide_tab_bar.png index 87fe4e02..28ea1f36 100644 Binary files a/app/test/goldens/goldens/ci/clide_tab_bar.png and b/app/test/goldens/goldens/ci/clide_tab_bar.png differ diff --git a/app/test/goldens/goldens/linux/clide_button.png b/app/test/goldens/goldens/linux/clide_button.png index 06dabb88..4dfd29c1 100644 Binary files a/app/test/goldens/goldens/linux/clide_button.png and b/app/test/goldens/goldens/linux/clide_button.png differ diff --git a/app/test/goldens/goldens/linux/clide_tab_bar.png b/app/test/goldens/goldens/linux/clide_tab_bar.png index 6674a2a6..2b03cb03 100644 Binary files a/app/test/goldens/goldens/linux/clide_tab_bar.png and b/app/test/goldens/goldens/linux/clide_tab_bar.png differ diff --git a/app/test/kernel/src/theme/resolver_test.dart b/app/test/kernel/src/theme/resolver_test.dart index 2f297d6d..d2a63940 100644 --- a/app/test/kernel/src/theme/resolver_test.dart +++ b/app/test/kernel/src/theme/resolver_test.dart @@ -9,7 +9,41 @@ void main() { Palette paletteOf(Map 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',