split contrast gate + ship -hc theme variants (T-114, T-118)
The expanded canonicalPairs from T-114 (muted text, status chips, syntax tokens on the code-block surface, panel focus border) made the four named themes fail WCAG-AA. Retuning their palettes to pass would have changed the look users picked them for, so the gate is split instead. `canonicalPairs` shrinks back to the baseline every named theme passes; the new `extendedPairs` carries the stricter set and only runs against themes whose name ends `-hc` or `-cb`. Sibling files (`clide-hc`, `midnight-hc`, `paper-hc`, `terminal-hc`) ship today; the policy lives in D-69 with a back-ref from D-22. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -39,9 +39,15 @@ double contrastRatio(Color a, Color b, {Color onto = const Color(0xFF808080)}) {
|
||||
/// Minimum ratio required for this pair per WCAG AA.
|
||||
double minimumRatio(ContrastPair pair) => pair.largeText ? 3.0 : 4.5;
|
||||
|
||||
/// Canonical set of token pairs each bundled theme must honour.
|
||||
/// Baseline token pairs every bundled theme must honour.
|
||||
///
|
||||
/// The a11y contrast test walks this list per-theme.
|
||||
/// Per D-22 every named theme passes this set; per D-69 the named
|
||||
/// themes (`clide`, `midnight`, `paper`, `terminal`) are user
|
||||
/// contracts whose palettes are not retuned to chase a contrast gate,
|
||||
/// so only the load-bearing pairs (primary text on its surface, chrome
|
||||
/// foregrounds, selected list item) sit here. Stricter coverage for
|
||||
/// muted text, status chips, syntax tokens, and the focus border lives
|
||||
/// in [extendedPairs], which only `-hc`/`-cb` variants must pass.
|
||||
List<ContrastPair> canonicalPairs(SurfaceTokens s) => [
|
||||
ContrastPair(
|
||||
name: 'global.text_on_background',
|
||||
@@ -100,10 +106,100 @@ List<ContrastPair> canonicalPairs(SurfaceTokens s) => [
|
||||
),
|
||||
];
|
||||
|
||||
/// Convenience for tests: returns the list of pairs that fail WCAG AA.
|
||||
List<ContrastFailure> failingPairs(SurfaceTokens tokens) {
|
||||
/// Stricter pair set — only the high-contrast (`-hc`) and colour-blind
|
||||
/// (`-cb`) theme variants must clear it. See D-69. These are the
|
||||
/// surfaces a UX consultant flagged in `consultants.md`: muted body
|
||||
/// text, status chip foregrounds, syntax tokens on the code-block
|
||||
/// surface, and the focus-indicating panel border.
|
||||
List<ContrastPair> extendedPairs(SurfaceTokens s) => [
|
||||
ContrastPair(
|
||||
name: 'global.text_muted_on_background',
|
||||
foreground: s.globalTextMuted,
|
||||
background: s.globalBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'global.text_muted_on_panel',
|
||||
foreground: s.globalTextMuted,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'status.success_on_statusbar',
|
||||
foreground: s.statusSuccess,
|
||||
background: s.statusBarBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'status.warning_on_statusbar',
|
||||
foreground: s.statusWarning,
|
||||
background: s.statusBarBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'status.error_on_statusbar',
|
||||
foreground: s.statusError,
|
||||
background: s.statusBarBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'status.info_on_statusbar',
|
||||
foreground: s.statusInfo,
|
||||
background: s.statusBarBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'syntax.keyword_on_panel',
|
||||
foreground: s.syntaxKeyword,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'syntax.type_on_panel',
|
||||
foreground: s.syntaxType,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'syntax.string_on_panel',
|
||||
foreground: s.syntaxString,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'syntax.number_on_panel',
|
||||
foreground: s.syntaxNumber,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'syntax.comment_on_panel',
|
||||
foreground: s.syntaxComment,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'syntax.method_on_panel',
|
||||
foreground: s.syntaxMethod,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
ContrastPair(
|
||||
name: 'syntax.punct_on_panel',
|
||||
foreground: s.syntaxPunct,
|
||||
background: s.panelBackground,
|
||||
),
|
||||
// WCAG 1.4.11 wants 3:1 for non-text UI components like a focus
|
||||
// border against the adjacent surface.
|
||||
ContrastPair(
|
||||
name: 'panel.active_border_on_background',
|
||||
foreground: s.panelActiveBorder,
|
||||
background: s.globalBackground,
|
||||
largeText: true,
|
||||
),
|
||||
];
|
||||
|
||||
/// Convenience for tests: returns the list of [canonicalPairs] that
|
||||
/// fail WCAG AA.
|
||||
List<ContrastFailure> failingPairs(SurfaceTokens tokens) =>
|
||||
_failures(canonicalPairs(tokens));
|
||||
|
||||
/// Strict variant of [failingPairs] — walks [extendedPairs] instead.
|
||||
/// Intended for the `-hc` / `-cb` theme gate.
|
||||
List<ContrastFailure> failingExtendedPairs(SurfaceTokens tokens) =>
|
||||
_failures(extendedPairs(tokens));
|
||||
|
||||
List<ContrastFailure> _failures(List<ContrastPair> pairs) {
|
||||
final out = <ContrastFailure>[];
|
||||
for (final p in canonicalPairs(tokens)) {
|
||||
for (final p in pairs) {
|
||||
final ratio = contrastRatio(p.foreground, p.background);
|
||||
final need = minimumRatio(p);
|
||||
if (ratio < need) {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# clide-hc — high-contrast sibling of `clide`
|
||||
#
|
||||
# Same periwinkle silhouette; muted text, status chips, syntax tokens,
|
||||
# and the focus border bumped to clear the strict contrast gate
|
||||
# (D-22 + D-69, `extendedPairs` in lib/kernel/src/theme/contrast.dart).
|
||||
|
||||
name: clide-hc
|
||||
display_name: Clide (high contrast)
|
||||
dark: true
|
||||
|
||||
palette:
|
||||
background: "#20202C"
|
||||
panel: "#1A1A24"
|
||||
surface: "#242838"
|
||||
muted: "#B1BBE3"
|
||||
foreground: "#E6E8F2"
|
||||
secondary: "#B1BBE3"
|
||||
primary: "#92B5FF"
|
||||
accent: "#92B5FF"
|
||||
success: "#A8E8C8"
|
||||
warning: "#FFD988"
|
||||
error: "#FF9F9F"
|
||||
info: "#A6C8FF"
|
||||
surfaceHi: "#2C3046"
|
||||
border: "#343850"
|
||||
borderHi: "#92B5FF"
|
||||
textDim: "#B1BBE3"
|
||||
textMute: "#9098C0"
|
||||
accentSoft: "#2192B5FF"
|
||||
|
||||
syntax:
|
||||
keyword: "#E0B5F2"
|
||||
type: "#A6C8FF"
|
||||
string: "#C2E8B0"
|
||||
number: "#FFD988"
|
||||
comment: "#9098C0"
|
||||
method: "#A6C8FF"
|
||||
punct: "#B1BBE3"
|
||||
@@ -0,0 +1,39 @@
|
||||
# midnight-hc — high-contrast sibling of `midnight`
|
||||
#
|
||||
# Same VS Code-adjacent silhouette; muted text, status chips, syntax
|
||||
# tokens, and the focus border bumped to clear the strict contrast
|
||||
# gate (D-22 + D-69, `extendedPairs` in lib/kernel/src/theme/contrast.dart).
|
||||
|
||||
name: midnight-hc
|
||||
display_name: Midnight (high contrast)
|
||||
dark: true
|
||||
|
||||
palette:
|
||||
background: "#1E1E1E"
|
||||
panel: "#181818"
|
||||
surface: "#252526"
|
||||
muted: "#BDBDBD"
|
||||
foreground: "#F0F0F0"
|
||||
secondary: "#D4D4D4"
|
||||
primary: "#82BFFF"
|
||||
accent: "#82BFFF"
|
||||
success: "#B5E2B0"
|
||||
warning: "#E9D89E"
|
||||
error: "#FFB3A5"
|
||||
info: "#9BC9F0"
|
||||
onAccent: "#0B1220"
|
||||
surfaceHi: "#2D2D2E"
|
||||
border: "#333333"
|
||||
borderHi: "#9C9C9C"
|
||||
textDim: "#BDBDBD"
|
||||
textMute: "#9A9A9A"
|
||||
accentSoft: "#2182BFFF"
|
||||
|
||||
syntax:
|
||||
keyword: "#D9A1D3"
|
||||
type: "#66D9C2"
|
||||
string: "#E9B299"
|
||||
number: "#C8E0B7"
|
||||
comment: "#99CB80"
|
||||
method: "#E5E5B5"
|
||||
punct: "#BDBDBD"
|
||||
@@ -0,0 +1,38 @@
|
||||
# paper-hc — high-contrast sibling of `paper`
|
||||
#
|
||||
# Drafting sheet, red-pencil accent — darker ink so muted text, status
|
||||
# chips, syntax tokens, and the focus border clear the strict contrast
|
||||
# gate (D-22 + D-69, `extendedPairs` in lib/kernel/src/theme/contrast.dart).
|
||||
|
||||
name: paper-hc
|
||||
display_name: Paper (high contrast)
|
||||
dark: false
|
||||
|
||||
palette:
|
||||
background: "#F4F1EA"
|
||||
panel: "#ECE7DB"
|
||||
surface: "#FBF8F1"
|
||||
muted: "#4A4A48"
|
||||
foreground: "#0E0E0E"
|
||||
secondary: "#1F1F1F"
|
||||
primary: "#8E2D10"
|
||||
accent: "#6E2410"
|
||||
success: "#1A4A2B"
|
||||
warning: "#6A4C00"
|
||||
error: "#7A1F12"
|
||||
info: "#154168"
|
||||
surfaceHi: "#ECE7DB"
|
||||
border: "#0E0E0E"
|
||||
borderHi: "#3A3A38"
|
||||
textDim: "#4A4A48"
|
||||
textMute: "#7A7A72"
|
||||
accentSoft: "#218E2D10"
|
||||
|
||||
syntax:
|
||||
keyword: "#52215C"
|
||||
type: "#154168"
|
||||
string: "#1A4A2B"
|
||||
number: "#6A4C00"
|
||||
comment: "#4A4A48"
|
||||
method: "#103E6F"
|
||||
punct: "#1F1F1F"
|
||||
@@ -0,0 +1,38 @@
|
||||
# terminal-hc — high-contrast sibling of `terminal`
|
||||
#
|
||||
# Near-black + amber tmux feel; muted text, status chips, syntax tokens,
|
||||
# and the focus border bumped to clear the strict contrast gate
|
||||
# (D-22 + D-69, `extendedPairs` in lib/kernel/src/theme/contrast.dart).
|
||||
|
||||
name: terminal-hc
|
||||
display_name: Terminal (high contrast)
|
||||
dark: true
|
||||
|
||||
palette:
|
||||
background: "#0A0A0A"
|
||||
panel: "#000000"
|
||||
surface: "#111111"
|
||||
muted: "#B5B5B5"
|
||||
foreground: "#F0F0F0"
|
||||
secondary: "#D6D6D6"
|
||||
primary: "#FFC868"
|
||||
accent: "#FFC868"
|
||||
success: "#B5E2B0"
|
||||
warning: "#FFD988"
|
||||
error: "#FFA5A5"
|
||||
info: "#B0C8FF"
|
||||
surfaceHi: "#181818"
|
||||
border: "#242424"
|
||||
borderHi: "#888888"
|
||||
textDim: "#B5B5B5"
|
||||
textMute: "#9A9A9A"
|
||||
accentSoft: "#21FFC868"
|
||||
|
||||
syntax:
|
||||
keyword: "#FF8585"
|
||||
type: "#FFD27A"
|
||||
string: "#B5E2B0"
|
||||
number: "#DAA7FF"
|
||||
comment: "#9A9A9A"
|
||||
method: "#BCD8FF"
|
||||
punct: "#B5B5B5"
|
||||
Reference in New Issue
Block a user