feat(accessibility): wire BORDER_SUBTLE_HC into the modal scaffold
Resume-prompt Option E, part 2 of 2 — HC chrome borders. Pairs with the reduce-motion gating in `ed152e2`. v0.21.1 introduced `BORDER_SUBTLE_HC` (#a0a0a0) but never wired it: the constant existed, no consumer used it. Spec at `design-system.md` §Accessibility (#2) mandates outline boost from `#505050` (BORDER_STRONG) to `#a0a0a0` under high-contrast mode so panels and popovers stay legible on low-quality displays. ### Architecture - New `HighContrastBorder` component in `ui_theme` carrying a `default_color: Color` field that records the off-state colour the entity was spawned with. Tag any UI node where border legibility is accessibility-critical. - New `update_high_contrast_borders` system in `settings_plugin` walks all tagged entities each Update tick, sets `BorderColor` to `BORDER_SUBTLE_HC` when `Settings::high_contrast_mode` is on, otherwise to `marker.default_color`. Compares against current `BorderColor` and only mutates when different so Bevy's change-detection doesn't trigger repaints every frame. ### Tagged in this commit - The modal scaffold's card border (`ui_modal::spawn_modal`). This is the primary accessibility target — modals demand attention and a low-vision player needs to perceive the panel boundary. Default colour: `BORDER_STRONG` (#505050); HC variant: `BORDER_SUBTLE_HC` (#a0a0a0). ### Future scope Other `BORDER_SUBTLE` / `BORDER_STRONG` consumer sites (help panel, stats panel, tooltip, action buttons, settings rows, etc.) can be tagged in follow-ups by adding `HighContrastBorder::with_default(...)` to their spawn tuple. The system handles any entity carrying the marker — no further changes needed once a site is tagged. Started small here to keep the commit reviewable and prove the architecture before rolling out broadly. Workspace clippy + cargo test --workspace clean. 1193 passing (unchanged from prior — no new tests added; the system is small enough that the running-game verification is the meaningful check). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -34,9 +34,9 @@ use crate::ui_modal::{
|
|||||||
};
|
};
|
||||||
use crate::ui_tooltip::Tooltip;
|
use crate::ui_tooltip::Tooltip;
|
||||||
use crate::ui_theme::{
|
use crate::ui_theme::{
|
||||||
BG_BASE, BG_ELEVATED, BG_ELEVATED_HI, BORDER_SUBTLE, RADIUS_SM, SPACE_2, STATE_SUCCESS,
|
BG_BASE, BG_ELEVATED, BG_ELEVATED_HI, BORDER_SUBTLE, BORDER_SUBTLE_HC, HighContrastBorder,
|
||||||
TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG, TYPE_CAPTION, VAL_SPACE_2, VAL_SPACE_3,
|
RADIUS_SM, SPACE_2, STATE_SUCCESS, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG,
|
||||||
Z_MODAL_PANEL,
|
TYPE_CAPTION, VAL_SPACE_2, VAL_SPACE_3, Z_MODAL_PANEL,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Side length of a swatch button in the card-back / background pickers.
|
/// Side length of a swatch button in the card-back / background pickers.
|
||||||
@@ -364,6 +364,7 @@ impl Plugin for SettingsPlugin {
|
|||||||
update_anim_speed_text,
|
update_anim_speed_text,
|
||||||
update_color_blind_text,
|
update_color_blind_text,
|
||||||
update_high_contrast_text,
|
update_high_contrast_text,
|
||||||
|
update_high_contrast_borders,
|
||||||
update_reduce_motion_text,
|
update_reduce_motion_text,
|
||||||
update_tooltip_delay_text,
|
update_tooltip_delay_text,
|
||||||
update_time_bonus_multiplier_text,
|
update_time_bonus_multiplier_text,
|
||||||
@@ -637,6 +638,42 @@ fn update_high_contrast_text(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Repaints `BorderColor` on every entity tagged with
|
||||||
|
/// [`HighContrastBorder`] based on `Settings::high_contrast_mode`.
|
||||||
|
/// Off → the marker's `default_color`; on → `BORDER_SUBTLE_HC`
|
||||||
|
/// (`#a0a0a0`). Compares against the current border colour and
|
||||||
|
/// only mutates when different so Bevy's change-detection
|
||||||
|
/// doesn't trigger repaints every frame.
|
||||||
|
///
|
||||||
|
/// Spec at `design-system.md` §Accessibility (#2): under HC,
|
||||||
|
/// outlines boost from `#505050` (BORDER_STRONG) to `#a0a0a0` so
|
||||||
|
/// modal panels, popover edges, and focus-ring carriers stay
|
||||||
|
/// legible on low-quality displays / for low-vision users.
|
||||||
|
///
|
||||||
|
/// Tagged sites in v0.21.x: the modal scaffold's card border
|
||||||
|
/// (`ui_modal::spawn_modal`). More sites can be tagged in
|
||||||
|
/// follow-ups by adding `HighContrastBorder::with_default(...)`
|
||||||
|
/// to their spawn tuple.
|
||||||
|
fn update_high_contrast_borders(
|
||||||
|
settings: Res<SettingsResource>,
|
||||||
|
mut borders: Query<(&HighContrastBorder, &mut BorderColor)>,
|
||||||
|
) {
|
||||||
|
let high_contrast = settings.0.high_contrast_mode;
|
||||||
|
for (marker, mut border) in borders.iter_mut() {
|
||||||
|
let target = if high_contrast {
|
||||||
|
BORDER_SUBTLE_HC
|
||||||
|
} else {
|
||||||
|
marker.default_color
|
||||||
|
};
|
||||||
|
// Only mutate when actually different — avoids per-frame
|
||||||
|
// change-detection churn. `border.left` is representative
|
||||||
|
// because every tagged site uses `BorderColor::all(...)`.
|
||||||
|
if border.left != target {
|
||||||
|
*border = BorderColor::all(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn update_reduce_motion_text(
|
fn update_reduce_motion_text(
|
||||||
settings: Res<SettingsResource>,
|
settings: Res<SettingsResource>,
|
||||||
mut text_nodes: Query<&mut Text, With<ReduceMotionText>>,
|
mut text_nodes: Query<&mut Text, With<ReduceMotionText>>,
|
||||||
|
|||||||
@@ -58,8 +58,9 @@ use crate::settings_plugin::SettingsResource;
|
|||||||
use crate::ui_theme::{
|
use crate::ui_theme::{
|
||||||
scaled_duration, ACCENT_PRIMARY, ACCENT_PRIMARY_HOVER, ACCENT_SECONDARY, BG_BASE, BG_ELEVATED,
|
scaled_duration, ACCENT_PRIMARY, ACCENT_PRIMARY_HOVER, ACCENT_SECONDARY, BG_BASE, BG_ELEVATED,
|
||||||
BG_ELEVATED_HI, BG_ELEVATED_PRESSED, BG_ELEVATED_TOP, BORDER_STRONG, BORDER_SUBTLE,
|
BG_ELEVATED_HI, BG_ELEVATED_PRESSED, BG_ELEVATED_TOP, BORDER_STRONG, BORDER_SUBTLE,
|
||||||
MOTION_MODAL_SECS, RADIUS_LG, RADIUS_MD, SCRIM, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY_LG,
|
HighContrastBorder, MOTION_MODAL_SECS, RADIUS_LG, RADIUS_MD, SCRIM, TEXT_PRIMARY,
|
||||||
TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_2, VAL_SPACE_3, VAL_SPACE_4, VAL_SPACE_5,
|
TEXT_SECONDARY, TYPE_BODY_LG, TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_2, VAL_SPACE_3,
|
||||||
|
VAL_SPACE_4, VAL_SPACE_5,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -230,6 +231,13 @@ where
|
|||||||
Transform::from_scale(Vec3::splat(MODAL_ENTER_START_SCALE)),
|
Transform::from_scale(Vec3::splat(MODAL_ENTER_START_SCALE)),
|
||||||
BackgroundColor(BG_ELEVATED),
|
BackgroundColor(BG_ELEVATED),
|
||||||
BorderColor::all(BORDER_STRONG),
|
BorderColor::all(BORDER_STRONG),
|
||||||
|
// Honour `Settings::high_contrast_mode`: under HC the
|
||||||
|
// border boosts from `BORDER_STRONG` (#505050) to
|
||||||
|
// `BORDER_SUBTLE_HC` (#a0a0a0) so the modal panel
|
||||||
|
// edge stays clearly visible against the scrim and
|
||||||
|
// surface beneath. `update_high_contrast_borders` in
|
||||||
|
// `settings_plugin` does the per-frame swap.
|
||||||
|
HighContrastBorder::with_default(BORDER_STRONG),
|
||||||
))
|
))
|
||||||
.with_children(build_card);
|
.with_children(build_card);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -226,6 +226,32 @@ pub const BORDER_SUBTLE: Color = Color::srgba(0.208, 0.208, 0.208, 1.0);
|
|||||||
/// vision users.
|
/// vision users.
|
||||||
pub const BORDER_SUBTLE_HC: Color = Color::srgba(0.627, 0.627, 0.627, 1.0);
|
pub const BORDER_SUBTLE_HC: Color = Color::srgba(0.627, 0.627, 0.627, 1.0);
|
||||||
|
|
||||||
|
/// Marker for entities whose [`BorderColor`] should swap to
|
||||||
|
/// [`BORDER_SUBTLE_HC`] when `Settings::high_contrast_mode` is on.
|
||||||
|
/// Tag any UI node where border legibility is accessibility-critical
|
||||||
|
/// — modal panels, popovers, settings rows, focus-ring carriers —
|
||||||
|
/// then add the `apply_high_contrast_borders` system to react to
|
||||||
|
/// settings changes.
|
||||||
|
///
|
||||||
|
/// `default_color` records the off-state colour the entity was
|
||||||
|
/// spawned with so the system can revert when HC is toggled back
|
||||||
|
/// off. Different sites use different defaults (`BORDER_SUBTLE` for
|
||||||
|
/// idle popover edges, `BORDER_STRONG` for active modal cards) — the
|
||||||
|
/// marker captures whichever one applies at this entity.
|
||||||
|
#[derive(bevy::prelude::Component, Debug, Clone, Copy)]
|
||||||
|
pub struct HighContrastBorder {
|
||||||
|
/// Border colour to use when high-contrast mode is *off* — the
|
||||||
|
/// site's normal idle / active-state colour.
|
||||||
|
pub default_color: bevy::prelude::Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HighContrastBorder {
|
||||||
|
/// Convenience constructor — `HighContrastBorder::with_default(BORDER_SUBTLE)`.
|
||||||
|
pub const fn with_default(default_color: bevy::prelude::Color) -> Self {
|
||||||
|
Self { default_color }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Strong border — hover outline, focused button, active popover.
|
/// Strong border — hover outline, focused button, active popover.
|
||||||
/// `outline` from the design system. `#505050`.
|
/// `outline` from the design system. `#505050`.
|
||||||
pub const BORDER_STRONG: Color = Color::srgba(0.314, 0.314, 0.314, 1.0);
|
pub const BORDER_STRONG: Color = Color::srgba(0.314, 0.314, 0.314, 1.0);
|
||||||
|
|||||||
Reference in New Issue
Block a user