diff --git a/lib/core/theme/parser/querya_theme_color_scheme.dart b/lib/core/theme/parser/querya_theme_color_scheme.dart new file mode 100644 index 00000000..026f191c --- /dev/null +++ b/lib/core/theme/parser/querya_theme_color_scheme.dart @@ -0,0 +1,98 @@ +import 'package:flutter/foundation.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +import '../querya_theme.dart'; +import 'color_parser.dart'; + +const _knownShadcnColorKeys = { + 'background', + 'foreground', + 'card', + 'cardForeground', + 'popover', + 'popoverForeground', + 'primary', + 'primaryForeground', + 'secondary', + 'secondaryForeground', + 'muted', + 'mutedForeground', + 'accent', + 'accentForeground', + 'destructive', + 'destructiveForeground', + 'border', + 'input', + 'ring', + 'chart1', + 'chart2', + 'chart3', + 'chart4', + 'chart5', +}; + +/// Builds a shadcn [ColorScheme] from Querya custom `shadcn_colors`. +/// +/// Missing keys and invalid optional colors fall back to [fallback.colorScheme]. +/// [ColorScheme.brightness] always comes from [fallback], not from [colors]. +ColorScheme colorSchemeFromQueryaThemeColors({ + required Map colors, + required QueryaTheme fallback, +}) { + final base = fallback.colorScheme; + + if (kDebugMode) { + for (final key in colors.keys) { + if (!_knownShadcnColorKeys.contains(key)) { + debugPrint('Querya theme: ignored shadcn_colors key "$key"'); + } + } + } + + Color pick(String key, Color defaultValue) { + final raw = colors[key]; + if (raw == null) return defaultValue; + try { + return parseQueryaThemeColor(raw); + } on FormatException { + if (kDebugMode) { + debugPrint('Querya theme: invalid shadcn_colors."$key": $raw'); + } + return defaultValue; + } + } + + final destructive = pick('destructive', base.destructive); + // querya.theme.v1 still maps this key; shadcn marks the ColorScheme field legacy. + // ignore: deprecated_member_use + final destructiveForeground = pick('destructiveForeground', base.destructiveForeground); + + return ColorScheme( + brightness: base.brightness, + background: pick('background', base.background), + foreground: pick('foreground', base.foreground), + card: pick('card', base.card), + cardForeground: pick('cardForeground', base.cardForeground), + popover: pick('popover', base.popover), + popoverForeground: pick('popoverForeground', base.popoverForeground), + primary: pick('primary', base.primary), + primaryForeground: pick('primaryForeground', base.primaryForeground), + secondary: pick('secondary', base.secondary), + secondaryForeground: pick('secondaryForeground', base.secondaryForeground), + muted: pick('muted', base.muted), + mutedForeground: pick('mutedForeground', base.mutedForeground), + accent: pick('accent', base.accent), + accentForeground: pick('accentForeground', base.accentForeground), + destructive: destructive, + // ignore: deprecated_member_use + destructiveForeground: destructiveForeground, + border: pick('border', base.border), + input: pick('input', base.input), + ring: pick('ring', base.ring), + chart1: pick('chart1', base.chart1), + chart2: pick('chart2', base.chart2), + chart3: pick('chart3', base.chart3), + chart4: pick('chart4', base.chart4), + chart5: pick('chart5', base.chart5), + ); +} diff --git a/test/core/theme/parser/querya_theme_color_scheme_test.dart b/test/core/theme/parser/querya_theme_color_scheme_test.dart new file mode 100644 index 00000000..49b0af75 --- /dev/null +++ b/test/core/theme/parser/querya_theme_color_scheme_test.dart @@ -0,0 +1,86 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/theme/parser/color_parser.dart'; +import 'package:querya_desktop/core/theme/parser/querya_theme_color_scheme.dart'; +import 'package:querya_desktop/core/theme/parser/querya_theme_manifest.dart'; +import 'package:querya_desktop/core/theme/querya_theme.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +void main() { + group('colorSchemeFromQueryaThemeColors', () { + test('full fixture maps custom shadcn values', () { + final raw = + File('test/fixtures/themes/querya_custom_dark.json').readAsStringSync(); + final manifest = QueryaThemeManifest.fromJsonString(raw); + final scheme = colorSchemeFromQueryaThemeColors( + colors: manifest.shadcnColors, + fallback: QueryaTheme.darkDefault, + ); + + expect(scheme.brightness, Brightness.dark); + expect(scheme.background, parseQueryaThemeColor('#101014')); + expect(scheme.foreground, parseQueryaThemeColor('#E2E8F0')); + expect(scheme.primary, parseQueryaThemeColor('#38BDF8')); + expect(scheme.accent, parseQueryaThemeColor('#6366F1')); + expect(scheme.chart3, parseQueryaThemeColor('#F472B6')); + expect(scheme.chart5, parseQueryaThemeColor('#34D399')); + }); + + test('missing keys preserve fallback colorScheme values', () { + final scheme = colorSchemeFromQueryaThemeColors( + colors: const {'primary': '#FF00AA'}, + fallback: QueryaTheme.darkDefault, + ); + final fallback = QueryaTheme.darkDefault.colorScheme; + + expect(scheme.primary, parseQueryaThemeColor('#FF00AA')); + expect(scheme.background, fallback.background); + expect(scheme.foreground, fallback.foreground); + expect(scheme.border, fallback.border); + expect(scheme.chart1, fallback.chart1); + }); + + test('invalid optional color uses fallback value', () { + final fallback = QueryaTheme.darkDefault.colorScheme; + final scheme = colorSchemeFromQueryaThemeColors( + colors: const { + 'primary': 'not-a-color', + 'background': '#101014', + }, + fallback: QueryaTheme.darkDefault, + ); + + expect(scheme.primary, fallback.primary); + expect(scheme.background, parseQueryaThemeColor('#101014')); + }); + + test('chart colors fall back when omitted', () { + final fallback = QueryaTheme.lightDefault.colorScheme; + final scheme = colorSchemeFromQueryaThemeColors( + colors: const {'background': '#FFFFFF'}, + fallback: QueryaTheme.lightDefault, + ); + + expect(scheme.background, parseQueryaThemeColor('#FFFFFF')); + expect(scheme.chart1, fallback.chart1); + expect(scheme.chart2, fallback.chart2); + expect(scheme.chart3, fallback.chart3); + expect(scheme.chart4, fallback.chart4); + expect(scheme.chart5, fallback.chart5); + }); + + test('brightness comes from fallback theme, not colors map', () { + final scheme = colorSchemeFromQueryaThemeColors( + colors: const { + 'background': '#101014', + 'foreground': '#E2E8F0', + }, + fallback: QueryaTheme.lightDefault, + ); + + expect(scheme.brightness, Brightness.light); + expect(scheme.background, parseQueryaThemeColor('#101014')); + }); + }); +}