mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
ux: i18n leaks, focus rings, tap-target floor, admin chip-scroll, empty-state CTA vocabulary
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.
This commit is contained in:
1 parent
6885703505
commit
1768b1d971
22 files changed
+235
-39
No files matched your search
@@ -156,6 +156,26 @@ export function AdminShell({ active, children }: AdminShellProps) {
|
||||
const { t } = useTranslations();
|
||||
const activeSlug = deriveActiveSlug(pathname, active);
|
||||
|
||||
// v1.4.34 IW-G — mirror the settings-shell auto-scroll pattern so
|
||||
// the active chip stays in view when the user lands on a deeper
|
||||
// admin route. Without it the 16-chip strip stays scrolled to the
|
||||
// left and the active chip lives off-screen, reading as a broken nav.
|
||||
const mobileStripRef = React.useRef<HTMLElement | null>(null);
|
||||
React.useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const strip = mobileStripRef.current;
|
||||
if (!strip) return;
|
||||
const activeChip = strip.querySelector<HTMLElement>(
|
||||
'[aria-current="page"]',
|
||||
);
|
||||
if (!activeChip) return;
|
||||
activeChip.scrollIntoView({
|
||||
block: "nearest",
|
||||
inline: "center",
|
||||
behavior: "smooth",
|
||||
});
|
||||
}, [activeSlug]);
|
||||
|
||||
// v1.4.25 W8 — AuthShell wraps the page in `px-4 py-6 md:px-6`
|
||||
// already, so this inner shell only carries the wider max-width.
|
||||
// Previously the duplicate `px-4 py-6 md:px-6 md:py-8` here was
|
||||
@@ -169,13 +189,19 @@ export function AdminShell({ active, children }: AdminShellProps) {
|
||||
horizontal swipe + keyboard arrow scrolling still work, the
|
||||
bar just doesn't draw. Without this the 13-section strip
|
||||
would render an always-on scrollbar that the maintainer kept
|
||||
mis-attributing to the api-tokens table below it. */}
|
||||
mis-attributing to the api-tokens table below it.
|
||||
|
||||
v1.4.34 IW-G — `snap-x snap-mandatory` plus per-chip
|
||||
`snap-start` lets a swipe-flick land on the next chip's
|
||||
leading edge instead of the in-between dead zone, matching
|
||||
the settings-shell polish. */}
|
||||
<nav
|
||||
ref={mobileStripRef}
|
||||
aria-label={t("admin.shell.sectionsNav")}
|
||||
className="no-scrollbar -mx-4 mb-4 overflow-x-auto px-4 md:hidden"
|
||||
className="no-scrollbar -mx-4 mb-4 snap-x snap-mandatory overflow-x-auto px-4 md:hidden"
|
||||
>
|
||||
<ul className="flex min-w-max gap-2">
|
||||
<li>
|
||||
<li className="snap-start">
|
||||
<Link
|
||||
href="/admin"
|
||||
aria-current={activeSlug === null ? "page" : undefined}
|
||||
@@ -197,7 +223,7 @@ export function AdminShell({ active, children }: AdminShellProps) {
|
||||
const isActive = section.slug === activeSlug;
|
||||
const Icon = section.icon;
|
||||
return (
|
||||
<li key={section.slug}>
|
||||
<li key={section.slug} className="snap-start">
|
||||
<Link
|
||||
href={`/admin/${section.slug}`}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
|
||||
Reference in new issue
Block a user