Appearance
[UX] tt-time-tracker — Settings
Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: Dr-Wade/tt-time-tracker · Branch:
develop@bb3238c· Files reviewed: 6 Patterns: forms/toggle
Summary
The Settings page (org logo, theme, feature toggles, email-sync IMAP config, admins) is well built: the approval-workflow toggle uses the correct "confirmed toggle" variation (a cutoff-date modal that reverts on dismiss), admin removal confirms first (MSG-05), feature toggles have properly associated labels, and the email password uses a reveal toggle. The real gaps are accessibility of the page's structure and of the one control that saves everything — the sticky unsaved-changes bar, which appears with no live-region announcement. No blocker.
Findings
1. Page has no <h1>; title and every card share <h2> — Medium · A11Y-04
Where: components/Layout/LayoutMain.vue:6 and :74 (page title rendered as <h2>); components/OrganizationSettings.vue:9,46,66,93,137 (card headings also <h2>) What: The shared shell renders the route title (« Paramètres ») in an <h2>, and the section cards (Logo, Couleur du thème, Fonctionnalités, Synchronisation email, Administrateurs) are also <h2>. There is no <h1> on the page, and the page title sits at the same heading level as the sections under it, so the outline is flat. Why it matters: Screen-reader users navigating by heading get no top-level landmark and cannot tell the page title from a section; WCAG 2.4.6/1.3.1. Because this originates in LayoutMain, it affects every feature that uses the shell, not just Settings — worth promoting to a project-level shell fix. Fix: Make the LayoutMain page title an <h1> and demote the card headings to <h2> (they already are), giving one <h1> per page and a correct h1→h2 hierarchy.
2. The sticky "unsaved changes" save bar is not announced — Medium · MSG-01
Where: views/Admin/Settings.vue:24-49What: Editing any setting reveals a fixed bottom bar (« Modifications non enregistrées » + Enregistrer/Annuler) via a <Transition>. The bar is the only way to save — there is deliberately no header button (see comment at :7). It carries no role="status" / aria-live="polite", and the amber status dot is decorative. A screen-reader user who toggles a feature is told nothing changed and that a save control has appeared. Why it matters: The user can still tab to reach the bar, but nothing signals that unsaved changes exist or that the save affordance is now present, so a blind user may leave the page believing changes were applied (feature toggles look applied immediately). WCAG 4.1.3. Rated Medium not High because the control remains keyboard-reachable — the gap is perception, not operability. Fix: Wrap the bar text in a role="status" aria-live="polite" region so its appearance announces "Modifications non enregistrées"; keep the dot aria-hidden.
3. Logo « Supprimer » link is likely under the 24×24 minimum target — Low · A11Y-02
Where: components/OrganizationSettings.vue:34-40What: The remove-logo control is a bare <button class="text-xs ... font-medium"> with no padding; text-xs yields roughly a 16–20 px box. Why it matters: Falls short of WCAG 2.5.8 (24×24 CSS px), harder to hit on touch/coarse pointers. Fix: Add vertical padding (e.g. py-1) or bump the hit area to ≥24 px, as the admin-remove trash button already does (p-1.5).
4. Confirm buttons are disabled while empty, with no explanation — Low · FORM-05
Where: components/OrganizationSettings.vue:164 (:disabled="!newAdmin") and :231 (cutoff « Confirmer » :disabled="!cutoffDate") What: The add-admin check button and the cutoff-date confirm button are disabled until their field is filled, giving the user no message about what is missing. Why it matters: Baseline prefers enabled submits with validation messaging; a disabled control offers nothing to act on. Low because the required input (pick a user / a date) is visually obvious and adjacent. Fix: Keep the button enabled and surface a short "Sélectionnez un utilisateur" / "Choisissez une date" hint on activation, or leave as-is given the tight coupling.
5. Theme swatch buttons rely on title alone for their accessible name — Low · A11Y-05
Where: components/OrganizationSettings.vue:50-60What: Each colour swatch is an empty <button> with :title="color.label" and no text content, aria-label, or type="button". Its accessible name comes only from the title attribute. Why it matters: title is an unreliable accessible-name source (not surfaced by all AT, never on touch), so the swatch may announce as an unnamed button. Selection state itself is fine — it uses ring-2 + scale-110, not colour alone. Fix: Add aria-label="Thème {{ color.label }}" (and type="button") to each swatch.
Unverified
- A11Y-01 (contrast) — the amber status dot,
text-surface-500field sub-labels and the red « Supprimer » link need a contrast tool against computed values; not assessable from classes. - A11Y-06 (responsive / short viewport) — the fixed save bar overlaps content (the page adds
pb-28to compensate); needs a rendered short viewport with a mobile keyboard open to confirm the last setting and the bar don't collide. - FORM-06 — the « Enregistrer » button uses
:loading="saving"; PrimeVueButtonis reported to maploading→nativedisabled, which would drop focus to<body>on save.node_modulesabsent, so unverifiable here. - Toggle / MSG-01 internals —
ToggleSwitcharia-checkedand the success/errorToastARIA role come from PrimeVue internals that cannot be read from source. - SEC-04 — adding/removing an administrator is a security-relevant change; whether the affected user is notified out of band is server-side and not determinable from this client code.
Baseline additions
None. (One candidate not filed: logo upload persists immediately server-side via :auto FileUpload — OrganizationSettings.vue:24-33,343 — while every other setting is batched behind the save bar, so « Annuler » reverts the displayed logo but not the already-uploaded file. Real but minor consistency wrinkle; no clean rule and no user-visible harm, so noted, not filed.)
Cross-project note
- NAV-03 — fails project-wide (
index.html:17is « Tim » for every route); see PROJECT-LEVEL.md, not re-filed here. - CONTENT-01 — not-applicable, see project-level i18n finding.
- Persisted scope — Settings edits
organization.datafor the org id resolved fromlocalStorage.lastOrganizationId(stores/organization.store.ts:11,20), the same stored-scope-trusted-as-authoritative root cause recorded in PROJECT-LEVEL.md; not re-filed. - A11Y-04 (no
<h1>, shell-level) is a strong candidate to also affect any other project whose page shell renders the title as<h2>; worth a shell check in customer-portal and members.