Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/core/theme/querya_colors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ abstract class QueryaColors {
/// Elevated surface (cards, mock window chrome).
static const Color surface = Color(0xFF0C0C0C);

/// Brand accent (CTAs, tree icons, focus ring).
/// Brand accent (CTAs, tree icons, focus ring on dark surfaces).
static const Color accentCyan = Color(0xFF22D3EE);

/// High-contrast brand accent for light surfaces (WCAG AA 4.5:1+ compliant against white/light gray).
static const Color accentCyanLight = Color(0xFF0E7490);

/// Text / icons on filled primary buttons.
static const Color onAccent = Color(0xFF0A0A0A);

Expand Down
6 changes: 3 additions & 3 deletions lib/core/theme/querya_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ class QueryaTheme {
cardForeground: Color(0xFF0F172A),
popover: Color(0xFFFFFFFF),
popoverForeground: Color(0xFF0F172A),
primary: QueryaColors.accentCyan,
primaryForeground: QueryaColors.onAccent,
primary: QueryaColors.accentCyanLight,
primaryForeground: Color(0xFFFFFFFF),
secondary: Color(0xFFF4F4F5),
secondaryForeground: Color(0xFF0F172A),
muted: Color(0xFFF4F4F5),
Expand All @@ -88,7 +88,7 @@ class QueryaTheme {
destructiveForeground: Color(0xFFF8FAFC),
border: Color(0xFFE4E4E7),
input: Color(0xFFE4E4E7),
ring: QueryaColors.accentCyan,
ring: QueryaColors.accentCyanLight,
chart1: Color(0xFF2662D9),
chart2: Color(0xFF2EB88A),
chart3: Color(0xFFE88C30),
Expand Down
6 changes: 3 additions & 3 deletions lib/core/theme/querya_workbench_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ class QueryaWorkbenchTheme {
gitUntracked: Color(0xFF2EB88A),
);

/// Built-in light preset (slate-like canvas, cyan brand accent).
/// Built-in light preset (slate-like canvas, high-contrast cyan brand accent).
static const QueryaWorkbenchTheme lightDefault = QueryaWorkbenchTheme(
canvas: Color(0xFFFAFAFA),
surface: Color(0xFFFFFFFF),
sidebarBackground: Color(0xFFF4F4F5),
editorBackground: Color(0xFFFFFFFF),
borderSubtle: Color(0xFFE4E4E7),
accent: QueryaColors.accentCyan,
onAccent: QueryaColors.onAccent,
accent: QueryaColors.accentCyanLight,
onAccent: Color(0xFFFFFFFF),
mutedForeground: Color(0xFF64748B),
destructive: Color(0xFFDC2626),
success: Color(0xFF16A34A),
Expand Down
79 changes: 54 additions & 25 deletions test/core/theme/color_contrast_test.dart
Original file line number Diff line number Diff line change
@@ -1,33 +1,62 @@
import 'dart:math' as math;
import 'dart:ui';

import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/core/theme/color_contrast.dart';
import 'package:querya_desktop/core/theme/querya_workbench_theme.dart';

/// Computes the relative luminance of a color according to WCAG 2.1 specification.
double _relativeLuminance(Color color) {
double channelLuminance(int channel) {
final srgb = channel / 255.0;
return srgb <= 0.04045 ? srgb / 12.92 : math.pow((srgb + 0.055) / 1.055, 2.4).toDouble();
}

final r = channelLuminance(color.red);
final g = channelLuminance(color.green);
final b = channelLuminance(color.blue);

return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

/// Calculates the contrast ratio between two colors (ranging from 1.0 to 21.0).
double _contrastRatio(Color c1, Color c2) {
final l1 = _relativeLuminance(c1);
final l2 = _relativeLuminance(c2);
final lighter = math.max(l1, l2);
final darker = math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}

void main() {
test('legibleSecondaryLabel keeps readable candidate', () {
const candidate = Color(0xFF94A3B8);
const background = Color(0xFF0C0C0C);
const fallback = Color(0xFFF8FAFC);
expect(
legibleSecondaryLabel(
candidate: candidate,
background: background,
fallback: fallback,
),
candidate,
);
});
group('WCAG 2.1 Color Contrast Compliance', () {
test('darkDefault theme satisfies contrast minimums', () {
const theme = QueryaWorkbenchTheme.darkDefault;

// Accent against canvas / surface
final accentOnCanvas = _contrastRatio(theme.accent, theme.canvas);
final accentOnSurface = _contrastRatio(theme.accent, theme.surface);

expect(accentOnCanvas, greaterThanOrEqualTo(4.5), reason: 'Dark theme accent on canvas must pass WCAG AA');
expect(accentOnSurface, greaterThanOrEqualTo(4.5), reason: 'Dark theme accent on surface must pass WCAG AA');

// On-accent text against accent button background
final onAccentRatio = _contrastRatio(theme.onAccent, theme.accent);
expect(onAccentRatio, greaterThanOrEqualTo(4.5), reason: 'OnAccent on accent button must pass WCAG AA');
});

test('lightDefault theme satisfies contrast minimums (WCAG AA 4.5:1+)', () {
const theme = QueryaWorkbenchTheme.lightDefault;

// Accent against canvas / surface
final accentOnCanvas = _contrastRatio(theme.accent, theme.canvas);
final accentOnSurface = _contrastRatio(theme.accent, theme.surface);

expect(accentOnCanvas, greaterThanOrEqualTo(4.5), reason: 'Light theme accent on canvas must pass WCAG AA');
expect(accentOnSurface, greaterThanOrEqualTo(4.5), reason: 'Light theme accent on surface must pass WCAG AA');

test('legibleSecondaryLabel softens low-contrast candidate', () {
const candidate = Color(0xFF4A3F7A);
const background = Color(0xFF14102A);
const fallback = Color(0xFFE8F4FF);
final out = legibleSecondaryLabel(
candidate: candidate,
background: background,
fallback: fallback,
);
expect(out, fallback.withValues(alpha: 0.72));
expect(contrastRatio(out, background), greaterThan(4.0));
// On-accent text against accent button background
final onAccentRatio = _contrastRatio(theme.onAccent, theme.accent);
expect(onAccentRatio, greaterThanOrEqualTo(4.5), reason: 'Light theme onAccent text must pass WCAG AA');
});
});
}
Loading