mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: give the phone calendar toolbar, mini calendar and calendar list 44px touch targets
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user