Skip to content

Commit 301f88e

Browse files
mizu-junclaudehappy-otter
authored
feat(client): announce every InfoBar kind to a screen reader (UI/UX v3 P6c) (#94)
Before this change the update banner was the only one of the three top-of-screen messages with an AccessKit node. The offline bar had none — it was in the tree hash, so it forced a rebuild that added nothing — and the server error, the surface that reports "your shell could not be launched" and "your config failed to load", occurred zero times in accessibility.rs. A screen-reader user was never told. One builder driven by `InfoBarKind` replaces the update-only one: - `build_info_bar_nodes` emits one `Role::Alert` per queued bar in stack order, labelled with the same text the bar draws so the two cannot drift. An error is `Live::Assertive`; the offline and update bars are `Live::Polite` — the offline bar reports a condition, not an event. - Node ids are keyed by slot (`info_bar_node_id`), not by queue position, so the id of "the error bar" does not move when the bar above it is dismissed. The match is exhaustive, so a fourth slot cannot be added without a node (G-a11y). `NodeIdKind::UpdateBanner` and NodeId(10) are retired in favour of `NodeIdKind::InfoBar { slot }` at 28..=30. - Bars past the drawn cap are announced in full: `+{count} more` is a constraint of a two-bar stack, not of a screen reader. The tree hash already covered slot + message from P6b; this adds the gates it was missing — a bar appearing, being reworded or being removed changes it, and the offline bar's elapsed seconds do not (G-hash). Completes the phase's one new string: `infobar-more-count` in all 8 locales (G-i18n), drawn on the bottom bar via `StackLayout::more_label` so the cap and the way the cap is reported cannot disagree. The `VecDeque` flattening the vertex builder open-coded moves to `infobar::contiguous`, now that the AccessKit path needs it too. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Happy <yesreply@happy.engineering>
1 parent 5e44080 commit 301f88e

13 files changed

Lines changed: 420 additions & 68 deletions

File tree

docs/plans/ui-ux-modernization-v3.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -713,8 +713,11 @@ gated behind a spike.
713713
- [x] P6b migrate the three banners onto the stack — the three `Option`
714714
fields and the three builders are gone; the stack draws below the tab bar,
715715
capped at two bars, and `Enter` acts on the top bar only (2026-08-29)
716-
- [ ] P6c AccessKit nodes ×kind + tree hash + `infobar-more-count` ×8 locales
717-
— the phase's most valuable change: the error bar is still unannounced
716+
- [x] P6c AccessKit nodes ×kind + tree hash + `infobar-more-count` ×8 locales
717+
— every kind now has a `Role::Alert` node keyed by its slot, so the server
718+
error is announceable for the first time (assertive; the other two polite),
719+
bars past the drawn cap are announced in full, and the cap reports the rest
720+
as `+{count} more` (2026-08-29)
718721
- [ ] P6d entrance/exit motion + auto-dismissal for the info severity
719722
- [x] P7 base `notitle` custom title bar — shipped early via #46 (2026-07-30)
720723
- [x] P7 spike: Windows 11 snap layouts — answered in production via #49 (2026-07-31)

nexterm-client-gpu/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Guidance for working inside the GPU client crate. The repo-wide rules — langua
1717
- `settings_<tab>.rs` — one module per migrated category, each exposing `<tab>_widget_descs` (semantics), `build_<tab>_widgets` (semantics + geometry) and `apply_<tab>_action` (the single state transition the mouse, keyboard and AccessKit paths all call). All nine categories are migrated: Theme, Window, Font, Startup, Blocks, Security, Profiles, Ssh and Keybindings. The three list-shaped tabs (Profiles, Ssh, Keybindings) window their entry list via `layout::list_window`, and reserve index 0 for the entry list itself (which entry is selected lives in `selected_host_index` / `selected_key_index`, because that outlives focus moving to the fields); their delete-confirmation dialogs stay hand-written, because a modal over the panel is not a settings row.
1818
- **Focus** is one field: `SettingsPanel.focused_widget_index` (a `WidgetId.index` for the current category), replacing the seven `<tab>_field_focus` counters. What an index means is defined by that category's descriptor builder — its `row` constants where it has them, otherwise the order descriptors are pushed. Every category change goes through `SettingsPanel::set_category`, which resets the index, the scroll offset and any in-flight field edit; do not assign `panel.category` directly. Keyboard ↑/↓ walk the descriptors via `widgets/navigation.rs` (`focus_next` / `focus_prev`), skipping anything that is not a focus stop — `!enabled`, `Label`, and `Swatch` (a swatch duplicates the cycler row above it, so nine extra stops would buy nothing). A new category therefore gains keyboard navigation by describing its controls, with no navigation code of its own. Ssh and Keybindings are the exception and keep bespoke arrow arms, because index 0 addresses their entry list as a whole rather than a described widget.
1919
- `settings/hover.rs``HoverDwell`, the pointer-dwell timer that gates tooltips (500 ms).
20-
- `renderer/overlay/infobar.rs` — the InfoBar stack (UI/UX v3 P6): every non-blocking status message — update notice, offline, server error — as one `InfoBarKind` queued on `ClientState.info_bars`, one slot per kind. `bar_rects` is the only function that computes a bar's `y`, and `stack_order` the only one that decides which bar is on top; a new message type is an enum arm, not a fourth builder, and it must not gain stacking arithmetic of its own. Drawn by `ui_verts::build_info_bar_verts` below the tab bar, capped at two visible bars.
20+
- `renderer/overlay/infobar.rs` — the InfoBar stack (UI/UX v3 P6): every non-blocking status message — update notice, offline, server error — as one `InfoBarKind` queued on `ClientState.info_bars`, one slot per kind. `bar_rects` is the only function that computes a bar's `y`, and `stack_order` the only one that decides which bar is on top; a new message type is an enum arm, not a fourth builder, and it must not gain stacking arithmetic of its own. Drawn by `ui_verts::build_info_bar_verts` below the tab bar, capped at two visible bars, with `StackLayout::more_label` reporting the rest on the bottom bar. Accessibility is driven from the same enum (P6c): `accessibility::build_info_bar_nodes` emits one `Role::Alert` per queued bar — including bars past the drawn cap — at the stable id `info_bar_node_id(slot)`, so a new slot cannot be added without a node.
2121
- `palette.rs` — Command palette (`Ctrl+Shift+P`). Fuzzy search via `SkimMatcherV2`. Sprint 5-7 / Phase 3-3 covers all 25 actions in `execute_action` (Quit, ClosePane, NewWindow, QuickSelect, SetBroadcastOn/Off, …) and persists usage history at `~/.local/state/nexterm/palette_history.json` (atomic write, mode 0600). The pure `rank_actions` function orders by history when the query is empty (last_used desc → use_count desc) and combines fuzzy score with a `history_bonus` (use_count×10 capped at 100, +100 within 1 day, +50 within 1 week) when a query is present. `record_use` records the selection.
2222
- `host_manager.rs` — SSH host manager UI. `load_history()` / `save_history()` persist connection frequency to `host_history.json`. The `PasswordModal` struct handles the password prompt for `auth_type="password"` hosts.
2323
- `update_checker.rs` — Polls the GitHub Releases API five seconds after startup. Disabled by `auto_check_update = false`. Results are queued onto `ClientState.info_bars` as `InfoBarKind::UpdateAvailable`; `Esc` dismisses, `Enter` opens the release page while it is the top bar.

0 commit comments

Comments
 (0)