fix: give the phone calendar toolbar, mini calendar and calendar list 44px touch targets

This commit is contained in:
Linus Rath
2026-10-28 04:30:48 +01:00
parent c6b7b8edae
commit 72d033bcb3
3 changed files with 70 additions and 75 deletions
+16 -19
View File
@@ -2,6 +2,7 @@
import { useMemo, useRef, useState } from "react";
import { useLongPress } from "@/hooks/use-long-press";
import { IconButton } from "@/components/ui/icon-button";
import { useTranslations } from "next-intl";
import { Check, ChevronDown, ChevronRight, Globe, ListTodo, MoreVertical, Pencil, RefreshCw, Share2, Star, Trash2, Cake, User, Users, Plus, Eraser, Palette, Shuffle } from "@/components/icons";
import { cn, formatDateTime } from "@/lib/utils";
@@ -297,11 +298,11 @@ export function CalendarSidebarPanel({
data-account={cal.accountName ?? ''}
data-visible={isVisible}
className={cn(
"flex items-center gap-2 w-full ps-5 pe-3 text-sm text-foreground/80 transition-colors duration-150",
"flex items-center gap-2 w-full ps-5 pe-3 min-h-8 pointer-coarse:min-h-11 text-sm text-foreground/80 transition-colors duration-150",
"hover:bg-muted",
hasMenu && "pe-9"
hasMenu && "pe-11"
)}
style={{ paddingBlock: 'var(--density-sidebar-py, 4px)', minHeight: '32px' }}
style={{ paddingBlock: 'var(--density-sidebar-py, 4px)' }}
>
{/* A checkbox filled in the calendar colour, empty when hidden. */}
<span
@@ -339,21 +340,19 @@ export function CalendarSidebarPanel({
})()}
</button>
{hasMenu && (
<button
type="button"
<IconButton
onClick={(e) => openContextMenuFrom(e.currentTarget, cal, "end")}
aria-haspopup="menu"
className={cn(
"absolute end-2 top-1/2 -translate-y-1/2 p-0.5 rounded text-muted-foreground hover:text-foreground hover:bg-accent transition-opacity",
"absolute end-1 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-opacity",
// Hover shows it; keyboard focus and touch screens always do (R7).
"opacity-0 group-hover/cal:opacity-100 group-focus-within/cal:opacity-100 focus-visible:opacity-100 pointer-coarse:opacity-100",
contextMenu.isOpen && contextMenu.data?.id === cal.id && "opacity-100"
)}
aria-label={t('calendar_options', { name: cal.name })}
title={t('calendar_options', { name: cal.name })}
label={t('calendar_options', { name: cal.name })}
>
<MoreVertical className="w-3.5 h-3.5" />
</button>
<MoreVertical />
</IconButton>
)}
</div>
);
@@ -487,15 +486,13 @@ export function CalendarSidebarPanel({
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider truncate">{label}</span>
</button>
{onAdd && (
<button
type="button"
<IconButton
onClick={onAdd}
className="p-0.5 rounded text-muted-foreground opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 pointer-coarse:opacity-100 hover:text-foreground hover:bg-muted transition-opacity"
title={tMgmt('add_calendar')}
aria-label={tMgmt('add_calendar')}
className="text-muted-foreground opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 pointer-coarse:opacity-100 hover:text-foreground transition-opacity"
label={tMgmt('add_calendar')}
>
<Plus className="w-3.5 h-3.5" />
</button>
<Plus />
</IconButton>
)}
</div>
);
@@ -517,8 +514,8 @@ export function CalendarSidebarPanel({
{enableCalendarTasks && (
<button
onClick={() => setViewMode('tasks')}
className="flex items-center gap-2 w-full px-3 mb-2 text-sm text-foreground/80 hover:bg-muted transition-colors"
style={{ paddingBlock: 'var(--density-sidebar-py, 4px)', minHeight: '32px' }}
className="flex items-center gap-2 w-full px-3 mb-2 min-h-8 pointer-coarse:min-h-11 text-sm text-foreground/80 hover:bg-muted transition-colors"
style={{ paddingBlock: 'var(--density-sidebar-py, 4px)' }}
>
<ListTodo className="w-4 h-4 text-muted-foreground" />
<span>{t('tasks.label')}</span>
+26 -33
View File
@@ -3,6 +3,7 @@
import { useState, useRef, useEffect } from "react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { IconButton } from "@/components/ui/icon-button";
import { ChevronLeft, ChevronRight, Plus, Upload, CalendarDays, Globe, ChevronDown, ArrowLeft, Menu, MoreVertical, RefreshCw } from "@/components/icons";
import { ContextMenu, ContextMenuItem, ContextMenuSeparator } from "@/components/ui/context-menu";
import { useContextMenu } from "@/hooks/use-context-menu";
@@ -162,7 +163,7 @@ export function CalendarToolbar({
<>
<Button
variant="ghost"
size="icon-sm"
size={isMobile ? "icon-lg" : "icon-sm"}
className={cn(moreMenu.isOpen && "bg-accent text-accent-foreground")}
onClick={(e) => openMoreMenuFrom(e.currentTarget, true, "end")}
aria-label={t("toolbar.more")}
@@ -212,37 +213,29 @@ export function CalendarToolbar({
{/* Row 1: Back / Date nav / Today */}
<div className="flex items-center gap-1">
{onMenuClick && (
<button
onClick={onMenuClick}
className="p-1.5 -ms-1 rounded-md hover:bg-muted transition-colors touch-manipulation"
aria-label={t("nav_open_menu")}
>
<Menu className="w-4 h-4" />
</button>
<IconButton size="icon-lg" label={t("nav_open_menu")} onClick={onMenuClick} className="-ms-1 touch-manipulation">
<Menu />
</IconButton>
)}
{onNavigateBack && (
<button
onClick={onNavigateBack}
className="p-1.5 -ms-1 rounded-md hover:bg-muted transition-colors touch-manipulation"
aria-label={t("back_to_month")}
>
<ArrowLeft className="w-4 h-4" />
</button>
<IconButton size="icon-lg" label={t("back_to_month")} onClick={onNavigateBack} className="-ms-1 touch-manipulation">
<ArrowLeft />
</IconButton>
)}
{hasDateNav && (
<button onClick={onPrev} className="p-1.5 rounded-md hover:bg-muted transition-colors touch-manipulation" aria-label={t("nav_prev")}>
<ChevronLeft className="w-4 h-4" />
</button>
<IconButton size="icon-lg" label={t("nav_prev")} onClick={onPrev} className="touch-manipulation">
<ChevronLeft />
</IconButton>
)}
<span className="text-sm font-semibold text-center flex-1 select-none truncate">
{getDateLabel()}
</span>
{hasDateNav && (
<>
<button onClick={onNext} className="p-1.5 rounded-md hover:bg-muted transition-colors touch-manipulation" aria-label={t("nav_next")}>
<ChevronRight className="w-4 h-4" />
</button>
<Button variant="ghost" size="sm" onClick={onToday} className="touch-manipulation text-xs h-7 px-2 ms-0.5">
<IconButton size="icon-lg" label={t("nav_next")} onClick={onNext} className="touch-manipulation">
<ChevronRight />
</IconButton>
<Button variant="ghost" size="xs" onClick={onToday} className="touch-manipulation ms-0.5">
{t("views.today")}
</Button>
</>
@@ -258,7 +251,7 @@ export function CalendarToolbar({
key={v}
onClick={() => onViewModeChange(v)}
className={cn(
"flex-1 py-1.5 text-[11px] font-medium transition-colors touch-manipulation",
"flex-1 min-h-11 text-[11px] font-medium transition-colors touch-manipulation",
v === viewMode
? "bg-primary text-primary-foreground"
: "text-muted-foreground active:bg-muted"
@@ -271,16 +264,16 @@ export function CalendarToolbar({
{calendars && selectedCalendarIds && onToggleVisibility && (
<div className="relative" ref={dropdownRef}>
<button
<IconButton
size="icon-lg"
variant="outline"
label={t("my_calendars")}
aria-expanded={showCalendarDropdown}
onClick={() => setShowCalendarDropdown((v) => !v)}
className={cn(
"p-1.5 rounded-md border border-border transition-colors touch-manipulation",
showCalendarDropdown ? "bg-muted" : "hover:bg-muted"
)}
aria-label={t("my_calendars")}
className={cn("touch-manipulation", showCalendarDropdown && "bg-muted")}
>
<CalendarDays className="w-4 h-4" />
</button>
<CalendarDays />
</IconButton>
{showCalendarDropdown && (
<div className="absolute top-full end-0 mt-1 z-50 bg-popover border border-border rounded-lg shadow-lg p-2 min-w-[180px]">
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2 px-1">
@@ -295,7 +288,7 @@ export function CalendarToolbar({
key={cal.id}
onClick={() => onToggleVisibility(cal.id)}
className={cn(
"flex items-center gap-2 w-full px-2 py-2 rounded-md text-sm transition-colors duration-150 touch-manipulation",
"flex items-center gap-2 w-full px-2 min-h-11 rounded-md text-sm transition-colors duration-150 touch-manipulation",
"hover:bg-muted"
)}
>
@@ -335,7 +328,7 @@ export function CalendarToolbar({
key={cal.id}
onClick={() => onToggleVisibility(cal.id)}
className={cn(
"flex items-center gap-2 w-full px-2 py-2 rounded-md text-sm transition-colors duration-150 touch-manipulation",
"flex items-center gap-2 w-full px-2 min-h-11 rounded-md text-sm transition-colors duration-150 touch-manipulation",
"hover:bg-muted"
)}
>
+28 -23
View File
@@ -13,6 +13,7 @@ import { displayNow } from "@/lib/timezone";
import type { CalendarEvent } from "@/lib/jmap/types";
import { useCalendarLocale } from "@/hooks/use-calendar-locale";
import { PluginDayLabel } from "./plugin-day-label";
import { IconButton } from "@/components/ui/icon-button";
type PickerView = "days" | "months" | "years";
@@ -124,13 +125,13 @@ export function MiniCalendar({
return (
<div className="select-none">
<div className="flex items-center justify-between mb-2">
<button
<IconButton
onClick={handlePrev}
className="p-1 rounded hover:bg-muted transition-colors"
aria-label={t(pickerView === "days" ? "mini_prev_month" : pickerView === "months" ? "mini_prev_year" : "mini_prev_years")}
className="text-muted-foreground"
label={t(pickerView === "days" ? "mini_prev_month" : pickerView === "months" ? "mini_prev_year" : "mini_prev_years")}
>
<ChevronLeft className="w-4 h-4 text-muted-foreground" />
</button>
<ChevronLeft />
</IconButton>
<button
onClick={handleHeaderClick}
disabled={pickerView === "years"}
@@ -146,13 +147,13 @@ export function MiniCalendar({
<ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />
)}
</button>
<button
<IconButton
onClick={handleNext}
className="p-1 rounded hover:bg-muted transition-colors"
aria-label={t(pickerView === "days" ? "mini_next_month" : pickerView === "months" ? "mini_next_year" : "mini_next_years")}
className="text-muted-foreground"
label={t(pickerView === "days" ? "mini_next_month" : pickerView === "months" ? "mini_next_year" : "mini_next_years")}
>
<ChevronRight className="w-4 h-4 text-muted-foreground" />
</button>
<ChevronRight />
</IconButton>
</div>
{pickerView === "days" && (
@@ -185,22 +186,26 @@ export function MiniCalendar({
<button
key={`day-${day.toISOString()}`}
onClick={() => onSelectDate(day)}
className={cn(
"relative flex items-center justify-center w-7 h-7 text-xs rounded-full transition-colors",
!inMonth && "text-muted-foreground/40",
// Today is the filled circle; the selected day is tinted.
today && "bg-primary text-primary-foreground font-semibold",
selected && !today && "bg-primary/15 text-primary font-semibold",
!today && !selected && "hover:bg-muted"
)}
className="group/day flex items-center justify-center w-7 h-7 pointer-coarse:w-full pointer-coarse:h-11 text-xs"
aria-current={today ? "date" : undefined}
aria-pressed={selected}
>
{formatDayNumber(day)}
{hasEvent && !selected && !today && (
<span className="absolute bottom-0.5 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-primary" />
)}
<PluginDayLabel surface="mini-calendar-marks" date={format(day, "yyyy-MM-dd")} />
<span
className={cn(
"relative flex items-center justify-center w-7 h-7 rounded-full transition-colors",
!inMonth && "text-muted-foreground/40",
// Today is the filled circle; the selected day is tinted.
today && "bg-primary text-primary-foreground font-semibold",
selected && !today && "bg-primary/15 text-primary font-semibold",
!today && !selected && "group-hover/day:bg-muted"
)}
>
{formatDayNumber(day)}
{hasEvent && !selected && !today && (
<span className="absolute bottom-0.5 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-primary" />
)}
<PluginDayLabel surface="mini-calendar-marks" date={format(day, "yyyy-MM-dd")} />
</span>
</button>
</Fragment>
);