Add WCAG 2.2 AAA compliance and automated AT-SPI audit tool

- Bring all UI widgets to WCAG 2.2 AAA conformance across all views
- Add accessible labels, roles, descriptions, and announcements
- Bump focus outlines to 3px, target sizes to 44px AAA minimum
- Fix announce()/announce_result() to walk widget tree via parent()
- Add AT-SPI accessibility audit script (tools/a11y-audit.py) that
  checks SC 4.1.2, 1.1.1, 1.3.1, 2.1.1, 2.5.5, 2.5.8, 2.4.8,
  2.4.9, 2.4.10, 2.1.3 with JSON report output for CI
- Clean up project structure, archive old plan documents
This commit is contained in:
lashman
2026-03-01 12:44:21 +02:00
parent abb69dc753
commit 7e55d5796f
23 changed files with 2758 additions and 472 deletions

View File

@@ -1,4 +1,5 @@
use gtk::prelude::*;
use gtk::accessible::Property as AccessibleProperty;
use crate::core::database::CatalogApp;
use super::widgets;
@@ -95,6 +96,7 @@ pub fn build_catalog_tile(app: &CatalogApp, installed: bool) -> gtk::FlowBoxChil
.build();
let dl_icon = gtk::Image::from_icon_name("folder-download-symbolic");
dl_icon.set_pixel_size(12);
dl_icon.update_property(&[AccessibleProperty::Label("Downloads")]);
dl_box.append(&dl_icon);
let dl_label = gtk::Label::new(Some(&widgets::format_count(downloads)));
dl_label.add_css_class("caption");
@@ -111,6 +113,7 @@ pub fn build_catalog_tile(app: &CatalogApp, installed: bool) -> gtk::FlowBoxChil
.build();
let star_icon = gtk::Image::from_icon_name("starred-symbolic");
star_icon.set_pixel_size(12);
star_icon.update_property(&[AccessibleProperty::Label("Stars")]);
star_box.append(&star_icon);
let star_label = gtk::Label::new(Some(&widgets::format_count(stars)));
star_box.append(&star_label);
@@ -124,6 +127,7 @@ pub fn build_catalog_tile(app: &CatalogApp, installed: bool) -> gtk::FlowBoxChil
.build();
let ver_icon = gtk::Image::from_icon_name("tag-symbolic");
ver_icon.set_pixel_size(12);
ver_icon.update_property(&[AccessibleProperty::Label("Version")]);
ver_box.append(&ver_icon);
let ver_label = gtk::Label::builder()
.label(ver.as_str())
@@ -161,13 +165,6 @@ pub fn build_catalog_tile(app: &CatalogApp, installed: bool) -> gtk::FlowBoxChil
inner.append(&installed_badge);
}
// Source badge - show which source this app came from
let source_label = if app.ocs_id.is_some() { "AppImageHub" } else { "Community" };
let source_badge = widgets::status_badge(source_label, "neutral");
source_badge.set_halign(gtk::Align::Start);
source_badge.set_margin_top(2);
inner.append(&source_badge);
card.append(&inner);
let child = gtk::FlowBoxChild::builder()
@@ -176,6 +173,30 @@ pub fn build_catalog_tile(app: &CatalogApp, installed: bool) -> gtk::FlowBoxChil
child.add_css_class("activatable");
widgets::set_pointer_cursor(&child);
// Accessible label for screen readers
let mut a11y_parts = vec![app.name.clone()];
if !plain.is_empty() {
a11y_parts.push(plain.chars().take(80).collect());
}
if let Some(downloads) = app.ocs_downloads.filter(|&d| d > 0) {
a11y_parts.push(format!("{} downloads", downloads));
}
if let Some(stars) = app.github_stars.filter(|&s| s > 0) {
a11y_parts.push(format!("{} stars", stars));
}
if let Some(ref cats) = app.categories {
if let Some(cat) = cats.split(';').next().or_else(|| cats.split(',').next()) {
let cat = cat.trim();
if !cat.is_empty() {
a11y_parts.push(format!("category: {}", cat));
}
}
}
if installed {
a11y_parts.push("installed".to_string());
}
child.update_property(&[AccessibleProperty::Label(&a11y_parts.join(", "))]);
child
}
@@ -262,6 +283,22 @@ pub fn build_catalog_row(app: &CatalogApp, installed: bool) -> gtk::FlowBoxChild
.build();
child.add_css_class("activatable");
widgets::set_pointer_cursor(&child);
// Accessible label for screen readers
let mut a11y_parts = vec![app.name.clone()];
if !plain.is_empty() {
a11y_parts.push(plain.chars().take(60).collect());
}
if let Some(downloads) = app.ocs_downloads.filter(|&d| d > 0) {
a11y_parts.push(format!("{} downloads", downloads));
} else if let Some(stars) = app.github_stars.filter(|&s| s > 0) {
a11y_parts.push(format!("{} stars", stars));
}
if installed {
a11y_parts.push("installed".to_string());
}
child.update_property(&[AccessibleProperty::Label(&a11y_parts.join(", "))]);
child
}
@@ -283,6 +320,24 @@ pub fn build_featured_tile(app: &CatalogApp) -> gtk::Box {
widgets::set_pointer_cursor(&card);
card.set_widget_name(&format!("featured-{}", app.id));
// Accessible label for screen readers
let mut a11y_parts = vec![app.name.clone()];
if let Some(desc) = app.ocs_summary.as_deref()
.filter(|d| !d.is_empty())
.or(app.github_description.as_deref().filter(|d| !d.is_empty()))
{
a11y_parts.push(strip_html(desc).chars().take(60).collect());
}
if let Some(ref cats) = app.categories {
if let Some(cat) = cats.split(';').next().or_else(|| cats.split(',').next()) {
let cat = cat.trim();
if !cat.is_empty() {
a11y_parts.push(format!("category: {}", cat));
}
}
}
card.update_property(&[AccessibleProperty::Label(&a11y_parts.join(", "))]);
// Screenshot preview area (top)
let screenshot_frame = gtk::Frame::new(None);
screenshot_frame.add_css_class("catalog-featured-screenshot");
@@ -297,6 +352,7 @@ pub fn build_featured_tile(app: &CatalogApp) -> gtk::Box {
.width_request(32)
.height_request(32)
.build();
spinner.update_property(&[gtk::accessible::Property::Label("Loading screenshot")]);
screenshot_frame.set_child(Some(&spinner));
card.append(&screenshot_frame);