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:
parent
b05adf7f12
commit
f7ec04241b
@ -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();
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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); }}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user