Reformat detail panel view modes to 2-column grid layout

Match TaskDetailPanel gold standard: short fields use grid grid-cols-2
with icon+label headers, full-width fields (description) below the grid.
All grid slots render with "—" fallback to keep alignment consistent.

- Todo: Priority, Category, Due Date, Recurrence in grid
- Reminder: Status, Recurrence, Remind At, Snoozed Until in grid
- Calendar: Calendar, Starred, Start, End, Location, Recurrence in grid
- Task: Add "Updated X ago" footer (was missing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-02-25 23:52:15 +08:00
parent 2e2466bfa6
commit 87a7a4ae32
4 changed files with 238 additions and 197 deletions

View File

@ -807,9 +807,14 @@ export default function EventDetailPanel({
) : ( ) : (
/* View mode */ /* View mode */
<> <>
{/* 2-column grid: Calendar, Starred, Start, End, Location, Recurrence */}
<div className="grid grid-cols-2 gap-3">
{/* Calendar */} {/* Calendar */}
<div className="space-y-0.5"> <div className="space-y-1">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Calendar</p> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<Calendar className="h-3 w-3" />
Calendar
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className="w-2 h-2 rounded-full shrink-0" className="w-2 h-2 rounded-full shrink-0"
@ -819,68 +824,79 @@ export default function EventDetailPanel({
</div> </div>
</div> </div>
{/* Starred */}
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<Star className="h-3 w-3" />
Starred
</div>
{event?.is_starred ? (
<p className="text-sm text-amber-200/90">Starred</p>
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</div>
{/* Start */} {/* Start */}
<div className="space-y-0.5"> <div className="space-y-1">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Start</p> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<Clock className="h-3 w-3" />
Start
</div>
<CopyableField value={startStr} icon={Clock} label="Start time" /> <CopyableField value={startStr} icon={Clock} label="Start time" />
</div> </div>
{/* End */} {/* End */}
{endStr && ( <div className="space-y-1">
<div className="space-y-0.5"> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">End</p> <Clock className="h-3 w-3" />
<CopyableField value={endStr} icon={Clock} label="End time" /> End
</div> </div>
{endStr ? (
<CopyableField value={endStr} icon={Clock} label="End time" />
) : (
<p className="text-sm text-muted-foreground"></p>
)} )}
</div>
{/* Location */} {/* Location */}
{locationName && ( <div className="space-y-1">
<div className="space-y-0.5"> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Location</p> <MapPin className="h-3 w-3" />
Location
</div>
{locationName ? (
<CopyableField value={locationName} icon={MapPin} label="Location" /> <CopyableField value={locationName} icon={MapPin} label="Location" />
</div> ) : (
<p className="text-sm text-muted-foreground"></p>
)} )}
{/* Description */}
{event?.description && (
<div className="space-y-0.5">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Description</p>
<div className="flex items-start gap-2">
<AlignLeft className="h-3.5 w-3.5 text-muted-foreground shrink-0 mt-0.5" />
<p className="text-sm whitespace-pre-wrap">{event.description}</p>
</div> </div>
</div>
)}
{/* Starred */}
{event?.is_starred && (
<div className="space-y-0.5">
<div className="flex items-center gap-2">
<Star className="h-3.5 w-3.5 text-amber-400 fill-amber-400 shrink-0" />
<span className="text-sm text-amber-200/90">Starred event</span>
</div>
</div>
)}
{/* Recurrence */} {/* Recurrence */}
{isRecurring && event?.recurrence_rule && ( <div className="space-y-1">
<div className="space-y-0.5"> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Recurrence</p> <Repeat className="h-3 w-3" />
<div className="flex items-center gap-2"> Recurrence
<Repeat className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<span className="text-sm">{formatRecurrenceRule(event.recurrence_rule)}</span>
</div>
</div> </div>
{isRecurring && event?.recurrence_rule ? (
<p className="text-sm">{formatRecurrenceRule(event.recurrence_rule)}</p>
) : isRecurring ? (
<p className="text-sm">Recurring event</p>
) : (
<p className="text-sm text-muted-foreground"></p>
)} )}
{isRecurring && !event?.recurrence_rule && (
<div className="space-y-0.5">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Recurrence</p>
<div className="flex items-center gap-2">
<Repeat className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<span className="text-sm">Recurring event</span>
</div> </div>
</div> </div>
{/* Description — full width */}
{event?.description && (
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<AlignLeft className="h-3 w-3" />
Description
</div>
<p className="text-sm whitespace-pre-wrap">{event.description}</p>
</div>
)} )}
{/* Updated at */} {/* Updated at */}

View File

@ -7,6 +7,7 @@ import {
Calendar, User, Flag, Activity, Send, X, Save, Calendar, User, Flag, Activity, Send, X, Save,
} from 'lucide-react'; } from 'lucide-react';
import api, { getErrorMessage } from '@/lib/api'; import api, { getErrorMessage } from '@/lib/api';
import { formatUpdatedAt } from '@/components/shared/utils';
import type { ProjectTask, TaskComment, Person } from '@/types'; import type { ProjectTask, TaskComment, Person } from '@/types';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@ -558,6 +559,15 @@ export default function TaskDetailPanel({
<Send className="h-4 w-4" /> <Send className="h-4 w-4" />
</Button> </Button>
</div> </div>
{/* Updated at footer */}
{task.updated_at && (
<div className="pt-2 border-t border-border">
<span className="text-[11px] text-muted-foreground">
{formatUpdatedAt(task.updated_at)}
</span>
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { format, parseISO, isPast, isToday } from 'date-fns'; import { format, parseISO, isPast, isToday } from 'date-fns';
import { import {
X, Pencil, Trash2, Save, Bell, BellOff, Clock, Repeat, AlertCircle, X, Pencil, Trash2, Save, Bell, BellOff, Clock, Repeat, AlertCircle, AlignLeft,
} from 'lucide-react'; } from 'lucide-react';
import api, { getErrorMessage } from '@/lib/api'; import api, { getErrorMessage } from '@/lib/api';
import type { Reminder } from '@/types'; import type { Reminder } from '@/types';
@ -377,76 +377,82 @@ export default function ReminderDetailPanel({
) : ( ) : (
/* View mode */ /* View mode */
<> <>
{/* 2-column grid: Status, Recurrence, Remind At, Snoozed Until */}
<div className="grid grid-cols-2 gap-3">
{/* Status */} {/* Status */}
<div className="space-y-0.5"> <div className="space-y-1">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Status</p> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<div className="flex items-center gap-2">
{reminder!.is_dismissed ? ( {reminder!.is_dismissed ? (
<> <BellOff className="h-3 w-3" />
<BellOff className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<span className="text-sm text-muted-foreground">Dismissed</span>
</>
) : isOverdue ? ( ) : isOverdue ? (
<> <AlertCircle className="h-3 w-3" />
<AlertCircle className="h-3.5 w-3.5 text-red-400 shrink-0" />
<span className="text-sm text-red-400">Overdue</span>
</>
) : isDueToday ? (
<>
<Bell className="h-3.5 w-3.5 text-yellow-400 shrink-0" />
<span className="text-sm text-yellow-400">Due today</span>
</>
) : ( ) : (
<> <Bell className="h-3 w-3" />
<Bell className="h-3.5 w-3.5 text-orange-400 shrink-0" /> )}
<span className="text-sm text-orange-400">Active</span> Status
</> </div>
{reminder!.is_dismissed ? (
<p className="text-sm text-muted-foreground">Dismissed</p>
) : isOverdue ? (
<p className="text-sm text-red-400">Overdue</p>
) : isDueToday ? (
<p className="text-sm text-yellow-400">Due today</p>
) : (
<p className="text-sm text-orange-400">Active</p>
)} )}
</div> </div>
{/* Recurrence */}
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<Repeat className="h-3 w-3" />
Recurrence
</div>
{reminder!.recurrence_rule ? (
<p className="text-sm">{recurrenceLabels[reminder!.recurrence_rule] || reminder!.recurrence_rule}</p>
) : (
<p className="text-sm text-muted-foreground"></p>
)}
</div> </div>
{/* Remind At */} {/* Remind At */}
{remindDate && ( <div className="space-y-1">
<div className="space-y-0.5"> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Remind At</p> <Clock className="h-3 w-3" />
Remind At
</div>
{remindDate ? (
<CopyableField <CopyableField
value={format(remindDate, 'EEEE, MMMM d, yyyy · h:mm a')} value={format(remindDate, 'EEEE, MMMM d, yyyy · h:mm a')}
icon={Clock} icon={Clock}
label="Remind at" label="Remind at"
/> />
</div> ) : (
<p className="text-sm text-muted-foreground"></p>
)} )}
</div>
{/* Snoozed */} {/* Snoozed Until */}
{reminder!.snoozed_until && ( <div className="space-y-1">
<div className="space-y-0.5"> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Snoozed Until</p> <Clock className="h-3 w-3" />
<div className="flex items-center gap-2"> Snoozed Until
<Clock className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<span className="text-sm">
{format(parseISO(reminder!.snoozed_until), 'MMM d, h:mm a')}
</span>
</div>
</div> </div>
{reminder!.snoozed_until ? (
<p className="text-sm">{format(parseISO(reminder!.snoozed_until), 'MMM d, h:mm a')}</p>
) : (
<p className="text-sm text-muted-foreground"></p>
)} )}
{/* Recurrence */}
{reminder!.recurrence_rule && (
<div className="space-y-0.5">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Recurrence</p>
<div className="flex items-center gap-2">
<Repeat className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<span className="text-sm">
{recurrenceLabels[reminder!.recurrence_rule] || reminder!.recurrence_rule}
</span>
</div> </div>
</div> </div>
)}
{/* Description */} {/* Description — full width */}
{reminder!.description && ( {reminder!.description && (
<div className="space-y-0.5"> <div className="space-y-1">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Description</p> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<AlignLeft className="h-3 w-3" />
Description
</div>
<p className="text-sm whitespace-pre-wrap text-muted-foreground leading-relaxed"> <p className="text-sm whitespace-pre-wrap text-muted-foreground leading-relaxed">
{reminder!.description} {reminder!.description}
</p> </p>

View File

@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { format, parseISO, isToday } from 'date-fns'; import { format, parseISO, isToday } from 'date-fns';
import { import {
X, Pencil, Trash2, Save, Clock, Calendar, Flag, Tag, Repeat, CheckSquare, AlertCircle, X, Pencil, Trash2, Save, Clock, Calendar, Flag, Tag, Repeat, CheckSquare, AlertCircle, AlignLeft,
} from 'lucide-react'; } from 'lucide-react';
import api, { getErrorMessage } from '@/lib/api'; import api, { getErrorMessage } from '@/lib/api';
import type { Todo } from '@/types'; import type { Todo } from '@/types';
@ -433,95 +433,104 @@ export default function TodoDetailPanel({
) : ( ) : (
/* View mode */ /* View mode */
<> <>
{/* 2-column grid: Priority, Category, Due Date, Recurrence */}
<div className="grid grid-cols-2 gap-3">
{/* Priority */} {/* Priority */}
<div className="space-y-0.5"> <div className="space-y-1">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Priority</p> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<div className="flex items-center gap-2"> <Flag className="h-3 w-3" />
<Flag className="h-3.5 w-3.5 text-muted-foreground shrink-0" /> Priority
</div>
<Badge className={`text-[9px] px-1.5 py-0.5 rounded-full ${priorityColors[todo!.priority] ?? ''}`}> <Badge className={`text-[9px] px-1.5 py-0.5 rounded-full ${priorityColors[todo!.priority] ?? ''}`}>
{todo!.priority} {todo!.priority}
</Badge> </Badge>
</div> </div>
</div>
{/* Category */} {/* Category */}
{todo!.category && ( <div className="space-y-1">
<div className="space-y-0.5"> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Category</p> <Tag className="h-3 w-3" />
<div className="flex items-center gap-2"> Category
<Tag className="h-3.5 w-3.5 text-muted-foreground shrink-0" /> </div>
{todo!.category ? (
<Badge className="text-[9px] px-1.5 py-0.5 bg-blue-500/15 text-blue-400"> <Badge className="text-[9px] px-1.5 py-0.5 bg-blue-500/15 text-blue-400">
{todo!.category} {todo!.category}
</Badge> </Badge>
</div> ) : (
</div> <p className="text-sm text-muted-foreground"></p>
)} )}
</div>
{/* Due Date */} {/* Due Date */}
{dueDate && ( <div className="space-y-1">
<div className="space-y-0.5"> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Due Date</p> {isOverdue ? <AlertCircle className="h-3 w-3" /> : <Calendar className="h-3 w-3" />}
Due Date
</div>
{dueDate ? (
<CopyableField <CopyableField
value={`${isOverdue ? 'Overdue · ' : isDueToday ? 'Today · ' : ''}${format(dueDate, 'EEEE, MMMM d, yyyy')}${todo!.due_time ? ` at ${todo!.due_time.slice(0, 5)}` : ''}`} value={`${isOverdue ? 'Overdue · ' : isDueToday ? 'Today · ' : ''}${format(dueDate, 'EEEE, MMMM d, yyyy')}${todo!.due_time ? ` at ${todo!.due_time.slice(0, 5)}` : ''}`}
icon={isOverdue ? AlertCircle : Calendar} icon={isOverdue ? AlertCircle : Calendar}
label="Due date" label="Due date"
/> />
</div> ) : todo!.due_time ? (
)}
{/* Due Time (if no date but has time) */}
{!dueDate && todo!.due_time && (
<div className="space-y-0.5">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Due Time</p>
<CopyableField value={todo!.due_time.slice(0, 5)} icon={Clock} label="Due time" /> <CopyableField value={todo!.due_time.slice(0, 5)} icon={Clock} label="Due time" />
</div> ) : (
<p className="text-sm text-muted-foreground"></p>
)} )}
</div>
{/* Recurrence */} {/* Recurrence */}
{todo!.recurrence_rule && ( <div className="space-y-1">
<div className="space-y-0.5"> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Recurrence</p> <Repeat className="h-3 w-3" />
<div className="flex items-center gap-2"> Recurrence
<Repeat className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<span className="text-sm">{recurrenceLabels[todo!.recurrence_rule] || todo!.recurrence_rule}</span>
</div>
</div> </div>
{todo!.recurrence_rule ? (
<p className="text-sm">{recurrenceLabels[todo!.recurrence_rule] || todo!.recurrence_rule}</p>
) : (
<p className="text-sm text-muted-foreground"></p>
)} )}
</div>
</div>
{/* Description */} {/* Completion status — full width */}
{todo!.description && ( {todo!.completed && todo!.completed_at && (
<div className="space-y-0.5"> <div className="space-y-1">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Description</p> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<p className="text-sm whitespace-pre-wrap text-muted-foreground leading-relaxed"> <CheckSquare className="h-3 w-3" />
{todo!.description} Completed
</div>
<p className="text-sm text-green-400">
{format(parseISO(todo!.completed_at), 'MMM d, yyyy · h:mm a')}
</p> </p>
</div> </div>
)} )}
{/* Completion status */} {/* Reset info — full width */}
{todo!.completed && todo!.completed_at && ( {todo!.completed && todo!.recurrence_rule && todo!.reset_at && (
<div className="space-y-0.5"> <div className="space-y-1">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Completed</p> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<div className="flex items-center gap-2"> <Repeat className="h-3 w-3" />
<CheckSquare className="h-3.5 w-3.5 text-green-400 shrink-0" /> Resets
<span className="text-sm text-green-400">
{format(parseISO(todo!.completed_at), 'MMM d, yyyy · h:mm a')}
</span>
</div> </div>
<p className="text-sm text-purple-400">
{format(parseISO(todo!.reset_at), 'EEE, MMM d')}
{todo!.next_due_date && ` · Next due ${format(parseISO(todo!.next_due_date), 'MMM d')}`}
</p>
</div> </div>
)} )}
{/* Reset info for recurring */} {/* Description — full width */}
{todo!.completed && todo!.recurrence_rule && todo!.reset_at && ( {todo!.description && (
<div className="space-y-0.5"> <div className="space-y-1">
<p className="text-[11px] uppercase tracking-wider text-muted-foreground">Resets</p> <div className="flex items-center gap-1.5 text-[11px] text-muted-foreground uppercase tracking-wider">
<div className="flex items-center gap-2"> <AlignLeft className="h-3 w-3" />
<Repeat className="h-3.5 w-3.5 text-purple-400 shrink-0" /> Description
<span className="text-sm text-purple-400">
{format(parseISO(todo!.reset_at), 'EEE, MMM d')}
{todo!.next_due_date && ` · Next due ${format(parseISO(todo!.next_due_date), 'MMM d')}`}
</span>
</div> </div>
<p className="text-sm whitespace-pre-wrap text-muted-foreground leading-relaxed">
{todo!.description}
</p>
</div> </div>
)} )}