fix(ui): mobile responsiveness pass + dashboard polish

- Topbar: responsive padding, hidden search text/shortcuts on mobile,
  hidden domain picker on mobile, notifications dropdown prevents overflow
- App layout: responsive main content padding (p-3 md:p-6)
- Daily Notes: stack calendar sidebar above editor on mobile
- Notes/Graph/Templates: remove negative margins causing horizontal overflow
- Tasks: add overflow-x-auto to table, responsive saved views select
- Habits: card actions stack below on mobile
- Calendar: view buttons wrap on narrow screens
- Projects: responsive status filter width
- Project detail: task creation row stacks on mobile
- Settings: shortcut rows stack on mobile
- Dashboard: improved widget card borders, spacing, and content overflow
This commit is contained in:
2026-09-10 11:47:34 +00:00
parent ddcb707190
commit 9fac1d42de
13 changed files with 90 additions and 95 deletions
+8 -6
View File
@@ -67,7 +67,7 @@ export function Topbar() {
return ( return (
<header <header
className="sticky top-0 z-10 flex h-10 items-center gap-4 border-b bg-topbar px-6 backdrop-blur-sm" className="sticky top-0 z-10 flex h-10 items-center gap-2 border-b bg-topbar px-3 md:gap-4 md:px-6 backdrop-blur-sm"
role="banner" role="banner"
> >
{/* Mobile menu */} {/* Mobile menu */}
@@ -91,18 +91,20 @@ export function Topbar() {
aria-label="Open search (Cmd+K)" aria-label="Open search (Cmd+K)"
> >
<Search className="mr-2 h-4 w-4 shrink-0" aria-hidden="true" /> <Search className="mr-2 h-4 w-4 shrink-0" aria-hidden="true" />
<span className="truncate">Search or jump to...</span> <span className="truncate hidden sm:inline">Search or jump to...</span>
<kbd className="ml-auto pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground"> <kbd className="ml-auto pointer-events-none hidden sm:inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
K K
</kbd> </kbd>
</Button> </Button>
</div> </div>
{/* Domain picker */} {/* Domain picker */}
<DomainPicker /> <div className="hidden sm:block">
<DomainPicker />
</div>
{/* Right side */} {/* Right side */}
<div className="ml-auto flex items-center gap-1"> <div className="ml-auto flex items-center gap-0.5 md:gap-1">
{/* Quick add */} {/* Quick add */}
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
@@ -142,7 +144,7 @@ export function Topbar() {
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{tooltipText}</TooltipContent> <TooltipContent>{tooltipText}</TooltipContent>
</Tooltip> </Tooltip>
<DropdownMenuContent align="end" className="w-80"> <DropdownMenuContent align="end" className="w-[calc(100vw-2rem)] max-w-80">
<div className="flex items-center justify-between px-2 py-1.5"> <div className="flex items-center justify-between px-2 py-1.5">
<span className="text-sm font-medium">Notifications</span> <span className="text-sm font-medium">Notifications</span>
<Button <Button
+1 -1
View File
@@ -370,7 +370,7 @@ function CalendarPage() {
))} ))}
</select> </select>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1 flex-wrap">
{["month", "week", "day", "agenda"].map((name) => ( {["month", "week", "day", "agenda"].map((name) => (
<Button <Button
key={name} key={name}
+4 -9
View File
@@ -30,15 +30,10 @@ function CalendarSidebar({ selectedDate, onSelectDate }: { selectedDate: Date; o
const activeDomainId = useApiDomain(); const activeDomainId = useApiDomain();
const { data } = useApiQuery<{ items: DailyNote[]; totalItems: number }>(["daily-notes-list", activeDomainId], "/daily-notes" + (activeDomainId ? "?domain=" + activeDomainId : "")); const { data } = useApiQuery<{ items: DailyNote[]; totalItems: number }>(["daily-notes-list", activeDomainId], "/daily-notes" + (activeDomainId ? "?domain=" + activeDomainId : ""));
const notes = data?.items || []; const notes = data?.items || [];
// The API stores daily notes at UTC midnight (YYYY-MM-DDT00:00:00.000Z).
// Slicing off the time portion yields the calendar date the note belongs to
// regardless of the browser's timezone. parseISO + format would re-render the
// UTC instant in the local zone and shift the marker to the previous day for
// users west of UTC.
const noteDates = new Set(notes.map((n) => n.date.slice(0, 10))); const noteDates = new Set(notes.map((n) => n.date.slice(0, 10)));
return ( return (
<div className="w-56 shrink-0"> <div className="w-full md:w-56 shrink-0">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}> <Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}>
<ChevronLeft className="h-3.5 w-3.5" /> <ChevronLeft className="h-3.5 w-3.5" />
@@ -361,10 +356,10 @@ function DailyNotesPage() {
const [selectedDate, setSelectedDate] = useState(new Date()); const [selectedDate, setSelectedDate] = useState(new Date());
return ( return (
<div className="flex gap-4 h-[calc(100vh-5rem)]"> <div className="flex flex-col md:flex-row gap-4 h-auto md:h-[calc(100vh-5rem)]">
<CalendarSidebar selectedDate={selectedDate} onSelectDate={setSelectedDate} /> <CalendarSidebar selectedDate={selectedDate} onSelectDate={setSelectedDate} />
<Separator orientation="vertical" /> <Separator className="hidden md:block" orientation="vertical" />
<ScrollArea className="flex-1 pr-3"> <ScrollArea className="flex-1 md:pr-3 min-h-[60vh] md:min-h-0">
<DailyNoteEditor date={selectedDate} /> <DailyNoteEditor date={selectedDate} />
</ScrollArea> </ScrollArea>
</div> </div>
+63 -66
View File
@@ -9,7 +9,6 @@ import { Plus, Settings2, Trash2, ListTodo, Flame, FileText, FolderKanban, Calen
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@@ -48,16 +47,16 @@ function TasksDueWidget() {
const today = tasks.filter((t) => t.dueDate && isToday(parseISO(t.dueDate))); const today = tasks.filter((t) => t.dueDate && isToday(parseISO(t.dueDate)));
const overdue = tasks.filter((t) => t.dueDate && isPast(parseISO(t.dueDate)) && t.status !== "done"); const overdue = tasks.filter((t) => t.dueDate && isPast(parseISO(t.dueDate)) && t.status !== "done");
return ( return (
<div className="space-y-1.5"> <div className="space-y-2">
{today.length === 0 && overdue.length === 0 ? ( {today.length === 0 && overdue.length === 0 ? (
<p className="text-xs text-muted-foreground">No tasks due today</p> <p className="text-xs text-muted-foreground">No tasks due today</p>
) : ( ) : (
<> <>
{overdue.length > 0 && ( {overdue.length > 0 && (
<div> <div className="space-y-1">
<p className="text-[10px] font-semibold text-destructive mb-0.5">Overdue ({overdue.length})</p> <p className="text-[10px] font-semibold text-destructive uppercase tracking-wide">Overdue ({overdue.length})</p>
{overdue.slice(0, 3).map((t) => ( {overdue.slice(0, 3).map((t) => (
<div key={t.id} className="flex items-center gap-1.5 text-xs py-0.5"> <div key={t.id} className="flex items-center gap-2 text-xs py-0.5">
<div className="w-1.5 h-1.5 rounded-full bg-destructive shrink-0" /> <div className="w-1.5 h-1.5 rounded-full bg-destructive shrink-0" />
<span className="truncate flex-1">{t.title}</span> <span className="truncate flex-1">{t.title}</span>
</div> </div>
@@ -65,10 +64,10 @@ function TasksDueWidget() {
</div> </div>
)} )}
{today.length > 0 && ( {today.length > 0 && (
<div> <div className="space-y-1">
<p className="text-[10px] font-semibold text-amber-500 mb-0.5">Today ({today.length})</p> <p className="text-[10px] font-semibold text-amber-500 uppercase tracking-wide">Today ({today.length})</p>
{today.slice(0, 5).map((t) => ( {today.slice(0, 5).map((t) => (
<div key={t.id} className="flex items-center gap-1.5 text-xs py-0.5"> <div key={t.id} className="flex items-center gap-2 text-xs py-0.5">
<div className="w-1.5 h-1.5 rounded-full bg-amber-500 shrink-0" /> <div className="w-1.5 h-1.5 rounded-full bg-amber-500 shrink-0" />
<span className="truncate flex-1">{t.title}</span> <span className="truncate flex-1">{t.title}</span>
</div> </div>
@@ -97,7 +96,7 @@ function HabitsTodayWidget() {
onError: (err) => toast.error(err.message || "Failed to complete habit"), onError: (err) => toast.error(err.message || "Failed to complete habit"),
}); });
return ( return (
<div className="space-y-0.5"> <div className="space-y-1">
{habits.length === 0 ? ( {habits.length === 0 ? (
<p className="text-xs text-muted-foreground">No habits yet</p> <p className="text-xs text-muted-foreground">No habits yet</p>
) : ( ) : (
@@ -108,7 +107,7 @@ function HabitsTodayWidget() {
return d.getUTCFullYear() === today.getUTCFullYear() && d.getUTCMonth() === today.getUTCMonth() && d.getUTCDate() === today.getUTCDate(); return d.getUTCFullYear() === today.getUTCFullYear() && d.getUTCMonth() === today.getUTCMonth() && d.getUTCDate() === today.getUTCDate();
}); });
return ( return (
<div key={h.id} className="flex items-center gap-1.5 py-0.5"> <div key={h.id} className="flex items-center gap-2 py-0.5">
<button <button
onClick={() => completeMutation.mutate(h.id)} onClick={() => completeMutation.mutate(h.id)}
className={cn("w-3.5 h-3.5 rounded border shrink-0 flex items-center justify-center", doneToday ? "bg-green-500 border-green-500" : "border-muted-foreground/30 hover:border-primary")} className={cn("w-3.5 h-3.5 rounded border shrink-0 flex items-center justify-center", doneToday ? "bg-green-500 border-green-500" : "border-muted-foreground/30 hover:border-primary")}
@@ -118,9 +117,9 @@ function HabitsTodayWidget() {
</button> </button>
<span className="text-xs truncate flex-1">{h.name}</span> <span className="text-xs truncate flex-1">{h.name}</span>
{h.streakCount > 0 && ( {h.streakCount > 0 && (
<Badge variant="secondary" className="text-[9px] px-1 shrink-0"> <span className="inline-flex items-center gap-0.5 text-[10px] text-muted-foreground shrink-0">
<Flame className="h-2 w-2 mr-0" />{h.streakCount} <Flame className="h-2.5 w-2.5 text-orange-400" />{h.streakCount}
</Badge> </span>
)} )}
</div> </div>
); );
@@ -136,14 +135,14 @@ function RecentNotesWidget() {
const { data } = useApiQuery<PaginatedResponse<Note>>(["recent-notes", activeDomainId], "/notes?limit=5&sort=-updated" + domainSuffix); const { data } = useApiQuery<PaginatedResponse<Note>>(["recent-notes", activeDomainId], "/notes?limit=5&sort=-updated" + domainSuffix);
const notes = data?.items || []; const notes = data?.items || [];
return ( return (
<div className="space-y-1.5"> <div className="space-y-2">
{notes.length === 0 ? ( {notes.length === 0 ? (
<p className="text-xs text-muted-foreground">No notes yet</p> <p className="text-xs text-muted-foreground">No notes yet</p>
) : ( ) : (
notes.map((n) => ( notes.map((n) => (
<div key={n.id} className="text-xs py-1 border-b last:border-0"> <div key={n.id} className="text-xs py-1.5 border-b border-border/40 last:border-0">
<p className="font-medium truncate">{n.title}</p> <p className="font-medium truncate">{n.title}</p>
<p className="text-[10px] text-muted-foreground">{format(parseISO(n.updatedAt), "MMM d, HH:mm")}</p> <p className="text-[10px] text-muted-foreground mt-0.5">{format(parseISO(n.updatedAt), "MMM d, HH:mm")}</p>
</div> </div>
)) ))
)} )}
@@ -157,17 +156,17 @@ function ActiveProjectsWidget() {
const { data } = useApiQuery<PaginatedResponse<Project>>(["active-projects", activeDomainId], "/projects?limit=10&status=active" + domainSuffix); const { data } = useApiQuery<PaginatedResponse<Project>>(["active-projects", activeDomainId], "/projects?limit=10&status=active" + domainSuffix);
const projects = data?.items || []; const projects = data?.items || [];
return ( return (
<div className="space-y-2"> <div className="space-y-3">
{projects.length === 0 ? ( {projects.length === 0 ? (
<p className="text-xs text-muted-foreground">No active projects</p> <p className="text-xs text-muted-foreground">No active projects</p>
) : ( ) : (
projects.slice(0, 5).map((p) => ( projects.slice(0, 5).map((p) => (
<div key={p.id} className="space-y-0.5"> <div key={p.id} className="space-y-1">
<div className="flex items-center justify-between text-xs"> <div className="flex items-center justify-between text-xs">
<span className="truncate flex-1">{p.name}</span> <span className="truncate flex-1 font-medium">{p.name}</span>
<span className="text-[10px] font-mono text-muted-foreground shrink-0 ml-2">{p.progress || 0}%</span> <span className="text-[10px] font-mono text-muted-foreground shrink-0 ml-2">{p.progress || 0}%</span>
</div> </div>
<Progress value={p.progress || 0} className="h-1" /> <Progress value={p.progress || 0} className="h-1.5 bg-muted/50" />
</div> </div>
)) ))
)} )}
@@ -187,12 +186,12 @@ function UpcomingEventsWidget() {
return start >= now && start <= weekFromNow; return start >= now && start <= weekFromNow;
}).sort((a, b) => parseISO(a.startTime).getTime() - parseISO(b.startTime).getTime()); }).sort((a, b) => parseISO(a.startTime).getTime() - parseISO(b.startTime).getTime());
return ( return (
<div className="space-y-1.5"> <div className="space-y-2">
{upcoming.length === 0 ? ( {upcoming.length === 0 ? (
<p className="text-xs text-muted-foreground">No upcoming events</p> <p className="text-xs text-muted-foreground">No upcoming events</p>
) : ( ) : (
upcoming.slice(0, 5).map((e) => ( upcoming.slice(0, 5).map((e) => (
<div key={e.id} className="flex items-center gap-1.5 text-xs py-0.5"> <div key={e.id} className="flex items-center gap-2 text-xs py-0.5">
<div className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: e.color || "#3b82f6" }} /> <div className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: e.color || "#3b82f6" }} />
<span className="truncate flex-1">{e.title}</span> <span className="truncate flex-1">{e.title}</span>
<span className="text-[10px] text-muted-foreground shrink-0">{format(parseISO(e.startTime), "MMM d, HH:mm")}</span> <span className="text-[10px] text-muted-foreground shrink-0">{format(parseISO(e.startTime), "MMM d, HH:mm")}</span>
@@ -211,16 +210,16 @@ function StreakCounterWidget() {
const bestStreak = Math.max(...habits.map((h) => h.streakCount || 0), 0); const bestStreak = Math.max(...habits.map((h) => h.streakCount || 0), 0);
const totalActive = habits.filter((h) => h.streakCount > 0).length; const totalActive = habits.filter((h) => h.streakCount > 0).length;
return ( return (
<div className="flex items-center gap-3 h-full"> <div className="flex items-center justify-center gap-6 h-full">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center gap-0.5">
<Flame className="h-6 w-6 text-orange-500" /> <Flame className="h-5 w-5 text-orange-500" />
<span className="text-xl font-mono font-semibold">{bestStreak}</span> <span className="text-lg font-mono font-semibold">{bestStreak}</span>
<span className="text-[10px] text-muted-foreground">Best streak</span> <span className="text-[10px] text-muted-foreground">Best streak</span>
</div> </div>
<Separator orientation="vertical" /> <Separator orientation="vertical" className="h-8" />
<div className="flex flex-col items-center"> <div className="flex flex-col items-center gap-0.5">
<span className="text-xl font-mono font-semibold">{totalActive}</span> <span className="text-lg font-mono font-semibold">{totalActive}</span>
<span className="text-[10px] text-muted-foreground">Active streaks</span> <span className="text-[10px] text-muted-foreground">Active</span>
</div> </div>
</div> </div>
); );
@@ -257,18 +256,16 @@ function QuickCaptureWidget() {
else createNote.mutate(text.trim()); else createNote.mutate(text.trim());
}; };
return ( return (
<form onSubmit={handleSubmit} className="flex gap-1.5"> <form onSubmit={handleSubmit} className="flex items-center gap-2">
<div className="flex-1 flex gap-1.5"> <Select value={type} onValueChange={(v) => setType(v as "task" | "note")}>
<Select value={type} onValueChange={(v) => setType(v as "task" | "note")}> <SelectTrigger className="w-[72px] h-8 text-xs shrink-0"><SelectValue /></SelectTrigger>
<SelectTrigger className="w-18 h-8 text-xs"><SelectValue /></SelectTrigger> <SelectContent>
<SelectContent> <SelectItem value="task">Task</SelectItem>
<SelectItem value="task">Task</SelectItem> <SelectItem value="note">Note</SelectItem>
<SelectItem value="note">Note</SelectItem> </SelectContent>
</SelectContent> </Select>
</Select> <Input placeholder="Quick capture..." value={text} onChange={(e) => setText(e.target.value)} className="h-8 flex-1 font-mono text-xs" />
<Input placeholder="Quick capture..." value={text} onChange={(e) => setText(e.target.value)} className="h-8 font-mono text-xs" /> <Button type="button" size="sm" className="h-8 px-3 text-xs shrink-0" disabled={!text.trim() || createTask.isPending || createNote.isPending} onClick={handleSubmit}>Add</Button>
</div>
<Button type="button" size="sm" className="h-8 px-2.5 text-xs" disabled={!text.trim() || createTask.isPending || createNote.isPending} onClick={handleSubmit}>Add</Button>
</form> </form>
); );
} }
@@ -280,19 +277,19 @@ function ProductivityChartWidget() {
const stats = data; const stats = data;
if (!stats) return <p className="text-xs text-muted-foreground">Loading...</p>; if (!stats) return <p className="text-xs text-muted-foreground">Loading...</p>;
return ( return (
<div className="space-y-2"> <div className="space-y-3">
<div className="grid grid-cols-3 gap-1.5"> <div className="grid grid-cols-3 gap-2">
<div className="text-center p-1.5 bg-muted/30 rounded"> <div className="text-center p-2 bg-muted/30 rounded-md">
<p className="text-base font-mono font-semibold">{stats.totalTasks || 0}</p> <p className="text-base font-mono font-semibold">{stats.totalTasks || 0}</p>
<p className="text-[9px] text-muted-foreground">Total</p> <p className="text-[10px] text-muted-foreground mt-0.5">Total</p>
</div> </div>
<div className="text-center p-1.5 bg-muted/30 rounded"> <div className="text-center p-2 bg-muted/30 rounded-md">
<p className="text-base font-mono font-semibold text-green-500">{stats.completedTasks || 0}</p> <p className="text-base font-mono font-semibold text-green-500">{stats.completedTasks || 0}</p>
<p className="text-[9px] text-muted-foreground">Done</p> <p className="text-[10px] text-muted-foreground mt-0.5">Done</p>
</div> </div>
<div className="text-center p-1.5 bg-muted/30 rounded"> <div className="text-center p-2 bg-muted/30 rounded-md">
<p className="text-base font-mono font-semibold">{stats.taskCompletionRate || 0}%</p> <p className="text-base font-mono font-semibold">{stats.taskCompletionRate || 0}%</p>
<p className="text-[9px] text-muted-foreground">Rate</p> <p className="text-[10px] text-muted-foreground mt-0.5">Rate</p>
</div> </div>
</div> </div>
<p className="text-[10px] text-muted-foreground text-center">Last {stats.period || 30} days</p> <p className="text-[10px] text-muted-foreground text-center">Last {stats.period || 30} days</p>
@@ -310,15 +307,15 @@ function StatsWidget() {
<div className="grid grid-cols-3 gap-2 h-full items-center"> <div className="grid grid-cols-3 gap-2 h-full items-center">
<div className="text-center"> <div className="text-center">
<p className="text-xl font-mono font-semibold">{stats.totalTasks || 0}</p> <p className="text-xl font-mono font-semibold">{stats.totalTasks || 0}</p>
<p className="text-[10px] text-muted-foreground">Total Tasks</p> <p className="text-[10px] text-muted-foreground mt-0.5">Total Tasks</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-xl font-mono font-semibold text-green-500">{stats.completedTasks || 0}</p> <p className="text-xl font-mono font-semibold text-green-500">{stats.completedTasks || 0}</p>
<p className="text-[10px] text-muted-foreground">Completed</p> <p className="text-[10px] text-muted-foreground mt-0.5">Completed</p>
</div> </div>
<div className="text-center"> <div className="text-center">
<p className="text-xl font-mono font-semibold">{stats.taskCompletionRate || 0}%</p> <p className="text-xl font-mono font-semibold">{stats.taskCompletionRate || 0}%</p>
<p className="text-[10px] text-muted-foreground">Rate</p> <p className="text-[10px] text-muted-foreground mt-0.5">Rate</p>
</div> </div>
</div> </div>
); );
@@ -345,12 +342,12 @@ function WidgetCard({ widget, onConfigure, onDelete }: { widget: DashboardWidget
return ( return (
<div <div
className={cn( className={cn(
"h-full flex flex-col group border rounded-lg border-l-[3px] transition-colors hover:bg-muted/20 lg:[grid-column:span_var(--w)] lg:[grid-row:span_var(--h)]", "min-h-0 h-full flex flex-col group border border-border/60 rounded-lg border-l-[3px] bg-card transition-shadow hover:shadow-md hover:shadow-black/5 lg:[grid-column:span_var(--w)] lg:[grid-row:span_var(--h)]",
getWidgetBorder(widget.type) getWidgetBorder(widget.type)
)} )}
style={{ "--w": Math.min(widget.layout.w || 2, 4), "--h": widget.layout.h || 2 } as React.CSSProperties} style={{ "--w": Math.min(widget.layout.w || 2, 4), "--h": widget.layout.h || 2 } as React.CSSProperties}
> >
<div className="px-2.5 pt-2.5 pb-0 flex flex-row items-center justify-between gap-2"> <div className="px-3 pt-2.5 pb-1 flex flex-row items-center justify-between gap-2">
<div className="flex items-center gap-1.5 min-w-0"> <div className="flex items-center gap-1.5 min-w-0">
<Icon className={cn("h-3.5 w-3.5 shrink-0", getWidgetAccent(widget.type))} /> <Icon className={cn("h-3.5 w-3.5 shrink-0", getWidgetAccent(widget.type))} />
<span className="text-xs font-semibold truncate">{widget.title || typeInfo?.label || widget.type}</span> <span className="text-xs font-semibold truncate">{widget.title || typeInfo?.label || widget.type}</span>
@@ -360,7 +357,7 @@ function WidgetCard({ widget, onConfigure, onDelete }: { widget: DashboardWidget
<Button variant="ghost" size="icon" className="h-5 w-5 text-destructive" onClick={onDelete} aria-label="Remove widget"><Trash2 className="h-3 w-3" /></Button> <Button variant="ghost" size="icon" className="h-5 w-5 text-destructive" onClick={onDelete} aria-label="Remove widget"><Trash2 className="h-3 w-3" /></Button>
</div> </div>
</div> </div>
<div className="p-2.5 flex-1 overflow-auto"> <div className="px-3 pb-3 pt-1 flex-1 overflow-hidden">
<WidgetRenderer type={widget.type} /> <WidgetRenderer type={widget.type} />
</div> </div>
</div> </div>
@@ -377,9 +374,9 @@ function AddWidgetDialog({ open, onOpenChange, onAdd }: { open: boolean; onOpenC
const Icon = wt.icon; const Icon = wt.icon;
return ( return (
<button key={wt.id} onClick={() => { onAdd(wt.id); onOpenChange(false); }} <button key={wt.id} onClick={() => { onAdd(wt.id); onOpenChange(false); }}
className="flex flex-col items-center gap-1.5 p-3 rounded-lg border hover:bg-accent hover:border-primary transition-colors text-center"> className="flex flex-col items-center gap-2 p-4 rounded-lg border border-border/60 hover:bg-accent hover:border-primary/40 transition-colors text-center">
<Icon className={cn("h-5 w-5", wt.accent)} /> <Icon className={cn("h-5 w-5", wt.accent)} />
<span className="text-[10px] font-medium">{wt.label}</span> <span className="text-[10px] font-medium leading-tight">{wt.label}</span>
</button> </button>
); );
})} })}
@@ -401,16 +398,16 @@ function ConfigureWidgetDialog({ widget, open, onOpenChange, onSave }: { widget:
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent> <DialogContent>
<DialogHeader><DialogTitle className="text-sm">Configure Widget</DialogTitle></DialogHeader> <DialogHeader><DialogTitle className="text-sm">Configure Widget</DialogTitle></DialogHeader>
<div className="space-y-3"> <div className="space-y-4">
<div> <div>
<Label htmlFor="widget-title" className="text-xs">Title</Label> <Label htmlFor="widget-title" className="text-xs">Title</Label>
<Input id="widget-title" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Widget title" className="h-8 text-xs mt-1" /> <Input id="widget-title" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Widget title" className="h-8 text-xs mt-1.5" />
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<Label htmlFor="widget-w" className="text-xs">Width (columns)</Label> <Label htmlFor="widget-w" className="text-xs">Width (columns)</Label>
<Select value={String(w)} onValueChange={(v) => setW(parseInt(v))}> <Select value={String(w)} onValueChange={(v) => setW(parseInt(v))}>
<SelectTrigger id="widget-w" className="h-8 text-xs mt-1"><SelectValue /></SelectTrigger> <SelectTrigger id="widget-w" className="h-8 text-xs mt-1.5"><SelectValue /></SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="1">1 column</SelectItem> <SelectItem value="1">1 column</SelectItem>
<SelectItem value="2">2 columns</SelectItem> <SelectItem value="2">2 columns</SelectItem>
@@ -422,7 +419,7 @@ function ConfigureWidgetDialog({ widget, open, onOpenChange, onSave }: { widget:
<div> <div>
<Label htmlFor="widget-h" className="text-xs">Height (rows)</Label> <Label htmlFor="widget-h" className="text-xs">Height (rows)</Label>
<Select value={String(h)} onValueChange={(v) => setH(parseInt(v))}> <Select value={String(h)} onValueChange={(v) => setH(parseInt(v))}>
<SelectTrigger id="widget-h" className="h-8 text-xs mt-1"><SelectValue /></SelectTrigger> <SelectTrigger id="widget-h" className="h-8 text-xs mt-1.5"><SelectValue /></SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="1">1 row</SelectItem> <SelectItem value="1">1 row</SelectItem>
<SelectItem value="2">2 rows</SelectItem> <SelectItem value="2">2 rows</SelectItem>
@@ -432,7 +429,7 @@ function ConfigureWidgetDialog({ widget, open, onOpenChange, onSave }: { widget:
</Select> </Select>
</div> </div>
</div> </div>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2 pt-1">
<Button variant="outline" size="sm" onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="outline" size="sm" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button size="sm" onClick={handleSave}>Save</Button> <Button size="sm" onClick={handleSave}>Save</Button>
</div> </div>
@@ -485,7 +482,7 @@ function DashboardPage() {
}; };
const handleDelete = (id: string) => { deleteMutation.mutate(id); }; const handleDelete = (id: string) => { deleteMutation.mutate(id); };
return ( return (
<div className="space-y-3"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-xl font-bold">Dashboard</h1> <h1 className="text-xl font-bold">Dashboard</h1>
<Button onClick={() => setAddOpen(true)} size="sm" aria-label="Add widget"><Plus className="h-3.5 w-3.5 mr-1.5" />Add Widget</Button> <Button onClick={() => setAddOpen(true)} size="sm" aria-label="Add widget"><Plus className="h-3.5 w-3.5 mr-1.5" />Add Widget</Button>
@@ -493,7 +490,7 @@ function DashboardPage() {
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center py-10 text-xs text-muted-foreground">Loading dashboard...</div> <div className="flex items-center justify-center py-10 text-xs text-muted-foreground">Loading dashboard...</div>
) : widgets.length === 0 ? ( ) : widgets.length === 0 ? (
<div className="text-center py-10 border rounded-lg border-dashed"> <div className="text-center py-12 border rounded-lg border-dashed border-border/60">
<Zap className="h-6 w-6 mx-auto mb-2 text-muted-foreground/50" /> <Zap className="h-6 w-6 mx-auto mb-2 text-muted-foreground/50" />
<p className="text-xs text-muted-foreground mb-3">Your dashboard is empty. Add some widgets to get started.</p> <p className="text-xs text-muted-foreground mb-3">Your dashboard is empty. Add some widgets to get started.</p>
<Button size="sm" onClick={() => { <Button size="sm" onClick={() => {
@@ -505,7 +502,7 @@ function DashboardPage() {
}}><Plus className="h-3.5 w-3.5 mr-1.5" />Add Default Widgets</Button> }}><Plus className="h-3.5 w-3.5 mr-1.5" />Add Default Widgets</Button>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 gap-3 auto-rows-[minmax(110px,auto)] sm:grid-cols-2 lg:grid-cols-4"> <div className="grid grid-cols-1 gap-4 auto-rows-[minmax(120px,auto)] sm:grid-cols-2 lg:grid-cols-4">
{widgets.map((w) => ( {widgets.map((w) => (
<WidgetCard key={w.id} widget={w} onConfigure={() => handleConfigure(w)} onDelete={() => handleDelete(w.id)} /> <WidgetCard key={w.id} widget={w} onConfigure={() => handleConfigure(w)} onDelete={() => handleDelete(w.id)} />
))} ))}
+1 -1
View File
@@ -338,7 +338,7 @@ function GraphPage() {
}, []); }, []);
return ( return (
<div className="flex h-[calc(100vh-8rem)] -m-4 md:-m-6"> <div className="flex h-[calc(100vh-8rem)]">
{/* Graph canvas area */} {/* Graph canvas area */}
<div ref={containerRef} className="flex-1 relative bg-muted/20"> <div ref={containerRef} className="flex-1 relative bg-muted/20">
{/* Toolbar */} {/* Toolbar */}
+3 -3
View File
@@ -227,12 +227,12 @@ function HabitsPage() {
{habits.map((habit) => ( {habits.map((habit) => (
<Card key={habit.id} className="cursor-pointer border hover:bg-muted/20 transition-colors" onClick={() => openHabitDetail(habit)}> <Card key={habit.id} className="cursor-pointer border hover:bg-muted/20 transition-colors" onClick={() => openHabitDetail(habit)}>
<CardContent className="p-3"> <CardContent className="p-3">
<div className="flex items-center justify-between"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div className="flex items-center gap-3 flex-1 min-w-0"> <div className="flex items-center gap-3 flex-1 min-w-0">
<Flame className={cn("h-5 w-5 shrink-0", habit.streakCount > 0 ? "text-orange-500" : "text-muted-foreground")} /> <Flame className={cn("h-5 w-5 shrink-0", habit.streakCount > 0 ? "text-orange-500" : "text-muted-foreground")} />
<div className="min-w-0"> <div className="min-w-0">
<p className="font-medium truncate">{habit.name}</p> <p className="font-medium truncate">{habit.name}</p>
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1 flex-wrap">
<span className="font-mono text-xs text-muted-foreground"> <span className="font-mono text-xs text-muted-foreground">
<Flame className="h-3 w-3 inline mr-0.5" /> <Flame className="h-3 w-3 inline mr-0.5" />
{habit.streakCount} day streak {habit.streakCount} day streak
@@ -242,7 +242,7 @@ function HabitsPage() {
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-3 shrink-0"> <div className="flex items-center gap-2 sm:gap-3 shrink-0 pl-8 sm:pl-0">
<MiniGrid completions={habit.recentCompletions || []} /> <MiniGrid completions={habit.recentCompletions || []} />
<Button <Button
size="sm" size="sm"
+1 -1
View File
@@ -14,7 +14,7 @@ function AppLayout() {
<Sidebar /> <Sidebar />
<div className="flex flex-1 flex-col overflow-hidden"> <div className="flex flex-1 flex-col overflow-hidden">
<Topbar /> <Topbar />
<main className="flex-1 overflow-auto p-6"> <main className="flex-1 overflow-auto p-3 md:p-6">
<Outlet /> <Outlet />
</main> </main>
</div> </div>
+1 -1
View File
@@ -253,7 +253,7 @@ function NotesPage() {
}, [deleteMutation]); }, [deleteMutation]);
return ( return (
<div className="flex flex-col md:flex-row h-auto min-h-[calc(100vh-8rem)] md:h-[calc(100vh-8rem)] -m-4 md:-m-6"> <div className="flex flex-col md:flex-row h-auto min-h-[calc(100vh-8rem)] md:h-[calc(100vh-8rem)]">
{/* Left pane - note list */} {/* Left pane - note list */}
<div className="w-full md:w-72 h-64 md:h-auto border-b md:border-b-0 md:border-r flex flex-col shrink-0"> <div className="w-full md:w-72 h-64 md:h-auto border-b md:border-b-0 md:border-r flex flex-col shrink-0">
<div className="px-3 py-2 border-b"> <div className="px-3 py-2 border-b">
+1 -1
View File
@@ -271,7 +271,7 @@ function ProjectsPage() {
<Input placeholder="Search projects..." value={search} onChange={(e) => setSearch(e.target.value)} className="h-8 pl-8" aria-label="Search projects" /> <Input placeholder="Search projects..." value={search} onChange={(e) => setSearch(e.target.value)} className="h-8 pl-8" aria-label="Search projects" />
</div> </div>
<Select value={statusFilter || "__all__"} onValueChange={(v) => setStatusFilter(v === "__all__" ? "" : v)}> <Select value={statusFilter || "__all__"} onValueChange={(v) => setStatusFilter(v === "__all__" ? "" : v)}>
<SelectTrigger className="h-8 w-40" aria-label="Filter by status"><SelectValue placeholder="All statuses" /></SelectTrigger> <SelectTrigger className="h-8 w-full sm:w-40" aria-label="Filter by status"><SelectValue placeholder="All statuses" /></SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="__all__">All statuses</SelectItem> <SelectItem value="__all__">All statuses</SelectItem>
<SelectItem value="active">Active</SelectItem> <SelectItem value="active">Active</SelectItem>
+3 -2
View File
@@ -446,7 +446,7 @@ function ProjectTasks({ project }: { project: Project }) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex gap-2"> <div className="flex flex-col sm:flex-row gap-2">
<Input <Input
value={newTitle} value={newTitle}
onChange={(e) => setNewTitle(e.target.value)} onChange={(e) => setNewTitle(e.target.value)}
@@ -460,7 +460,7 @@ function ProjectTasks({ project }: { project: Project }) {
className="h-9" className="h-9"
/> />
<Select value={sectionId} onValueChange={setSectionId}> <Select value={sectionId} onValueChange={setSectionId}>
<SelectTrigger className="h-9 w-44" aria-label="Section"> <SelectTrigger className="h-9 w-full sm:w-44" aria-label="Section">
<SelectValue placeholder="No section" /> <SelectValue placeholder="No section" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@@ -476,6 +476,7 @@ function ProjectTasks({ project }: { project: Project }) {
size="sm" size="sm"
onClick={submitNewTask} onClick={submitNewTask}
disabled={!newTitle.trim() || addMutation.isPending} disabled={!newTitle.trim() || addMutation.isPending}
className="sm:w-auto"
> >
<Plus className="h-4 w-4" /> Add <Plus className="h-4 w-4" /> Add
</Button> </Button>
+1 -1
View File
@@ -582,7 +582,7 @@ function ShortcutsTab() {
<div <div
key={shortcut.id} key={shortcut.id}
className={cn( className={cn(
"flex items-center justify-between py-2 px-3 rounded-lg transition-colors", "flex flex-col sm:flex-row sm:items-center sm:justify-between py-2 px-3 rounded-lg transition-colors gap-1 sm:gap-0",
isRecording ? "bg-primary/5 ring-1 ring-primary" : "hover:bg-muted/50" isRecording ? "bg-primary/5 ring-1 ring-primary" : "hover:bg-muted/50"
)} )}
> >
+2 -2
View File
@@ -823,7 +823,7 @@ function TasksPage() {
} }
}} }}
> >
<SelectTrigger className="h-7 w-44 text-xs"><SelectValue placeholder="Saved views" /></SelectTrigger> <SelectTrigger className="h-7 w-full sm:w-44 text-xs"><SelectValue placeholder="Saved views" /></SelectTrigger>
<SelectContent> <SelectContent>
{savedViews.map((v) => ( {savedViews.map((v) => (
<SelectItem key={v.id} value={v.id}> <SelectItem key={v.id} value={v.id}>
@@ -945,7 +945,7 @@ function TasksPage() {
) : view === "calendar" ? ( ) : view === "calendar" ? (
<TasksCalendar tasks={tasks} onSelectTask={openTaskPanel} /> <TasksCalendar tasks={tasks} onSelectTask={openTaskPanel} />
) : ( ) : (
<div className="relative rounded-lg border border-[#d9dee7] bg-white"> <div className="relative rounded-lg border border-[#d9dee7] bg-white overflow-x-auto">
{view === "list" && showKbdHint && ( {view === "list" && showKbdHint && (
<div className="absolute right-0 top-0 z-10 flex gap-3 p-2 text-[10px] text-muted-foreground"> <div className="absolute right-0 top-0 z-10 flex gap-3 p-2 text-[10px] text-muted-foreground">
<span><kbd className="rounded border bg-muted px-1 py-0.5 font-mono">j</kbd>/<kbd className="rounded border bg-muted px-1 py-0.5 font-mono">k</kbd> navigate</span> <span><kbd className="rounded border bg-muted px-1 py-0.5 font-mono">j</kbd>/<kbd className="rounded border bg-muted px-1 py-0.5 font-mono">k</kbd> navigate</span>
+1 -1
View File
@@ -117,7 +117,7 @@ function TemplatesPage() {
); );
return ( return (
<div className="flex flex-col h-auto min-h-[calc(100vh-8rem)] md:h-[calc(100vh-8rem)] -m-4 md:-m-6"> <div className="flex flex-col h-auto min-h-[calc(100vh-8rem)] md:h-[calc(100vh-8rem)]">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b shrink-0"> <div className="flex items-center justify-between px-4 py-3 border-b shrink-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">