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

@@ -7,7 +7,7 @@ use gtk::gio;
use crate::config::APP_ID;
use crate::core::database::Database;
use crate::core::updater;
use crate::i18n::i18n;
use crate::i18n::{i18n, ni18n_f};
use crate::ui::update_dialog;
use crate::ui::widgets;
@@ -23,10 +23,11 @@ pub fn build_updates_view(db: &Rc<Database>) -> adw::ToolbarView {
header.set_title_widget(Some(&title));
// Check Now button
let check_btn = gtk::Button::builder()
.icon_name("view-refresh-symbolic")
.tooltip_text(&i18n("Check for updates (Ctrl+U)"))
.build();
let check_btn = widgets::accessible_icon_button(
"view-refresh-symbolic",
"Check for updates",
&i18n("Check for updates (Ctrl+U)"),
);
header.pack_end(&check_btn);
// Update All button (only visible when updates exist)
@@ -60,6 +61,7 @@ pub fn build_updates_view(db: &Rc<Database>) -> adw::ToolbarView {
.height_request(32)
.halign(gtk::Align::Center)
.build();
spinner.update_property(&[gtk::accessible::Property::Label("Checking for updates")]);
checking_page.set_child(Some(&spinner));
stack.add_named(&checking_page, Some("checking"));
@@ -157,7 +159,7 @@ pub fn build_updates_view(db: &Rc<Database>) -> adw::ToolbarView {
// Re-read records from the shared db so UI picks up changes from the bg thread
drop(fresh_db);
populate_update_list(&state_c);
state_c.toast_overlay.add_toast(adw::Toast::new(&i18n("Update check complete")));
state_c.toast_overlay.add_toast(widgets::info_toast(&i18n("Update check complete")));
});
});
}
@@ -198,7 +200,7 @@ pub fn build_updates_view(db: &Rc<Database>) -> adw::ToolbarView {
let count = result.unwrap_or(0);
if count > 0 {
state_c.toast_overlay.add_toast(
adw::Toast::new(&format!("{} apps updated", count)),
widgets::info_toast(&ni18n_f("{} app updated", "{} apps updated", count, &[("{}", &count.to_string())])),
);
}
populate_update_list(&state_c);
@@ -257,7 +259,9 @@ fn populate_update_list(state: &Rc<UpdatesState>) {
state.stack.set_visible_child_name("updates");
state.update_all_btn.set_visible(true);
state.title.set_subtitle(&format!("{} updates available", updatable.len()));
let count = updatable.len();
state.title.set_subtitle(&format!("{} updates available", count));
widgets::announce(state.list_box.upcast_ref::<gtk::Widget>(), &format!("{} updates available", count));
for record in &updatable {
let name = record.app_name.as_deref().unwrap_or(&record.filename);
@@ -284,8 +288,9 @@ fn populate_update_list(state: &Rc<UpdatesState>) {
.expanded(false)
.build();
// App icon
// App icon (decorative - row title already names the app)
let icon = widgets::app_icon(record.icon_path.as_deref(), name, 32);
icon.set_accessible_role(gtk::AccessibleRole::Presentation);
row.add_prefix(&icon);
// "What's new" content inside the expander
@@ -317,6 +322,7 @@ fn populate_update_list(state: &Rc<UpdatesState>) {
.tooltip_text(&i18n("Update this app"))
.css_classes(["flat"])
.build();
update_btn.update_property(&[gtk::accessible::Property::Label(&format!("Update {}", name))]);
let app_id = record.id;
let update_url = record.update_url.clone();
@@ -355,11 +361,11 @@ fn populate_update_list(state: &Rc<UpdatesState>) {
btn_c.set_sensitive(true);
if result.unwrap_or(false) {
state_c.toast_overlay.add_toast(
adw::Toast::new(&i18n("Update complete")),
widgets::info_toast(&i18n("Update complete")),
);
} else {
state_c.toast_overlay.add_toast(
adw::Toast::new(&i18n("Update failed")),
widgets::error_toast(&i18n("Update failed")),
);
}
populate_update_list(&state_c);