Phase 2: toolbar responsive patterns

- All page toolbars now flex-wrap on mobile with min-h instead of fixed h-16
- Segmented button filters (priority, status, view) hidden on mobile, replaced
  with compact Select dropdowns
- Search inputs hidden on mobile where CategoryFilterBar already has search
- CategoryFilterBar wraps to full-width row on mobile (order-last)
- Action buttons show icon-only on mobile, full text on md+
- Calendar title hidden on xs screens for space
- Desktop layout completely unchanged (md:flex-nowrap restores original)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-07 16:56:08 +08:00
parent 1c16df4db0
commit d0477b1c13
7 changed files with 68 additions and 35 deletions

View File

@ -16,6 +16,7 @@ import { useCalendars } from '@/hooks/useCalendars';
import { useSettings } from '@/hooks/useSettings'; import { useSettings } from '@/hooks/useSettings';
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 { Select } from '@/components/ui/select';
import CalendarSidebar from './CalendarSidebar'; import CalendarSidebar from './CalendarSidebar';
import EventDetailPanel from './EventDetailPanel'; import EventDetailPanel from './EventDetailPanel';
import type { CreateDefaults } from './EventDetailPanel'; import type { CreateDefaults } from './EventDetailPanel';
@ -478,7 +479,7 @@ export default function CalendarPage() {
<div ref={calendarContainerRef} className="flex-1 flex flex-col overflow-hidden"> <div ref={calendarContainerRef} className="flex-1 flex flex-col overflow-hidden">
{/* Custom toolbar */} {/* Custom toolbar */}
<div className="border-b bg-card px-4 md:px-6 h-16 flex items-center gap-4 shrink-0"> <div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-2 md:gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={navigatePrev}> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={navigatePrev}>
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
@ -491,7 +492,17 @@ export default function CalendarPage() {
Today Today
</Button> </Button>
<div className="flex items-center rounded-md border border-border overflow-hidden"> <Select
value={currentView}
onChange={(e) => changeView(e.target.value as CalendarView)}
className="h-8 text-sm w-auto md:hidden"
>
{(Object.entries(viewLabels) as [CalendarView, string][]).map(([view, label]) => (
<option key={view} value={view}>{label}</option>
))}
</Select>
<div className="hidden md:flex items-center rounded-md border border-border overflow-hidden">
{(Object.entries(viewLabels) as [CalendarView, string][]).map(([view, label]) => ( {(Object.entries(viewLabels) as [CalendarView, string][]).map(([view, label]) => (
<button <button
key={view} key={view}
@ -511,12 +522,12 @@ export default function CalendarPage() {
))} ))}
</div> </div>
<h2 className="text-lg font-semibold font-heading">{calendarTitle}</h2> <h2 className="text-lg font-semibold font-heading hidden sm:block">{calendarTitle}</h2>
<div className="flex-1" /> <div className="flex-1" />
{/* Event search */} {/* Event search */}
<div className="relative"> <div className="relative hidden md:block">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" /> <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input <Input
placeholder="Search events..." placeholder="Search events..."
@ -553,8 +564,7 @@ export default function CalendarPage() {
</div> </div>
<Button size="sm" onClick={handleCreateNew}> <Button size="sm" onClick={handleCreateNew}>
<Plus className="mr-2 h-4 w-4" /> <Plus className="h-4 w-4 md:mr-2" /><span className="hidden md:inline">Create Event</span>
Create Event
</Button> </Button>
</div> </div>

View File

@ -285,10 +285,10 @@ export default function LocationsPage() {
return ( return (
<div className="flex flex-col h-full animate-fade-in"> <div className="flex flex-col h-full animate-fade-in">
{/* Header */} {/* Header */}
<div className="border-b bg-card px-4 md:px-6 h-16 flex items-center gap-4 shrink-0"> <div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
<h1 className="font-heading text-2xl font-bold tracking-tight">Locations</h1> <h1 className="font-heading text-2xl font-bold tracking-tight">Locations</h1>
<div className="flex-1 min-w-0"> <div className="w-full md:flex-1 md:w-auto min-w-0 order-last md:order-none">
<CategoryFilterBar <CategoryFilterBar
categories={orderedCategories} categories={orderedCategories}
activeFilters={activeFilters} activeFilters={activeFilters}
@ -305,8 +305,7 @@ export default function LocationsPage() {
</div> </div>
<Button onClick={() => setShowForm(true)} size="sm" aria-label="Add location"> <Button onClick={() => setShowForm(true)} size="sm" aria-label="Add location">
<Plus className="mr-2 h-4 w-4" /> <Plus className="h-4 w-4 md:mr-2" /><span className="hidden md:inline">Add Location</span>
Add Location
</Button> </Button>
</div> </div>

View File

@ -555,9 +555,9 @@ export default function PeoplePage() {
return ( return (
<div className="flex flex-col h-full animate-fade-in"> <div className="flex flex-col h-full animate-fade-in">
{/* Header */} {/* Header */}
<div className="border-b bg-card px-4 md:px-6 h-16 flex items-center gap-4 shrink-0"> <div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
<h1 className="font-heading text-2xl font-bold tracking-tight">People</h1> <h1 className="font-heading text-2xl font-bold tracking-tight">People</h1>
<div className="flex-1 min-w-0"> <div className="w-full md:flex-1 md:w-auto min-w-0 order-last md:order-none">
<CategoryFilterBar <CategoryFilterBar
activeFilters={activeFilters} activeFilters={activeFilters}
pinnedLabel="Favourites" pinnedLabel="Favourites"
@ -587,8 +587,7 @@ export default function PeoplePage() {
aria-label="Add person" aria-label="Add person"
className="rounded-r-none" className="rounded-r-none"
> >
<Plus className="mr-2 h-4 w-4" /> <Plus className="h-4 w-4 md:mr-2" /><span className="hidden md:inline">Add Person</span>
Add Person
</Button> </Button>
<Button <Button
size="sm" size="sm"

View File

@ -345,7 +345,7 @@ export default function ProjectDetail() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex flex-col h-full animate-fade-in"> <div className="flex flex-col h-full animate-fade-in">
<div className="border-b bg-card px-4 md:px-6 h-16 flex items-center gap-4 shrink-0"> <div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-2 md:gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
<Button variant="ghost" size="icon" onClick={() => navigate('/projects')}> <Button variant="ghost" size="icon" onClick={() => navigate('/projects')}>
<ArrowLeft className="h-5 w-5" /> <ArrowLeft className="h-5 w-5" />
</Button> </Button>
@ -396,8 +396,7 @@ export default function ProjectDetail() {
<Pin className={`h-4 w-4 ${project.is_tracked ? 'fill-current' : ''}`} /> <Pin className={`h-4 w-4 ${project.is_tracked ? 'fill-current' : ''}`} />
</Button> </Button>
<Button variant="outline" size="sm" onClick={() => setShowProjectForm(true)}> <Button variant="outline" size="sm" onClick={() => setShowProjectForm(true)}>
<Pencil className="mr-2 h-3.5 w-3.5" /> <Pencil className="h-3.5 w-3.5 md:mr-2" /><span className="hidden md:inline">Edit</span>
Edit
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
@ -409,8 +408,7 @@ export default function ProjectDetail() {
}} }}
disabled={deleteProjectMutation.isPending} disabled={deleteProjectMutation.isPending}
> >
<Trash2 className="mr-2 h-3.5 w-3.5" /> <Trash2 className="h-3.5 w-3.5 md:mr-2" /><span className="hidden md:inline">Delete</span>
Delete
</Button> </Button>
</div> </div>
@ -490,7 +488,7 @@ export default function ProjectDetail() {
</div> </div>
{/* Task list header + view controls */} {/* Task list header + view controls */}
<div className="px-4 md:px-6 pb-3 flex items-center justify-between shrink-0"> <div className="px-4 md:px-6 pb-3 flex items-center justify-between flex-wrap gap-2 shrink-0">
<h2 className="font-heading text-lg font-semibold">Tasks</h2> <h2 className="font-heading text-lg font-semibold">Tasks</h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{/* View toggle */} {/* View toggle */}

View File

@ -5,6 +5,7 @@ import { useQuery } from '@tanstack/react-query';
import api from '@/lib/api'; import api from '@/lib/api';
import type { Project } from '@/types'; import type { Project } from '@/types';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Select } from '@/components/ui/select';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { GridSkeleton } from '@/components/ui/skeleton'; import { GridSkeleton } from '@/components/ui/skeleton';
@ -70,10 +71,19 @@ export default function ProjectsPage() {
return ( return (
<div className="flex flex-col h-full animate-fade-in"> <div className="flex flex-col h-full animate-fade-in">
{/* Header */} {/* Header */}
<div className="border-b bg-card px-4 md:px-6 h-16 flex items-center gap-4 shrink-0"> <div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
<h1 className="font-heading text-2xl font-bold tracking-tight">Projects</h1> <h1 className="font-heading text-2xl font-bold tracking-tight">Projects</h1>
<div className="flex items-center rounded-md border border-border overflow-hidden ml-4"> <Select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as typeof statusFilter)}
className="h-8 text-sm w-auto md:hidden"
>
{statusFilters.map((sf) => (
<option key={sf.value} value={sf.value}>{sf.label}</option>
))}
</Select>
<div className="hidden md:flex items-center rounded-md border border-border overflow-hidden ml-4">
{statusFilters.map((sf) => ( {statusFilters.map((sf) => (
<button <button
key={sf.value} key={sf.value}
@ -95,7 +105,7 @@ export default function ProjectsPage() {
<div className="flex-1" /> <div className="flex-1" />
<div className="relative"> <div className="relative hidden md:block">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" /> <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input <Input
placeholder="Search..." placeholder="Search..."
@ -106,8 +116,7 @@ export default function ProjectsPage() {
</div> </div>
<Button onClick={() => setShowForm(true)} size="sm"> <Button onClick={() => setShowForm(true)} size="sm">
<Plus className="mr-2 h-4 w-4" /> <Plus className="h-4 w-4 md:mr-2" /><span className="hidden md:inline">New Project</span>
New Project
</Button> </Button>
</div> </div>

View File

@ -6,6 +6,7 @@ import { isPast, isToday, parseISO } from 'date-fns';
import api from '@/lib/api'; import api from '@/lib/api';
import type { Reminder } from '@/types'; import type { Reminder } from '@/types';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Select } from '@/components/ui/select';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { ListSkeleton } from '@/components/ui/skeleton'; import { ListSkeleton } from '@/components/ui/skeleton';
@ -99,10 +100,19 @@ export default function RemindersPage() {
return ( return (
<div className="flex flex-col h-full animate-fade-in"> <div className="flex flex-col h-full animate-fade-in">
{/* Header */} {/* Header */}
<div className="border-b bg-card px-4 md:px-6 h-16 flex items-center gap-4 shrink-0"> <div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
<h1 className="font-heading text-2xl font-bold tracking-tight">Reminders</h1> <h1 className="font-heading text-2xl font-bold tracking-tight">Reminders</h1>
<div className="flex items-center rounded-md border border-border overflow-hidden ml-4"> <Select
value={filter}
onChange={(e) => setFilter(e.target.value as typeof filter)}
className="h-8 text-sm w-auto md:hidden"
>
{statusFilters.map((sf) => (
<option key={sf.value} value={sf.value}>{sf.label}</option>
))}
</Select>
<div className="hidden md:flex items-center rounded-md border border-border overflow-hidden ml-4">
{statusFilters.map((sf) => ( {statusFilters.map((sf) => (
<button <button
key={sf.value} key={sf.value}
@ -125,7 +135,7 @@ export default function RemindersPage() {
<div className="flex-1" /> <div className="flex-1" />
<div className="relative"> <div className="relative hidden md:block">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" /> <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input <Input
placeholder="Search..." placeholder="Search..."
@ -136,8 +146,7 @@ export default function RemindersPage() {
</div> </div>
<Button onClick={handleCreateNew} size="sm"> <Button onClick={handleCreateNew} size="sm">
<Plus className="mr-2 h-4 w-4" /> <Plus className="h-4 w-4 md:mr-2" /><span className="hidden md:inline">Add Reminder</span>
Add Reminder
</Button> </Button>
</div> </div>

View File

@ -6,6 +6,7 @@ import api from '@/lib/api';
import type { Todo } from '@/types'; import type { Todo } from '@/types';
import { isTodoOverdue } from '@/lib/utils'; import { isTodoOverdue } from '@/lib/utils';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Select } from '@/components/ui/select';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { ListSkeleton } from '@/components/ui/skeleton'; import { ListSkeleton } from '@/components/ui/skeleton';
import { CategoryFilterBar } from '@/components/shared'; import { CategoryFilterBar } from '@/components/shared';
@ -128,11 +129,20 @@ export default function TodosPage() {
return ( return (
<div className="flex flex-col h-full animate-fade-in"> <div className="flex flex-col h-full animate-fade-in">
{/* Header */} {/* Header */}
<div className="border-b bg-card px-4 md:px-6 h-16 flex items-center gap-4 shrink-0"> <div className="border-b bg-card px-4 md:px-6 min-h-[4rem] flex items-center gap-4 flex-wrap py-2 md:py-0 md:h-16 md:flex-nowrap shrink-0">
<h1 className="font-heading text-2xl font-bold tracking-tight">Todos</h1> <h1 className="font-heading text-2xl font-bold tracking-tight">Todos</h1>
{/* Priority filter */} {/* Priority filter */}
<div className="flex items-center rounded-md border border-border overflow-hidden ml-4"> <Select
value={priorityFilter}
onChange={(e) => setPriorityFilter(e.target.value as typeof priorityFilter)}
className="h-8 text-sm w-auto md:hidden"
>
{priorityFilters.map((pf) => (
<option key={pf.value} value={pf.value}>{pf.label}</option>
))}
</Select>
<div className="hidden md:flex items-center rounded-md border border-border overflow-hidden ml-4">
{priorityFilters.map((pf) => ( {priorityFilters.map((pf) => (
<button <button
key={pf.value} key={pf.value}
@ -154,7 +164,7 @@ export default function TodosPage() {
</div> </div>
{/* Category filter bar (All + Completed + Categories with drag) */} {/* Category filter bar (All + Completed + Categories with drag) */}
<div className="flex-1 min-w-0"> <div className="w-full md:flex-1 md:w-auto min-w-0 order-last md:order-none">
<CategoryFilterBar <CategoryFilterBar
activeFilters={activeFilters} activeFilters={activeFilters}
pinnedLabel="Completed" pinnedLabel="Completed"
@ -171,8 +181,7 @@ export default function TodosPage() {
</div> </div>
<Button onClick={handleCreateNew} size="sm"> <Button onClick={handleCreateNew} size="sm">
<Plus className="mr-2 h-4 w-4" /> <Plus className="h-4 w-4 md:mr-2" /><span className="hidden md:inline">Add Todo</span>
Add Todo
</Button> </Button>
</div> </div>