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:
@@ -67,7 +67,7 @@ export function Topbar() {
|
||||
|
||||
return (
|
||||
<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"
|
||||
>
|
||||
{/* Mobile menu */}
|
||||
@@ -91,18 +91,20 @@ export function Topbar() {
|
||||
aria-label="Open search (Cmd+K)"
|
||||
>
|
||||
<Search className="mr-2 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||
<span className="truncate">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">
|
||||
<span className="truncate hidden sm:inline">Search or jump to...</span>
|
||||
<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
|
||||
</kbd>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Domain picker */}
|
||||
<DomainPicker />
|
||||
<div className="hidden sm:block">
|
||||
<DomainPicker />
|
||||
</div>
|
||||
|
||||
{/* 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 */}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -142,7 +144,7 @@ export function Topbar() {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltipText}</TooltipContent>
|
||||
</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">
|
||||
<span className="text-sm font-medium">Notifications</span>
|
||||
<Button
|
||||
|
||||
@@ -370,7 +370,7 @@ function CalendarPage() {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{["month", "week", "day", "agenda"].map((name) => (
|
||||
<Button
|
||||
key={name}
|
||||
|
||||
@@ -30,15 +30,10 @@ function CalendarSidebar({ selectedDate, onSelectDate }: { selectedDate: Date; o
|
||||
const activeDomainId = useApiDomain();
|
||||
const { data } = useApiQuery<{ items: DailyNote[]; totalItems: number }>(["daily-notes-list", activeDomainId], "/daily-notes" + (activeDomainId ? "?domain=" + activeDomainId : ""));
|
||||
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)));
|
||||
|
||||
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">
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}>
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
@@ -361,10 +356,10 @@ function DailyNotesPage() {
|
||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||
|
||||
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} />
|
||||
<Separator orientation="vertical" />
|
||||
<ScrollArea className="flex-1 pr-3">
|
||||
<Separator className="hidden md:block" orientation="vertical" />
|
||||
<ScrollArea className="flex-1 md:pr-3 min-h-[60vh] md:min-h-0">
|
||||
<DailyNoteEditor date={selectedDate} />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Plus, Settings2, Trash2, ListTodo, Flame, FileText, FolderKanban, Calen
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -48,16 +47,16 @@ function TasksDueWidget() {
|
||||
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");
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-2">
|
||||
{today.length === 0 && overdue.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">No tasks due today</p>
|
||||
) : (
|
||||
<>
|
||||
{overdue.length > 0 && (
|
||||
<div>
|
||||
<p className="text-[10px] font-semibold text-destructive mb-0.5">Overdue ({overdue.length})</p>
|
||||
<div className="space-y-1">
|
||||
<p className="text-[10px] font-semibold text-destructive uppercase tracking-wide">Overdue ({overdue.length})</p>
|
||||
{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" />
|
||||
<span className="truncate flex-1">{t.title}</span>
|
||||
</div>
|
||||
@@ -65,10 +64,10 @@ function TasksDueWidget() {
|
||||
</div>
|
||||
)}
|
||||
{today.length > 0 && (
|
||||
<div>
|
||||
<p className="text-[10px] font-semibold text-amber-500 mb-0.5">Today ({today.length})</p>
|
||||
<div className="space-y-1">
|
||||
<p className="text-[10px] font-semibold text-amber-500 uppercase tracking-wide">Today ({today.length})</p>
|
||||
{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" />
|
||||
<span className="truncate flex-1">{t.title}</span>
|
||||
</div>
|
||||
@@ -97,7 +96,7 @@ function HabitsTodayWidget() {
|
||||
onError: (err) => toast.error(err.message || "Failed to complete habit"),
|
||||
});
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<div className="space-y-1">
|
||||
{habits.length === 0 ? (
|
||||
<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 (
|
||||
<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
|
||||
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")}
|
||||
@@ -118,9 +117,9 @@ function HabitsTodayWidget() {
|
||||
</button>
|
||||
<span className="text-xs truncate flex-1">{h.name}</span>
|
||||
{h.streakCount > 0 && (
|
||||
<Badge variant="secondary" className="text-[9px] px-1 shrink-0">
|
||||
<Flame className="h-2 w-2 mr-0" />{h.streakCount}
|
||||
</Badge>
|
||||
<span className="inline-flex items-center gap-0.5 text-[10px] text-muted-foreground shrink-0">
|
||||
<Flame className="h-2.5 w-2.5 text-orange-400" />{h.streakCount}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -136,14 +135,14 @@ function RecentNotesWidget() {
|
||||
const { data } = useApiQuery<PaginatedResponse<Note>>(["recent-notes", activeDomainId], "/notes?limit=5&sort=-updated" + domainSuffix);
|
||||
const notes = data?.items || [];
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-2">
|
||||
{notes.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">No notes yet</p>
|
||||
) : (
|
||||
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="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>
|
||||
))
|
||||
)}
|
||||
@@ -157,17 +156,17 @@ function ActiveProjectsWidget() {
|
||||
const { data } = useApiQuery<PaginatedResponse<Project>>(["active-projects", activeDomainId], "/projects?limit=10&status=active" + domainSuffix);
|
||||
const projects = data?.items || [];
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
{projects.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">No active projects</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">
|
||||
<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>
|
||||
</div>
|
||||
<Progress value={p.progress || 0} className="h-1" />
|
||||
<Progress value={p.progress || 0} className="h-1.5 bg-muted/50" />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -187,12 +186,12 @@ function UpcomingEventsWidget() {
|
||||
return start >= now && start <= weekFromNow;
|
||||
}).sort((a, b) => parseISO(a.startTime).getTime() - parseISO(b.startTime).getTime());
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-2">
|
||||
{upcoming.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">No upcoming events</p>
|
||||
) : (
|
||||
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" }} />
|
||||
<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>
|
||||
@@ -211,16 +210,16 @@ function StreakCounterWidget() {
|
||||
const bestStreak = Math.max(...habits.map((h) => h.streakCount || 0), 0);
|
||||
const totalActive = habits.filter((h) => h.streakCount > 0).length;
|
||||
return (
|
||||
<div className="flex items-center gap-3 h-full">
|
||||
<div className="flex flex-col items-center">
|
||||
<Flame className="h-6 w-6 text-orange-500" />
|
||||
<span className="text-xl font-mono font-semibold">{bestStreak}</span>
|
||||
<div className="flex items-center justify-center gap-6 h-full">
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<Flame className="h-5 w-5 text-orange-500" />
|
||||
<span className="text-lg font-mono font-semibold">{bestStreak}</span>
|
||||
<span className="text-[10px] text-muted-foreground">Best streak</span>
|
||||
</div>
|
||||
<Separator orientation="vertical" />
|
||||
<div className="flex flex-col items-center">
|
||||
<span className="text-xl font-mono font-semibold">{totalActive}</span>
|
||||
<span className="text-[10px] text-muted-foreground">Active streaks</span>
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<span className="text-lg font-mono font-semibold">{totalActive}</span>
|
||||
<span className="text-[10px] text-muted-foreground">Active</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -257,18 +256,16 @@ function QuickCaptureWidget() {
|
||||
else createNote.mutate(text.trim());
|
||||
};
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex gap-1.5">
|
||||
<div className="flex-1 flex gap-1.5">
|
||||
<Select value={type} onValueChange={(v) => setType(v as "task" | "note")}>
|
||||
<SelectTrigger className="w-18 h-8 text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="task">Task</SelectItem>
|
||||
<SelectItem value="note">Note</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input placeholder="Quick capture..." value={text} onChange={(e) => setText(e.target.value)} className="h-8 font-mono text-xs" />
|
||||
</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 onSubmit={handleSubmit} className="flex items-center gap-2">
|
||||
<Select value={type} onValueChange={(v) => setType(v as "task" | "note")}>
|
||||
<SelectTrigger className="w-[72px] h-8 text-xs shrink-0"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="task">Task</SelectItem>
|
||||
<SelectItem value="note">Note</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input placeholder="Quick capture..." value={text} onChange={(e) => setText(e.target.value)} className="h-8 flex-1 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>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -280,19 +277,19 @@ function ProductivityChartWidget() {
|
||||
const stats = data;
|
||||
if (!stats) return <p className="text-xs text-muted-foreground">Loading...</p>;
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
<div className="text-center p-1.5 bg-muted/30 rounded">
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<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-[9px] text-muted-foreground">Total</p>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">Total</p>
|
||||
</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-[9px] text-muted-foreground">Done</p>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">Done</p>
|
||||
</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-[9px] text-muted-foreground">Rate</p>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">Rate</p>
|
||||
</div>
|
||||
</div>
|
||||
<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="text-center">
|
||||
<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 className="text-center">
|
||||
<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 className="text-center">
|
||||
<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>
|
||||
);
|
||||
@@ -345,12 +342,12 @@ function WidgetCard({ widget, onConfigure, onDelete }: { widget: DashboardWidget
|
||||
return (
|
||||
<div
|
||||
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)
|
||||
)}
|
||||
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">
|
||||
<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>
|
||||
@@ -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>
|
||||
</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} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -377,9 +374,9 @@ function AddWidgetDialog({ open, onOpenChange, onAdd }: { open: boolean; onOpenC
|
||||
const Icon = wt.icon;
|
||||
return (
|
||||
<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)} />
|
||||
<span className="text-[10px] font-medium">{wt.label}</span>
|
||||
<span className="text-[10px] font-medium leading-tight">{wt.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -401,16 +398,16 @@ function ConfigureWidgetDialog({ widget, open, onOpenChange, onSave }: { widget:
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader><DialogTitle className="text-sm">Configure Widget</DialogTitle></DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<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 className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="widget-w" className="text-xs">Width (columns)</Label>
|
||||
<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>
|
||||
<SelectItem value="1">1 column</SelectItem>
|
||||
<SelectItem value="2">2 columns</SelectItem>
|
||||
@@ -422,7 +419,7 @@ function ConfigureWidgetDialog({ widget, open, onOpenChange, onSave }: { widget:
|
||||
<div>
|
||||
<Label htmlFor="widget-h" className="text-xs">Height (rows)</Label>
|
||||
<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>
|
||||
<SelectItem value="1">1 row</SelectItem>
|
||||
<SelectItem value="2">2 rows</SelectItem>
|
||||
@@ -432,7 +429,7 @@ function ConfigureWidgetDialog({ widget, open, onOpenChange, onSave }: { widget:
|
||||
</Select>
|
||||
</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 size="sm" onClick={handleSave}>Save</Button>
|
||||
</div>
|
||||
@@ -485,7 +482,7 @@ function DashboardPage() {
|
||||
};
|
||||
const handleDelete = (id: string) => { deleteMutation.mutate(id); };
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<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>
|
||||
@@ -493,7 +490,7 @@ function DashboardPage() {
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-10 text-xs text-muted-foreground">Loading dashboard...</div>
|
||||
) : 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" />
|
||||
<p className="text-xs text-muted-foreground mb-3">Your dashboard is empty. Add some widgets to get started.</p>
|
||||
<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>
|
||||
</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) => (
|
||||
<WidgetCard key={w.id} widget={w} onConfigure={() => handleConfigure(w)} onDelete={() => handleDelete(w.id)} />
|
||||
))}
|
||||
|
||||
@@ -338,7 +338,7 @@ function GraphPage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-8rem)] -m-4 md:-m-6">
|
||||
<div className="flex h-[calc(100vh-8rem)]">
|
||||
{/* Graph canvas area */}
|
||||
<div ref={containerRef} className="flex-1 relative bg-muted/20">
|
||||
{/* Toolbar */}
|
||||
|
||||
@@ -227,12 +227,12 @@ function HabitsPage() {
|
||||
{habits.map((habit) => (
|
||||
<Card key={habit.id} className="cursor-pointer border hover:bg-muted/20 transition-colors" onClick={() => openHabitDetail(habit)}>
|
||||
<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">
|
||||
<Flame className={cn("h-5 w-5 shrink-0", habit.streakCount > 0 ? "text-orange-500" : "text-muted-foreground")} />
|
||||
<div className="min-w-0">
|
||||
<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">
|
||||
<Flame className="h-3 w-3 inline mr-0.5" />
|
||||
{habit.streakCount} day streak
|
||||
@@ -242,7 +242,7 @@ function HabitsPage() {
|
||||
</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 || []} />
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -14,7 +14,7 @@ function AppLayout() {
|
||||
<Sidebar />
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<Topbar />
|
||||
<main className="flex-1 overflow-auto p-6">
|
||||
<main className="flex-1 overflow-auto p-3 md:p-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -253,7 +253,7 @@ function NotesPage() {
|
||||
}, [deleteMutation]);
|
||||
|
||||
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 */}
|
||||
<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">
|
||||
|
||||
@@ -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" />
|
||||
</div>
|
||||
<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>
|
||||
<SelectItem value="__all__">All statuses</SelectItem>
|
||||
<SelectItem value="active">Active</SelectItem>
|
||||
|
||||
@@ -446,7 +446,7 @@ function ProjectTasks({ project }: { project: Project }) {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Input
|
||||
value={newTitle}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
@@ -460,7 +460,7 @@ function ProjectTasks({ project }: { project: Project }) {
|
||||
className="h-9"
|
||||
/>
|
||||
<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" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -476,6 +476,7 @@ function ProjectTasks({ project }: { project: Project }) {
|
||||
size="sm"
|
||||
onClick={submitNewTask}
|
||||
disabled={!newTitle.trim() || addMutation.isPending}
|
||||
className="sm:w-auto"
|
||||
>
|
||||
<Plus className="h-4 w-4" /> Add
|
||||
</Button>
|
||||
|
||||
@@ -582,7 +582,7 @@ function ShortcutsTab() {
|
||||
<div
|
||||
key={shortcut.id}
|
||||
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"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
{savedViews.map((v) => (
|
||||
<SelectItem key={v.id} value={v.id}>
|
||||
@@ -945,7 +945,7 @@ function TasksPage() {
|
||||
) : view === "calendar" ? (
|
||||
<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 && (
|
||||
<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>
|
||||
|
||||
@@ -117,7 +117,7 @@ function TemplatesPage() {
|
||||
);
|
||||
|
||||
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 */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user