Fix mobile density issues from S24 Ultra testing

- Page titles: text-xl on mobile, text-2xl on desktop (7 pages)
- Stat cards: reduce padding/gap on mobile, hide icons below sm (3 pages)
- TodoItem: two-line layout on mobile (title row + metadata row)
- ReminderItem: same two-line treatment
- FullCalendar: smaller event font/padding on mobile via CSS media query

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-07 18:02:42 +08:00
parent 0b84352b09
commit ec8f5a9b4e
10 changed files with 244 additions and 195 deletions

View File

@ -23,7 +23,7 @@ export default function AdminPortal() {
<div className="p-1.5 rounded-md bg-red-500/10"> <div className="p-1.5 rounded-md bg-red-500/10">
<ShieldCheck className="h-5 w-5 text-red-400" /> <ShieldCheck className="h-5 w-5 text-red-400" />
</div> </div>
<h1 className="font-heading text-2xl font-bold tracking-tight">Admin Portal</h1> <h1 className="font-heading text-xl md:text-2xl font-bold tracking-tight">Admin Portal</h1>
</div> </div>
{/* Horizontal tab navigation */} {/* Horizontal tab navigation */}

View File

@ -286,7 +286,7 @@ export default function LocationsPage() {
<div className="flex flex-col h-full animate-fade-in"> <div className="flex flex-col h-full animate-fade-in">
{/* Header */} {/* Header */}
<div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0"> <div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
<h1 className="font-heading text-2xl font-bold tracking-tight">Locations</h1> <h1 className="font-heading text-xl md:text-2xl font-bold tracking-tight">Locations</h1>
<div className="w-full md:flex-1 md:w-auto min-w-0 order-last md:order-none"> <div className="w-full md:flex-1 md:w-auto min-w-0 order-last md:order-none">
<CategoryFilterBar <CategoryFilterBar

View File

@ -556,7 +556,7 @@ export default function PeoplePage() {
<div className="flex flex-col h-full animate-fade-in"> <div className="flex flex-col h-full animate-fade-in">
{/* Header */} {/* Header */}
<div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0"> <div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
<h1 className="font-heading text-2xl font-bold tracking-tight">People</h1> <h1 className="font-heading text-xl md:text-2xl font-bold tracking-tight">People</h1>
<div className="w-full md:flex-1 md:w-auto min-w-0 order-last md:order-none"> <div className="w-full md:flex-1 md:w-auto min-w-0 order-last md:order-none">
<CategoryFilterBar <CategoryFilterBar
activeFilters={activeFilters} activeFilters={activeFilters}

View File

@ -349,7 +349,7 @@ export default function ProjectDetail() {
<Button variant="ghost" size="icon" onClick={() => navigate('/projects')}> <Button variant="ghost" size="icon" onClick={() => navigate('/projects')}>
<ArrowLeft className="h-5 w-5" /> <ArrowLeft className="h-5 w-5" />
</Button> </Button>
<h1 className="font-heading text-2xl font-bold tracking-tight flex-1">Loading...</h1> <h1 className="font-heading text-xl md:text-2xl font-bold tracking-tight flex-1">Loading...</h1>
</div> </div>
<div className="flex-1 overflow-y-auto px-4 md:px-6 py-5"> <div className="flex-1 overflow-y-auto px-4 md:px-6 py-5">
<ListSkeleton rows={4} /> <ListSkeleton rows={4} />
@ -379,7 +379,7 @@ export default function ProjectDetail() {
<Button variant="ghost" size="icon" onClick={() => navigate('/projects')}> <Button variant="ghost" size="icon" onClick={() => navigate('/projects')}>
<ArrowLeft className="h-5 w-5" /> <ArrowLeft className="h-5 w-5" />
</Button> </Button>
<h1 className="font-heading text-2xl font-bold tracking-tight flex-1 truncate"> <h1 className="font-heading text-xl md:text-2xl font-bold tracking-tight flex-1 truncate">
{project.name} {project.name}
</h1> </h1>
<Badge className={statusColors[project.status]}> <Badge className={statusColors[project.status]}>

View File

@ -72,7 +72,7 @@ export default function ProjectsPage() {
<div className="flex flex-col h-full animate-fade-in"> <div className="flex flex-col h-full animate-fade-in">
{/* Header */} {/* Header */}
<div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0"> <div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
<h1 className="font-heading text-2xl font-bold tracking-tight">Projects</h1> <h1 className="font-heading text-xl md:text-2xl font-bold tracking-tight">Projects</h1>
<Select <Select
value={statusFilter} value={statusFilter}
@ -123,37 +123,37 @@ export default function ProjectsPage() {
<div className="flex-1 overflow-y-auto px-4 md:px-6 py-5"> <div className="flex-1 overflow-y-auto px-4 md:px-6 py-5">
{/* Summary stats */} {/* Summary stats */}
{!isLoading && projects.length > 0 && ( {!isLoading && projects.length > 0 && (
<div className="grid gap-2.5 grid-cols-3 mb-5"> <div className="grid gap-1.5 md:gap-2.5 grid-cols-3 mb-5">
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent"> <Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
<CardContent className="p-4 flex items-center gap-3"> <CardContent className="p-2.5 md:p-4 flex items-center gap-2 md:gap-3">
<div className="p-1.5 rounded-md bg-blue-500/10"> <div className="p-1.5 rounded-md bg-blue-500/10 hidden sm:block">
<Layers className="h-4 w-4 text-blue-400" /> <Layers className="h-4 w-4 text-blue-400" />
</div> </div>
<div> <div>
<p className="text-[10px] tracking-wider uppercase text-muted-foreground">Total</p> <p className="text-[9px] md:text-[10px] tracking-wider uppercase text-muted-foreground">Total</p>
<p className="font-heading text-xl font-bold tabular-nums">{projects.length}</p> <p className="font-heading text-lg md:text-xl font-bold tabular-nums">{projects.length}</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent"> <Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
<CardContent className="p-4 flex items-center gap-3"> <CardContent className="p-2.5 md:p-4 flex items-center gap-2 md:gap-3">
<div className="p-1.5 rounded-md bg-purple-500/10"> <div className="p-1.5 rounded-md bg-purple-500/10 hidden sm:block">
<PlayCircle className="h-4 w-4 text-purple-400" /> <PlayCircle className="h-4 w-4 text-purple-400" />
</div> </div>
<div> <div>
<p className="text-[10px] tracking-wider uppercase text-muted-foreground">In Progress</p> <p className="text-[9px] md:text-[10px] tracking-wider uppercase text-muted-foreground">In Progress</p>
<p className="font-heading text-xl font-bold tabular-nums">{inProgressCount}</p> <p className="font-heading text-lg md:text-xl font-bold tabular-nums">{inProgressCount}</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent"> <Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
<CardContent className="p-4 flex items-center gap-3"> <CardContent className="p-2.5 md:p-4 flex items-center gap-2 md:gap-3">
<div className="p-1.5 rounded-md bg-green-500/10"> <div className="p-1.5 rounded-md bg-green-500/10 hidden sm:block">
<CheckCircle2 className="h-4 w-4 text-green-400" /> <CheckCircle2 className="h-4 w-4 text-green-400" />
</div> </div>
<div> <div>
<p className="text-[10px] tracking-wider uppercase text-muted-foreground">Completed</p> <p className="text-[9px] md:text-[10px] tracking-wider uppercase text-muted-foreground">Completed</p>
<p className="font-heading text-xl font-bold tabular-nums">{completedCount}</p> <p className="font-heading text-lg md:text-xl font-bold tabular-nums">{completedCount}</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -73,14 +73,14 @@ export default function ReminderItem({ reminder, onEdit }: ReminderItemProps) {
return ( return (
<div <div
className={cn( className={cn(
'flex items-center gap-3 px-3 py-2 rounded-md transition-colors duration-150', 'flex items-start md:items-center gap-2 md:gap-3 px-3 py-2 rounded-md transition-colors duration-150',
'hover:bg-card-elevated', 'hover:bg-card-elevated',
reminder.is_dismissed && 'opacity-50' reminder.is_dismissed && 'opacity-50'
)} )}
> >
<Bell <Bell
className={cn( className={cn(
'h-4 w-4 shrink-0', 'h-4 w-4 shrink-0 mt-0.5 md:mt-0',
isOverdue isOverdue
? 'text-red-400' ? 'text-red-400'
: reminder.is_dismissed : reminder.is_dismissed
@ -89,78 +89,86 @@ export default function ReminderItem({ reminder, onEdit }: ReminderItemProps) {
)} )}
/> />
<span {/* Content wrapper — stacks on mobile, inline on desktop */}
className={cn( <div className="flex-1 min-w-0 flex flex-col md:flex-row md:items-center gap-1 md:gap-3">
'text-sm font-medium truncate flex-1 min-w-0 cursor-pointer',
reminder.is_dismissed && 'line-through text-muted-foreground'
)}
onClick={() => onEdit(reminder)}
>
{reminder.title}
</span>
{reminder.recurrence_rule && (
<span className="text-[9px] px-1.5 py-0.5 rounded font-medium uppercase tracking-wide bg-purple-500/15 text-purple-400 shrink-0">
{recurrenceLabels[reminder.recurrence_rule] || reminder.recurrence_rule}
</span>
)}
{remindDate && (
<span <span
className={cn( className={cn(
'text-[11px] shrink-0', 'text-sm font-medium truncate cursor-pointer md:flex-1 md:min-w-0',
isOverdue ? 'text-red-400' : isDueToday ? 'text-yellow-400' : 'text-muted-foreground' reminder.is_dismissed && 'line-through text-muted-foreground'
)} )}
onClick={() => onEdit(reminder)}
> >
{format(remindDate, 'MMM d, h:mm a')} {reminder.title}
</span> </span>
)}
{!reminder.is_dismissed && ( <div className="flex items-center gap-1.5 flex-wrap">
{reminder.recurrence_rule && (
<span className="text-[9px] px-1.5 py-0.5 rounded font-medium uppercase tracking-wide bg-purple-500/15 text-purple-400 shrink-0">
{recurrenceLabels[reminder.recurrence_rule] || reminder.recurrence_rule}
</span>
)}
{remindDate && (
<span
className={cn(
'text-[11px] shrink-0',
isOverdue ? 'text-red-400' : isDueToday ? 'text-yellow-400' : 'text-muted-foreground'
)}
>
{format(remindDate, 'MMM d, h:mm a')}
</span>
)}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-0.5 shrink-0">
{!reminder.is_dismissed && (
<Button
variant="ghost"
size="icon"
onClick={() => dismissMutation.mutate()}
disabled={dismissMutation.isPending}
className="h-7 w-7 hover:bg-orange-500/10 hover:text-orange-400"
aria-label="Dismiss reminder"
>
<BellOff className="h-3 w-3" />
</Button>
)}
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => dismissMutation.mutate()} onClick={() => onEdit(reminder)}
disabled={dismissMutation.isPending} className="h-7 w-7"
className="h-7 w-7 shrink-0 hover:bg-orange-500/10 hover:text-orange-400" aria-label="Edit reminder"
aria-label="Dismiss reminder"
> >
<BellOff className="h-3 w-3" /> <Pencil className="h-3 w-3" />
</Button> </Button>
)}
<Button {confirmingDelete ? (
variant="ghost" <Button
size="icon" variant="ghost"
onClick={() => onEdit(reminder)} aria-label="Confirm delete"
className="h-7 w-7 shrink-0" onClick={handleDelete}
aria-label="Edit reminder" disabled={deleteMutation.isPending}
> className="h-7 px-2 bg-destructive/20 text-destructive text-[11px] font-medium"
<Pencil className="h-3 w-3" /> >
</Button> Sure?
</Button>
{confirmingDelete ? ( ) : (
<Button <Button
variant="ghost" variant="ghost"
aria-label="Confirm delete" size="icon"
onClick={handleDelete} aria-label="Delete reminder"
disabled={deleteMutation.isPending} onClick={handleDelete}
className="h-7 shrink-0 px-2 bg-destructive/20 text-destructive text-[11px] font-medium" disabled={deleteMutation.isPending}
> className="h-7 w-7 hover:bg-destructive/10 hover:text-destructive"
Sure? >
</Button> <Trash2 className="h-3 w-3" />
) : ( </Button>
<Button )}
variant="ghost" </div>
size="icon"
aria-label="Delete reminder"
onClick={handleDelete}
disabled={deleteMutation.isPending}
className="h-7 w-7 shrink-0 hover:bg-destructive/10 hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
)}
</div> </div>
); );
} }

View File

@ -101,7 +101,7 @@ export default function RemindersPage() {
<div className="flex flex-col h-full animate-fade-in"> <div className="flex flex-col h-full animate-fade-in">
{/* Header */} {/* Header */}
<div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0"> <div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
<h1 className="font-heading text-2xl font-bold tracking-tight">Reminders</h1> <h1 className="font-heading text-xl md:text-2xl font-bold tracking-tight">Reminders</h1>
<Select <Select
value={filter} value={filter}
@ -160,43 +160,43 @@ export default function RemindersPage() {
<div className="px-4 md:px-6 py-5"> <div className="px-4 md:px-6 py-5">
{/* Summary stats */} {/* Summary stats */}
{!isLoading && reminders.length > 0 && ( {!isLoading && reminders.length > 0 && (
<div className="grid gap-2.5 grid-cols-3 mb-5"> <div className="grid gap-1.5 md:gap-2.5 grid-cols-3 mb-5">
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent"> <Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
<CardContent className="p-4 flex items-center gap-3"> <CardContent className="p-2.5 md:p-4 flex items-center gap-2 md:gap-3">
<div className="p-1.5 rounded-md bg-orange-500/10"> <div className="p-1.5 rounded-md bg-orange-500/10 hidden sm:block">
<Bell className="h-4 w-4 text-orange-400" /> <Bell className="h-4 w-4 text-orange-400" />
</div> </div>
<div> <div>
<p className="text-[10px] tracking-wider uppercase text-muted-foreground"> <p className="text-[9px] md:text-[10px] tracking-wider uppercase text-muted-foreground">
Active Active
</p> </p>
<p className="font-heading text-xl font-bold tabular-nums">{activeCount}</p> <p className="font-heading text-lg md:text-xl font-bold tabular-nums">{activeCount}</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent"> <Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
<CardContent className="p-4 flex items-center gap-3"> <CardContent className="p-2.5 md:p-4 flex items-center gap-2 md:gap-3">
<div className="p-1.5 rounded-md bg-red-500/10"> <div className="p-1.5 rounded-md bg-red-500/10 hidden sm:block">
<AlertCircle className="h-4 w-4 text-red-400" /> <AlertCircle className="h-4 w-4 text-red-400" />
</div> </div>
<div> <div>
<p className="text-[10px] tracking-wider uppercase text-muted-foreground"> <p className="text-[9px] md:text-[10px] tracking-wider uppercase text-muted-foreground">
Overdue Overdue
</p> </p>
<p className="font-heading text-xl font-bold tabular-nums">{overdueCount}</p> <p className="font-heading text-lg md:text-xl font-bold tabular-nums">{overdueCount}</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent"> <Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
<CardContent className="p-4 flex items-center gap-3"> <CardContent className="p-2.5 md:p-4 flex items-center gap-2 md:gap-3">
<div className="p-1.5 rounded-md bg-gray-500/10"> <div className="p-1.5 rounded-md bg-gray-500/10 hidden sm:block">
<BellOff className="h-4 w-4 text-gray-400" /> <BellOff className="h-4 w-4 text-gray-400" />
</div> </div>
<div> <div>
<p className="text-[10px] tracking-wider uppercase text-muted-foreground"> <p className="text-[9px] md:text-[10px] tracking-wider uppercase text-muted-foreground">
Dismissed Dismissed
</p> </p>
<p className="font-heading text-xl font-bold tabular-nums">{dismissedCount}</p> <p className="font-heading text-lg md:text-xl font-bold tabular-nums">{dismissedCount}</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -52,7 +52,6 @@ export default function TodoItem({ todo, onEdit }: TodoItemProps) {
await api.delete(`/todos/${todo.id}`); await api.delete(`/todos/${todo.id}`);
}, },
onMutate: async () => { onMutate: async () => {
// Optimistic removal
await queryClient.cancelQueries({ queryKey: ['todos'] }); await queryClient.cancelQueries({ queryKey: ['todos'] });
const previous = queryClient.getQueryData<Todo[]>(['todos']); const previous = queryClient.getQueryData<Todo[]>(['todos']);
queryClient.setQueryData<Todo[]>(['todos'], (old) => queryClient.setQueryData<Todo[]>(['todos'], (old) =>
@ -65,7 +64,6 @@ export default function TodoItem({ todo, onEdit }: TodoItemProps) {
toast.success('Todo deleted'); toast.success('Todo deleted');
}, },
onError: (_err, _vars, context) => { onError: (_err, _vars, context) => {
// Rollback on failure
if (context?.previous) { if (context?.previous) {
queryClient.setQueryData(['todos'], context.previous); queryClient.setQueryData(['todos'], context.previous);
} }
@ -87,7 +85,7 @@ export default function TodoItem({ todo, onEdit }: TodoItemProps) {
return ( return (
<div <div
className={cn( className={cn(
'flex items-center gap-3 px-3 py-2 rounded-md transition-colors duration-150', 'flex items-start md:items-center gap-2 md:gap-3 px-3 py-2 rounded-md transition-colors duration-150',
'hover:bg-card-elevated', 'hover:bg-card-elevated',
todo.completed && 'opacity-50' todo.completed && 'opacity-50'
)} )}
@ -96,100 +94,108 @@ export default function TodoItem({ todo, onEdit }: TodoItemProps) {
checked={todo.completed} checked={todo.completed}
onChange={() => toggleMutation.mutate()} onChange={() => toggleMutation.mutate()}
disabled={toggleMutation.isPending} disabled={toggleMutation.isPending}
className="mt-0.5 md:mt-0"
/> />
<span {/* Content wrapper — stacks on mobile, inline on desktop */}
className={cn( <div className="flex-1 min-w-0 flex flex-col md:flex-row md:items-center gap-1 md:gap-3">
'text-sm font-medium truncate flex-1 min-w-0 cursor-pointer', {/* Title row — always takes full width on mobile */}
todo.completed && 'line-through text-muted-foreground' <span
)} className={cn(
onClick={() => onEdit(todo)} 'text-sm font-medium truncate cursor-pointer md:flex-1 md:min-w-0',
> todo.completed && 'line-through text-muted-foreground'
{todo.title} )}
</span> onClick={() => onEdit(todo)}
>
{/* Inline pills */} {todo.title}
<span
className={cn(
'text-[9px] px-1.5 py-0.5 rounded-full font-medium shrink-0',
priorityStyles[todo.priority]
)}
>
{todo.priority}
</span>
{todo.category && (
<span className="text-[9px] px-1.5 py-0.5 rounded font-medium uppercase tracking-wide bg-blue-500/15 text-blue-400 shrink-0">
{todo.category}
</span> </span>
)}
{todo.recurrence_rule && ( {/* Metadata row — wraps on second line on mobile */}
<span className="text-[9px] px-1.5 py-0.5 rounded font-medium uppercase tracking-wide bg-purple-500/15 text-purple-400 shrink-0"> <div className="flex items-center gap-1.5 flex-wrap">
{recurrenceLabels[todo.recurrence_rule] || todo.recurrence_rule} <span
</span> className={cn(
)} 'text-[9px] px-1.5 py-0.5 rounded-full font-medium shrink-0',
priorityStyles[todo.priority]
{/* Date / time / reset info — right-aligned cluster */}
{showResetInfo ? (
<div className="flex items-center gap-1 text-[11px] text-purple-400 shrink-0">
<RefreshCw className="h-3 w-3" />
<span>
Resets {format(resetDate, 'EEE dd/MM')}
{nextDueDate && (
<> · Due {format(nextDueDate, 'dd/MM')}{todo.due_time ? ` ${todo.due_time.slice(0, 5)}` : ''}</>
)} )}
>
{todo.priority}
</span> </span>
</div>
) : ( {todo.category && (
<> <span className="text-[9px] px-1.5 py-0.5 rounded font-medium uppercase tracking-wide bg-blue-500/15 text-blue-400 shrink-0">
{dueDate && ( {todo.category}
<span </span>
className={cn( )}
'flex items-center gap-1 text-[11px] shrink-0',
isOverdue ? 'text-red-400' : isDueToday ? 'text-yellow-400' : 'text-muted-foreground' {todo.recurrence_rule && (
<span className="text-[9px] px-1.5 py-0.5 rounded font-medium uppercase tracking-wide bg-purple-500/15 text-purple-400 shrink-0">
{recurrenceLabels[todo.recurrence_rule] || todo.recurrence_rule}
</span>
)}
{showResetInfo ? (
<div className="flex items-center gap-1 text-[11px] text-purple-400 shrink-0">
<RefreshCw className="h-3 w-3" />
<span>
Resets {format(resetDate, 'EEE dd/MM')}
{nextDueDate && (
<>{' \u00b7 '}Due {format(nextDueDate, 'dd/MM')}{todo.due_time ? ` ${todo.due_time.slice(0, 5)}` : ''}</>
)}
</span>
</div>
) : (
<>
{dueDate && (
<span
className={cn(
'flex items-center gap-1 text-[11px] shrink-0',
isOverdue ? 'text-red-400' : isDueToday ? 'text-yellow-400' : 'text-muted-foreground'
)}
>
{isOverdue ? <AlertCircle className="h-3 w-3" /> : <Calendar className="h-3 w-3" />}
{isOverdue ? 'Overdue \u00b7 ' : isDueToday ? 'Today \u00b7 ' : ''}
{format(dueDate, 'MMM d')}
</span>
)} )}
> {todo.due_time && (
{isOverdue ? <AlertCircle className="h-3 w-3" /> : <Calendar className="h-3 w-3" />} <span className="flex items-center gap-1 text-[11px] text-muted-foreground shrink-0">
{isOverdue ? 'Overdue · ' : isDueToday ? 'Today · ' : ''} <Clock className="h-3 w-3" />
{format(dueDate, 'MMM d')} {todo.due_time.slice(0, 5)}
</span> </span>
)}
</>
)} )}
{todo.due_time && ( </div>
<span className="flex items-center gap-1 text-[11px] text-muted-foreground shrink-0"> </div>
<Clock className="h-3 w-3" />
{todo.due_time.slice(0, 5)}
</span>
)}
</>
)}
{/* Actions */} {/* Actions */}
<Button variant="ghost" size="icon" onClick={() => onEdit(todo)} className="h-7 w-7 shrink-0" aria-label="Edit todo"> <div className="flex items-center gap-0.5 shrink-0">
<Pencil className="h-3 w-3" /> <Button variant="ghost" size="icon" onClick={() => onEdit(todo)} className="h-7 w-7" aria-label="Edit todo">
</Button> <Pencil className="h-3 w-3" />
{confirmingDelete ? (
<Button
variant="ghost"
aria-label="Confirm delete"
onClick={handleDelete}
disabled={deleteMutation.isPending}
className="h-7 shrink-0 px-2 bg-destructive/20 text-destructive text-[11px] font-medium"
>
Sure?
</Button> </Button>
) : ( {confirmingDelete ? (
<Button <Button
variant="ghost" variant="ghost"
size="icon" aria-label="Confirm delete"
aria-label="Delete todo" onClick={handleDelete}
onClick={handleDelete} disabled={deleteMutation.isPending}
disabled={deleteMutation.isPending} className="h-7 px-2 bg-destructive/20 text-destructive text-[11px] font-medium"
className="h-7 w-7 shrink-0 hover:bg-destructive/10 hover:text-destructive" >
> Sure?
<Trash2 className="h-3 w-3" /> </Button>
</Button> ) : (
)} <Button
variant="ghost"
size="icon"
aria-label="Delete todo"
onClick={handleDelete}
disabled={deleteMutation.isPending}
className="h-7 w-7 hover:bg-destructive/10 hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</Button>
)}
</div>
</div> </div>
); );
} }

View File

@ -130,7 +130,7 @@ export default function TodosPage() {
<div className="flex flex-col h-full animate-fade-in"> <div className="flex flex-col h-full animate-fade-in">
{/* Header */} {/* Header */}
<div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0"> <div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
<h1 className="font-heading text-2xl font-bold tracking-tight">Todos</h1> <h1 className="font-heading text-xl md:text-2xl font-bold tracking-tight">Todos</h1>
{/* Priority filter */} {/* Priority filter */}
<Select <Select
@ -195,43 +195,43 @@ export default function TodosPage() {
<div className="px-4 md:px-6 py-5"> <div className="px-4 md:px-6 py-5">
{/* Summary stats */} {/* Summary stats */}
{!isLoading && todos.length > 0 && ( {!isLoading && todos.length > 0 && (
<div className="grid gap-2.5 grid-cols-3 mb-5"> <div className="grid gap-1.5 md:gap-2.5 grid-cols-3 mb-5">
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent"> <Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
<CardContent className="p-4 flex items-center gap-3"> <CardContent className="p-2.5 md:p-4 flex items-center gap-2 md:gap-3">
<div className="p-1.5 rounded-md bg-blue-500/10"> <div className="p-1.5 rounded-md bg-blue-500/10 hidden sm:block">
<CheckSquare className="h-4 w-4 text-blue-400" /> <CheckSquare className="h-4 w-4 text-blue-400" />
</div> </div>
<div> <div>
<p className="text-[10px] tracking-wider uppercase text-muted-foreground"> <p className="text-[9px] md:text-[10px] tracking-wider uppercase text-muted-foreground">
Open Open
</p> </p>
<p className="font-heading text-xl font-bold tabular-nums">{totalCount}</p> <p className="font-heading text-lg md:text-xl font-bold tabular-nums">{totalCount}</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent"> <Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
<CardContent className="p-4 flex items-center gap-3"> <CardContent className="p-2.5 md:p-4 flex items-center gap-2 md:gap-3">
<div className="p-1.5 rounded-md bg-green-500/10"> <div className="p-1.5 rounded-md bg-green-500/10 hidden sm:block">
<CheckCircle2 className="h-4 w-4 text-green-400" /> <CheckCircle2 className="h-4 w-4 text-green-400" />
</div> </div>
<div> <div>
<p className="text-[10px] tracking-wider uppercase text-muted-foreground"> <p className="text-[9px] md:text-[10px] tracking-wider uppercase text-muted-foreground">
Completed Completed
</p> </p>
<p className="font-heading text-xl font-bold tabular-nums">{completedCount}</p> <p className="font-heading text-lg md:text-xl font-bold tabular-nums">{completedCount}</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card className="bg-gradient-to-br from-accent/[0.03] to-transparent"> <Card className="bg-gradient-to-br from-accent/[0.03] to-transparent">
<CardContent className="p-4 flex items-center gap-3"> <CardContent className="p-2.5 md:p-4 flex items-center gap-2 md:gap-3">
<div className="p-1.5 rounded-md bg-red-500/10"> <div className="p-1.5 rounded-md bg-red-500/10 hidden sm:block">
<AlertCircle className="h-4 w-4 text-red-400" /> <AlertCircle className="h-4 w-4 text-red-400" />
</div> </div>
<div> <div>
<p className="text-[10px] tracking-wider uppercase text-muted-foreground"> <p className="text-[9px] md:text-[10px] tracking-wider uppercase text-muted-foreground">
Overdue Overdue
</p> </p>
<p className="font-heading text-xl font-bold tabular-nums">{overdueCount}</p> <p className="font-heading text-lg md:text-xl font-bold tabular-nums">{overdueCount}</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -223,6 +223,41 @@ form[data-submitted] input:invalid + button {
box-shadow: 0 0 0 2px hsl(0 62.8% 50% / 0.25); box-shadow: 0 0 0 2px hsl(0 62.8% 50% / 0.25);
} }
/* ── FullCalendar mobile overrides ── */
@media (max-width: 767px) {
.fc .fc-daygrid-event {
font-size: 0.65rem;
padding: 0 2px;
line-height: 1.3;
}
.fc .fc-timegrid-event {
font-size: 0.65rem;
}
.fc .fc-timegrid-event .fc-event-main {
padding: 1px 2px;
}
.fc .fc-daygrid-day-number {
font-size: 0.75rem;
padding: 2px 4px;
}
.fc .fc-col-header-cell-cushion {
font-size: 0.7rem;
padding: 4px 2px;
}
.fc .fc-timegrid-slot-label {
font-size: 0.65rem;
}
.fc .fc-daygrid-more-link {
font-size: 0.6rem;
}
}
/* ── Ambient background animations ── */ /* ── Ambient background animations ── */
@keyframes drift-1 { @keyframes drift-1 {