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;
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
const handleWheel = (e: WheelEvent) => {
// Skip wheel navigation on touch devices (let them scroll normally)
if ('ontouchstart' in window) return;
const api = calendarRef.current?.getApi();
if (!api || api.view.type !== 'dayGridMonth') return;
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>
<button
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" />
</button>
@ -184,7 +184,7 @@ const CalendarSidebar = forwardRef<HTMLDivElement, CalendarSidebarProps>(functio
setEditingTemplate(tmpl);
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" />
</button>
@ -194,7 +194,7 @@ const CalendarSidebar = forwardRef<HTMLDivElement, CalendarSidebarProps>(functio
if (!window.confirm(`Delete template "${tmpl.name}"?`)) return;
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" />
</button>

View File

@ -73,7 +73,7 @@ export default function SharedCalendarSection({
<button
type="button"
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" />
</button>
@ -104,7 +104,7 @@ export default function SharedCalendarSection({
<span className="text-sm text-foreground truncate flex-1">{m.calendar_name}</span>
<button
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" />
</button>

View File

@ -316,7 +316,7 @@ export default function NotificationsPage() {
<span className="text-[11px] text-muted-foreground tabular-nums">
{formatDistanceToNow(new Date(notification.created_at), { addSuffix: true })}
</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 && (
<button
onClick={(e) => { e.stopPropagation(); handleMarkRead(notification.id); }}

View File

@ -484,7 +484,7 @@ export default function TaskDetailPanel({
<Button
variant="ghost"
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) => {
e.stopPropagation();
handleDeleteSubtask(subtask.id, subtask.title);
@ -527,7 +527,7 @@ export default function TaskDetailPanel({
<Button
variant="ghost"
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={() => {
if (!window.confirm('Delete this comment?')) return;
deleteCommentMutation.mutate(comment.id);

View File

@ -27,7 +27,7 @@ export default function CopyableField({ value, icon: Icon, label }: CopyableFiel
type="button"
onClick={handleCopy}
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" />}
</button>