fix(mobile): close safe-area gaps and lift sub-44px tap targets

Subtract both the top and bottom safe-area insets from the full-page
Coach height so the composer no longer slides behind the bottom-nav on a
notched phone, and pad the Coach drawer composer and the ResponsiveSheet
bottom-sheet footer for the home-indicator band.

Raise the cycle and illness day-log severity controls to a 44px touch
floor, add hit-slop to the custom-symptom remove button, give the shared
day-log chips a min height, and let the mood-tag action cluster wrap so a
360px row no longer clips the tag name.
This commit is contained in:
Marc-André Bombeck committed 2026-06-28 02:53:06 +02:00
1 parent a74a0c9b09
commit b02e3e4072
6 files changed
+114 -110

No files matched your search

+1 -1
View File
@@ -136,7 +136,7 @@ export default function CoachPage() {
// `env(safe-area-inset-bottom)`) so the docked composer / Stop control
// always clears the nav instead of sitting under it. On `md+` the nav
// is hidden, so the page reclaims the full height below the TopBar.
className="bg-background -mx-4 -mt-6 -mb-20 flex h-[calc(100dvh-8rem-env(safe-area-inset-bottom,0px))] min-h-[32rem] flex-col overflow-hidden md:-mx-6 md:h-[calc(100dvh-4rem)]"
className="bg-background -mx-4 -mt-6 -mb-20 flex h-[calc(100dvh-8rem-env(safe-area-inset-top,0px)-env(safe-area-inset-bottom,0px))] min-h-[32rem] flex-col overflow-hidden md:-mx-6 md:h-[calc(100dvh-4rem)]"
>
{/* `useSearchParams` requires a Suspense boundary so the client-search
bailout never opts the whole route out of static optimisation. */}
+3 -3
View File
@@ -423,7 +423,7 @@ function Chip({
aria-pressed={active}
onClick={onClick}
className={cn(
"focus-visible:ring-ring/50 rounded-full border px-3 py-1.5 text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none",
"focus-visible:ring-ring/50 inline-flex min-h-11 items-center rounded-full border px-3 py-1.5 text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none sm:min-h-8",
active
? "border-primary bg-primary/10 text-primary"
: "border-border text-foreground hover:bg-accent",
@@ -1225,7 +1225,7 @@ function SymptomChip({
onClick={handleRemove}
disabled={deleteCustom.isPending}
aria-label={t("cycle.symptom.custom.remove", { label })}
className="border-border bg-background text-muted-foreground hover:text-destructive hover:border-destructive focus-visible:ring-ring/50 -ml-1.5 grid size-4 shrink-0 place-items-center rounded-full border transition-colors focus-visible:ring-2 focus-visible:outline-none"
className="border-border bg-background text-muted-foreground hover:text-destructive hover:border-destructive focus-visible:ring-ring/50 relative -ml-1.5 grid size-4 shrink-0 place-items-center rounded-full border transition-colors before:absolute before:-inset-2 before:content-[''] focus-visible:ring-2 focus-visible:outline-none"
>
<X className="size-2.5" aria-hidden="true" />
</button>
@@ -1245,7 +1245,7 @@ function SymptomChip({
aria-label={t("cycle.sheet.severityLevel", { level: lvl })}
onClick={() => onSeverity(symptomKey, lvl)}
className={cn(
"focus-visible:ring-ring/50 size-6 rounded-full border text-xs tabular-nums transition-colors focus-visible:ring-2 focus-visible:outline-none",
"focus-visible:ring-ring/50 grid size-11 place-items-center rounded-full border text-xs tabular-nums transition-colors focus-visible:ring-2 focus-visible:outline-none sm:size-8",
severity === lvl
? "border-primary bg-primary/15 text-primary font-semibold"
: "border-border text-muted-foreground hover:bg-accent",
+3 -3
View File
@@ -60,7 +60,7 @@ function Chip({
aria-pressed={active}
onClick={onClick}
className={cn(
"rounded-full border px-3 py-1.5 text-sm transition-colors",
"inline-flex min-h-11 items-center rounded-full border px-3 py-1.5 text-sm transition-colors sm:min-h-8",
active
? "border-primary bg-primary/10 text-foreground"
: "border-border text-muted-foreground hover:bg-muted",
@@ -95,7 +95,7 @@ function SymptomChip({
aria-pressed={active}
onClick={onToggle}
className={cn(
"flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm transition-colors",
"flex min-h-11 items-center gap-2 rounded-full border px-3 py-1.5 text-sm transition-colors sm:min-h-8",
active
? "border-primary bg-primary/10 text-foreground"
: "border-border text-muted-foreground hover:bg-muted",
@@ -117,7 +117,7 @@ function SymptomChip({
aria-pressed={severity === value}
onClick={() => onSeverity(value)}
className={cn(
"h-6 w-6 rounded-md border text-xs",
"grid h-11 w-11 place-items-center rounded-md border text-xs sm:h-8 sm:w-8",
severity === value
? "border-primary bg-primary/10 text-foreground"
: "border-border text-muted-foreground hover:bg-muted",
@@ -234,7 +234,7 @@ export function CoachDrawerBody({
{/* Composer pinned to the bottom. */}
<div
data-slot="coach-drawer-composer"
className="border-border/70 flex flex-col gap-2 border-t p-3 sm:p-4"
className="border-border/70 flex flex-col gap-2 border-t p-3 pb-[calc(env(safe-area-inset-bottom,0px)+0.75rem)] sm:p-4 sm:pb-[calc(env(safe-area-inset-bottom,0px)+1rem)]"
>
{composer}
</div>
+105 -101
View File
@@ -502,7 +502,7 @@ function SortableTagRow({
data-tag={tag.key}
data-hidden={isHidden ? "true" : undefined}
data-dragging={isDragging ? "true" : undefined}
className={`border-border bg-background/30 flex min-h-12 items-center gap-2 rounded-md border px-3 py-2 ${
className={`border-border bg-background/30 flex min-h-12 flex-wrap items-center gap-2 rounded-md border px-3 py-2 ${
isDragging ? "ring-primary z-10 opacity-90 shadow-lg ring-2" : ""
} ${isHidden ? "opacity-60" : ""}`}
>
@@ -523,7 +523,7 @@ function SortableTagRow({
className="text-muted-foreground h-4 w-4 shrink-0"
aria-hidden="true"
/>
<span className="min-w-0 flex-1 truncate text-sm" title={name}>
<span className="min-w-[8rem] flex-1 truncate text-sm" title={name}>
{name}
</span>
{tag.kind === "RATED" && (
@@ -536,106 +536,110 @@ function SortableTagRow({
{t("mood.manage.usageCount", { count: String(tag.usageCount) })}
</Badge>
)}
<Button
type="button"
variant="ghost"
size="icon"
className="size-11 sm:size-9"
onClick={() => onToggleVisibility(tag)}
disabled={disabled}
aria-pressed={tag.custom ? undefined : isHidden}
aria-label={`${
tag.custom
? t("mood.manage.archive")
: isHidden
? t("mood.manage.showTag")
: t("mood.manage.hideTag")
} — ${name}`}
title={
tag.custom
? t("mood.manage.archive")
: isHidden
? t("mood.manage.showTag")
: t("mood.manage.hideTag")
}
>
{tag.custom ? (
<Archive className="h-4 w-4" aria-hidden="true" />
) : isHidden ? (
<EyeOff className="h-4 w-4" aria-hidden="true" />
) : (
<Eye className="h-4 w-4" aria-hidden="true" />
)}
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="size-11 sm:size-9"
onClick={() => onMove(groupKey, tag.key, -1)}
disabled={index === 0 || disabled}
aria-label={`${t("mood.manage.moveUp")} — ${name}`}
>
<ArrowUp className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="size-11 sm:size-9"
onClick={() => onMove(groupKey, tag.key, 1)}
disabled={index === total - 1 || disabled}
aria-label={`${t("mood.manage.moveDown")} — ${name}`}
>
<ArrowDown className="h-4 w-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="size-11 sm:size-9"
aria-label={`${t("common.moreOptions")} — ${name}`}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{tag.custom && (
<DropdownMenuItem onClick={() => onEdit(tag)}>
<Pencil className="mr-2 h-4 w-4" />
{t("common.edit")}
</DropdownMenuItem>
{/* The action cluster wraps to its own line on a narrow row so the
tag name keeps a readable floor instead of truncating away. */}
<div className="ml-auto flex shrink-0 items-center gap-1">
<Button
type="button"
variant="ghost"
size="icon"
className="size-11 sm:size-9"
onClick={() => onToggleVisibility(tag)}
disabled={disabled}
aria-pressed={tag.custom ? undefined : isHidden}
aria-label={`${
tag.custom
? t("mood.manage.archive")
: isHidden
? t("mood.manage.showTag")
: t("mood.manage.hideTag")
} — ${name}`}
title={
tag.custom
? t("mood.manage.archive")
: isHidden
? t("mood.manage.showTag")
: t("mood.manage.hideTag")
}
>
{tag.custom ? (
<Archive className="h-4 w-4" aria-hidden="true" />
) : isHidden ? (
<EyeOff className="h-4 w-4" aria-hidden="true" />
) : (
<Eye className="h-4 w-4" aria-hidden="true" />
)}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<FolderInput className="mr-2 h-4 w-4" />
{t("mood.manage.moveToGroup")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
{groupOptions
.filter((group) => group.key !== groupKey)
.map((group) => (
<DropdownMenuItem
key={group.key}
onClick={() => onMoveToGroup(tag, group.key)}
>
{group.name}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
{tag.custom && (
<DropdownMenuItem onClick={() => onToggleVisibility(tag)}>
<Archive className="mr-2 h-4 w-4" />
{t("mood.manage.archive")}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="size-11 sm:size-9"
onClick={() => onMove(groupKey, tag.key, -1)}
disabled={index === 0 || disabled}
aria-label={`${t("mood.manage.moveUp")} — ${name}`}
>
<ArrowUp className="h-4 w-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="size-11 sm:size-9"
onClick={() => onMove(groupKey, tag.key, 1)}
disabled={index === total - 1 || disabled}
aria-label={`${t("mood.manage.moveDown")} — ${name}`}
>
<ArrowDown className="h-4 w-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
className="size-11 sm:size-9"
aria-label={`${t("common.moreOptions")} — ${name}`}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{tag.custom && (
<DropdownMenuItem onClick={() => onEdit(tag)}>
<Pencil className="mr-2 h-4 w-4" />
{t("common.edit")}
</DropdownMenuItem>
)}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<FolderInput className="mr-2 h-4 w-4" />
{t("mood.manage.moveToGroup")}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
{groupOptions
.filter((group) => group.key !== groupKey)
.map((group) => (
<DropdownMenuItem
key={group.key}
onClick={() => onMoveToGroup(tag, group.key)}
>
{group.name}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
{tag.custom && (
<DropdownMenuItem onClick={() => onToggleVisibility(tag)}>
<Archive className="mr-2 h-4 w-4" />
{t("mood.manage.archive")}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
}
+1 -1
View File
@@ -180,7 +180,7 @@ export function ResponsiveSheet({
{footer ? (
<SheetFooter
data-slot="responsive-sheet-footer"
className="border-border/70 bg-background/95 supports-[backdrop-filter]:bg-background/80 sticky bottom-0 mt-0 flex-row justify-end gap-2 border-t p-4 backdrop-blur"
className="border-border/70 bg-background/95 supports-[backdrop-filter]:bg-background/80 sticky bottom-0 mt-0 flex-row justify-end gap-2 border-t p-4 pb-[calc(env(safe-area-inset-bottom,0px)+1rem)] backdrop-blur"
>
{footer}
</SheetFooter>