Mobile UI polish: global font scaling, tighter dashboard, cleaner calendar
Scale down all content text on mobile via .mobile-scale CSS class (excludes navbar/UMBRA title). Hide calendar event times in month view (Google Calendar style). Restructure CategoryFilterBar so categories display on a separate row when toggled instead of being hidden behind the search bar. Reduce dashboard widget density with hidden badges and tighter spacing on small screens. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ec8f5a9b4e
commit
023fa86b65
@ -110,7 +110,7 @@ export default function DashboardPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Header — greeting + date + quick add */}
|
{/* Header — greeting + date + quick add */}
|
||||||
<div className="px-4 md:px-6 pt-6 pb-2 flex items-center justify-between">
|
<div className="px-4 md:px-6 pt-4 sm:pt-6 pb-1 sm:pb-2 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-heading text-3xl font-bold tracking-tight animate-fade-in">
|
<h1 className="font-heading text-3xl font-bold tracking-tight animate-fade-in">
|
||||||
{getGreeting(settings?.preferred_name || undefined)}
|
{getGreeting(settings?.preferred_name || undefined)}
|
||||||
@ -157,7 +157,7 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto px-4 md:px-6 pb-6">
|
<div className="flex-1 overflow-y-auto px-4 md:px-6 pb-6">
|
||||||
<div className="space-y-5">
|
<div className="space-y-3 sm:space-y-5">
|
||||||
{/* Week Timeline */}
|
{/* Week Timeline */}
|
||||||
{upcomingData && (
|
{upcomingData && (
|
||||||
<div className="animate-slide-up">
|
<div className="animate-slide-up">
|
||||||
@ -187,7 +187,7 @@ export default function DashboardPage() {
|
|||||||
<AlertBanner alerts={alerts} onDismiss={dismissAlert} onSnooze={snoozeAlert} />
|
<AlertBanner alerts={alerts} onDismiss={dismissAlert} onSnooze={snoozeAlert} />
|
||||||
|
|
||||||
{/* Main Content — 2 columns */}
|
{/* Main Content — 2 columns */}
|
||||||
<div className="grid gap-5 lg:grid-cols-5 animate-slide-up" style={{ animationDelay: '100ms', animationFillMode: 'backwards' }}>
|
<div className="grid gap-3 sm:gap-5 lg:grid-cols-5 animate-slide-up" style={{ animationDelay: '100ms', animationFillMode: 'backwards' }}>
|
||||||
{/* Left: Upcoming feed (wider) */}
|
{/* Left: Upcoming feed (wider) */}
|
||||||
<div className="lg:col-span-3 flex flex-col">
|
<div className="lg:col-span-3 flex flex-col">
|
||||||
{upcomingData && upcomingData.items.length > 0 ? (
|
{upcomingData && upcomingData.items.length > 0 ? (
|
||||||
@ -207,7 +207,7 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: Countdown + Today's events + todos stacked */}
|
{/* Right: Countdown + Today's events + todos stacked */}
|
||||||
<div className="lg:col-span-2 flex flex-col gap-5">
|
<div className="lg:col-span-2 flex flex-col gap-3 sm:gap-5">
|
||||||
{data.starred_events.length > 0 && (
|
{data.starred_events.length > 0 && (
|
||||||
<CountdownWidget events={data.starred_events} />
|
<CountdownWidget events={data.starred_events} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export default function StatsWidget({ projectStats, totalIncompleteTodos, weathe
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-2.5 grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-1.5 sm:gap-2.5 grid-cols-2 sm:grid-cols-4">
|
||||||
{statCards.map((stat) => (
|
{statCards.map((stat) => (
|
||||||
<Card
|
<Card
|
||||||
key={stat.label}
|
key={stat.label}
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export default function TrackedProjectsWidget() {
|
|||||||
<span className="text-[11px] text-muted-foreground truncate block">{task.parent_task_title}</span>
|
<span className="text-[11px] text-muted-foreground truncate block">{task.parent_task_title}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[11px] text-muted-foreground truncate shrink-0 max-w-[5rem]">{task.project_name}</span>
|
<span className="text-[11px] text-muted-foreground truncate shrink-0 max-w-[5rem] hidden sm:block">{task.project_name}</span>
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
'text-xs shrink-0 whitespace-nowrap tabular-nums',
|
'text-xs shrink-0 whitespace-nowrap tabular-nums',
|
||||||
overdue ? 'text-red-400' : isToday(dueDate) ? 'text-accent' : 'text-muted-foreground'
|
overdue ? 'text-red-400' : isToday(dueDate) ? 'text-accent' : 'text-muted-foreground'
|
||||||
@ -94,7 +94,7 @@ export default function TrackedProjectsWidget() {
|
|||||||
{isToday(dueDate) ? 'Today' : format(dueDate, 'MMM d')}
|
{isToday(dueDate) ? 'Today' : format(dueDate, 'MMM d')}
|
||||||
</span>
|
</span>
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
'text-[9px] font-semibold px-1.5 py-0.5 rounded shrink-0',
|
'text-[9px] font-semibold px-1.5 py-0.5 rounded shrink-0 hidden sm:block',
|
||||||
statusBadgeColors[task.status] || 'bg-gray-500/10 text-gray-400'
|
statusBadgeColors[task.status] || 'bg-gray-500/10 text-gray-400'
|
||||||
)}>
|
)}>
|
||||||
{statusLabels[task.status] || task.status}
|
{statusLabels[task.status] || task.status}
|
||||||
|
|||||||
@ -75,11 +75,11 @@ export default function UpcomingWidget({ items, days = 7 }: UpcomingWidgetProps)
|
|||||||
? format(new Date(item.datetime), 'MMM d, h:mm a')
|
? format(new Date(item.datetime), 'MMM d, h:mm a')
|
||||||
: format(new Date(item.date), 'MMM d')}
|
: format(new Date(item.date), 'MMM d')}
|
||||||
</span>
|
</span>
|
||||||
<span className={cn('text-[9px] font-semibold uppercase tracking-wider shrink-0 w-14 text-right', config.color)}>
|
<span className={cn('text-[9px] font-semibold uppercase tracking-wider shrink-0 w-14 text-right hidden sm:block', config.color)}>
|
||||||
{config.label}
|
{config.label}
|
||||||
</span>
|
</span>
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
'text-[9px] font-semibold px-1.5 py-0.5 rounded shrink-0 w-14 text-center',
|
'text-[9px] font-semibold px-1.5 py-0.5 rounded shrink-0 w-14 text-center hidden sm:block',
|
||||||
item.priority === 'high' ? 'bg-red-500/10 text-red-400' :
|
item.priority === 'high' ? 'bg-red-500/10 text-red-400' :
|
||||||
item.priority === 'medium' ? 'bg-yellow-500/10 text-yellow-400' :
|
item.priority === 'medium' ? 'bg-yellow-500/10 text-yellow-400' :
|
||||||
item.priority === 'low' ? 'bg-green-500/10 text-green-400' :
|
item.priority === 'low' ? 'bg-green-500/10 text-green-400' :
|
||||||
|
|||||||
@ -39,13 +39,13 @@ export default function WeekTimeline({ items }: WeekTimelineProps) {
|
|||||||
}, [weekStart, today, items]);
|
}, [weekStart, today, items]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-stretch gap-2">
|
<div className="flex items-stretch gap-1 sm:gap-2">
|
||||||
{days.map((day) => (
|
{days.map((day) => (
|
||||||
<div
|
<div
|
||||||
key={day.key}
|
key={day.key}
|
||||||
onClick={() => navigate('/calendar', { state: { date: day.key, view: 'timeGridDay' } })}
|
onClick={() => navigate('/calendar', { state: { date: day.key, view: 'timeGridDay' } })}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex-1 flex flex-col items-center gap-1.5 rounded-lg py-3 px-2 transition-all duration-200 border cursor-pointer',
|
'flex-1 flex flex-col items-center gap-1 sm:gap-1.5 rounded-lg py-2 sm:py-3 px-1 sm:px-2 transition-all duration-200 border cursor-pointer',
|
||||||
day.isToday
|
day.isToday
|
||||||
? 'bg-accent/10 border-accent/30 shadow-[0_0_12px_hsl(var(--accent-color)/0.15)]'
|
? 'bg-accent/10 border-accent/30 shadow-[0_0_12px_hsl(var(--accent-color)/0.15)]'
|
||||||
: day.isPast
|
: day.isPast
|
||||||
@ -55,7 +55,7 @@ export default function WeekTimeline({ items }: WeekTimelineProps) {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[11px] font-medium uppercase tracking-wider',
|
'text-[9px] sm:text-[11px] font-medium uppercase tracking-wider',
|
||||||
day.isToday ? 'text-accent' : 'text-muted-foreground'
|
day.isToday ? 'text-accent' : 'text-muted-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -63,7 +63,7 @@ export default function WeekTimeline({ items }: WeekTimelineProps) {
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'font-heading text-lg font-semibold leading-none',
|
'font-heading text-sm sm:text-lg font-semibold leading-none',
|
||||||
day.isToday ? 'text-accent' : 'text-foreground'
|
day.isToday ? 'text-accent' : 'text-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export default function AppLayout() {
|
|||||||
</Button>
|
</Button>
|
||||||
<h1 className="text-lg font-bold text-accent ml-3">UMBRA</h1>
|
<h1 className="text-lg font-bold text-accent ml-3">UMBRA</h1>
|
||||||
</div>
|
</div>
|
||||||
<main className="flex-1 overflow-y-auto">
|
<main className="flex-1 overflow-y-auto mobile-scale">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -146,7 +146,9 @@ export default function CategoryFilterBar({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 overflow-x-auto">
|
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-2">
|
||||||
|
{/* Top row: pills + search */}
|
||||||
|
<div className="flex items-center gap-2 overflow-x-auto min-w-0 flex-1">
|
||||||
{/* All pill */}
|
{/* All pill */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -193,9 +195,8 @@ export default function CategoryFilterBar({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Categories pill + expandable chips */}
|
{/* Categories pill */}
|
||||||
{categories.length > 0 && (
|
{categories.length > 0 && (
|
||||||
<>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOtherOpen((p) => !p)}
|
onClick={() => setOtherOpen((p) => !p)}
|
||||||
@ -207,15 +208,27 @@ export default function CategoryFilterBar({
|
|||||||
Categories
|
Categories
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
{/* Search */}
|
||||||
className="flex items-center gap-1.5 overflow-x-auto transition-all duration-200 ease-out"
|
<div className="flex-1" />
|
||||||
style={{
|
<div className="relative shrink-0">
|
||||||
maxWidth: otherOpen ? '100vw' : '0px',
|
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
|
||||||
opacity: otherOpen ? 1 : 0,
|
<Input
|
||||||
overflow: 'hidden',
|
ref={searchInputRef}
|
||||||
}}
|
type="search"
|
||||||
>
|
placeholder="Search..."
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(e) => onSearchChange(e.target.value)}
|
||||||
|
className="w-28 sm:w-52 h-8 pl-8 text-sm ring-inset"
|
||||||
|
aria-label="Search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Expanded categories row — shows below on mobile, inline on desktop */}
|
||||||
|
{categories.length > 0 && otherOpen && (
|
||||||
|
<div className="flex items-center gap-1.5 overflow-x-auto pb-1 md:pb-0">
|
||||||
{/* "All" chip inside categories — non-draggable */}
|
{/* "All" chip inside categories — non-draggable */}
|
||||||
{onSelectAllCategories && (
|
{onSelectAllCategories && (
|
||||||
<button
|
<button
|
||||||
@ -260,25 +273,7 @@ export default function CategoryFilterBar({
|
|||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Spacer */}
|
|
||||||
<div className="flex-1" />
|
|
||||||
|
|
||||||
{/* Search */}
|
|
||||||
<div className="relative shrink-0">
|
|
||||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground pointer-events-none" />
|
|
||||||
<Input
|
|
||||||
ref={searchInputRef}
|
|
||||||
type="search"
|
|
||||||
placeholder="Search..."
|
|
||||||
value={searchValue}
|
|
||||||
onChange={(e) => onSearchChange(e.target.value)}
|
|
||||||
className="w-52 h-8 pl-8 text-sm ring-inset"
|
|
||||||
aria-label="Search"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -224,16 +224,54 @@ form[data-submitted] input:invalid + button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ── Global mobile content scaling ── */
|
||||||
|
/* Scales down all page content text on mobile. Navbar and UMBRA title are excluded
|
||||||
|
because they live outside .mobile-scale and use their own sizing. */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.mobile-scale {
|
||||||
|
font-size: 0.8125rem; /* 13px base instead of 16px */
|
||||||
|
}
|
||||||
|
.mobile-scale h1 {
|
||||||
|
font-size: 1.375rem !important; /* 22px instead of 30px */
|
||||||
|
}
|
||||||
|
.mobile-scale h2 {
|
||||||
|
font-size: 1.125rem !important;
|
||||||
|
}
|
||||||
|
.mobile-scale .text-sm {
|
||||||
|
font-size: 0.6875rem; /* 11px */
|
||||||
|
}
|
||||||
|
.mobile-scale .text-xs {
|
||||||
|
font-size: 0.625rem; /* 10px — floor for readability */
|
||||||
|
}
|
||||||
|
.mobile-scale .text-lg {
|
||||||
|
font-size: 0.9375rem; /* 15px */
|
||||||
|
}
|
||||||
|
.mobile-scale .text-xl {
|
||||||
|
font-size: 1.0625rem; /* 17px */
|
||||||
|
}
|
||||||
|
.mobile-scale .text-2xl {
|
||||||
|
font-size: 1.1875rem; /* 19px */
|
||||||
|
}
|
||||||
|
.mobile-scale .text-3xl {
|
||||||
|
font-size: 1.375rem; /* 22px */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ── FullCalendar mobile overrides ── */
|
/* ── FullCalendar mobile overrides ── */
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.fc .fc-daygrid-event {
|
.fc .fc-daygrid-event {
|
||||||
font-size: 0.65rem;
|
font-size: 0.6rem;
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
line-height: 1.3;
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide event times in month view on mobile — Google Calendar style */
|
||||||
|
.fc .fc-daygrid-event .fc-event-time {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc .fc-timegrid-event {
|
.fc .fc-timegrid-event {
|
||||||
font-size: 0.65rem;
|
font-size: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc .fc-timegrid-event .fc-event-main {
|
.fc .fc-timegrid-event .fc-event-main {
|
||||||
@ -241,21 +279,21 @@ form[data-submitted] input:invalid + button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fc .fc-daygrid-day-number {
|
.fc .fc-daygrid-day-number {
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc .fc-col-header-cell-cushion {
|
.fc .fc-col-header-cell-cushion {
|
||||||
font-size: 0.7rem;
|
font-size: 0.65rem;
|
||||||
padding: 4px 2px;
|
padding: 3px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc .fc-timegrid-slot-label {
|
.fc .fc-timegrid-slot-label {
|
||||||
font-size: 0.65rem;
|
font-size: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc .fc-daygrid-more-link {
|
.fc .fc-daygrid-more-link {
|
||||||
font-size: 0.6rem;
|
font-size: 0.55rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* ── Ambient background animations ── */
|
/* ── Ambient background animations ── */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user