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 (
|
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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user