Phase 4: mobile polish and touch fallbacks

4a. Touch fallbacks for group-hover actions:
  - 9 occurrences across 5 files changed from opacity-0 group-hover:opacity-100
    to opacity-100 md:opacity-0 md:group-hover:opacity-100
  - CalendarSidebar (3), SharedCalendarSection (2), TaskDetailPanel (2),
    NotificationsPage (1), CopyableField (1)
  - Action buttons now always visible on touch, hover-revealed on desktop

4b. FullCalendar mobile touch:
  - Wheel navigation disabled on touch devices (ontouchstart check)
  - Prevents scroll hijacking on mobile, allows native scroll

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-07 17:04:44 +08:00
parent b05adf7f12
commit f7ec04241b
6 changed files with 11 additions and 9 deletions

View File

@ -189,6 +189,8 @@ export default function CalendarPage() {
if (!el) return; if (!el) return;
let debounceTimer: ReturnType<typeof setTimeout> | null = null; let debounceTimer: ReturnType<typeof setTimeout> | null = null;
const handleWheel = (e: WheelEvent) => { const handleWheel = (e: WheelEvent) => {
// Skip wheel navigation on touch devices (let them scroll normally)
if ('ontouchstart' in window) return;
const api = calendarRef.current?.getApi(); const api = calendarRef.current?.getApi();
if (!api || api.view.type !== 'dayGridMonth') return; if (!api || api.view.type !== 'dayGridMonth') return;
e.preventDefault(); e.preventDefault();

View File

@ -131,7 +131,7 @@ const CalendarSidebar = forwardRef<HTMLDivElement, CalendarSidebarProps>(functio
<span className="text-sm text-foreground truncate flex-1">{cal.name}</span> <span className="text-sm text-foreground truncate flex-1">{cal.name}</span>
<button <button
onClick={() => handleEdit(cal)} onClick={() => handleEdit(cal)}
className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-foreground" className="opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-foreground"
> >
<Pencil className="h-3.5 w-3.5" /> <Pencil className="h-3.5 w-3.5" />
</button> </button>
@ -184,7 +184,7 @@ const CalendarSidebar = forwardRef<HTMLDivElement, CalendarSidebarProps>(functio
setEditingTemplate(tmpl); setEditingTemplate(tmpl);
setShowTemplateForm(true); setShowTemplateForm(true);
}} }}
className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-foreground" className="opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-foreground"
> >
<Pencil className="h-3 w-3" /> <Pencil className="h-3 w-3" />
</button> </button>
@ -194,7 +194,7 @@ const CalendarSidebar = forwardRef<HTMLDivElement, CalendarSidebarProps>(functio
if (!window.confirm(`Delete template "${tmpl.name}"?`)) return; if (!window.confirm(`Delete template "${tmpl.name}"?`)) return;
deleteTemplateMutation.mutate(tmpl.id); deleteTemplateMutation.mutate(tmpl.id);
}} }}
className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-destructive" className="opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-destructive"
> >
<Trash2 className="h-3 w-3" /> <Trash2 className="h-3 w-3" />
</button> </button>

View File

@ -73,7 +73,7 @@ export default function SharedCalendarSection({
<button <button
type="button" type="button"
onClick={() => onEditCalendar?.(cal)} onClick={() => onEditCalendar?.(cal)}
className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-foreground" className="opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-foreground"
> >
<Pencil className="h-3.5 w-3.5" /> <Pencil className="h-3.5 w-3.5" />
</button> </button>
@ -104,7 +104,7 @@ export default function SharedCalendarSection({
<span className="text-sm text-foreground truncate flex-1">{m.calendar_name}</span> <span className="text-sm text-foreground truncate flex-1">{m.calendar_name}</span>
<button <button
onClick={() => setSettingsFor(m)} onClick={() => setSettingsFor(m)}
className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-foreground" className="opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-foreground"
> >
<Pencil className="h-3.5 w-3.5" /> <Pencil className="h-3.5 w-3.5" />
</button> </button>

View File

@ -316,7 +316,7 @@ export default function NotificationsPage() {
<span className="text-[11px] text-muted-foreground tabular-nums"> <span className="text-[11px] text-muted-foreground tabular-nums">
{formatDistanceToNow(new Date(notification.created_at), { addSuffix: true })} {formatDistanceToNow(new Date(notification.created_at), { addSuffix: true })}
</span> </span>
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="flex items-center gap-0.5 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity">
{!notification.is_read && ( {!notification.is_read && (
<button <button
onClick={(e) => { e.stopPropagation(); handleMarkRead(notification.id); }} onClick={(e) => { e.stopPropagation(); handleMarkRead(notification.id); }}

View File

@ -484,7 +484,7 @@ export default function TaskDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-5 w-5 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-destructive shrink-0" className="h-5 w-5 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-destructive shrink-0"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
handleDeleteSubtask(subtask.id, subtask.title); handleDeleteSubtask(subtask.id, subtask.title);
@ -527,7 +527,7 @@ export default function TaskDetailPanel({
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-5 w-5 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-destructive" className="h-5 w-5 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-destructive"
onClick={() => { onClick={() => {
if (!window.confirm('Delete this comment?')) return; if (!window.confirm('Delete this comment?')) return;
deleteCommentMutation.mutate(comment.id); deleteCommentMutation.mutate(comment.id);

View File

@ -27,7 +27,7 @@ export default function CopyableField({ value, icon: Icon, label }: CopyableFiel
type="button" type="button"
onClick={handleCopy} onClick={handleCopy}
aria-label={`Copy ${label || value}`} aria-label={`Copy ${label || value}`}
className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 p-0.5 rounded text-muted-foreground hover:text-foreground shrink-0" className="opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-150 p-0.5 rounded text-muted-foreground hover:text-foreground shrink-0"
> >
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />} {copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
</button> </button>