Eight hotfix-ready UX/a11y findings from the v1.4.34.3 UX audit
land as one batch.
i18n leaks (3 primitives):
- Dialog + Sheet close-X carried hardcoded `<span sr-only>Close</span>`
so a German user heard 'Close, button' on every modal. Both lift
`useTranslations()` and read `common.close`.
- ChartSkeleton's sr-only loading announcement was English-only.
The component is already a client island; reads `charts.loadingLabel`
from each locale now. New key shipped in DE / EN / FR / ES / IT / PL.
- Notifications-section breadcrumb `<nav aria-label="Breadcrumb">`
was hardcoded. Routes through `t("nav.breadcrumb")` with a new
key in each locale.
Focus rings (mobile + tablet nav):
- Bottom-nav primary links + the More overflow trigger relied on a
`text-muted-foreground` → `text-primary` colour transition only;
keyboard / switch-control users saw no focus indicator at all.
Added `focus-visible:ring-ring/50 focus-visible:ring-2
focus-visible:outline-none focus-visible:ring-offset-2` matching
the v1.4.33 sidebar treatment.
- Mobile top-bar trigger had `focus:outline-none` without a
`focus-visible` replacement. Same treatment applied.
Tap-target floor (WCAG 2.5.5):
- Input / NativeSelect / Select / DateTimeInput were locked to
h-10 (40 px) on every viewport. Switched to `h-11 sm:h-10` so
mobile clears the 44 px floor while desktop keeps the 40 px norm
that the Button primitive already converged on in v1.4.33.
Admin chip-strip auto-scroll-into-view:
- The admin shell shipped 13 chips but lacked the
`scrollIntoView({inline: 'center'})` effect the settings shell
got in v1.4.33. Admins tapping a chip past the right edge landed
on the new page with the strip scrolled to the left and the
active chip off-screen. Copied the settings-shell pattern verbatim.
List-page subtitles on mobile:
- Measurements / Mood / Medications pages hid their descriptive
subtitle on `<sm` with `hidden sm:block`. Comparable apps keep
the subtitle on mobile because it doubles as contextual help.
Dropped the hide rule; the H1 still anchors the visual.
Insights empty-state CTA vocabulary:
- `ALLOWED_ADD_TYPES` on `/measurements` accepted tokens (`GLUCOSE`,
`TEMPERATURE`, `HEART_RATE`, `BMI`) that didn't match the
`MEASUREMENT_TYPES` enum the form uses. Several `/insights/*`
empty-state CTAs emitted those mismatched tokens and the dialog
silently failed to open. Replaced with a single source of truth
derived from `MEASUREMENT_TYPES` plus a normalisation map for
legacy tokens. Added BODY_TEMPERATURE to the form's enum row.
New unit test walks every `?add=` token emitted under
`src/app/insights/**` and asserts it resolves to a real form type.
MB7 extracted three drifted local `NATIVE_SELECT_CLASS` constants
into the shared primitive at `h-9` (36 px) — the pre-v1.4.27 floor.
MB2 had already lifted Input + Select + SelectTrigger to `h-10`
(40 px) for the WCAG 2.5.5 tap-target sweep; the NativeSelect
extraction missed that contract bump and the three settings + admin
surfaces consuming it (account-section gender + language + timezone,
timezone-picker, general-settings-section default-language) now
render one step shorter than the Input rows they share a grid with.
The settings-mobile-consistency e2e spec already asserts a uniform
40 px floor across every `section input + section select` on
/settings/account and a 40 px floor across every `section select`
on /settings/ai; the primitive bump aligns the visual rhythm with
both the rest of the form and the test contract.
v1.4.27 MB7 / CF-52 — three settings/admin surfaces each declared a
local `NATIVE_SELECT_CLASS` constant tracing the same visual contract
as the shadcn `<SelectTrigger>`. The copies had drifted slightly so
`AccountSection`, `TimezonePicker`, and `GeneralSettingsSection`
rendered at subtly different focus-ring weights. The new
`<NativeSelect>` primitive owns the contract; all three call sites
import it and the in-file constants retire. The native picker remains
the right experience for the 400-entry timezone list and the
admin-only locale dropdowns, so the primitive deliberately stays on a
styled `<select>` rather than wrapping the radix popover-driven
`<Select>`.