mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
fix(data-list): lift the selection bar above the mobile nav and fix tap targets
The page-scoped selection bar sat sticky bottom-2, behind the fixed 64px mobile bottom-nav, so the destructive Delete (N) button was unreachable on phones and ignored the iOS home-indicator inset. Lift it to clear the nav band + safe area on mobile and drop back to bottom-4 at sm+ where the nav is hidden. The per-row select checkbox wrapped a Radix checkbox (a 16px button) in a 44px label; a label does not forward taps to a button, so the effective target failed WCAG 2.5.5. Make the 44px area a single button that owns the toggle, with the checkbox a pointer-events-none controlled visual, avoiding any double-toggle. Bring the mood pagination buttons to the 44px target used by the measurements list. Add (sort: Desc) to the two source-filter indexes so the schema matches the DESC migration SQL; schema-fidelity only, no new migration.
This commit is contained in:
1 parent
f2bd051fa7
commit
4038a34ae6
10 files changed
+71
-14
No files matched your search
@@ -546,6 +546,8 @@
|
||||
"deleteConfirmTitle": "Eintrag löschen?",
|
||||
"deleteConfirmDescription": "Dieser Stimmungseintrag wird unwiderruflich gelöscht.",
|
||||
"pageInfo": "Seite {page} von {total}",
|
||||
"previousPage": "Vorherige Seite",
|
||||
"nextPage": "Nächste Seite",
|
||||
"loginRequired": "Bitte anmelden, um Stimmungseinträge zu sehen.",
|
||||
"note": "Notiz",
|
||||
"notePlaceholder": "Notiz dazu, wie du dich fühlst…",
|
||||
|
||||
@@ -546,6 +546,8 @@
|
||||
"deleteConfirmTitle": "Delete entry?",
|
||||
"deleteConfirmDescription": "This mood entry will be permanently deleted.",
|
||||
"pageInfo": "Page {page} of {total}",
|
||||
"previousPage": "Previous page",
|
||||
"nextPage": "Next page",
|
||||
"loginRequired": "Please sign in to view mood entries.",
|
||||
"note": "Note",
|
||||
"notePlaceholder": "Add a note about how you're feeling…",
|
||||
|
||||
@@ -546,6 +546,8 @@
|
||||
"deleteConfirmTitle": "¿Eliminar entrada?",
|
||||
"deleteConfirmDescription": "Esta entrada de estado de ánimo se eliminará de forma permanente.",
|
||||
"pageInfo": "Página {page} de {total}",
|
||||
"previousPage": "Página anterior",
|
||||
"nextPage": "Página siguiente",
|
||||
"loginRequired": "Inicia sesión para ver tus estados de ánimo.",
|
||||
"note": "Nota",
|
||||
"notePlaceholder": "Añade una nota sobre cómo te sientes…",
|
||||
|
||||
@@ -546,6 +546,8 @@
|
||||
"deleteConfirmTitle": "Supprimer l’entrée ?",
|
||||
"deleteConfirmDescription": "Cette entrée d’humeur sera supprimée définitivement.",
|
||||
"pageInfo": "Page {page} sur {total}",
|
||||
"previousPage": "Page précédente",
|
||||
"nextPage": "Page suivante",
|
||||
"loginRequired": "Connecte-toi pour voir tes entrées d’humeur.",
|
||||
"note": "Note",
|
||||
"notePlaceholder": "Ajoutez une note sur votre ressenti…",
|
||||
|
||||
@@ -546,6 +546,8 @@
|
||||
"deleteConfirmTitle": "Eliminare la voce?",
|
||||
"deleteConfirmDescription": "Questa voce dell’umore sarà eliminata in modo definitivo.",
|
||||
"pageInfo": "Pagina {page} di {total}",
|
||||
"previousPage": "Pagina precedente",
|
||||
"nextPage": "Pagina successiva",
|
||||
"loginRequired": "Accedi per vedere le voci dell’umore.",
|
||||
"note": "Nota",
|
||||
"notePlaceholder": "Aggiungi una nota su come ti senti…",
|
||||
|
||||
@@ -546,6 +546,8 @@
|
||||
"deleteConfirmTitle": "Usunąć wpis?",
|
||||
"deleteConfirmDescription": "Ten wpis nastroju zostanie nieodwracalnie usunięty.",
|
||||
"pageInfo": "Strona {page} z {total}",
|
||||
"previousPage": "Poprzednia strona",
|
||||
"nextPage": "Następna strona",
|
||||
"loginRequired": "Zaloguj się, aby zobaczyć wpisy nastroju.",
|
||||
"note": "Notatka",
|
||||
"notePlaceholder": "Dodaj notatkę o swoim samopoczuciu…",
|
||||
|
||||
@@ -853,7 +853,7 @@ model Measurement {
|
||||
// v1.15.13 — leads with `(userId, source)` so the management list's
|
||||
// source filter + recency sort is an index range scan instead of a
|
||||
// `userId` scan + in-memory sort. See migration 0136.
|
||||
@@index([userId, source, measuredAt])
|
||||
@@index([userId, source, measuredAt(sort: Desc)])
|
||||
@@index([externalId])
|
||||
// v1.7.0 — supports the `/api/sync/changes` keyset feed, which walks
|
||||
// `where userId` ordered by `(updatedAt asc, id asc)` with `take
|
||||
@@ -2089,7 +2089,7 @@ model MoodEntry {
|
||||
// v1.15.13 — leads with `(userId, source)` so the mood management
|
||||
// list's source filter + recency sort is an index range scan. See
|
||||
// migration 0136.
|
||||
@@index([userId, source, moodLoggedAt])
|
||||
@@index([userId, source, moodLoggedAt(sort: Desc)])
|
||||
// v1.7.0 sync — keyset support for the `/api/sync/changes` mood feed
|
||||
// (`where userId` ordered by `(updatedAt asc, id asc)`).
|
||||
@@index([userId, updatedAt, id])
|
||||
|
||||
@@ -53,7 +53,15 @@ export function SelectionActionBar({
|
||||
<div
|
||||
role="region"
|
||||
aria-label={t("dataList.selectionBarLabel")}
|
||||
className="bg-card border-border sticky bottom-2 z-10 mt-2 flex items-center justify-between gap-2 rounded-lg border p-2 shadow-lg sm:bottom-4"
|
||||
// v1.15.13 HIGH-1 — on mobile the fixed bottom-nav (64px, `md:hidden`,
|
||||
// z-50) plus the iOS home-indicator inset sit over the page's lower
|
||||
// edge. A `sticky bottom-2` bar lands BEHIND the nav, hiding the
|
||||
// destructive "Delete (N)" button. Lift the bar to clear the nav band
|
||||
// + safe area using the same offset the Coach FAB uses
|
||||
// (`layout-coach-fab.tsx`); at `sm:`/desktop the nav is gone, so drop
|
||||
// back to `bottom-4`. z-30 keeps the bar above page content without
|
||||
// needing to beat the (now-cleared) nav's z-50.
|
||||
className="bg-card border-border sticky bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)] z-30 mt-2 flex items-center justify-between gap-2 rounded-lg border p-2 shadow-lg sm:bottom-4"
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
|
||||
@@ -1056,13 +1056,30 @@ export function MeasurementList({
|
||||
{/* v1.15.13 — multi-select checkbox in a 44px tap
|
||||
target; absent for synthetic grouped rows. */}
|
||||
{!isGrouped && (
|
||||
<label className="flex size-11 shrink-0 items-center justify-center">
|
||||
// v1.15.13 MEDIUM-1 — a Radix Checkbox renders a
|
||||
// 16px `<button role=checkbox>`; a wrapping
|
||||
// `<label>` does NOT forward taps to a button (label
|
||||
// forwarding only works for native form controls),
|
||||
// so the effective tap target was 16px (fails WCAG
|
||||
// 2.5.5). The 44px button below owns the whole hit
|
||||
// area and is the single toggle source; the inner
|
||||
// Checkbox is a `pointer-events-none` controlled
|
||||
// visual, so a tap can fire the handler exactly once.
|
||||
<button
|
||||
type="button"
|
||||
role="checkbox"
|
||||
aria-checked={isSelected}
|
||||
aria-label={t("dataList.selectRow")}
|
||||
onClick={() => onToggleRow(m.id)}
|
||||
className="focus-visible:ring-ring/50 flex size-11 shrink-0 items-center justify-center rounded focus-visible:ring-2 focus-visible:outline-none"
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => onToggleRow(m.id)}
|
||||
aria-label={t("dataList.selectRow")}
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
</label>
|
||||
</button>
|
||||
)}
|
||||
{Icon && (
|
||||
<div
|
||||
|
||||
@@ -695,14 +695,28 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
className="bg-card border-border flex items-center justify-between rounded-lg border p-3 data-[state=selected]:border-dracula-purple/60 data-[state=selected]:bg-dracula-purple/5"
|
||||
>
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
{/* v1.15.13 — multi-select checkbox in a 44px target. */}
|
||||
<label className="flex size-11 shrink-0 items-center justify-center">
|
||||
{/* v1.15.13 MEDIUM-1 — a Radix Checkbox renders a 16px
|
||||
`<button role=checkbox>`; a wrapping `<label>` does
|
||||
NOT forward taps to a button, so the effective tap
|
||||
target was 16px (fails WCAG 2.5.5). The 44px button
|
||||
owns the whole hit area and is the single toggle
|
||||
source; the inner Checkbox is a `pointer-events-none`
|
||||
controlled visual, so a tap fires the handler once. */}
|
||||
<button
|
||||
type="button"
|
||||
role="checkbox"
|
||||
aria-checked={isSelected}
|
||||
aria-label={t("dataList.selectRow")}
|
||||
onClick={() => onToggleRow(entry.id)}
|
||||
className="focus-visible:ring-ring/50 flex size-11 shrink-0 items-center justify-center rounded focus-visible:ring-2 focus-visible:outline-none"
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onCheckedChange={() => onToggleRow(entry.id)}
|
||||
aria-label={t("dataList.selectRow")}
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
</label>
|
||||
</button>
|
||||
<div className="bg-muted flex h-8 w-8 shrink-0 items-center justify-center rounded-lg">
|
||||
<span
|
||||
data-testid="mood-row-score"
|
||||
@@ -785,19 +799,25 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
})}
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
{/* v1.15.13 L2 — match the measurements list's 44px pagination
|
||||
target (was `size="sm"`, below the WCAG 2.5.5 floor). */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
size="icon"
|
||||
className="size-11"
|
||||
disabled={page <= 1}
|
||||
onClick={() => goToPage((p) => p - 1)}
|
||||
aria-label={t("mood.previousPage")}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
size="icon"
|
||||
className="size-11"
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => goToPage((p) => p + 1)}
|
||||
aria-label={t("mood.nextPage")}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
Reference in new issue
Block a user