Skip to content

[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.

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-500 field 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-28 to 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"; PrimeVue Button is reported to map loading→native disabled, which would drop focus to <body> on save. node_modules absent, so unverifiable here.
  • Toggle / MSG-01 internals — ToggleSwitch aria-checked and the success/error Toast ARIA 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:17 is « 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.data for the org id resolved from localStorage.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.